1写在前面
路由器是从一个物理网向另一个物理网发送数据包的装置,路由器通常被称为网关,它承但着分发数据包的任务。
查看主机当前的路由表。
$ netstat -rn
$ route
查看主机的网络信息
$ netstat
2 路由表
2.1 路由表的构成
一个路由表的每一项至少包含两个信息:
路由表项:|目的主机IP地址 | 下一个路由的IP地址 | ....|
IP数据报文头中的“目的IP地址”和路由表中的每一项中的“目的主机IP地址”进行匹配,查找最匹配的表项,此表项中的“下一个路由的IP地址”指定了IP数据报的路由出口。
2.2 路由表查表原则
路由查表原则为:
● 首先,路由表会去搜索一个“目的IP地址”字段与数据报文中目的IP地址完全相同的条目。这就意味着IP地址的主机ID与网络ID完全的匹配。如果找到,则数据包被发送到相应接口或中间路由器。
● 如果没有找到一个完全的匹配IP,那么就接着搜索相匹配的网络ID。如果找到,那么该数据报文会被转发到指定的路由器。所以我们看到,这个网络上的所有主机都通过这个路由表中的单个(这个)条目来管理。
● 如果上述两个条件都不匹配,那么该数据报文将转发到一个“默认路由器”。
● 如果上述步骤失败,即没有默认路由器,那么该数据报文最终无法被转发。任何无法投递的数据报文都将产生一个ICMP主机不可达或ICMP网络不可达的错误,并将此错误返回给生成此数据报文的应用程序。
有了更精确的主机条目为什么还需要网络相关的条目?在路由表中包含与网络相关的路由条目是一个很大的优点。其优点在于,拥有一个与完整网络相关的条目,能够避免包含此网络中所有单独的主机条目(这个数据量非常巨大)。这使得路由表的大小降到一个可收受的数量级,这样就非常好。
(来源网络)
3 Linux 路由检
3.1过程概述
1)查路由缓存hash表 rt_hash_table(最近使用的路由表项)
它记录着最近使用过的路由转发表项,每个表项是根据saddr,daddr,tos计算而得出hash值进行hash链的归类存放,方便对缓存中rt_hash_table的快速检索。
2)查路由表FIB
在路由缓存查找失败后,到路由表中查找路由项。
这个过程和上述路由查表原则类似,具体实现过程为:Linux的路由表的数据结构是struct fib_table{},记录IP转发信息的索引表, 转发表的每一记录(节点)描述了具有某一类目的地址的IP包应该使用哪一输出设备发给哪一目的主机. 转发表记录按网络区进行分类, 每一网络区描述了在特定网络地址位长下具有不同网络号的目的地址的转发信息. 第0区的网络地址位长为0, 与所有的IP地址都匹配, 用来描述缺省网关, 第32区的网络地址位长为32, 用来与完整的IP地址匹配. 在建立网络区时, 它们按网络地址位长从大到小的顺序排列, 在搜索IP地址时, 先从全主机地址的第32区开始匹配, 最后第0区与所有的地址都匹配, 产生缺省网关.
(reference http://blog.chinaunix.net/uid-488742-id-2113729.html)
3.2源码分析
3.2.1 路由入口数据结构
描述IP数据报在路由过程中的下一个入口:(下一跳要往路由的哪个口出去)
struct dst_entry
{
struct dst_entry *next;
atomic_t __refcnt; //"目的入口"的引用计数,创建成功后即设为1/* client references */
int __use; //一个统计数值,该"目的入口"被使用一次(发送一个IP数据报),__use就加1
struct net_device *dev; //该路由的输出网络设备接口
int obsolete;
int flags; //标志位,其取值可以是DST_HOST, DST_NOXFRM, DST_NOPOLICY, DST_NOHASH, DST_BALANCED(用在路由有多路径的情况下)
#define DST_HOST 1
unsigned long lastuse; //最近发数据报的时间,用于维护路由缓存表:删除最长时间未是使用的项目
unsigned long expires;
unsigned mxlock;
unsigned pmtu;
unsigned window;
unsigned rtt;
unsigned rttvar;
unsigned ssthresh;
unsigned cwnd;
unsigned advmss;
unsigned reordering;
unsigned long rate_last; /* rate limiting for ICMP */
unsigned long rate_tokens;
int error;
struct neighbour *neighbour; //为该路由绑定的邻居节点(与ARP相关)
struct hh_cache *hh; //ARP:mac-IP地址缓存|硬件头缓存,ARP解析得到的邻居的mac地址缓存在这里,再次发送IP数据报的时候,就不需要再到ARP缓存中去取硬件头。
//input和output分别是该目的入口的输入和输出函数。
int (*input)(struct sk_buff*);
int (*output)(struct sk_buff*);
#ifdef CONFIG_NET_CLS_ROUTE
__u32 tclassid;
#endif
struct dst_ops *ops;
char info[0];
};
3.2.2路由hash缓存表项
struct rtable
{
union{
struct dst_entry dst;
struct rtable *rt_next;
}u;
struct in_device *idev;
unsigned rt_flags;
__u16 rt_type;
__u16 rt_multipath_alg;
__u32 rt_dst; //路由的目的地址
__u32 rt_src; //是路由的源地址
int rt_iif; //路由的输入设备接口的索引号
__u32 rt_gateway; //路由网关的IP地址
struct flowi fl;
__u32 rt_spec_dst;
struct inet_peer *peer;
};
Rtable描述了路由hash缓存中的每个路由表项,总体可分为三部分
1)除Union外的其他成员
路由表项主要路由查找信息,记录着前一次该表项路由了IP包的源地址,目的地址,转发标记等信息。如果IP数据报中的信息和这些信息匹配,那么说明在路由缓冲找找到了路由表项了。
2)联合体中的u.dst
指明了该路由表项的出口 entry。如果确定该路由表项是IP包的路由出口节点,那么就可以将u.dst地址赋值给skb->dst,这样就确定了IP数据包的下一个路由入口。
3)联合体中的u.rt_next
用于路由表链:它占用了entry中的第一个成员struct dst_entry *next; 实现了内存的union共享
(详见:http://blog.csdn.net/ordeder/article/details/10054151)
3.2.3路由hash缓冲表
struct rt_hash_bucket {
struct rtable *chain;
rwlock_t lock;
} __attribute__((__aligned__(8)));
static struct rt_hash_bucket *rt_hash_table; //路由hash缓存表
static unsigned rt_hash_mask; //有这么条hash链
3.2.4 路由缓冲表初始化
1)计算申请连续的内存页数
2)调整rt_hash_mask到达2^n的上限
3)申请内存页,rt_hash_table指向它
4)初始化rt_hash_table[]
void __init ip_rt_init(void)
{
int i, order, goal;
#ifdef CONFIG_NET_CLS_ROUTE
for (order=0;
(PAGE_SIZE<<order) < 256*sizeof(ip_rt_acct)*NR_CPUS; order++)
/* NOTHING */;
ip_rt_acct = (struct ip_rt_acct *)__get_free_pages(GFP_KERNEL, order);
if (!ip_rt_acct)
panic("IP: failed to allocate ip_rt_acct\n");
memset(ip_rt_acct, 0, PAGE_SIZE<<order);
#endif
ipv4_dst_ops.kmem_cachep = kmem_cache_create("ip_dst_cache",
sizeof(struct rtable),
0, SLAB_HWCACHE_ALIGN,
NULL, NULL);
if (!ipv4_dst_ops.kmem_cachep)
panic("IP: failed to allocate ip_dst_cache\n");
goal = num_physpages >> (26 - PAGE_SHIFT); //计算要申请连续的页框:goal
for (order = 0; (1UL << order) < goal; order++)
/* NOTHING */;
do {
rt_hash_mask = (1UL << order) * PAGE_SIZE /
sizeof(struct rt_hash_bucket); //上限2^order 连续的页框能装下几个rt_hash_bucket
while (rt_hash_mask & (rt_hash_mask-1)) //调整 到2的n次方
rt_hash_mask--;
rt_hash_table = (struct rt_hash_bucket *)
__get_free_pages(GFP_ATOMIC, order); //请求2^order个连续的页框,每个rt_hash_table 占用一个页框
} while (rt_hash_table == NULL && --order > 0);
...
rt_hash_mask--;
for (i = 0; i <= rt_hash_mask; i++) { //初始化rt_hash_table[]数组
rt_hash_table[i].lock = RW_LOCK_UNLOCKED;
rt_hash_table[i].chain = NULL;
}
ipv4_dst_ops.gc_thresh = (rt_hash_mask+1);
ip_rt_max_size = (rt_hash_mask+1)*16;
devinet_init();
ip_fib_init();
rt_flush_timer.function = rt_run_flush;
rt_periodic_timer.function = rt_check_expire;
/* All the timers, started at system startup tend
to synchronize. Perturb it a bit.
*/
rt_periodic_timer.expires = jiffies + net_random()%ip_rt_gc_interval
+ ip_rt_gc_interval;
add_timer(&rt_periodic_timer);
proc_net_create ("rt_cache", 0, rt_cache_get_info);
#ifdef CONFIG_NET_CLS_ROUTE
create_proc_read_entry("net/rt_acct", 0, 0, ip_rt_acct_read, NULL);
#endif
}
3.2.5路由查找
1)查缓冲路由rt_hash_table
2)判断是否为多播,多播处理: ip_route_input_mc()
3)进入FIB路由表中查路由:ip_route_input_slow
int ip_route_input(struct sk_buff *skb, u32 daddr, u32 saddr,
u8 tos, struct net_device *dev)
{
struct rtable * rth;
unsigned hash;
int iif = dev->ifindex;
tos &= IPTOS_RT_MASK;
hash = rt_hash_code(daddr, saddr^(iif<<5), tos);
read_lock(&rt_hash_table[hash].lock);
for (rth=rt_hash_table[hash].chain; rth; rth=rth->u.rt_next) { //在路由缓存hash表中查找
if (rth->key.dst == daddr && //源地址
rth->key.src == saddr && //目标地址
rth->key.iif == iif && //输入设备接口
rth->key.oif == 0 &&
#ifdef CONFIG_IP_ROUTE_FWMARK
rth->key.fwmark == skb->nfmark && //转发标记
#endif
rth->key.tos == tos) { //服务类型
rth->u.dst.lastuse = jiffies; //bingo! 更新表项最近使用的时间戳
dst_hold(&rth->u.dst); //该dst_entry->__refcnt++ 出口引用次数
rth->u.dst.__use++; //缓存表项使用次数++
read_unlock(&rt_hash_table[hash].lock);
skb->dst = (struct dst_entry*)rth; //在缓冲表中找到了对应路由表项,将其作为路由出口
return 0;
}
}
read_unlock(&rt_hash_table[hash].lock);
/* Multicast recognition logic is moved from route cache to here.
多播的处理从路由缓存中移到这儿,
The problem was that too many Ethernet cards have broken/missing
hardware multicast filters :-( As result the host on multicasting
network acquires a lot of useless route cache entries, sort of
以太网有太多的多播数据报,导致路由
SDR messages from all the world. Now we try to get rid of them.
缓存的表项被大量占用
Really, provided software IP multicast filter is organized
reasonably (at least, hashed), it does not result in a slowdown
comparing with route cache reject entries.
Note, that multicast routers are not affected, because
route cache entry is created eventually.
*/
if (MULTICAST(daddr)) { //多播数据报的路由处理
struct in_device *in_dev;
read_lock(&inetdev_lock);
if ((in_dev = __in_dev_get(dev)) != NULL) {
int our = ip_check_mc(in_dev, daddr);
if (our
#ifdef CONFIG_IP_MROUTE
|| (!LOCAL_MCAST(daddr) && IN_DEV_MFORWARD(in_dev))
#endif
) {
read_unlock(&inetdev_lock);
return ip_route_input_mc(skb, daddr, saddr, tos, dev, our);
}
}
read_unlock(&inetdev_lock);
return -EINVAL;
}
//到FIB(路由表) 中查找路由,前面路由缓存没找到:-(
return ip_route_input_slow(skb, daddr, saddr, tos, dev);
}
4写在后面
本文描述了路由表及其查表过程,并以Linux内核为例进行分析。主要分析的是路由入口、路由缓冲表。以及分析了路由缓冲表的初始化和查表过程ip_route_input()。
FIB查表过程 ip_route_input_slow()是路由的核心部分,由于篇幅原因,以后再做分析。