四.主要路由流程分析
前面已经介绍过,IP层会在输入和输出两个时候去调用路由部分代码。输入路由过程更为复杂一些也更具代表性,所以我们下面主要分析一下IP包输入时的路由流程。
下图描述了这个流程:
当有数据到达网络设备的时候,会产生一个中断,中断处理函数会调用驱动层的net_rx函数,net_rx进而产生个软中断进入net_rx_action函数,进而如是发现这个数据帧是IP包的话,它就调用IP协议层的ip_rcv函数,它进而又调用ip_rcv_finish函数。在这个函数,它调用路由代码的IP接口函数ip_route_input进行路由。可以看到传递给路由代码的参数有5个:skb IP包缓冲区,iph->daddr IP包的目的地址,iph->saddr IP包源地址,iph->tos 服务类型,dev 输入的网络设备。当这个ip_route_input函数返回时,就意味着路由工作已经结束,如果返回值是0,那么就说明已经成功找到了路由。那么这个路由查询结果放在哪里呢?它就在skb->dst,它指向的就是查到的路由缓存中的一个结点。下边通过调用skb->dst->input(skb)就可以对这个IP进行处理了。这个input是路由缓存结点中的一个函数指针,如果这个路由项表示转发的,那么这个指针实际上指向的是ip_local_deliver,而如果是传送给本地的,那么指向的是ip_forward。ip_local_deliver会将这个IP包进一步传给上层协议层处理,ip_forward则会再将这个IP包从网络设备发送出去。
我们再来看一下路由的具体流程。
首先调用的是ip_route_input,它的任务主要是查路由缓存,如果找到了那么它给skb->dst赋值并返回,如是没找到,它会调用ip_route_input_slow去查询路由策略数据库。
下面是经过简化的代码和注释:
int ip_route_input(struct sk_buff *skb, u32 daddr, u32 saddr, u8 tos, struct net_device *dev)
{
int iif = dev->ifindex;
hash = rt_hash_code(daddr, saddr ^ (iif << 5), tos);
/* 遍历hash table */
for (rth = rt_hash_table[hash].chain; rth; rth = rth->u.rt_next ) {
/* 只有这五个量都匹配才算命中,要比较这么多量是因为在基于策略的路由中,有一个量不同就有可能选择不同的策略。 */
if ( rth->key.dst == daddr && rth->key.src == saddr &&
rth->key.iif == iif && rth->key.oif == 0 &&
rth->key.tos == tos ) {
rth->u.dst.lastuse = jiffies;
dst_hold(&rth->u.dst);
rth->u.dst.__use++;
/* 关键的一步,为dst为赋值 */
skb->dst = (struct dst_entry*)rth;
return 0;
}
}
/* 如果缓存查不到,那么调用这个函数 */
return ip_route_input_slow(skb, daddr, saddr, tos, dev);
}
ip_route_input_slow函数的主要任务是去调用路由策略数据库的查找函数fib_lookup进行查找,然后更新路由缓存。
因为这个函数很长,我们用下面的流程图来表示一些主要的流程:
当调用过fib_lookup后,函数会根据查找的结构进行不同的处理。一般情况是转发或者本地,这两种的情况都会先分配一个新的路由缓存结点,填充适当的值然后插入到缓存中;两者的不同主要在于,设置dst.input函数分别为ip_forward或ip_local_deliver,转发的情况还要绑定关于下一跳信息的neighbour(这个结构主要用来得到网段上邻居的物理地址)。除了转发或本地还有可能是其它情况,比如有错误,没查到,丢弃,NAT等。
fib_lookup函数是路由策略数据库的查询接口,它首先查找策略表,找到一条匹配的策略,然后再执行该策略所对应的动作,动作一般来说就是要查找对应的一张路由表,所以接下来会调用fn_hash_lookup函数进行处理。
下面是这个函数的简化后的代码和相关注释:
fib_lookup(const struct rt_key *key, struct fib_result *res)
{
/* 循环遍历策略表 */
for (r = fib_rules; r; r=r->r_next) {
/* 如果有一项不符,继续查找下一个 */
if ( ((saddr^r->r_src) & r->r_srcmask) ||
((daddr^r->r_dst) & r->r_dstmask) ||
(r->r_tos && r->r_tos != key->tos) ||
(r->r_ifindex && r->r_ifindex != key->iif) )
continue;
/* 判断策略的动作 */
switch (r->r_action) {
case RTN_UNICAST:
case RTN_NAT:
policy = r;
break;
default:
case RTN_BLACKHOLE:
read_unlock(&fib_rules_lock);
return -EINVAL;
}
/* 得到策略所对应的路由表 */
if ((tb = fib_get_table(r->r_table)) == NULL) continue;
/* 查找路由表 */
err = tb->tb_lookup(tb, key, res);
/* 返回0表示查找成功 */
if (err == 0) { res->r = policy; return 0; }
/* 如果有错误,则返回错误号,如果是-EAGAIN或正数则查下一策略 */
if (err < 0 && err != -EAGAIN) return err;
}
return -ENETUNREACH;
}
fn_hash_lookup函数的主要功能即是对路由表的查找。如下:
int fn_hash_lookup(struct fib_table *tb, const struct rt_key *key, struct fib_result *res)
{
/* 从大到小遍历区域 */
for (fz = t->fn_zone_list; fz; fz = fz->fz_next) {
fn_key_t k = fz_key(key->dst, fz);
/* 遍历一区域内的hash table */
for (f = fz_chain(k, fz); f; f = f->fn_next) {
if (!fn_key_eq(k, f->fn_key)) {
if (fn_key_leq(k, f->fn_key)) break;
else continue;
}
/* 找到匹配的路由项 */
if (f->fn_state&FN_S_ZOMBIE) continue;
/* 进行语义上的检查和设置
如果是单播,把fib_info赋给res
如果是其它,相应作一些处理 */
err = fib_semantic_match(f->fn_type, FIB_INFO(f), key, res);
/* 没有错误的情况 */
if (err == 0) {
res->type = f->fn_type;
res->prefixlen = fz->fz_order;
goto out;
}
if (err < 0) goto out;
}
}
/* 如果没有找到匹配的路由项,返回正值表示上层函数处理下一个策略 */
err = 1;
out:
return err;
}
五.一些细节问题
1. 关于路由中的错误处理
这里的错误是指找不到路由项,还包括丢弃、禁止、不可到达等情况。这些情况产生的原因可能是因为路由表中找不到相应的项或是用户设置了相应的策略或路由项对特定IP包进行丢弃等处理。
在这种情况下fib_lookup会返回一个错误值,如-ENETUNREACH,-BLACKHOLE等。接着在ip_route_input_slow中
if ((err = fib_lookup(&key, &res)) != 0) {
if (!IN_DEV_FORWARD(in_dev))
goto e_inval;
goto no_route;
}
即会跳到no_route处:
no_route:
rt_cache_stat[smp_processor_id()].in_no_route++;
spec_dst = inet_select_addr(dev, 0, RT_SCOPE_UNIVERSE);
goto local_input;
它把res.type标记成RTN_UNREACHABLE然后跳到本地包情况的处理代码,先是更新路由缓存,然后遇到如下代码:
if (res.type == RTN_UNREACHABLE) {
rth->u.dst.input= ip_error;
rth->u.dst.error= -err;
rth->rt_flags &= ~RTCF_LOCAL;
}
rth->rt_type = res.type;
goto intern;
即判断如果res.type是RTN_UNREACHABLE标记,那么给函数指针dst.input赋为ip_err,将dst.error赋为-err。然后插入到缓存。
最后IP层调用的skb->dst->input实际上就是ip_err(),进行处理错误,如发送ICMP包。
2. 策略性路由NAT功能的实现
linux内核的路由机制是可以实现静态NAT的(即是IP影射是静态不变的)。其中,源地址的SNAT是通过动作为NAT的策略来完成的,目的地址的DNAT是通过类型为NAT的路由项来完成的。
在ip_route_input_slow中,执行完fib_lookup后会有如下代码:
u32 src_map = saddr;
src_map = fib_rules_policy(saddr, &res, &flags);
key.dst = fib_rules_map_destination(daddr, &res);
fib_res_put(&res);
free_res = 0;
if (fib_lookup(&key, &res))
goto e_inval;
free_res = 1;
goto e_inval;
}
首先,执行fib_rule_policy函数,将判断如果刚才查策略表时查到的是动作为NAT的策略,那么将策略对应的影射源地址赋给src_map,最后会将这个src_map赋给key.src。这就记录了SNAT的地址。
然后,if (res.type == RTN_NAT) 判断查路由表项的类型如果是NAT,那么将路由表项中的影射目的地址赋给key.dst,这就记录了DNAT的地址,然后用这个地址再调用fib_lookup函数查一遍影射后的目的地址的路由。
在下面更新缓存的时候有如下代码:
rth->rt_src_map = key.src;
rth->rt_dst_map = key.dst;
这就把影射后的地址入到了缓存结点中。
进而在执行ip_forward函数进行转发时,有如下代码:
if (rt->rt_flags & RTCF_NAT) {
if (ip_do_nat(skb)) {
kfree_skb(skb);
return NET_RX_BAD;
}
}
即如果是NAT,执行ip_do_nat函数做NAT,实际上就是根据skb-dst->rt_src_map和skb-dst->rt_dst_map做地址替换。
六.总结
通过对kernel路由代码的分析,使我加深了对操作系统特别是网络部分的理解。通过分析源码中的具体数据结构和算法,对“程序=数据结构+算法”这条简单的公式有了更加深刻的理解。
声明:本文转自:http://blog.csdn.net/bin323/article/details/642192