引言
网络接口层包括为网络硬件服务的设备驱动程序,以及与设备驱动程序相关的用于发送外发分组和接受传入分组的软件。设备驱动程序直接与网络硬件通信,并只能使用网络物理地址发送和接受分组
地址转换协议(ARP)软件也是网络接口层。ARP将上层的IP地址与底层的物理地址的下层进行绑定。地址绑定软件形成了只能使用IP地址的上层协议软件与只能使用物理地址的下层设备驱动软件之间的分界线。也就是说,ARP使得高层协议与物理地址完全独立
ARP软件维护管理一张地址映射表,用以将IP地址映射为物理地址,但高层协议软件并不直接访问该表。事实上,ARP软件拥有地址映射表,并能够用来完成表格数据的查找和修改
ARP软件在理论上的结构
从理论上讲,ARP软件可划分为三部分:输出模块、输入模块和一个高速缓存管理程序。
- 在发送数据报时网络接口软件调用输出模块中的一个过程,将高层协议地址(比如IP地址)与相应的物理地址向绑定。输出过程返回一个绑定,网络接口程序利用它封装和发送分组。
- 输入模块处理来自网络的ARP分组,并通过增加新的绑定来修改ARP高速缓存中的内容
- 高速缓存管理程序实现了高速缓存替换策略:它检测高速缓存中所有的表项,并删除已达到规定时限的表项
ARP设计方案举例
ARP协议看似简单,但是各种细节问题使得软件复杂化。很多实现方案并不能完全正确的阐释ARP协议规范,而其他一些则忽略了高速缓存中的超时处理,虽然这样做的本意是试图提高效率,但会生成错误的绑定。因此全面周密的考虑ARP软件的设计方案,并做到不漏掉协议的任何细节是十分重要的
ARP软件遵循以下几个设计规则:
- 单一的ARP高速缓存:
- 在单一的高速缓存中为所有网络保管表项,高速缓存的每个表项中有一个字段,指出此绑定来自于哪一个网络。
- 与之相反的是多个高速缓存方案,也就是为每个网络接口保留一个独立的ARP高速缓存。
- 使用单个,还是使用多个高速缓存,仅对网关或连接多个网络的多地址主机来讲是有区别的
- 全局替换策略:
- 高速缓存策略规定,如果高速缓存已满,而又必须向其中增加新的绑定时,此高速缓存中的某个表项将被删除,并且这与新的绑定是否和被删除表项中的绑定是否来自同一网络无关
- 另一种方法是本地替换策略,此时新的绑定只能替换来自相同网络的旧的绑定
- 从本质上来说,本地替换策略要求为每个网络接口预分配高速缓存空间,而其效果与使用多个独立的高速缓存时一样的
- 高速缓存中的超时与删除:
- 当一个表项在高速缓存中保留一定时间后,重新判定其有效性是很重要的
- 设计方案:高速缓存的每个表项都有相关的寿命字段。高速缓存中增加新表项(或者此表项变为有效)时,ARP软件初始化该表项中的寿命字段。随着时间的推移,高速缓存管理程序递减寿命字段中的值,并当值到达0时,丢弃该表项
- 高速缓存中表项的删除与它的使用频率无关
- 超时表项迫使ARP软件必须通过网络,从目的站中获取新的绑定,ARP不会主动使被删除的表项重新有效,要等到某个外发分组需要这个绑定时,ARP软件才会再次获取它
- 分组以多队列的形式等待发送:
- 我们的设计允许将多个还未发送出去的分组放置在队列中,等待地址的转换
- ARP高速缓存的每个表项都有一个外发分组队列,队列中的分组将被发往由该表项指出的目的站。
- 当含有发送所需要的物理地址ARP响应分组到达时,软件就从队列中取出这些分组,并把它们发送出去
- 互斥访问:
- 我们的软件不允许中断,并避免高速缓存中的内容替换,以保证在任何时间仅有一个进程访问ARP高速缓存
- 对高速缓存操作的过程(比如查找)要求互斥访问,但在该过程中并不包括完成互斥操作的程序代码,互斥机制由调用者完成。
一般来说,为每个接口分配一个独立的高速缓存,或者采用本地替换策略,这或多或少的保持了网络接口之间的相对独立性。因为在最糟糕的情况下,如果某个网络接口上的通信所设计的目的数数目大大超过了其他网络接口,那么,从使用频繁的网络接口发送来的绑定,通过替换来自其他网络的绑定,占据了大部分高速缓存。其弊病和运行不良的高速缓存一样:高速缓存总是保持100%的容量,但在其中找到某个表项的概念却很小。我们的设计方案假定管理员会监督网络性能问题,并在出现这样的问题时,分配附加高速缓存空间。
尽管我们的设计方案在最糟糕的情况下可能表项恶劣,但在正常情况下,它可以提供更多的灵活性,因为高速缓存的分配可以动态的跟随网络负载的变化。比如,在一端时间内,大部分数据流量仅设计少数几个网络,这些网络中的主机的绑定将在高速缓存中占主导地址。而如果稍后通信量又偏向了另一组网络,那么新的这组网络的主机的表项最终将占据高速缓存的大部分
ARP高速缓存的数据结构
如下arp.h中说明了ARP分组格式的数据结构、ARP高速缓存存储器的内部数据结构以及在整个ARP程序中使用的符号常量
/* arp.h - SHA, SPA, THA, TPA */
/* Internet 地址解析协议(请参阅 RFC 826、920) */
#define AR_HARDWARE 1 /* 以太网硬件类型代码 */
/* ARP报文操作域中使用的代码定义 */
#define AR_REQUEST 1 /* 解析地址的ARP请求 */
#define AR_REPLY 2 /* 回复解析请求 */
#define RA_REQUEST 3 /* 反向 ARP 请求(RARP 数据包) */
#define RA_REPLY 4 /* 回复反向请求 (RARP ") */
struct arp {
u_short ar_hwtype; /* 硬件类型 */
u_short ar_prtype; /* 协议类型 */
u_char ar_hwlen; /* 硬件地址长度 */
u_char ar_prlen; /* 协议地址长度 */
u_short ar_op; /* ARP 操作(见上表) */
u_char ar_addrs[1]; /* 发送方/目标硬件和原型地址 */
/* char ar_sha[???]; - sender's physical hardware address */
/* char ar_spa[???]; - sender's protocol address (IP addr.) */
/* char ar_tha[???]; - target's physical hardware address */
/* char ar_tpa[???]; - target's protocol address (IP) */
};
#define SHA(p) (&p->ar_addrs[0])
#define SPA(p) (&p->ar_addrs[p->ar_hwlen])
#define THA(p) (&p->ar_addrs[p->ar_hwlen + p->ar_prlen])
#define TPA(p) (&p->ar_addrs[(p->ar_hwlen*2) + p->ar_prlen])
#define MAXHWALEN EP_ALEN /* 以太网 */
#define MAXPRALEN IP_ALEN /* IP */
#define ARP_HLEN 8 /*ARP报头长度 */
#define ARP_TSIZE 50 /* ARP缓存大小 */
#define ARP_QSIZE 10 /* ARP 端口队列大小 */
/* 缓存超时 */
#define ARP_TIMEOUT 600 /* 10分钟 */
#define ARP_INF 0x7fffffff /* “无限”超时值 */
#define ARP_RESEND 1 /* 如果在 1 秒内没有回复,则重新发送 */
#define ARP_MAXRETRY 4 /* 约 30 秒后放弃 */
struct arpentry { /* ARP 缓存中的条目格式 */
short ae_state; /* 此条目的状态(见下文) */
short ae_hwtype; /* 硬件类型 */
short ae_prtype; /* 协议类型 */
char ae_hwlen; /* 硬件地址长度 */
char ae_prlen; /* 协议地址长度 */
struct netif *ae_pni; /* 指向接口结构的指针 */
int ae_queue; /* 此地址的数据包队列 */
int ae_attempts; /* 到目前为止的重试次数 */
int ae_ttl; /* 生存时间 */
u_char ae_hwa[MAXHWALEN]; /* 硬件地址 */
u_char ae_pra[MAXPRALEN]; /* 协议地址 */
};
#define AS_FREE 0 /* Entry is unused (initial value) */
#define AS_PENDING 1 /* Entry is used but incomplete */
#define AS_RESOLVED 2 /* Entry has been resolved */
/* RARP 变量*/
extern int rarppid; /* 等待 RARP 回复的进程 ID */
extern int rarpsem; /* 用于访问 RARP 服务的信号量 */
/* ARP变量 */
extern struct arpentry arptable[ARP_TSIZE];
/* ARP 函数声明 */
int arp_in(struct netif *, struct ep *);
struct arpentry *arpfind(u_char *, u_short, struct netif *);
struct arpentry *arpadd(struct netif *, struct arp *);
- 数组arptable构成了全局ARP高速缓存。数组的每一个元素对应于一个协议(IP)地址(
ae_pra
字段)和物理地址(ae_hwa
字段)之间的唯一绑定。 ae_state
字段给出了表项的状态,这个状态值如下三者之一:AS_FREE(此表项当前空闲)、AS_PENDING(此表项正在使用中,但绑定尚未找到)、AS_RESOLVED(此表项正在使用中,绑定正确)。- 每个表项还有硬件
ae_hwtype
和协议ae_prtype
类型以及硬件地址长度ae_hwlen
以及协议地址长度ae_prlen
ae_pni
字段对应于发送此绑定的网络的网络接口结构- 在地址尚未转换的表项中,
ae_queue
字段执行一个分组队列,当相应的ARP响应分组到达后,队列中的分组就可以被发送出去 - 对状态为AS_PENDING(此表项正在使用中,但绑定尚未找到)时,ae_attempts字段指出该表项的请求分组已经被广播了多少次
- 最后,
ae_ttl
字段给出了在订一起超时,该表项不得不删除以前,能在高速缓存中保留多少时间(以秒为单位)
数据结构arp定义arp分组的格式。字段ar_hwtype和ar_prtype指出硬件和协议类型,字段ar_hwlen和ar_prlen指出物理地址长度和协议地址长度,字段ar_op
指明此ARP分组的内容是一个请求该是一个响应
由于ARP分组中的地址字段长度与硬件类型以及被转换协议地址的类型有关,因此ARP的数据结构不能确定所有字段的长度。事实上,它只是将大小固定的字段放在分组的前部,然后使用一个名为ar_addrs的字段,标识处分组中的剩余内容。就概念上来看,以ar_addrs字段打头的一组字节中包括了四个字段:发送方和接收方的独立地址和协议地址各一对。每个地址字段的长度可由分组首部大小固定的字段所提供的信息来决定,因而能够有效的计算出每个地址的正确位置。计算分别由内部函数SHA、SPA、THA、TPA完成。每个函数均以ARP分组的地址作为其唯一的入口参数,并返回分组与函数名对应的地址字段所处的位置。
APR输出处理
搜索ARP高速缓存
处理输出的网络接口程序采用地址转换协议ARP将IP地址转换为对应的物理地址。实际上,网络输出进程调用arpfind过程,搜索并找出ARP高速缓存中某个与协议地址相匹配的表项
/* arpfind.c - arpfind */
#include <conf.h>
#include <kernel.h>
#include <network.h>
/*------------------------------------------------------------------------
* arpfind - find an ARP entry given a protocol address and interface
* pra - 指向被转换的上层(协议地址)
* prtype - 给出地址类型(采用ARP协议类型标准值)
* pni - 指向一个网络接口结构
* arpfind 在高速缓存中线性查找,直到找到与指向的IP地址向匹配的表项。它返回指向该表项的指针
*------------------------------------------------------------------------
*/
struct arpentry * arpfind(u_char *pra, u_short prtype, struct netif *pni)
{
struct arpentry *pae;
int i;
for (i=0; i<ARP_TSIZE; ++i) {
pae = &arptable[i];
if (pae->ae_state == AS_FREE)
continue;
if (pae->ae_prtype == prtype &&
pae->ae_pni == pni &&
BLKEQU(pae->ae_pra, pra, pae->ae_prlen))
return pae;
}
return 0;
}
回想一下,我们的设计方案是将所有的ARP绑定全部保存在一张唯一的表格中。对于类似以太网的技术,由于它们的物理地址是全球唯一的,使用单张表格不会出现问题。但有些网络技术允许在各自独立的多个物理网络中重复使用一个物理地址。因此,一个网关有可能会对它的高速缓存中的某个物理地址做出多种反应。参数pni可以保证arpfind选择处对应于正确的网络接口的绑定。因此从理论上来讲,参数pni可以保证arpfind选择出对应于正确的网络接口的绑定。因此从理论上来讲,这里使用了物理地址+网络接口号,以唯一的表示一个表项
ARP请求分组的广播
一旦在ARP高速缓存中为某个IP地址分配了一个表项后,网络接口软件调用过程arpsend,来过程并广播一个用来获取相应物理地址的ARP请求分组
/* arpsend.c - arpsend */
#include <conf.h>
#include <kernel.h>
#include <network.h>
/*------------------------------------------------------------------------
* arpsend - broadcast an ARP request N.B. Assumes interrupts disabled、
* pae - 指向ARP高速缓存中的一个表项
*------------------------------------------------------------------------
*/
int arpsend(struct arpentry *pae)
{
struct netif *pni = pae->ae_pni; /* 指向接口结构的指针 */
struct ep *pep; /* 以太网包的完整结构 */
struct arp *parp;
int arplen;
pep = (struct ep *) getbuf(Net.netpool);
if ((int)pep == SYSERR)
return SYSERR;
memcpy(pep->ep_dst, pni->ni_hwb.ha_addr, pae->ae_hwlen);
pep->ep_type = EPT_ARP;
pep->ep_order = EPO_NET; /* 字节顺序掩码(用于调试) */
parp = (struct arp *) pep->ep_data; /* 以太网数据包中的数据*/
parp->ar_hwtype = hs2net(pae->ae_hwtype);
parp->ar_prtype = hs2net(pae->ae_prtype);
parp->ar_hwlen = pae->ae_hwlen;
parp->ar_prlen = pae->ae_prlen;
parp->ar_op = hs2net(AR_REQUEST); //指明这是一个ARP请求分组
memcpy(SHA(parp), pni->ni_hwa.ha_addr, pae->ae_hwlen);
memcpy(SPA(parp), &pni->ni_ip, pae->ae_prlen);
memset(THA(parp), 0, pae->ae_hwlen);
memcpy(TPA(parp), pae->ae_pra, pae->ae_prlen);
arplen = ARP_HLEN + 2*(parp->ar_hwlen + parp->ar_prlen);
write(pni->ni_dev, pep, EP_HLEN+arplen); /* pni->ni_dev是设备描述符 */
return OK;
}
arpsend的入口参数pae是一个指针,指向高速缓存中的一个表项。ARP为该表项中的IP地址生成一个ARP请求分组,并发送请求分组:为这个请求分组分配到一个缓冲区之后,arpsend填写分组中的每个字段,其中大多数所需信息来自参数pae指向的高速缓存中的表项中。它使用物理广播地址作为其目的地址,并指明这是一个ARP请求分组(AR_REQUEST)。当物理地址和协议地址的长度字段被赋值之后,arpsend就可以利用内部函数SHA、SPA、THA、TPA来计算APR分组中各个变长地址字段的实际位置
arpsend生成ARP请求分组之后,唤醒系统调用write来发送分组
输出过程
过程netwrite接收将要被发往指定某个网络接口的分组
/* netwrite.c - netwrite */
#include <conf.h>
#include <kernel.h>
#include <network.h>
#include <ospf.h>
struct arpentry *arpalloc();
struct arpentry *arpfind(u_char *, u_short, struct netif *);
int arpsend(struct arpentry *);
int local_out(struct ep *);
/*#define DEBUG */
/*------------------------------------------------------------------------
* netwrite - write a packet on an interface, using ARP if needed
* struct netif *pni - 网络接口
* struct ep *pep - 以太网包的完整结构
*------------------------------------------------------------------------
*/
int netwrite(struct netif *pni, struct ep *pep, unsigned len)
{
struct arpentry *pae;
STATWORD ps;
if (pni->ni_state != NIS_UP) { // 接口状态
freebuf(pep);
return SYSERR;
}
pep->ep_len = len; /* 以太网包的长度 */
#ifdef DEBUG
if (pni != &nif[NI_LOCAL])
{
struct ip *pip = (struct ip *)pep->ep_data; /* 以太网数据包中的数据 */
if (pip->ip_proto == IPT_OSPF) {
struct ospf *po = (struct ospf *)pip->ip_data;
/* if (po->ospf_type != T_HELLO) { */
{
kprintf("netwrite(pep %X, len %d)\n", pep, len);
pdump(pep);
}
}
}
#endif /* DEBUG */
if (pni == &nif[NI_LOCAL])
return local_out(pep);
else if (isbrc(pep->ep_nexthop)) {
memcpy(pep->ep_dst, pni->ni_hwb.ha_addr, EP_ALEN);
write(pni->ni_dev, pep, len);
return OK;
}
/* else, look up the protocol address... */
disable(ps);
pae = arpfind((u_char *)&pep->ep_nexthop, pep->ep_type, pni);
if (pae && pae->ae_state == AS_RESOLVED) {
memcpy(pep->ep_dst, pae->ae_hwa, pae->ae_hwlen);
restore(ps);
return write(pni->ni_dev, pep, len);
}
if (IP_CLASSD(pep->ep_nexthop)) {
restore(ps);
return SYSERR;
}
if (pae == 0) {
pae = arpalloc();
pae->ae_hwtype = AR_HARDWARE;
pae->ae_prtype = EPT_IP;
pae->ae_hwlen = EP_ALEN;
pae->ae_prlen = IP_ALEN;
pae->ae_pni = pni;
pae->ae_queue = EMPTY;
memcpy(pae->ae_pra, &pep->ep_nexthop, pae->ae_prlen);
pae->ae_attempts = 0;
pae->ae_ttl = ARP_RESEND;
arpsend(pae);
}
if (pae->ae_queue == EMPTY)
pae->ae_queue = newq(ARP_QSIZE, QF_NOWAIT);
if (enq(pae->ae_queue, pep, 0) < 0)
freebuf(pep);
restore(ps);
return OK;
}
netwrite调用arpfind,为分组中的目的地址在高速缓存中查找相应表项。如果此表项中的地址已经被正确转换,netwrite将其中的物理地址复制到分组中,并调用write发送此分组。如果这个表项既没有被正确转换也没有等待转换,那么netwrite调用arpalloc分配一个ARP请求,然后填写ARP表项中的各个字段,并调用arpsend来广播请求分组
因为netwrite必须做到无延迟的返回主调程序,所以它让分组在一个与高速缓存中的某个表项相关联的队列中等待表项中的地址转换,netwrite首先检测这个队列是否存在。如有必要,它调用newq建立一个新队列。每个输出队列都有大小限制。如果netwrite向队列中加入一个分组时发现队列已满,则丢弃此分组
ARP输入处理
向表中增加已转换的表项
ARP在输入处理中使用了两个实用过程arpadd和arpsend。arpadd读取由网络传送来的ARP分组,为它在高速缓存中分配一个表项,并利用ARP分组中的信息填写该表项。由于它同时填写了物理地址字段和协议地址字段,所以arpadd将该表项的状态字段赋值为AS_RESOLVED。它还需要为寿命字段赋予最大超时时间,ARP_TIMEOUT
/* arpadd.c - arpadd */
#include <conf.h>
#include <kernel.h>
#include <network.h>
struct arpentry *arpalloc(void);
/*------------------------------------------------------------------------
* arpadd - Add a RESOLVED entry to the ARP cache
* N.B. Assumes interrupts disabled
*------------------------------------------------------------------------
*/
struct arpentry * arpadd(struct netif *pni, struct arp *parp)
{
struct arpentry *pae;
pae = arpalloc();
pae->ae_hwtype = parp->ar_hwtype;
pae->ae_prtype = parp->ar_prtype;
pae->ae_hwlen = parp->ar_hwlen;
pae->ae_prlen = parp->ar_prlen;
pae->ae_pni = pni;
pae->ae_queue = EMPTY;
memcpy(pae->ae_hwa, SHA(parp), parp->ar_hwlen);
memcpy(pae->ae_pra, SPA(parp), parp->ar_prlen);
pae->ae_ttl = ARP_TIMEOUT;
pae->ae_state = AS_RESOLVED;
return pae;
}
ARP输入过程
正如我们已看到的,当一个ARP分组到达时,网络设备驱动程序将它传递给过程arp_in做进一步处理
/* arp_in.c - arp_in */
#include <conf.h>
#include <kernel.h>
#include <network.h>
void arpqsend(struct arpentry *);
/*------------------------------------------------------------------------
* arp_in - handle ARP packet coming in from Ethernet network
* N.B. - Called by ni_in-- SHOULD NOT BLOCK
*------------------------------------------------------------------------
*/
int arp_in(struct netif *pni, struct ep *pep)
{
struct arp *parp = (struct arp *)pep->ep_data;
struct arpentry *pae;
int arplen;
parp->ar_hwtype = net2hs(parp->ar_hwtype);
parp->ar_prtype = net2hs(parp->ar_prtype);
parp->ar_op = net2hs(parp->ar_op);
if (parp->ar_hwtype != pni->ni_hwtype ||
parp->ar_prtype != EPT_IP) {
freebuf(pep);
return OK;
}
if (pae = arpfind(SPA(parp), parp->ar_prtype, pni)) {
memcpy(pae->ae_hwa, SHA(parp), pae->ae_hwlen);
pae->ae_ttl = ARP_TIMEOUT;
}
if (memcmp(TPA(parp), &pni->ni_ip, IP_ALEN)) { // 查看分组中的IP地址是否与本机的IP地址符合
freebuf(pep);
return OK;
}
if (pae == 0) // 符合
pae = arpadd(pni, parp); // 将其插入高速缓存中
if (pae->ae_state == AS_PENDING) { //此表项正在使用中,但绑定尚未找到 ---- 其中的地址正在等待转换
pae->ae_state = AS_RESOLVED; //此表项正在使用中,绑定正确
arpqsend(pae); // 发送队列中等待发送的分组
}
if (parp->ar_op == AR_REQUEST) { //查看分组中是否含有请求
parp->ar_op = AR_REPLY; //应答
// 交换分组中的目的地址和发送地址
memcpy(TPA(parp), SPA(parp), parp->ar_prlen);
memcpy(THA(parp), SHA(parp), parp->ar_hwlen);
memcpy(pep->ep_dst, THA(parp), EP_ALEN);
memcpy(SHA(parp), pni->ni_hwa.ha_addr,
pni->ni_hwa.ha_len);
memcpy(SPA(parp), &pni->ni_ip, IP_ALEN);
parp->ar_hwtype = hs2net(parp->ar_hwtype);
parp->ar_prtype = hs2net(parp->ar_prtype);
parp->ar_op = hs2net(parp->ar_op);
arplen = ARP_HLEN + 2*(parp->ar_prlen + parp->ar_hwlen);
write(pni->ni_dev, pep, arplen);
} else
freebuf(pep);
return OK;
}
协议标准规定,ARP应该丢弃任何含有设备不承认的上层协议类型的信息。上上面arp_in程序中,只承认协议地址类型为IP而且物理地址类型与发送该分组的网络接口类型相一致的ARP分组。如果接收到的分组中含有其他类型的地址,arp_in将其丢弃。
在处理一个有效的ARP分组时,arp_in调用arpfind,在ARP高速缓存中搜索与发送方的IP地址相匹配的表项。协议规定接收方应首先利用传入的请求分组来满足正在等待中的表项(即它应当利用发送方的地址修改自己的高速缓存)。因此,如果找到了一个匹配的表项,arp_in将利用分组中的发送方物理地址来修改表项的众物理地址,并将表项的超时字段设置为ARP_TIMEOUT
协议还规定,如果传入的ARP分组中含有一个指向接收方的请求,接收方也必须在其高速缓存中增加这个发送方的地址。因此,arp_in会查看分组中的IP地址是否与本机的IP地址符合,如果相符,arp_in调用arpadd将其插入到高速缓存中。在高速缓存中插入一个表项后,arp_in将查看其中的地址是否在等待转换。如果是,它调用arpqsend发送队列中等待发送的分组
最后,arp_in查看分组中是否含有请求。如果有,arp_in通过交换分组中的目的地址和发送地址,向发送方提供被请求的物理地址,然后将AR_REQEST改为AR_REPLY,arp_in直接发送响应分组
ARP高速缓存的管理
从输入输出中可以看到,ARP高速缓存的管理要求输入和输出软件之间协调。另外,它还需要定期的做一些与输入输出无关的计算
高速缓存表项的分配
如果一个进程(比如IP进程)需要发送一份数据报,但其目的地址不再ARP高速缓存的任何一个表项中,则IP必须创建一个新表项,然后广播响应的请求分组,并将等待发送的分组置入队列中。过程arpalloc在APR高速缓存中选取一个表项,用于存放新的绑定
/* arpalloc.c - arpalloc */
#include <conf.h>
#include <kernel.h>
#include <proc.h>
#include <network.h>
void arpdq(struct arpentry *);
/*------------------------------------------------------------------------
* arpalloc - allocate an entry in the ARP table
* N.B. Assumes interrupts DISABLED
*------------------------------------------------------------------------
*/
struct arpentry *arpalloc()
{
static int aenext = 0;
struct arpentry *pae;
int i;
for (i=0; i<ARP_TSIZE; ++i) {
if (arptable[aenext].ae_state == AS_FREE) //(此表项当前空闲)
break;
aenext = (aenext + 1) % ARP_TSIZE;
}
pae = & arptable[aenext];
aenext = (aenext + 1) % ARP_TSIZE;
if (pae->ae_state == AS_PENDING && pae->ae_queue >= 0)
arpdq(pae); // 释放队列中的分组
pae->ae_state = AS_PENDING;// (此表项正在使用中,但绑定尚未找到)
return pae;
}
arpalloc实现了高速缓存替换策略,因为当它为新表项寻找空间时,必须决定在已满的高速缓存中删除一个旧表项。我们选择了一种简单的替换策略:
在为ARP高速缓存中的新成员分配空间时,如果存在一个空闲表项,就选择此空闲表项。否则,采用循环发删除旧表项
在考虑ARP高速缓存替换策略时,必须注意到,高速缓存全满的情况总是不受欢迎的,因为这意味着系统操作出于饱和状态。这时,如果为了发送一个数据报而需要在高速缓存中插入一个新的绑定时,系统必须删除表中的某个绑定。当再次要用到这个旧的、被删除的绑定时,ARP还得删除表中另一个绑定,并广播请求分组。最糟糕的情况下,每发送一个数据报,ARP都需要广播请求分组。我们假设系统管理员在监察到类似的情况后,将为系统重新配置一个更大的高速缓存。这样,需要不断抢占某一项目的情况将很少发生,因而我们所列举的循环法在实际中工作良好
要完成抢占策略,arpalloc中有一个静态整型变量aenext。arpalloc中的for循环语句,从以aenext为索引的表项开始,搜索整张表格,表格的头尾链接,结束时回到出发点。如果找到了一个空闲表项,搜索立即停止。如果高速缓存中没有闲置空间,arpalloc删除索引aenext对应的表项。最后,arpalloc将aenext的值加1,使下一轮查找的起点跨过新分配的表项。
高速缓存的定期策略
本设计方案中,安排了一个独立的定时进程,以定期指向arptimer
/* arptimer.c - arptimer */
#include <conf.h>
#include <kernel.h>
#include <network.h>
void arpdq(struct arpentry *);
int arpsend(struct arpentry *);
/*------------------------------------------------------------------------
* arptimer - Iterate through ARP cache, aging (possibly removing) entries
*------------------------------------------------------------------------
* gran - time since last iteration
*/
void arptimer(int gran)
{
struct arpentry *pae;
STATWORD ps;
int i;
disable(ps); /* mutex */
for (i=0; i<ARP_TSIZE; ++i) {
if ((pae = &arptable[i])->ae_state == AS_FREE)
continue;
if (pae->ae_ttl == ARP_INF)
continue; /* don't time out permanent entry */
if ((pae->ae_ttl -= gran) <= 0)
if (pae->ae_state == AS_RESOLVED)
pae->ae_state = AS_FREE;
else if (++pae->ae_attempts > ARP_MAXRETRY) {
pae->ae_state = AS_FREE;
arpdq(pae);
} else {
pae->ae_ttl = ARP_RESEND;
arpsend(pae);
}
}
restore(ps);
}
当定时线程调用arptimer时,将向其以参数gran的形式给出此次调用和上一次调用之间的时间间隔。arptimer利用这个时间间隔值,计算每个表项遭高速缓存中的存在时间。这样,通过调用,arptimer遍历每个表项,并将表项的寿命字段中的值减去gran,其中gran指出从上次遍历到现在已经经过了多少秒。如果寿命字段变为0或负数,arptimer从高速缓存中删除此表项。删除一个地址已经转换的表项,仅仅是指将其状态变为AS_FREE,这样就运行arpalloc在下次需要的时候能够使用该表项。如果一个正在等待地址转换的表项中的寿命字段超时,arptimer检查ae_attempts字段,看它是否已经被广播了ARP_MAXRETRY次。如果没有,则arptimer调用arpsend再次广播此请求。如果已经已经被广播了ARP_MAXRETRY次,arptimer释放相关的等待发送的分组队列,然后删除该表项
释放队列中的分组
在ARP高速缓存全满的情况下,被arpalloc选中删除的表项可能有一个与之相关联的外发分组队列。如果是这样,arpalloc调用arpdq从队列中取出分组并丢弃
/* arpdq.c - arpdq */
#include <conf.h>
#include <kernel.h>
#include <network.h>
/*------------------------------------------------------------------------
* arpdq - destroy an arp queue that has expired
*------------------------------------------------------------------------
*/
void arpdq(struct arpentry *pae)
{
struct ep *pep;
struct ip *pip;
if (pae->ae_queue < 0) /* nothing to do */
return;
while (pep = (struct ep *)deq(pae->ae_queue)) {
if (gateway && pae->ae_prtype == EPT_IP) {
pip = (struct ip *)pep->ep_data;
icmp(ICT_DESTUR, ICC_HOSTUR, pip->ip_src, pep, 0);
} else
freebuf(pep);
}
freeq(pae->ae_queue);
pae->ae_queue = EMPTY;
}
arpdq遍历与ARP高速缓存中的某个表项相关联的分组队列,并逐一丢弃它们。如果被丢弃的分组时一个IP数据报,而且本机是一个网关,arpdq调用imcp过程为该数据报生成一个ICMP(“数据不可达”)信息。最后,arpdq调用freeq是否队列本身
ARP初始化
系统在启动时,调用一次arpinit过程。arpinit产生于RARP一起使用的互斥量rarpsem,并将ARP高速缓存中所有表项的状态设为AS_FREE。另外,arpinit为相应的RARP协议初始化几个数据项(无关细节)。注意arpinit并不初始化定时器进程或者设立arptime的调用(目的是剥离无关细节,众多协议共同使用者一个唯一的定时器进程)
/* arpinit.c - arpinit */
#include <conf.h>
#include <kernel.h>
#include <proc.h>
#include <network.h>
/*------------------------------------------------------------------------
* arpinit - initialize data structures for ARP processing
*------------------------------------------------------------------------
*/
void arpinit()
{
int i;
rarpsem = screate(1);
rarppid = BADPID;
for (i=0; i<ARP_TSIZE; ++i)
arptable[i].ae_state = AS_FREE;
}
int rarpsem;
int rarppid;
struct arpentry arptable[ARP_TSIZE];
ARP参数配置
在编写ARP软件时,程序员通过选取参数值来设置系统,比如:
- ARP高速缓存的大小
- 发送方等待一个ARP响应的时间限制
- 允许发送方重发一个请求分组的次数
- 重发请求分组的时间间隔
- 高速缓存表项的(生存时间字段)超时时间
- 等待发送的分组队列长度
在典型的设计中,为这些参数定义了一些符号常量,如高速缓存的大小,这样系统管理员就能根据对题的安装变换其设置
总结
本ARP设计中采用了唯一的全局高速缓存,用以保存来自所有网络的绑定。它允许多个分组以队列的形式等待地址的转换,并利用一个独立的定时器过程计算高速缓存中表项的生存时间。所有表项最终都会超时。当一个新表现必须被插入高速缓存时,如果高速缓存已满,则必须删除一个旧表项。这里采用了循环法的替换策略,并通过一个全局指针来实现。每当高速缓存中的一个表项被选中后,此指针依次移向下一个表项。