博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Linux 内核网络协议栈 ----- Linux 内核路由机制(一) (2.6.25)
阅读量:4055 次
发布时间:2019-05-25

本文共 14046 字,大约阅读时间需要 46 分钟。

   

       内核的路由部分是是网络中重要部分,目前在Linux内核中默认的路由查找算法使用的是Hash查找,所以你会看到很多的数据结构是XXX_hash什么之类(例如fn_hash)。Linux内核从2.1开始就支持基于策略的路由,那么什么是基于策略的路由呢?我们一般的最基本的路由转发是考虑IP包的目的地址,但是有些时候不仅仅是这些,还有例如IP协议,传输端口等之类的考虑因素,所以采用所谓基于策略的路由。

      或许这样理解更好,Linux默认有三种策略路由:本地路由,主路由和默认路由,那么与之对应的就是三张路由表:本地路由表,主路由表和默认路由表。

      那么我们需要理解是什么呢?当然是路由怎么转的过程。在这之前,先看看所涉及数据结构有哪些。

      介绍下面之前我们首先需要知道内核常用的结构之间的操作手法。说道这里不得不先说一下内核的链表结构。

      内核的链表结构主要是用来表示连接关系的

struct hlist_head {         struct hlist_node *first; };  struct hlist_node {         struct hlist_node *next, **pprev;   // 看这个你就知道,内核链表一般是双向链表(其实还是循环链表) };

     那么下面的很多结构之间的链接都是通过这样的链表的!但是就算我通过一个结构找到另一个结构的链表字段的时候,怎么确定结构真正的首地址呢?其实我们都不用担心,内核采取container_of这个宏定义来处理的!

#define container_of(ptr, type, member) ({                      \         const typeof( ((type *)0)->member ) *__mptr = (ptr);    \         (type *)( (char *)__mptr - offsetof(type,member) );})
很简单,其实就是通过偏移来做的,很easy、
struct fib_table {         struct hlist_node tb_hlist;// hash节点(通过ipv4的hlist_head可以得到属于自己的路由信息表FIB,这个就是链接字段)         u32             tb_id;     // 标识符(例如:本地路由,主路由,默认路由)         unsigned        tb_stamp;  // 时间戳         int             tb_default;// 路由信息结构队列序号         int             (*tb_lookup)(struct fib_table *tb, const struct flowi *flp, struct fib_result *res);// 查找函数         int             (*tb_insert)(struct fib_table *, struct fib_config *);// 插入函数         int             (*tb_delete)(struct fib_table *, struct fib_config *);// 删除路由函数         int             (*tb_dump)(struct fib_table *table, struct sk_buff *skb,                                       struct netlink_callback *cb);            // 用于路由转发         int             (*tb_flush)(struct fib_table *table);                 // 移除路由信息结构         void            (*tb_select_default)(struct fib_table *table,         // 设置默认路由                                              const struct flowi *flp, struct fib_result *res);          unsigned char   tb_data[0];   // 注意这个特殊字段,标识结构的结尾,分配fib_table同时分配fn_hash结构 };                                    // 也就是fib_table之后就是fn_hash结构

// 先介绍一下“路由区”定义:fn_zone,举个例子,子网掩码长度相同的认为是相同的路由区(ok)

struct fn_hash {     // 路由区结构体的数组( 包含所有的额路由区的情况 )         struct fn_zone  *fn_zones[33];// 路由区分成33份,why?仔细想想,子网掩码长度是1~32,0长度掩码代表网关,那么加起来就是33,即:fn_zone[0]的掩码是0.0.0.0,fn_zone[1]是10000000.00000000.00000000.0000000这一类  等等         struct fn_zone  *fn_zone_list;// 指向第一个活动的路由区 };

struct fn_zone {      // 路由区结构体(所有的子网长度相等的被分在同一个路由区)         struct fn_zone          *fz_next;       // 指向下一个不为空的路由区结构,那么所有的路由区就能链接起来         struct hlist_head       *fz_hash;       // 有一个hash数组,用来hash得到一个hlist_head,是很多的fib_node通过自己的字段连接在这个队列中,那么通过这个fz_hahs字段可以找到fib_node所在的队列的头hlist_head,进而找到对应的fib_node ( 注意:上面说的hash数组的长度是fz_divisor长度)         int                     fz_nent;        // 包含的路由节总数         int                     fz_divisor;     // hash头数量(上面说了)         u32                     fz_hashmask;    // 确定hash头的掩码 #define FZ_HASHMASK(fz)         ((fz)->fz_hashmask)          int                     fz_order;       // 子网掩码位数         __be32                  fz_mask;        // 子网掩码#define FZ_MASK(fz)             ((fz)->fz_mask)  // 获取子网掩码的宏定义};

struct fib_node {    // 路由节点结构体( 子网相等的路由被分在一起 )         struct hlist_node       fn_hash; // 链接到hash表节点( 注意到我们上面所说的fn_zone中的fz_hash了吗?fz_hash哈希之后得到的结果就是fib_node的这个字段,所以这个字段同样仅仅是作为链接作用而已 )         struct list_head        fn_alias;// 别名?其实更好的理解是这样的:虽然现在所有的路由都是同一个子网了,但是路由之间还会有其他的信息不同例如tos,路由类型,等等。所以依然存在不同的路由,所以这些都是通过fn_alias来区分。         __be32                  fn_key;  // 路由别名队列:即这个node下面所有的具体路由(不同的fn_alias的)都在这个队列中         struct fib_alias        fn_embedded_alias; // 分配路由节点的时候同时也分配一个路由别名,所以称为嵌入式的~ };

struct fib_alias {      // 路由别名结构,这个结构基本就是最后一次路由筛选了         struct list_head        fa_list;  // 这个是用于链接到fib_node节点中的,看上面的结构体的第二个字段的类型你就懂了~~~~~~         struct fib_info         *fa_info; // 这是很重要的字段:顾名思义,就是具体怎么处置这个数据包的操作等         u8                      fa_tos;   // 服务类型TOS         u8                      fa_type;  // 路由类型         u8                      fa_scope; // 路由范围         u8                      fa_state; // 路由状态 #ifdef CONFIG_IP_FIB_TRIE         struct rcu_head         rcu; #endif };

struct fib_info {      // 具体怎么路由这个数据包的信息         struct hlist_node       fib_hash;      // 链接到fib_info_hash队列         struct hlist_node       fib_lhash;     // 链接到fib_hash_laddrhash队列         struct net              *fib_net;      // 所属网络空间         int                     fib_treeref;   // 路由信息结构使用计数器         atomic_t                fib_clntref;   // 释放路由信息结构(fib)计数器         int                     fib_dead;      // 标志路由被删除了         unsigned                fib_flags;     // 标识位         int                     fib_protocol;  // 安装路由协议         __be32                  fib_prefsrc;   // 指定源IP,源地址和目的地址组成一个路由         u32                     fib_priority;  // 路由优先级         u32                     fib_metrics[RTAX_MAX]; // 保存负载值(例如MTU,MSS) #define fib_mtu fib_metrics[RTAX_MTU-1]        // MTU值 #define fib_window fib_metrics[RTAX_WINDOW-1]  // 窗口值 #define fib_rtt fib_metrics[RTAX_RTT-1]        // RTT值 #define fib_advmss fib_metrics[RTAX_ADVMSS-1]  // MSS值(对外公开的)         int                     fib_nhs;       // 倒数第二个字段即:跳转结构的数组个数 #ifdef CONFIG_IP_ROUTE_MULTIPATH         int                     fib_power;     // 支持多路径时候使用 #endif         struct fib_nh           fib_nh[0];     // 跳转结构(就是该怎么路由) #define fib_dev         fib_nh[0].nh_dev        };

对于上面的fib_nh[0],这样的操作手法在内核中也是常见的。代表会有这个字段的存在,但是具体是几个并不知道,因为可能是动态的,所以需要一个计数表示,也就是fib_power

OK,主要的数据结构已经介绍,后面的结构会边说边介绍,下面我们根据路由转发的顺序来梳理一下思路:

数据包的路由是通过函数ip_route_input来处理的,看这个函数:

extern int  ip_route_input(struct sk_buff*, __be32 dst, __be32 src, u8 tos, struct net_device *devin);
参数有5个:

      skb: IP包缓冲区,

      dst: IP包的目的地址,

      src: IP包源地址,

      tos: 服务类型,

      devin: 输入的网络设备。

怎么运行的呢?首先这个函数需要查路由缓存(cache),如果找到了那么它给skb->dst赋值并返回,如是没找到,它会调用ip_route_input_slow去查询路由数据库。

这里我们需要理解几个问题: 首先路由缓存到底是什么结构,怎么查找,这个我们马上就会说到。再次我们需要知道所谓路由就是最终找到这个路由条目,得到目的地址(吓一跳),然后赋值给skb->dst,然后通过skb->dst->input(skb)就可以进行操作。第三需要注意,这里的操作分成两类:第一类是投到本地,即数据是发到本机的,那么调用ip_local_deliver将数据包发送给上一层进行处理;第二类是转发,调用ip_forward函数进行处理,转发出去!最后注意:当路由缓冲找不到所需要的路由项,那么最终需要再次到fib中去查找,也就是完整的一个查找过程。

下面具体看看路由缓存问题:

首先是怎么建立这个缓存的呢?其实这个问题不需要特意来说,因为后面肯定会说到,为什么呢?缓存总是由不存在到存在的,当不存在的时候只能使用查询路由信息库来处理,但是同时需要注意:更新缓存cache、这个时候就是建立cache的时候。所以在后面说到的路由信息库查询和cache的建立是一样的,先不说这个,先直接看在cache中处理。

cache的结构定义为: 

static struct rt_hash_bucket    *rt_hash_table;

rt_hash_table就是路由cache,它是rt_hash_bucket结构。

struct rt_hash_bucket {         struct rtable   *chain;}
注意chain是一个rtable结构,看下面:

struct rtable {         union         {                 struct dst_entry        dst;   // 这是目的地址         } u;          /* Cache lookup keys */         struct flowi            fl;            // 注意在cache中的查找主要是通过路由键值和下面的信息          struct in_device        *idev;         // 设备                  int                     rt_genid;      // 路由id         unsigned                rt_flags;      // 标识         __u16                   rt_type;       // 路由类型          __be32                  rt_dst;        // 目的地址         __be32                  rt_src;        // 源地址         int                     rt_iif;        // 入端口          /* Info on neighbour */         __be32                  rt_gateway;    // 网关          /* Miscellaneous cached information */         __be32                  rt_spec_dst;  /* RFC1122 specific destination */         struct inet_peer        *peer;        /* long-living peer info */ };
我们看一下查询的一小段代码:

2048         for (rth = rcu_dereference(rt_hash_table[hash].chain); rth;2049              rth = rcu_dereference(rth->u.dst.rt_next)) {2050                 if (rth->fl.fl4_dst == daddr &&2051                     rth->fl.fl4_src == saddr &&2052                     rth->fl.iif == iif &&2053                     rth->fl.oif == 0 &&2054                     rth->fl.mark == skb->mark &&2055                     rth->fl.fl4_tos == tos &&2056                     rth->u.dst.dev->nd_net == net &&2057                     rth->rt_genid == atomic_read(&rt_genid)) {2058                         dst_use(&rth->u.dst, jiffies);2059                         RT_CACHE_STAT_INC(in_hit);2060                         rcu_read_unlock();2061                         skb->dst = (struct dst_entry*)rth;2062                         return 0;2063                 }2064                 RT_CACHE_STAT_INC(in_hlist_search);2065         }
所以很清晰的看到匹配的所有字段。下面看看我们构造一下在cache中查找的结构图:

首先通过hash找到这个队列首部的chain,然后在chain的队列中进行匹配,如果匹配到那么OK,否则进行完整的查询。

OK,假如现在在缓存cache中并没有找到,那么执行ip_route_input_slow函数进行完整查询。

我们知道Linux最多可以支持255张路由表,默认有三张路由表,即本地路由表,主路由表和默认路由表,三个优先级递减(数字越大优先级越小),也就是查询顺序递减。我们先需要知道怎么样得到这三张路由表先。三张路由表就是三个规则,所以需要看看下面的路由信息结构规则结构体。

        表255: 本地路由表(local ) 本地接口地址,广播地址,已及NAT地址都放在这个表。该路由表由系统自动维护,管理员不能直接修改。
  表254: 主路由表(main ) 如果没有指明路由所属的表,所有的路由都默认都放在这个表里,一般来说,旧的路由工具(如route)所添加的路由都会加到这个表。一般是普通的路由。
  表253: 默认路由表 (default ) 一般来说默认的路由都放在这张表。
  表 0 :保留

看一下它们是怎么被初始化的:

static int fib_default_rules_init(struct fib_rules_ops *ops) {         int err;          err = fib_default_rule_add(ops, 0, RT_TABLE_LOCAL, FIB_RULE_PERMANENT);  // 本地路由规则(本地路由表)         if (err < 0)                 return err;         err = fib_default_rule_add(ops, 0x7FFE, RT_TABLE_MAIN, 0);               // 主路由规则(主路由表)         if (err < 0)                 return err;         err = fib_default_rule_add(ops, 0x7FFF, RT_TABLE_DEFAULT, 0);            // 默认路由规则(默认路由表)         if (err < 0)                 return err;         return 0; }

// 本地规则local_rulestatic struct fib_rule local_rule = {r_next: &main_rule,            //下一条规则是主规则r_clntref: ATOMIC_INIT(2),r_table: RT_TABLE_LOCAL,       // 指向本地路由表r_action: RTN_UNICAST,         // 动作是返回路由};
// 主规则main_rulestatic struct fib_rule main_rule = {r_next: &default_rule,         // 下一条规则是默认规则r_clntref: ATOMIC_INIT(2),r_preference: 0x7FFE,          // 默认规则的优先级32766r_table: RT_TABLE_MAIN,        // 指向主路由表 r_action: RTN_UNICAST,         // 动作是返回路由};
// 默认规则default rulestatic struct fib_rule default_rule = {r_clntref: ATOMIC_INIT(2),r_preference: 0x7FFF,          // 默认规则的优先级32767r_table: RT_TABLE_DEFAULT,     // 指默认路由表r_action: RTN_UNICAST,         // 动作是返回路由};

注意:规则链的链头指向本地规则。

下面我们需要看看这个结构体:

struct fib_rule    // 规则结构体(在初始化的时候,会注册上面的三种规则,生成默认的三张表) {         struct list_head        list;              // 用来链入路由规则函数队列中(fib_rules_ops,下面介绍)         atomic_t                refcnt;            // 计数器         int                     ifindex;           // 网络设备id         char                    ifname[IFNAMSIZ];  // 设备名称         u32                     mark;              // 用于过滤作用         u32                     mark_mask;         // 掩码         u32                     pref;              // 优先级(例如上面代码中分别是0,0x7FEE,0x7FFF)         u32                     flags;             // 标识位         u32                     table;             // 路由函数表id(例如本地LOCAL,主路由MAIN...)         u8                      action;            // 动作,即怎么去处理这个数据包         u32                     target;         struct fib_rule *       ctarget;           // 当前规则         struct rcu_head         rcu;         struct net *            fr_net;            // 网络空间结构指针 };
同时看一下rule的规则函数:

struct fib_rules_ops {         int                     family;              // 协议族ID         struct list_head        list;                // 用于链接到网络空间队列中         int                     rule_size;           // 规则结构大小         int                     addr_size;           // 地址大小         int                     unresolved_rules;         int                     nr_goto_rules;          int                     (*action)(struct fib_rule *,         // 动作函数指针                                           struct flowi *, int,                                           struct fib_lookup_arg *);          int                     (*match)(struct fib_rule *,          // 匹配函数指针                                          struct flowi *, int);         int                     (*configure)(struct fib_rule *,      // 配置函数指针                                              struct sk_buff *,                                              struct nlmsghdr *,                                              struct fib_rule_hdr *,                                              struct nlattr **);         int                     (*compare)(struct fib_rule *,        // 对比函数指针                                            struct fib_rule_hdr *,                                            struct nlattr **);         int                     (*fill)(struct fib_rule *, struct sk_buff *,                                         struct nlmsghdr *,           // 填写函数指针                                         struct fib_rule_hdr *);         u32                     (*default_pref)(struct fib_rules_ops *ops);  // 查找优先级函数指针         size_t                  (*nlmsg_payload)(struct fib_rule *); // 统计负载数据能力函数指针          /* Called after modifications to the rules set, must flush          * the route cache if one exists. */         void                    (*flush_cache)(void);     // 修改规则之后刷新缓存函数指针          int                     nlgroup;      // 路由netlink组划分标识         const struct nla_policy *policy;      // netlink属性优先级         struct list_head        rules_list;   // 路由规则队列         struct module           *owner;       //          struct net              *fro_net;     // 网络空间结构指针 };

现在我们从宏观上应该有一个认识,当我们进入策略查找的时候,根据优先级,分别查找本地路由表->主路由表->默认路由表。

OK,我们需要看一下结构直接的关系:

ok,我们由规则找到了我们需要的三张表,三张表按照优先级的顺序进行查询,现在就以Local表为例进行下面具体的查询,看下图:

从图中我们可以看到四个等级查询:fib_table ---> fn_zone ---> fib_node ---> fib_info

> fib_table结构后面紧接着就是fn_hash数组,里面是33个数组元素,fn_hash[0]代表网关,fn_hash[1]代表子网掩码长度为一的情况... 为什么需要这样划分,因为我们知道,在匹配地址的时候遵循最长掩码优先原则,所以,精确度递减。  同时注意fn_zone_list指向第一个活动的路由区,将所有的路由区都链接在一起,从而提高查找的效率。fn_zone结构中最重要的就是fz_hash域了,它指向了一个hash table,这个hash table组织了这个区域下的所有路由项。( 一个fn_zone其实就是所有掩码长度相等的路由聚集在一起... )

> fn_zone路由区通过再次计算hash值,可以获得和自己相关的fib_node节点,fib_node节点是所有的子网相等的路由聚集在一起。

fn_key子网地址,也就hash查找的关键字;fn_type表示路由类型,即到底要怎处理数据,例如:单播转发,本地,丢弃,NAT等等对于大多数情况,路由项都是单播转发类型的;fn_info就是保存下一跳的信息,它指向一个fib_info结构。

> 需要注意的是,一个fib_node对应着很多fib_info,因为即使是子网相等,也不一定是相等的路由,还有很多其他的因素。fib_info结构被组织成一个双向链表,表头为fib_info_list。下一跳的具体信息是fib_nh[]数组,它表示一个下一跳动作可以对应着多个物理的下一跳,这是linux支持的一个MULITPATH功能。

说到这来,大致的印象应该是有的,下面需要做的就是深入代码细节。

待续...  后面会介绍相关的代码...

 

你可能感兴趣的文章
指针&数组&字符串&结构体
查看>>
Linux 内核api man 手册安装
查看>>
Linux 内核宏 container_of
查看>>
Ubuntu 安装bcompare
查看>>
电阻屏较准
查看>>
imx6 内核停止启动
查看>>
RTL8188EUS Anaroid M Porting
查看>>
omap 的framebuffer驱动程序
查看>>
android2.3 dvsdk
查看>>
QT Creater的安装配置
查看>>
QT5学习总结
查看>>
ubuntu 安装使用dbus
查看>>
QT QDbus
查看>>
android init launch
查看>>
nand booting
查看>>
android boot animation
查看>>
Python环境搭建
查看>>
Python3 基础
查看>>
Python3 基础(itcast学习笔记)
查看>>
ubuntu_fastboot
查看>>