Chinaunix首页 | 论坛 | 博客
  • 博客访问: 527304
  • 博文数量: 118
  • 博客积分: 2575
  • 博客等级: 大尉
  • 技术积分: 1263
  • 用 户 组: 普通用户
  • 注册时间: 2009-09-27 09:37
文章分类

全部博文(118)

文章存档

2017年(11)

2016年(8)

2015年(1)

2014年(9)

2013年(7)

2012年(38)

2011年(14)

2010年(18)

2009年(12)

分类: 系统运维

2009-10-29 19:48:16

相关概念

虽然是vxworks2.0.2版本中的,但是与老土的BSD代码基本一样,事实上,最新的ip协议栈的代码上虽然加上不少新鲜的功能,但是其主体也依旧一样.

ifnet

也就是协议栈中的接口的概念,跟arp相关处理的最重要的三个成员是:
  • if_ioctl 用于接口上的ioctl命令;
  • if_resolve 用于进行地址解析的函数;
  • if_output 用于在接口上发送数据包;
在ipAttach时,这三个值都进行了初始化:
    pIfp->if_ioctl  = (FUNCPTR) ipIoctl;
    pIfp->if_output = ipOutput;
    pIfp->if_resolve = muxAddrResFuncGet(mib2Tbl.ifType, 0x800);
其中if_resolve的值,实际上就是arpresolve函数.

in_ifaddr

in_ifaddr是ifaddr的一种特殊形式,即ipv4版本的的ifaddr.当我们给接口配置ip地址时,实际上要生成一个in_ifaddr结构体,并与ifnet相关联.那么它与arp最相关的内容实际上是在ifaddr结构体中,它们是:
  • ifa_rtrequest 这是一个处理arp相关的函数,在后面我们就会解释到它的用处.
  • ia_ifp 与地址相关联的接口.

sockaddr_dl

数据链路层地址,它的作用就是保存MAC地址,其中与ARP处理相关的内容包括:
  • sdl_len 长度,如果为0,表示mac信息无效,否则就是有效.这点很重要
  • sdl_data 如果有效,保存有mac信息.

llinfo_arp

它就是arp控制结构,整个系统中的llinfo_arp通过一个双向链表连接起来,链表头就是全局变量llinfo_arp.(C语言中,总是喜欢将全局变量定义成结构体的名字).其中现在我们关心的内容包括
  • la_rt 指向相关的rtentry,关于rtentry,后面马上就要讲到了.
  • la_hold 持有的数据,在arp处理中会使用到,现在只知道它是要通过接口发送的数据包;
  • la_asked 计数,用于统计在接收到arp回应前,发出了多少arp请求.

rtentry

路由表项,每一条路由都由一个rtentry表示,与arp相关的内容包括
  • rt_ifp 与路由相关联的接口;
  • rt_ifa 与路由相关的接口地址;
  • rt_genmask 用于clone路由时使用;
  • rt_llinfo 指向arp控制结构
  • rt_gateway 表示下一跳信息,可能保存mac地址.
  • rt_expire arp超时处理使用,如果为0,表示永久有效(用于静态配置的mac).

route

route数据结构主要用于路由处理,它包括两个成员:
  • ro_rt 路由引用的rtentry
  • ro_dst 目的地址

数据的发送过程

ip_output

ip协议栈发送数据总是以
int
ip_output(m0, opt, ro, flags, imo)
struct mbuf *m0;
struct mbuf *opt;
struct route *ro;
int flags;
struct ip_moptions *imo;
函数调用开发的,对于其中一些特殊情况的处理我们就不会加以描述,我们只对普通情况说明.m0表示要发送的数据,而ro就是发送的路由.ip_output在进行了一大堆的事情之后,就会调用
(*ifp->if_output)(ifp, m,
   (struct sockaddr *)dst, ro->ro_rt);
发送数据,其中ifp就是根据路由或者什么的,找到的要outgoing接口.前面我们说过,ipAttach时,就已经指定了if_output为ipOutput函数:
int ipOutput
    (
    register struct ifnet *ifp,
    struct mbuf *m0,
    struct sockaddr *dst,
    struct rtentry *rt0
    )
上面的几个参数比较明显
  • ifp为发送数据要使用的接口
  • m0是要发送的数据;
  • dst是目的地址;
  • rt0是使用的路由;
在ipOutput中与arp相关的最重要的一环,就是下面的switch-case语句:
switch (dst->sa_family)
        {
        case AF_INET:
            if (ifp->if_resolve != NULL)
                if (!ifp->if_resolve(ac, rt, m, dst, edst))
                    return (0); /* if not yet resolved */
            /* If broadcasting on a simplex interface, loopback a copy */
            if ((m->m_flags & M_BCAST) && (ifp->if_flags & IFF_SIMPLEX))
                mcopy = m_copy(m, 0, (int)M_COPYALL);
            off = m->m_pkthdr.len - m->m_len;
            etype = ETHERTYPE_IP;
            break;
        case AF_UNSPEC:
当dst->sa_family为AF_UNSPEC时,说明dst中保存着对方的MAC地址信息,会导致构造二层帧头,然后放到接口的发送队列中.而接口最终会把它发送出去.但是如果dst是一个ip地址(由AF_INET表示),则情况就不一样了.它首先调用ifp->if_resolve函数.if_resolve函数的作用就是进行ip地址到MAC地址的映射,如果if_resolve能够直接返回对应的mac地址,则调用返回之后,edst就保存着目的mac,随后就可以构造二层报头,入发送队列(跟AF_UNSPEC时的情况一样),否则就直接返回.

由于ipAttach时将if_resolve初始成arpresolve,所以我们还是看看arpresolve:
int
arpresolve(ac, rt, m, dst, desten)
register struct arpcom *ac;
register struct rtentry *rt;
struct mbuf *m;
register struct sockaddr *dst;
register u_char *desten;
前面说过,arpresolve的功能就是进行目的ip地址到mac地址的转换映射;目的地址由参数dst指定,如果arpresolve能够完成映射,则目的mac填写在desten中.这是由下面的代码段完成的
sdl = SDL(rt->rt_gateway);
if ((rt->rt_expire == 0 || rt->rt_expire > tickGet()) &&
   sdl->sdl_family == AF_LINK && sdl->sdl_alen != 0) {
bcopy(LLADDR(sdl), (char *)desten, sdl->sdl_alen);
return 1;
}
上面的意思是说,如果当前的arp信息有效,则直接返回mac信息.这个函数中,还处理一些NOARP的处理(对于NOARP的,自己搞定一个mac出来).
if (la->la_hold)
    m_freem(la->la_hold);
la->la_hold = m;
上面的这段代码说,将要发送的数据保存在la_hold中,从上面可以看出,arp只保存最后一次请求时的数据.同时也说明了,如果arp无效,则要发送的数据是保存在arp控制信息中的,在后面的分析中,我们可以看到,在处理arp回应时,这个被保存的数据,会被协议栈发送.再接着看代码:
if (rt->rt_expire) {
    rt->rt_flags &= ~RTF_REJECT;
    if (la->la_asked == 0 || 
       (tickGet () - rt->rt_expire >= arpRxmitTicks)) {
                rt->rt_expire = tickGet();
                if (la->la_asked++ < arp_maxtries)
                    arpwhohas(ac, &(SIN(dst)->sin_addr));
                else {
                    rt->rt_flags |= RTF_REJECT;
                    rt->rt_expire += (sysClkRateGet() * arpt_down);
                    la->la_asked = 0;
                }
        }
}
上面代码有三个主要功能:
  • 清除RTF_REJECT标志,RTF_REJECT标志用于控制发送arp信息的频度,后面会有专门的讲解.
  • 如果当前发送的arp请求次数小于arp_maxtries(5次),则调用arpshowhas发送arp请求;
  • 否则,设置RTF_REJECT,以抑制ARP请求的发送,并将抑制时间定为20秒;

arpwhohas实际调用arprequest,以请求ARP信息:
static void
arprequest(ac, sip, tip, enaddr)
register struct arpcom *ac;
register u_long *sip, *tip;
register u_char *enaddr;
arpwhohas首先构造一个报文,然后调用接口的发送函数:
bcopy((caddr_t)etherbroadcastaddr, (caddr_t)eh->ether_dhost,
   sizeof(eh->ether_dhost));
sa.sa_family = AF_UNSPEC;
sa.sa_len = sizeof(sa);
(*ac->ac_if.if_output)(&ac->ac_if, m, &sa, (struct rtentry *)0);
我们可以看到目的mac在这写成了广播地址(全1),而sa.sa_family设置为AF_UNSPEC,这与我们前面描述的ipOutput是一致的(由前面的描述我们知道,这里调用的函数if_output实际上就是ipOutput).我们也知道,这次调用由于给的目的地址参数sa_family为AF_UNSPEC,所以会导致直接把要发送的数据放到接口发送队列,而不会再调用if_resolve造成死循环.

arp的输入处理

现在,该看看另外一个分支了,arp输入处理,arp的输入由arpintr驱动,它调用in_arpinput以处理跟ip相碰的arp报文:

la = arplookup(isaddr.s_addr, itaddr.s_addr == myaddr.s_addr, 0);
if (la && (rt = la->la_rt) && (sdl = SDL(rt->rt_gateway))) {
if (sdl->sdl_alen &&
   bcmp((caddr_t)ea->arp_sha, LLADDR(sdl), sdl->sdl_alen))
logMsg("arp info overwritten for %08x by %s\n",
   (int) ntohl(isaddr.s_addr), 
   (int) ether_sprintf(ea->arp_sha),0,0,0,0);
bcopy((caddr_t)ea->arp_sha, LLADDR(sdl),
   sdl->sdl_alen = sizeof(ea->arp_sha));
if (rt->rt_expire)
rt->rt_expire = tickGet() + (sysClkRateGet() * 
    arpt_keep);
rt->rt_flags &= ~RTF_REJECT;
la->la_asked = 0;
if (la->la_hold) {
(*ac->ac_if.if_output)(&ac->ac_if, la->la_hold,
rt_key(rt), rt);
la->la_hold = 0;
}
}
首先,in_arpinput会调用arplookup,以检查发送者是不是在我们当前的arp缓存中(如果我们在前面发送了arp请求,那么就应该存在一个无效的arp项,如果是对方主请求,那么说明对方想与我们通讯,也需要检查对方在不在的).arplookup传入的第二个参数,表示在不存在arp项的时候,是不是要建立.那么什么时候建立呢?由于arp请求是广播发送的,所以我们可能接收到请求的目的ip不是我们自己的ip地址,在这种情况下,只需要更新arp信息(时标,以防止老化),只有请求是针对我自己的情况下(因为对方可能要与我通讯了,我马上就要使用对方的mac地址了),才会创建新的arp项.

如果arplookup查找出来一个有效的arp项,说明arp要被覆盖了,这也是我们看到
"arp info overwritten for 0xXXXXXXXX by xxxx \n"
这条信息的原因.

arp下面的处理,就是复制目的mac,然后
  • 设置rt_expire,将老化时间设置为20分钟.
  • 设置la_asked为0;
  • 调用if_output发送la_hold.这与我们前面说过的,arp输入处理时,发送保存的数据是一致的.这一次由于系统中arp项的存在,会导致ipPutput调用arpresolve时返回1(有效mac),从而能够正常发送数据.

arp的老化

arp的老化由arptimer完成,arptimer每分钟运行一次,它处理全局链表llinfo_arp,将老化的arp设置为无效.
if (rt->rt_expire && rt->rt_expire <= tickGet())
   {
   arptfree(la->la_prev); /* timer has expired; clear */
   }
我们看到老化操作是在arptfree完成的.
if (rt->rt_refcnt > 0 && (sdl = SDL(rt->rt_gateway)) &&
   sdl->sdl_family == AF_LINK) {
sdl->sdl_alen = 0;
la->la_asked = 0;
rt->rt_flags &= ~RTF_REJECT;
return;
}
rtrequest(RTM_DELETE, rt_key(rt), (struct sockaddr *)0, rt_mask(rt),
   0, (struct rtentry **)0);
从上面的代码中可以看出,在rt_refcnt大于0的情况下,仅仅将mac信息标志为无效(sdl_len=0),否则调用rtrequest删除相应的arp表项.

arp表项的建立

从前面我们大概知道了整个arp表项,但是我们还没有知道arp在内存中到底是什么.其实我们所谓的arp,就是主机直接路由,它通过clone接口子网路由产生.所以这一次,我们从arp的前生今世开始讲.

接口网络路由的建立

当我们给接口增加/删除地址的时候,都会影响路由表,通过设置地址/掩码,也就确定一个可以直达的网络,如ifAddrAdd("mottsec2","10.0.0.8","10.0.255.255",0xffff0000).这个函数调用经过一系列的传递之后,会调用到in_control,然后调用到in_ifinit,它里面有这样一段代码:
if (ifp->if_ioctl &&
   (error = (*ifp->if_ioctl)(ifp, SIOCSIFADDR, (caddr_t)ia))) {
splx(s);
ia->ia_addr = oldaddr;
return (error);
}
因为if_ioctl就是ipIoctl,所以来看看,它作了些什么事情:
    switch (cmd)
{
case SIOCSIFADDR:
            for (pIa = in_ifaddr; pIa; pIa = pIa->ia_next)
                if ((pIa->ia_ifp == (struct ifnet *)ifp) &&
                    (pIa->ia_addr.sin_addr.s_addr == dt_saddr))
                    break;
            pIa->ia_ifa.ifa_rtrequest = arp_rtrequest;
            pIa->ia_ifa.ifa_flags |= RTF_CLONING;
   ifp->ac_ipaddr = IA_SIN (data)->sin_addr;
            arpwhohas (ifp, &IA_SIN (data)->sin_addr);
   break;
其它的不管,其中对我们arp影响最大的两项,就是将ifa_rtrequest设置为arp_rtrequest,并置上了RTF_CLONING标志.incontrol最后会调用rtinit,并由rtinit调用rtrequest将接口网络路由加入到系统路由表中.

clone路由的产生

当调用rtalloc查找主机路由时,它实际调用的是rtalloc1,我们来看一下代码:
if (rnh && (rn = rnh->rnh_matchaddr((caddr_t)dst, rnh)) &&
   ((rn->rn_flags & RNF_ROOT) == 0)) {
        newrt = rt = (struct rtentry *)rn;
        if (report && (rt->rt_flags & RTF_CLONING)) {
            err = rtrequest(RTM_RESOLVE, dst, SA(0),
                 SA(0), 0, &newrt);
它首先在路由表中查找路由,如果找到了,并且具有clone标志RTF_CLONING,则需要调用rtrequest进行RTM_RESOLVE,这个过程就是clone过程,也就是建立主机直接路由的过程,也就是建立arp的过程.我们经常说过的arp实际上就是保存在路由表中,只不过是主机直接路由罢了.rtrequest在生成主机路由之后,执行下面的代码:
if (ifa->ifa_rtrequest)
    ifa->ifa_rtrequest(req, rt, SA(ret_nrt ? *ret_nrt : 0));
我们知道ifa_rtrequest函数就是arp_rtrequest函数,当以RTM_RESOLVE为参数调用时,它负责生成arp控制信息,并把它加入到全局的arp链表中,只是现在它的mac地址还是为空.arp的处理模块会使得它填写有效值.

被动创建的arp项

由前面我们知道,当对方请求本地接口地址的mac信息时,我们会创建相应的arp项,这其实是通过arplookup实现的,其实arplookup也是通过调用rtalloc1的.
阅读(4464) | 评论(1) | 转发(1) |
给主人留下些什么吧!~~

chinaunix网友2009-11-02 16:11:50

very good