选路软件在理论上可以划分为两组。其中一组由那些用来为数据报选择正确路由的过程组成,另一组由增加、改变或删除路由的过程组成。
因为网关必须为自己所处理的每个数据报判定路由,所以路由查找程序代码决定了网关的整体性能。因此,通常将查找程序代码优化,以期达到最高速度。
*路由的插入、改变或者删除的速率通常比为数据报选择路由的速率要低得多。计算新路由的程序需要与其他设备通信以建立其可达性。它们在重定向之前花任意长的时间。因此,路由更新过程不需要像查找操作那样被优化。其基本观点是:
应当选择能使路由花费最小的IP数据结构和算法,维护路由的花费并不那么重要
虽然在早期的TCP/IP软件中,选路表的查找经常采用线性查找,但现在大多数系统利用一张散列表,使之能够快速查找任意大小的选路表
如上,保存路由的主要数据结构是数组。数组中的每一个元素对应一个散列表元,并包含一个指针,指向被装入这个散列表中的通往目的站的路由记录链表。表中的每一个记录包含一个IP目的地址、子网掩码、下一跳地址、用于向下一跳地址发送数据的网络接口,以及其他在路由管理中使用的信息。
route 结构定义了链表中一个节点的内容,并包含一个可能的目的站的选路信息
剩下的几个字段供IP软件使用
/* route.h - RTFREE */
/* 路由表条目 : */
struct route {
IPaddr rt_net; /* 此路由的网络地址 */
IPaddr rt_mask; /* 此路由的掩码 */
IPaddr rt_gw; /* 下一跳 */
u_short rt_metric; /* 距离度量 */
u_short rt_ifnum; /* 接口号 */
short rt_key; /* 排序键 */
short rt_ttl; /* 生存时间(秒) */
struct route *rt_next; /* 此哈希值的下一个条目 */
/* stats */
int rt_refcnt; /* 当前引用计数 */
int rt_usecnt; /* 迄今为止的总使用次数 */
};
/* 路由表全局数据 : */
struct rtinfo {
struct route *ri_default;
int ri_bpool;
Bool ri_valid;
int ri_mutex;
};
#define RT_DEFAULT ip_anyaddr /* 默认网络 */
#define RT_LOOPBACK ip_loopback /* 环回网 */
#define RT_TSIZE 512 /* these are pointers; it's cheap */
#define RT_INF 999 /* 此路由不超时 */
#define RTM_INF 16 /* 无限度量 */
/* rtget() 的第二个参数... */
#define RTF_REMOTE 0 /* 流量来自远程主机 */
#define RTF_LOCAL 1 /* 流量是本地产生的 */
#define RT_BPSIZE 100 /* 最大路由数 */
/* RTFREE - 删除路由引用(假设 ri_mutex HELD) */
#define RTFREE(prt) \
if (--prt->rt_refcnt <= 0) { \
freebuf(prt); \
}
extern struct rtinfo Route;
extern struct route *rttable[];
int rtadd(IPaddr, IPaddr, IPaddr, unsigned, unsigned, unsigned);
int rtdel(IPaddr, IPaddr), rthash(IPaddr), rtfree(struct route *);
struct route *rtget(IPaddr, Bool);
void rtinit(void);
void ipredirect(struct ep *, unsigned, struct route *);
void ipdbc(unsigned, struct ep *, struct route *);
除router外,还定义了选路表rttable,如上图所示,rttable是指向route结构的指针数组
除选路表之外,IP还需要其他几个数据项,它们保存在全局结构rtinfo中。比如,
选路表中的信息有几个来源。通常,当系统启动时,初始化程序从部分存储器中获得一组初始路由,并将它们安装到选路表中。在执行过程中,传入的报文会导致ICMP或者选路协议软件改变现有的路由或者建立新路由。最后,网络管理员也可以增加路由或者重定向
选路表项的易失性与它的来源有关。比如,初始路由被认为是简单估计的,只要有其他地方发来的路由信息,它就应该被替换。然而,网络管理员必须能够推翻任何路由并建立不可变的永久性路由,以使它们能够排除网络中的路由问题,而无需路由协议的干涉。
为了适应路由的灵活性,每个选路表项中的rt_ttl(寿命)字段指出此表项还能保持多少时间。当rt_ttl达到0-时,路由失效并被丢弃。由选路协议建立的路由,其寿命根据协议规则计算得到,而管理员能够建立寿命无限长的路由,以保证它们不会被删除。
一些实用过程提供了选择路由时用到的功能。过程netnum利用地址类别确定在地址字段中哪些八位组包含的是网络部分,哪些八位组是主机部分,从而提取出IP地址中的网络部分。它返回给出的地址,但这时其主机地址字节为全0
/* netnum.c - netnum */
#include
#include
#include
/*------------------------------------------------------------------------
* netnum - compute the network portion of a given IP address
*------------------------------------------------------------------------
*/
IPaddr netnum(IPaddr ipa)
{
IPaddr mask = ~0;
if (IP_CLASSA(ipa)) mask = hl2net(0xff000000);
if (IP_CLASSB(ipa)) mask = hl2net(0xffff0000);
if (IP_CLASSC(ipa)) mask = hl2net(0xffffff00);
return ipa & mask;
}
在为数据报选择路由时,IP使用过程netmatch,将目的(主机)地址与选路表项中的地址进行比较,选路表中包含了子网掩码和给定网络的IP地址。netmatch使用子网掩码来屏蔽目的地址中的主机位,并将结果与表项中的网络地址相比较,如果1它们相符,netmatch返回TRUE,否则返回FALSE。
广播是一种特殊情况,因为要采取何种动作取决于数据报的来源。来自网络接口的广播数据报必须经由伪网络接口发送给本地机器,而本地生成的广播数据报必须被发送到适当的网络接口。为了区分这两种情况,软件使用了一个特定主机路由(全“1”的掩码)为来自网络的广播数据报选择路由,并用特定网络路由(掩码仅屏蔽网络部分)为向外发送的广播选择路由。这样netmatch明确的判定一个广播数据报的来源,并使用IP原地址来决定此广播是否与给定路由相符。
/* netmatch.c - netmatch */
#include
#include
#include
/*------------------------------------------------------------------------
* netmatch - Is "dst" on "net"?
*------------------------------------------------------------------------
*/
Bool netmatch(IPaddr dst, IPaddr net, IPaddr mask, Bool islocal)
{
if ((dst & mask) != (net & mask))
return FALSE;
/*
* local srcs should only match unicast addresses (host routes)
*/
if (islocal)
if (isbrc(dst) || IP_CLASSD(dst))
return mask != ip_maskall;
return TRUE;
}
要为一个数据报选择路由,IP必须查看它是否认识该数据报目的地址的有效子网掩码。要做到这一点,IP调用过程netmask
/* netmask.c - netmask */
#include
#include
#include
IPaddr netnum(IPaddr);
/*------------------------------------------------------------------------
* netmask - set the default mask for the given net
*------------------------------------------------------------------------
*/
IPaddr netmask(IPaddr net)
{
IPaddr netpart;
int i;
if (net == 0)
return net;
/* check for net match (for subnets) */
netpart = netnum(net);
for (i=0; i<Net.nif; ++i)
if (nif[i].ni_svalid && nif[i].ni_ivalid &&
nif[i].ni_net == netpart)
return nif[i].ni_mask;
if (IP_CLASSA(net)) return hl2net(0xff000000);
if (IP_CLASSB(net)) return hl2net(0xffff0000);
if (IP_CLASSC(net)) return hl2net(0xffffff00);
return ~0;
}
netmask把IP地址当中参数,然后检查几种情况。根据约定,如果目的地址全为“0”,则意味着使用默认路由,因此netmask返回全“0”的子网掩码。对其他类型的目的站,netmask调用netnum从目的地址提取出网络部分1,然后检测每个与本机直接相连接的网络,如果有任何与人地直接连接的网络地址与目的地址的网络部分相符,netmask从和此网络接口结构中提取出子网掩码,然后将其返回调用者。最后,如果IP找不到有关这个目的地址的子网掩码的信息,它就根据地址类别是A、B、还是C来设置屏蔽地址网络部分的子网掩码。
选路函数调用使用过程rthash计算目的地网络地址的散列值
/* rthash.c - rthash */
#include
#include
#include
/*------------------------------------------------------------------------
* rthash - compute the hash for "net"
*------------------------------------------------------------------------
*/
int rthash(IPaddr net)
{
int bc = IP_ALEN; /* # bytes to count */
unsigned int hv = 0; /* hash value */
if (IP_CLASSA(net)) bc = 1;
else if (IP_CLASSB(net)) bc = 2;
else if (IP_CLASSC(net)) bc = 3;
else if (IP_CLASSD(net))
return ((net>>24) & 0xf0) % RT_TSIZE;
while (bc--)
hv += ((char *)&net)[bc] & 0xff;
return hv % RT_TSIZE;
}
散列函数是一个简单有效的函数。对于A、B或者C类地址,rthash将网络地址的每个八位组相加,所得的和除以散列表长度,最后返回余数。对于D类地址,rthash将网络地址的前4为比特数乘以16,所得的值除以散列表长度,最后返回余数。
给定一个目的地址,过程rtget在选路表中查找,并返回一个指针,指向找到的路由的表现
/* rtget.c - rtget */
#include
#include
#include
/*------------------------------------------------------------------------
* rtget - get the route for a given IP destination
*------------------------------------------------------------------------
*/
struct route * rtget(IPaddr dest, Bool local)
{
struct route *prt;
int hv;
if (!Route.ri_valid) // 全局变量Route.ri_valid指出选路表是否已经被初始化了
rtinit(); // 路由表还没有被初始化, 调用rtinit()
wait(Route.ri_mutex);
hv = rthash(dest);
for (prt=rttable[hv]; prt; prt=prt->rt_next) {
if (prt->rt_ttl <= 0)
continue; /* route has expired */
if (netmatch(dest, prt->rt_net, prt->rt_mask, local))
if (prt->rt_metric < RTM_INF)
break;
}
if (prt == 0)
prt = Route.ri_default; /* may be NULL too... */
if (prt != 0 && prt->rt_metric >= RTM_INF)
prt = 0;
if (prt) {
prt->rt_refcnt++;
prt->rt_usecnt++;
}
signal(Route.ri_mutex);
return prt;
}
全局变量Route.ri_valid指出选路表是否已经被初始化了,如果没有则调用rtinit初始化选路表。一旦选路表以及相关的数据结构已经被初始化,rtget等待互斥信号量,以保证任何时间只有一个过程有权访问选路表。然后它计算目的地址值的散列值,以此作为表内查找的索引,并在选路表表项指向的链表内查找。
对于每一个路由表项,rtget调用netmatch,检查其入口参数给出的目的地址是否能与表项中的地址匹配。
过程rtinit初始化选路表和默认路由,生成互斥信号量,为路由链表中的节点分配存储区并将存储区链接成一个空闲列表:
/* rtinit.c - rtinit */
#include
#include
#include
#include
struct rtinfo Route;
struct route *rttable[RT_TSIZE];
/*------------------------------------------------------------------------
* rtinit - initialize the routing table
*------------------------------------------------------------------------
*/
void rtinit(void)
{
int i;
for (i=0; i<RT_TSIZE; ++i)
rttable[i] = 0;
Route.ri_bpool = mkpool(sizeof(struct route), RT_BPSIZE);
Route.ri_valid = TRUE;
Route.ri_mutex = screate(1);
Route.ri_default = NULL;
}
系统定期发起清理选路表的行动,它递减选路表中所有表项的寿命字段值,如果某个寿命字段超时,则丢弃对应的路由。过程rttimer用来实现这项定期性的表项更新任务
/* rttimer.c - rttimer */
#include
#include
#include
#include
extern Bool dorip; /* TRUE if we're running RIP output */
extern int rippid; /* RIP output pid, if running */
/*------------------------------------------------------------------------
* rttimer - update ttls and delete expired routes
*------------------------------------------------------------------------
*/
void rttimer(unsigned int delta)
{
struct route *prt, *prev;
Bool ripnotify;
int i;
if (!Route.ri_valid)
return;
wait(Route.ri_mutex);
ripnotify = FALSE;
for (i=0; i<RT_TSIZE; ++i) {
if (rttable[i] == 0)
continue;
for (prev = NULL, prt = rttable[i]; prt != NULL;) {
if (prt->rt_ttl != RT_INF)
prt->rt_ttl -= delta;
if (prt->rt_ttl <= 0) {
#ifdef RIP
if (dorip && prt->rt_metric < RTM_INF) {
prt->rt_metric = RTM_INF;
prt->rt_ttl = RIPZTIME;
ripnotify = TRUE;
continue;
}
#endif /* RIP */
if (prev) {
prev->rt_next = prt->rt_next;
RTFREE(prt);
prt = prev->rt_next;
} else {
rttable[i] = prt->rt_next;
RTFREE(prt);
prt = rttable[i];
}
continue;
}
prev = prt;
prt = prt->rt_next;
}
}
prt = Route.ri_default;
if (prt && (prt->rt_ttl<RT_INF) && (prt->rt_ttl -= delta) <= 0) {
#ifdef RIP
if (dorip && prt->rt_metric < RTM_INF) {
prt->rt_metric = RTM_INF;
prt->rt_ttl = RIPZTIME;
} else
#endif /* RIP */
{
RTFREE(Route.ri_default);
Route.ri_default = 0;
}
}
signal(Route.ri_mutex);
#ifdef RIP
if (dorip && ripnotify)
send(rippid, 0); /* send anything but TIMEOUT */
#endif /* RIP */
return;
}
定时进程大约每秒调用一次rttimer,利用入口参数delta,提供上次调用与这次调用之间的间隔时间。在等待互斥信号量之后,rttimer循环遍历选路表。对每个表项来说,它遍历并查看各个路由链表。对正常的路由,rttimer递减寿命字段计数值,如果计数值为0,从链表中删除该节点。然而,如果网关运行RIP,则rttimer将超时路由标记为费用无穷大,这样它就无法将其作为数据报的有效路由,同时又使得这些超时路由在表中短暂的保留一段时间。最后,rttimer递减默认路由中的寿命计数值。
网络管理软件和选路信息协议都可调用那些用来增加、删除或重定向的函数。比如,过程rtadd向表中曾一个新路由
/* rtadd.c - rtadd */
#include
#include
#include
#include
struct route *rtnew(IPaddr, IPaddr, IPaddr, unsigned,unsigned,unsigned);
void rtinit(void);
int rthash(IPaddr);
/*------------------------------------------------------------------------
* rtadd - add a route to the routing table
*------------------------------------------------------------------------
*/
int rtadd(IPaddr net, IPaddr mask, IPaddr gw, unsigned metric,
unsigned intf, unsigned ttl)
{
struct route *prt, *srt, *prev;
Bool isdup;
int hv, i;
if (!Route.ri_valid)
rtinit();
prt = rtnew(net, mask, gw, metric, intf, ttl); // rtnew分配一个新节点
if (prt == (struct route *)SYSERR)
return SYSERR;
/* compute the queue sort key for this route */
prt->rt_key = 0;
for (i=0; i<32; ++i)
prt->rt_key += (mask >> i) & 1;
wait(Route.ri_mutex);
/* special case for default routes */
if (net == RT_DEFAULT) {
if (Route.ri_default)
RTFREE(Route.ri_default);
Route.ri_default = prt;
signal(Route.ri_mutex);
return OK;
}
prev = NULL;
hv = rthash(net);
isdup = FALSE;
for (srt=rttable[hv]; srt; srt = srt->rt_next) {
if (prt->rt_key > srt->rt_key)
break;
if (srt->rt_net == prt->rt_net &&
srt->rt_mask == prt->rt_mask) {
isdup = TRUE;
break;
}
prev = srt;
}
if (isdup) {
struct route *tmprt;
if (srt->rt_gw == prt->rt_gw) {
/* just update the existing route */
#ifdef RIP
if (dorip) {
srt->rt_ttl = ttl;
if (srt->rt_metric != metric) {
if (metric == RTM_INF)
srt->rt_ttl = RIPZTIME;
send(rippid, 0);
}
}
#endif /* RIP */
srt->rt_metric = metric;
RTFREE(prt);
signal(Route.ri_mutex);
return OK;
}
/* else, someone else has a route there... */
if (srt->rt_metric <= prt->rt_metric) {
/* no better off to change; drop the new one */
RTFREE(prt);
signal(Route.ri_mutex);
return OK;
}
#ifdef RIP
else if (dorip)
send(rippid, 0);
#endif /* RIP */
tmprt = srt;
srt = srt->rt_next;
RTFREE(tmprt);
}
#ifdef RIP
else if (dorip)
send(rippid, 0);
#endif /* RIP */
prt->rt_next = srt;
if (prev)
prev->rt_next = prt;
else
rttable[hv] = prt;
signal(Route.ri_mutex);
return OK;
}
rtadd调用过程rtnew来分配一个新节点,并初始化它的各个字段。然后将默认路由当中特殊情况对待。对于非默认路由,tradd利用rthash计算新路由在路由表中的索引值,并遍历该表象中的路由链表。一旦它在链表中找到新路由应该被插入的地方,就查看链表是否包含一个具有相同目的站的已有路由。如果有,rtadd比较旧路由和新路由的度量,是否新路由更好。如果不是,就丢弃新路由。最后,rtadd或者在链表中插入一个新节点,或者将信息复制到具有同样地址的已有节点中。
过程rtnew分配并初始化一个新路由表项。它调用getbuf来为新节点分配存储器,然后填写首部信息
/* rtnew.c - rtnew */
#include
#include
#include
/*------------------------------------------------------------------------
* rtnew - create a route structure
*------------------------------------------------------------------------
*/
struct route * rtnew(IPaddr net, IPaddr mask, IPaddr gw, unsigned metric,
unsigned ifnum, unsigned ttl)
{
struct route *prt;
prt = (struct route *)getbuf(Route.ri_bpool);
if (prt == (struct route *)SYSERR) {
IpRoutingDiscards++;
return (struct route *)SYSERR;
}
prt->rt_net = net;
prt->rt_mask = mask;
prt->rt_gw = gw;
prt->rt_metric = metric;
prt->rt_ifnum = ifnum;
prt->rt_ttl = ttl;
prt->rt_refcnt = 1; /* our caller */
prt->rt_usecnt = 0;
prt->rt_next = NULL;
return prt;
}
过程rtdel以一个目的地址作为入口参数,并通过从选路表中移走节点并删除路由
/* rtdel.c - rtdel */
#include
#include
#include
/*------------------------------------------------------------------------
* rtdel - delete the route with the given net, mask
*------------------------------------------------------------------------
*/
int rtdel(IPaddr net, IPaddr mask)
{
struct route *prt, *prev;
int hv, i;
if (!Route.ri_valid)
return SYSERR;
wait(Route.ri_mutex);
if (Route.ri_default &&
net == Route.ri_default->rt_net) {
RTFREE(Route.ri_default);
Route.ri_default = 0;
signal(Route.ri_mutex);
return OK;
}
hv = rthash(net);
prev = NULL;
for (prt = rttable[hv]; prt; prt = prt->rt_next) {
if (net == prt->rt_net &&
mask == prt->rt_mask)
break;
prev = prt;
}
if (prt == NULL) {
signal(Route.ri_mutex);
return SYSERR;
}
if (prev)
prev->rt_next = prt->rt_next;
else
rttable[hv] = prt->rt_next;
RTFREE(prt);
signal(Route.ri_mutex);
return OK;
}
像往常那样,程序代码查看并以特殊情况对待默认路由。如果没有找到匹配表项,rtdel计算目的地址的散列值,并在路由链表中搜索。一旦它找到正确的路由,rtdel将该节点从链表中删除,并利用宏RTFREE来递减引用计数值。如果引用计数值到达0,RTFREE就将该节点返回空闲节点表。如果引用计算值扔为正值,表示肯定还有一个或者几个进程正在使用该节点。只有当最后一个进程将引用计数值减少到0时,此节点才被释放回空闲节点表
宏RTFREE假设正在执行中的进程已经获得了单独访问选路表的权利,因此它可以在诸如rtdel之类的过程中使用。任意一个需要递减选路表中引用计算值的过程都可以调用rtfree。当rtfree被唤醒后,首先它等待互斥信号量,然后唤醒宏RTFREE,最后释放信号量
/* rtfree.c - rtfree */
#include
#include
#include
/*------------------------------------------------------------------------
* rtfree - remove one reference to a route
*------------------------------------------------------------------------
*/
int rtfree(struct route *prt)
{
if (!Route.ri_valid)
return SYSERR;
wait(Route.ri_mutex);
RTFREE(prt);
signal(Route.ri_mutex);
return OK;
}
IP支持一组选项,用来控制IP如何处理主机和网关中的数据报。为了使例程代码简洁易懂,我们决定忽略对选项的处理。虽然如此,我们还是在程序代码中包括了两个实例程序的框架,用于死锁IP首部中的选项。网关调用过程ipdoopts,它仅仅返回调用者,使得网关在前送数据报时,并没有对选项做任何处理
/* ipdoopts.c - ipdoopts */
#include
#include
#include
/*------------------------------------------------------------------------
* ipdoopts - do gateway handling of IP options
*------------------------------------------------------------------------
*/
int ipdoopts(struct netif *pni, struct ep *pep)
{
return OK; /* not implemented yet */
}
主机调用过程ipdstopts来处理到达主机的数据报的选项。虽然我们在过程中并没有真正实现选项的处理,但它分析了IP首部中选项长度的八位组的信息,并将选项字段从IP首部中删除
/* ipdstopts.c - ipdstopts */
#include
#include
#include
/*------------------------------------------------------------------------
* ipdstopts - do host handling of IP options
*------------------------------------------------------------------------
*/
int ipdstopts(struct netif *pni, struct ep *pep)
{
struct ip *pip = (struct ip *)pep->ep_data;
u_char *popt, *popend;
int len;
if (IP_HLEN(pip) == IPMHLEN)
return OK;
popt = pip->ip_data;
popend = (u_char *)&pep->ep_data[IP_HLEN(pip)];
/* NOTE: options not implemented yet */
/* delete the options */
len = pip->ip_len-IP_HLEN(pip); /* data length */
if (len)
memcpy(pip->ip_data, &pep->ep_data[IP_HLEN(pip)], len);
pip->ip_len = IPMHLEN + len;
pip->ip_verlen = (pip->ip_verlen&0xf0) | IP_MINHLEN;
return OK;
}
IP选路表是一个至关重要的数据结构。当为数据报选择路由时,IP进程利用该选路表,在表中查找数据报目的站的下一跳路由。由于路由查找的频繁使用,因此选路表在其组织结构上尽量考虑提高查找的效率。同时,掌握新的路由信息的高层协议软件还要在选路表中插入、删除或更改路由
本节描述了负责选路表的查找和维护的过程。从中可以了解到选路表怎样利用桶散列结构提高效率,以及当进程正在删除选路路由时引用计数器怎样运行另一进程继续使用该路由。