IP路由cache桶
struct rt_hash_bucket {
struct rtable *chain;
};
rt_hash_table为struct rt_hash_bucket类型。
struct rtable
{
union
{
struct dst_entry dst;
} u;
struct flowi fl;
struct in_device *idev;
unsigned rt_flags;
__u16 rt_type;
__be32 rt_dst;
__be32 rt_src;
int rt_iif;
__be32 rt_gateway;
__be32 rt_spec_dst;
struct inet_peer *peer;
};
IP数据报的路由称为目的入口(dst_entry),目的入口反映了相邻的外部主机在本地主机内部的一种“映象”,目的入口在内核中的定义如下
struct dst_entry
{
struct dst_entry *next;
atomic_t __refcnt;
int __use;
struct dst_entry *child;
struct net_device *dev;
short error;
short obsolete;
int flags;
unsigned long lastuse;
unsigned long expires;
unsigned short header_len;
unsigned short nfheader_len;
unsigned short trailer_len;
u32 metrics[RTAX_MAX];
struct dst_entry *path;
unsigned long rate_last;
unsigned long rate_tokens;
struct neighbour *neighbour;
struct hh_cache *hh;
struct xfrm_state *xfrm;
int (*input)(struct sk_buff*);
int (*output)(struct sk_buff*);
#ifdef CONFIG_NET_CLS_ROUTE
__u32 tclassid;
#endif
struct dst_ops *ops;
struct rcu_head rcu_head;
char info[0];
};
dst_entry->__refcnt
"目的入口"的引用计数,创建成功后即设为1
dst_entry->__use
一个统计数值,该"目的入口"被使用一次(发送一个IP数据报),__use就加1
dst_entry->dev
该路由的输出网络设备接口
dst_entry->flags
标志位,其取值可以是DST_HOST, DST_NOXFRM, DST_NOPOLICY, DST_NOHASH, DST_BALANCED(用在路由有多路径的情况下)
dst_entry->lastuse
一个时间值,每次目的入口被用于发送IP数据报,就将该值设置为当前系统时间值。该值被用于几个地方,路由缓存表 rt_hash_table是一个很大的数组(依据系统的内存大小而定),每一项都是一个struct rtable的链表,当要往缓存表的某一个链表中插入一个新的struct rtable时,如果这个链表的长度已经超出ip_rt_gc_elasticity(值为8),则需要删掉一个当前使用价值最低的,已保持链表长度的平衡。函数rt_score()就是用于为每个struct rtable计算价值分数,分数是一个32位值,最高位表示非常有价值,当struct rtable的成员rt_flags上有标志RTCF_REDIRECTED或RTCF_NOTIFY,或者目的入口的超时时间未到时,置该位,次高位价值次之,余下的30位由lastuse决定,该目的入口距上次使用时间越长,价值越低。另外,用于在rt_may_expire函数中判断一个struct rtable是否超时。
dst_entry->expires
一个超时时间值,定时器rt_periodic_timer定期扫描路由缓存表rt_hash_table,如果发现expires值为0,或者小于当前系统时间值,并符合其它超时条件,则把该路由从缓存表中删除。
dst_entry->neighbour
为该路由绑定的邻居节点(与ARP相关)
dst_entry->hh
硬件头缓存,ARP解析得到的邻居的mac地址缓存在这里,再次发送IP数据报的时候,就不需要再到ARP缓存中去取硬件头。
dst_entry->input
dst_entry->output
input和output分别是该目的入口的输入和输出函数。
宏CONFIG_IP_MULTIPLE_TABLES表示路由策略,当定义了该宏,也即意味着内核配置了“路由策略”。产生的最大的不同就是内核可以使用多达256张FIB。其实,这256张FIB在内核中的表示是一个全局数组:
struct fib_table *myfib_tables[RT_TABLE_MAX+1];
而宏RT_TABLE_MAX定义如下:
enum rt_class_t
{
RT_TABLE_UNSPEC=0,
RT_TABLE_DEFAULT=253,
RT_TABLE_MAIN=254,
RT_TABLE_LOCAL=255,
__RT_TABLE_MAX
};
#define RT_TABLE_MAX (__RT_TABLE_MAX - 1)
我们可以看到,虽然这张表多达256项,但枚举类型rt_class_t给出的表示最常用的也就三项,在系统初始化时,由内核配置生成的路由表只有RT_TABLE_MAIN,RT_TABLE_LOCAL两张。
main表中存放的是路由类型为RTN_UNICAST的所有路由项,即网关或直接连接的路由。在myfib_add_ifaddr函数中是这样添加 main表项的:对于某个网络设备接口的一个IP地址,如果目的地址的网络号不是零网络(网络号与子网号全为零),并且它是primary地址,同时,它不是D类地址(网络号与子网号占32位)。最后一个条件是:它不是一个环回地址(device上有flag IFF_LOOPBACK)。那么,就添加为main表项,如果是环回地址,则添加为local表的一个表项。
在我们的系统中,有两个已开启的网络设备接口eth0和lo,eth0上配置的primary IP地址是172.16.48.2,所以,相应的,main表中就只有一项。为main表添加路由项的时候,该路由项的目的地址是子网内的所有主机(把主机号部分字节清零),而对应于lo,在local表中也有一项,其类型为RTN_LOCAL(注:前一篇文章中的local表的hash 8中的路由项表述有误,类型应该是RTN_LOCAL,而不是RTN_BORADCAST)。
而其它的路由项全部归入local表,主要是广播路由项和本地路由项。在我们的系统环境下,local表共有7项,每个网络设备接口占三项。分别是本地地址(源跟目的地址一致),子网广播地址(主机号全为1),子网广播地址(主机号为零)。再加上一个lo的RTN_LOCAL项。
现在我们再来看myfib_add_ifaddr函数的路由添加策略。对于一个传入的ip地址(结构struct in_ifaddr表示),如果它是secondary地址,首先要确保同一个网络设备接口上存在一个跟其同类型的primary地址(网络号与子网号完全一致),因为,路由项的信息中的源地址全是primary的,secondary地址其实没有实际使用,它不会在路由表中产生路由项。然后,向 local表添加一项目的地址是它本身的,类型为RTN_LOCAL的路由项;如果该ip地址结构中存在广播地址,并且不是受限广播地址 (255.255.255.255),那么向local表添加一个广播路由项;然后,对符合加入main表的条件进行判断,如果符合,除了加入main 表,最后,如果不是D类地址,还要加入两个广播地址(其实,已经跟前面有重叠,很多情况下不会实际触发加入的动作,只要记住,一个ip地址项对应最多有两个广播地址就可以了)。
FIB表
下面我们来看FIB的数据结构。一张FIB在内核中被表示为一个对象struct fib_table,之所以说它是一个对象,而不是一个结构,是因为它不仅仅是一组数据集合,它还包含了定义在该对象之上的方法,包括表项的插入,查找,删除,刷新等等。下面是其定义:
struct fib_table {
unsigned char tb_id;
unsigned tb_stamp;
int (*tb_lookup)(struct fib_table *tb,
const struct flowi *flp, struct fib_result *res);
int (*tb_insert)(struct fib_table *table, struct rtmsg *r,
struct kern_rta *rta, struct nlmsghdr *n,
struct netlink_skb_parms *req);
int (*tb_delete)(struct fib_table *table, struct rtmsg *r,
struct kern_rta *rta, struct nlmsghdr *n,
struct netlink_skb_parms *req);
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];
};
这些成员函数我们在分析代码时都会提供其完整的实现,现在重点关注其数据成员。tb_id表明该表的用途(RT_TABLE_LOCAL, RT_TABLE_MAIN等),同时也表明它在全局数组myfib_tables中的位置(RT_TABLE_LOCAL==255, RT_TABLE_MAIN==254)。tb_data是一个很重要的数据成员,它包含了所在FIB的全部路由信息,可能会有点令人费>,因为它的类型仅仅是一个unsigned char的数组而已,甚至更奇怪的是,它的长度是零,也就是说,根本不存在。看了下面的代码就可以明了:
struct fib_table *tb = kmalloc( sizeof(struct fib_table)
+ sizeof(struct fn_hash), GFP_KERNEL );
memset( tb->tb_data, 0, sizeof(struct fn_hash) );
所以,tb_data实际上是一个指向结构struct fn_hash的指针。下面是结构struct fn_hash的定义:
struct fn_hash{
struct fn_zone *fn_zones[33];
struct fn_zone *fn_zone_list;
};
在解释struct fn_hash之前,先看一下strut fn_zone:
struct fn_zone{
struct fn_zone *fz_next;
struct hlist_head *fz_hash;
int fz_nent;
int fz_divisor;
u32 fz_hashmask;
int fz_order;
u32 fz_mask;
};
struct hlist_head {
struct hlist_node *first;
};
这是一个区域,所有目的地址长度相同的路由项划入同一个区域,以链表的形式组织在fz_hash成员中,同时,fz_order记录目的地址长度, fz_mask为目的地址掩码(比如:fz_order为24,则fz_mask为ffffff)。比如,在我们的系统配置环境中,两个网络设备接口有共七个路由项。其中的6项,其目的地址长度为6,归入同一个zone中,放在fz_hash成员中。成员fz_nent表明该zone中的路由项的数量,为 6。fz_hash其实是一个哈项数组,共有fz_divisor项(初始为16),fz_hashmask为数组的掩码(初始为f)。路由项以目的IP 地址为主键,定位到数组的某一项。
对于ipv4来讲,目的地址不会超过32位,所以fz_order的取值范围是0-32。所以,struct fn_hash中fn_zones被定义为一个具有33项的数组,对应33个区域。在我们的配置系统中,fn_zones[32]和fn_zones [8]被用到了。同时,fn_zone_list把fn_zones中的已创建出来的zone按fz_order从大到小的顺序组织成一个链表。这些做法都是出于效率的考虑。
所以,fn_hash是一个组织路由域的数据结构,同一个域里的fn_zone通过fz_next组织成一个链表。
同一个域中,所有的路由项组织在哈希表fz_hash中,一个路由项由结构struct fib_node表示。下面是该结构的定义:
struct fib_node {
struct hlist_node fn_hash;
struct list_head fn_alias;
u32 fn_key;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
struct list_head {
struct list_head *next, *prev;
};
fn_hash用于在zone中组织链表,关于内核的一些基本数据结构,我们将专门进行分析,这里不再赘述。fn_key即目的地址IP, fn_alias指向一个结构体struct fib_alias的链表,下面是结构struct fib_alias的定义:
struct fib_alias {
struct list_head fa_list;
struct rcu_head rcu;
struct fib_info *fa_info;
u8 fa_tos;
u8 fa_type;
u8 fa_scope;
u8 fa_state;
};
fa_tos表示服务类型,一般为0,即一般服务;fa_type的值在我们的系统中为RTN_LOCAL, RTN_UNICAST和RTN_BORADCAST。fa_scope其实表示的是到目的地址的距离,对本地接收来说,就是 RT_SCOPE_HOST,对子网内广播和子网内其它地址来说,就是RT_SCOPE_LINK。fa_state只有在本地接收时,为 FA_S_ACCESSED,其它暂无定义。
fa_info作为struct fib_alias的成员,含有更为详细的路由项信息。下面是其定义:
struct fib_info {
struct hlist_node fib_hash;
struct hlist_node fib_lhash;
int fib_treeref;
atomic_t fib_clntref;
int fib_dead;
unsigned fib_flags;
int fib_protocol;
u32 fib_prefsrc;
u32 fib_priority;
u32 fib_metrics[RTAX_MAX];
#define fib_mtu fib_metrics[RTAX_MTU-1]
#define fib_window fib_metrics[RTAX_WINDOW-1]
#define fib_rtt fib_metrics[RTAX_RTT-1]
#define fib_advmss fib_metrics[RTAX_ADVMSS-1]
int fib_nhs;
#ifdef CONFIG_IP_ROUTE_MULTIPATH
int fib_power;
#endif
#ifdef CONFIG_IP_ROUTE_MULTIPATH_CACHED
u32 fib_mp_alg;
#endif
struct fib_nh fib_nh[0];
#define fib_dev fib_nh[0].nh_dev
};
fib_treeref表示本路由信息项在struct fib_table结构的整个树型结构中被引用的次数,而fib_clntref是另一个引用计数。fib_dead表示本项当前是否是活的。 fib_protocol表示该路由信息是通过什么途径建立起来的,其可能有取值有:
#define RTPROT_UNSPEC 0
#define RTPROT_REDIRECT 1 //该路由是由ICMP重定义安装的。
#define RTPROT_KERNEL 2 //该路由是由内核安装的。
#define RTPROT_BOOT 3 3 //该路由是在系统启动时安装的。
#define RTPROT_STATIC 4 //该路由是由管理员安装的。
除此之外,还有一些取值,不过是用于用户态的,我们当前在模块初始化过程中安装的路由都是RTPROT_KERNEL的。fib_prefsrc是我们的 local地址。最后,fib_nh是一个结构struct fib_nh的的数组,数组的大小由fib_nhs决定。该结构表示路由中的下一跳,下面是其定义:
struct fib_nh {
struct net_device *nh_dev;
struct hlist_node nh_hash;
struct fib_info *nh_parent;
unsigned nh_flags;
unsigned char nh_scope;
#ifdef CONFIG_IP_ROUTE_MULTIPATH
int nh_weight;
int nh_power;
#endif
#ifdef CONFIG_NET_CLS_ROUTE
__u32 nh_tclassid;
#endif
int nh_oif;
u32 nh_gw;
};
当要发关一个报文时,必定要查询发关接口,这个过程被Linux分为3个步骤,第一个步骤是查询路由cache,第二个步骤是查询FIB表,第三个步骤是将查询结果填入路由cache中以便将来查询。
路由规则
前面我们提到过路由策略的问题,策略性是指对于IP包的路由是以网络管理员根据需要定下的一些策略为主要依据进行路由的。例如我们可以有这样的策略:“所有来直自网A的包,选择X路径;其他选择Y路径”,或者是“所有TOS为A的包选择路径F;其他选者路径K”。
策略性路由机制与传统的路由算法相比主要是引入了多路由表以及规则的概念。关于多路由表,我们前面已经讲过,当内核配置了“路由策略”后,就可以使用多达 255张路由表(数组的第0项不能使用)。有三张路由表是系统内置的,即本地路由表,主路由表,和默认路由表。
在配置多路由表的情况下,查找路由时就存在了一个问题,即应该从哪个路由表中查找,如果找不到,接下来应该在哪个路由表中查找。对某个路由表应不应该进行查找等等。这一系例问题就引出了路由规则与规则之间的优先级的问题。
结构体struct fib_rule定义一条路由规则,而多个struct fib_rule构成一条链表,链表上的先后顺序表明了它们之间的优先级:
struct fib_rule
{
struct fib_rule *r_next;
atomic_t r_clntref;
u32 r_preference;
unsigned char r_table;
unsigned char r_action;
unsigned char r_dst_len;
unsigned char r_src_len;
u32 r_src;
u32 r_srcmask;
u32 r_dst;
u32 r_dstmask;
u32 r_srcmap;
u8 r_flags;
u8 r_tos;
#ifdef CONFIG_IP_ROUTE_FWMARK
u32 r_fwmark;
#endif
int r_ifindex;
#ifdef CONFIG_NET_CLS_ROUTE
__u32 r_tclassid;
#endif
char r_ifname[IFNAMSIZ];
int r_dead;
};
r_next指向下一条比本条优先级低的规则;r_table表明本条规则对应的路由表;r_action是本条规则的行为,比如为 RTN_UNREACHABLE,则表示符合本条规则的数据包都是目的地不可达的;r_src, s_srcmask, r_dst, r_dstmask定义了数据包的源和目地地址或网络,r_tos定义服务类型,r_ifindex定义输入网络设备接口的编号,再加上 r_fwmard,只有这些值都符合的数据包,才符合本条规则,可以进入本条规则指定的路由表进行路由查找,否则,不适用本规则,接着往下找低优先级的规则。
系统内置了三张路由表,所以,相应的,系统也内置了三条路由规则:
static struct fib_rule mydefault_rule = {
.r_clntref = ATOMIC_INIT(2),
.r_preference = 0x7FFF,
.r_table = RT_TABLE_DEFAULT,
.r_action = RTN_UNICAST,
};
static struct fib_rule mymain_rule = {
.r_next = &mydefault_rule,
.r_clntref = ATOMIC_INIT(2),
.r_preference = 0x7FFE,
.r_table = RT_TABLE_MAIN,
.r_action = RTN_UNICAST,
};
static struct fib_rule mylocal_rule = {
.r_next = &mymain_rule,
.r_clntref = ATOMIC_INIT(2),
.r_table = RT_TABLE_LOCAL,
.r_action = RTN_UNICAST,
};
r_preference是优先级,RT_TABLE_LOCAL优先级最高,为0,RT_TABLE_MAIN为0x7FFE, RT_TABLE_DEFAULT为0x7FFF。它们以链表的形式组织在一起,系统管理员可以通过系统命令操作向这个链表中再添加其它优先级的路由规则。三条内置规则对数据包的源和目的地址,以及服务类型都没有作任何特殊限制,所以任何一个数据包都可以按顺序查找这三个路由表,直到找到相应的路由为止。