内核版本:3.4.39

为什么需要邻居子系统呢?因为在网络上发送报文的时候除了需要知道目的IP地址还需要知道邻居的L2 mac地址,为什么是邻居的L2地址而不是目的地的L2地址呢,这是因为目的地网络可能不在同一个网段甚至不在同一个地区,因此需要借助其它离目的地近的网点帮我们传输下,这里离目的地近的网点通常就是网关,也就是邻居。如果目的地和我们在同一个LAN上的话,它们就是邻居。邻居子系统的核心功能就是完成L3地址到L2地址的映射,并提供网络层和驱动程序底层之间的接口。通过下面这张图可以看到邻居子系统在Linux内核协议栈的位置。IPv4和IPv6属于网络层,当需要传输数据的时候会通过邻居子系统提供的发送接口发送数据。

具体来说,当发送数据的时候,在邻居表里面查找邻居项,查找关键词就是设备和目的地址,找到这个邻居项之后,就调,用邻居项提供的接口发送出去。那么问题来了,邻居项是什么?邻居项是如何分配的?邻居项的组织结构又是什么样子?此外,邻居项的管理又该怎么做?以上这些问题就是邻居子系统需要解决的问题。一步步来分析。

首先邻居项是一个存储了到达邻居信息的结构体,如下:

struct neighbour {struct neighbour __rcu *next;                      //指向下一个邻居项struct neigh_table    *tbl;                   //邻居表struct neigh_parms *parms;                 //邻居协议参数unsigned long       confirmed;              //可到达性确认时间unsigned long     updated;                //邻居状态更新时间rwlock_t      lock;                       //读写锁atomic_t       refcnt;                     //引用计数struct sk_buff_head   arp_queue;              //发送缓存队列unsigned int        arp_queue_len_bytes;                    //发送缓存队列长度struct timer_list timer;                  //邻居项定时器unsigned long       used;                   //使用时间标志位atomic_t       probes;                     //探测次数__u8          flags;__u8          nud_state;                  //邻居状态标志位__u8           type;                       //地址类型__u8          dead;                       //废弃标志位seqlock_t        ha_lock;                    //地址保护锁unsigned char        ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];struct hh_cache      hh;                     //L2帧头缓存int         (*output)(struct neighbour *, struct sk_buff *);    //提供给L3的发送接口const struct neigh_ops  *ops;               //虚拟函数表,随邻居状态变更struct rcu_head       rcu;struct net_device   *dev;                   //设备u8          primary_key[0];                 //占位符,保存地址信息
};

结构体里面信息虽然比较多,但是每一个就是必要的。 邻居子系统提供了一套通用的框架,供邻居协议使用,目前使用的协议包括ARP(IPv4),NDIPv6)。虽然协议不同,但是都使用了同一套结构体。每个协议会建立自己的邻居表(struct neigh_table),arp使用的是arp_tbl,ND协议使用nd_tabl,table结构体如下:

struct neigh_table {struct neigh_table   *next;            //指向下一个邻居表int         family;           //协议,AF_INET, AF_INET6int          entry_size;       //邻居项大小int            key_len;          //地址长度,IPv4是4字节,IPv6是16字节__u32          (*hash)(const void *pkey,    //计算hash值的函数const struct net_device *dev,__u32 *hash_rnd);int          (*constructor)(struct neighbour *);        //邻居项构造函数int         (*pconstructor)(struct pneigh_entry *);    //代理邻居项的构造函数void         (*pdestructor)(struct pneigh_entry *);void          (*proxy_redo)(struct sk_buff *skb);char         *id;        //邻居表IDstruct neigh_parms   parms;     //邻居表配置参数/* HACK. gc_* should follow parms without a gap! */int          gc_interval;        //gc回收时间int         gc_thresh1;        //邻居表占用内存阈值int           gc_thresh2;int          gc_thresh3;unsigned long        last_flush;    //记录gc上一次清理时间struct delayed_work gc_work;       //gc任务队列struct timer_list    proxy_timer;    //代理功能定时器struct sk_buff_head    proxy_queue;    //代理队列atomic_t      entries;        //邻居项个数rwlock_t     lock;           //读写锁unsigned long      last_rand;struct neigh_statistics   __percpu *stats;        //统计信息struct neigh_hash_table __rcu *nht;             //邻居项hash表struct pneigh_entry **phash_buckets;        //代理邻居项表
};

邻居表的元素nht是一个hash链表,所有相同协议的邻居项都挂在这里。邻居表和邻居表项组织图如下:

上述就是邻居表项的组织结构。

当网络层发送报文前首先需要查找路由,出口路由是和邻居绑定的。路由查找完成后会调用邻居层提供的output接口发送。发送函数output会随着邻居项的状态改变。邻居项的状态?当然啦,邻居项也是有状态的,比如说刚建立邻居项的时候,这时候还不知道邻居的MAC地址,这个邻居项还不能使用,因为是初始化,所以状态时NONE,这个时候如果发送报文的话时没办法发送出去的,必须先使用邻居协议发送solicit请求,这个时候邻居项的状态就会变成INCOMPLETE(未完成),创建邻居项的时候会自动起一个定时器,当定时器超时的时候会检查当前邻居项的状态并作出适当改变。当发送solict请求一段时间没有响应回来的话定时器就会超时,这时候会根据当前状态判断是否需要重传,重传的次数一定的,不可能一直重传下去,每次重传后定时器会自动重启,定时器超时的时间也是根据配置来的,重传的定时器时间是neigh->parms->retrans_time。此外,在发送solicti请求期间是没法传输报文的,这个时候怎么办呢,总不能系统就停在这里吧,当然也不能丢弃报文,可能邻居一会儿就响应了。这个时候需要把这个报文放到neigh->arp_queue缓存队列里,当然队列是有长度的,不可能无线存储,不然内存就不够了,默认是存储三个报文,溢出后简单丢弃最先进来的。队列长度是可配的。

假设收到了响应,这时候邻居状态就会从INCOMPLETE状态迁移到REACHABLE(可到达),这个时候邻居是可到达的,除了迁移状态外还需要把缓存队列里面的报文发送出去。

当然状态不可能一直是Reachable(可到达),可能邻居down掉了,或者我们设备自己挂掉了,这个时候邻居状态必须更改。通常情况下,如果一段时间不用,邻居状态就会从reachable状态迁移到stale(旧)状态,这个时候需要可到达性确认了。

如果在gc_staletime没有使用的话状态就会迁移到fail,此时gc定时回收。如果在gc_staletime有使用的话,状态迁移到delay状态,相当于延迟迁移到fail状态,在delay状态经过delay_probe_time状态没有更新的话就会进入probe状态,这个状态下需要主动发送探测报文,发送探测报文的次数是有限的,超时的话就只能丢弃了。

邻居状态迁移图如下:

邻居子系统的内容基本上就是这些,提供的主要是抽象的公共框架,不同的邻居协议可以直接拿来使用,它的启动流程比起内核其它模块来说是简单的不能再简单了。

//邻居子系统初始化
static int __init neigh_init(void)
{//注册应用层回调处理函数,用于处理邻居添加、删除、查询等操作rtnl_register(PF_UNSPEC, RTM_NEWNEIGH, neigh_add, NULL, NULL);rtnl_register(PF_UNSPEC, RTM_DELNEIGH, neigh_delete, NULL, NULL);rtnl_register(PF_UNSPEC, RTM_GETNEIGH, NULL, neigh_dump_info, NULL);rtnl_register(PF_UNSPEC, RTM_GETNEIGHTBL, NULL, neightbl_dump_info,NULL);rtnl_register(PF_UNSPEC, RTM_SETNEIGHTBL, neightbl_set, NULL, NULL);return 0;
}

仅仅是注册应用层的回调处理函数,比如下面这条,添加一条邻居项,通过dev设备到达10.0.0.3需要发送到0:0:0:0:0:1

ip neigh add 10.0.0.3 lladdr 0:0:0:0:0:1 dev eth0 nud perm

这条命令会通过netlink下发到内核,最终由邻居子系统注册的neigh_add函数处理,这个函数首先进行参数的合理性检查,没问题的话就将其加入到对应的邻居表中。

//添加邻居
static int neigh_add(struct sk_buff *skb, struct nlmsghdr *nlh, void *arg)
{struct net *net = sock_net(skb->sk);struct ndmsg *ndm;struct nlattr *tb[NDA_MAX+1];struct neigh_table *tbl;struct net_device *dev = NULL;int err;ASSERT_RTNL();//参数合法性检查err = nlmsg_parse(nlh, sizeof(*ndm), tb, NDA_MAX, NULL);if (err < 0)goto out;err = -EINVAL;//邻居目的地址都不存在的   话就不要继续搞了//毕竟邻居项的灵魂之一就是L3地址if (tb[NDA_DST] == NULL)goto out;ndm = nlmsg_data(nlh);if (ndm->ndm_ifindex) {//提取出口设备,如果获取失败的话就返回错误,邻居项是和出口绑定的dev = __dev_get_by_index(net, ndm->ndm_ifindex);if (dev == NULL) {err = -ENODEV;goto out;}//检查邻居L2地址长度是否合法if (tb[NDA_LLADDR] && nla_len(tb[NDA_LLADDR]) < dev->addr_len)goto out;}read_lock(&neigh_tbl_lock);//遍历邻居表,可能的选项包括IPv4的arp_tbl和IPv6的nd_tblfor (tbl = neigh_tables; tbl; tbl = tbl->next) {//标志位表示admin用户权限和覆盖选项int flags = NEIGH_UPDATE_F_ADMIN | NEIGH_UPDATE_F_OVERRIDE;struct neighbour *neigh;void *dst, *lladdr;//协议要匹配,可能的值包括AF_INET和AF_INET6if (tbl->family != ndm->ndm_family)continue;read_unlock(&neigh_tbl_lock);//检查长度是否合法,IPv4长度为4,IPv6长度为16if (nla_len(tb[NDA_DST]) < tbl->key_len)goto out;dst = nla_data(tb[NDA_DST]);lladdr = tb[NDA_LLADDR] ? nla_data(tb[NDA_LLADDR]) : NULL;//添加代理if (ndm->ndm_flags & NTF_PROXY) {struct pneigh_entry *pn;err = -ENOBUFS;//查找代理表,如果存在的话则更新,不存在则新建pn = pneigh_lookup(tbl, net, dst, dev, 1);if (pn) {pn->flags = ndm->ndm_flags;err = 0;}goto out;}if (dev == NULL)goto out;//先查找邻居表项是否存在neigh = neigh_lookup(tbl, dst, dev);if (neigh == NULL) {//邻居不存在,如果没有创建标志位报错返回if (!(nlh->nlmsg_flags & NLM_F_CREATE)) {err = -ENOENT;goto out;}//和neigh_lookup类似,不过它在查找失败的话会自动创建新的邻居项neigh = __neigh_lookup_errno(tbl, dst, dev);if (IS_ERR(neigh)) {err = PTR_ERR(neigh);goto out;}} else {//如果存在排他标志位的话,返回已经在错误if (nlh->nlmsg_flags & NLM_F_EXCL) {err = -EEXIST;neigh_release(neigh);goto out;}//如果不存在替换标志位的话,就不要覆盖了if (!(nlh->nlmsg_flags & NLM_F_REPLACE))flags &= ~NEIGH_UPDATE_F_OVERRIDE;}if (ndm->ndm_flags & NTF_USE) {//发送探测报文,这个标志具体含义我还没搞清楚,可能是立即使用吧//这时候立即调用neigh_event_send发送solicit请求进行可到达性确认neigh_event_send(neigh, NULL);err = 0;} else//更新邻居表项err = neigh_update(neigh, lladdr, ndm->ndm_state, flags);//释放引用计数,查找的时候会加一个,这时候不用了减去 neigh_release(neigh);goto out;}read_unlock(&neigh_tbl_lock);err = -EAFNOSUPPORT;
out:return err;
}

参考文档:

1. 《深入理解Linux网络技术内幕》

tcp/ip 协议栈Linux内核源码分析十 邻居子系统分析一 概述通用邻居框架相关推荐

  1. tcp/ip 协议栈Linux内核源码分析14 udp套接字接收流程一

    内核版本:3.4.39 前面两篇文章分析了UDP套接字从应用层发送数据到内核层的处理流程,这里继续分析相反的流程,看看数据是怎么从内核送到应用层的. 与发送类似,内核也提供了多个接收数据的系统调用接口 ...

  2. tcp/ip 协议栈Linux内核源码分析九 IPv6分片ip6_fragment 分析

    内核版本:3.4.39 IPv6的分片流程和IPv4基本一致,这一点内核源码作者也说了.流程比较简单,分片的时候判断是否满足快速分片,满足的话直接一个接一个加上分片扩展选项发送出去,不满足的话就只能走 ...

  3. tcp/ip 协议栈Linux内核源码分析11 邻居子系统分析二 arp协议的实现处理

    内核版本:3.4.39 内核邻居子系统定义了一个基本的框架,使得不同的邻居协议可以共用一套代码.比起其它的内核模块,邻居子系统框架代码还是比较简单易懂的.邻居子系统位于网络层和流量控制子系统中间,它提 ...

  4. tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二

    内核版本:3.4.39 上篇我们分析了UDP套接字如何接收数据的流程,最终它是在内核套接字的接收队列里取出报文,剩下的问题就是谁会去写入这个队列,当然,这部分工作由内核来完成,本篇剩下的文章主要分析内 ...

  5. tcp/ip 协议栈Linux内核源码分析12 udp套接字发送流程一

    内核版本:3.4.39 因为过往的开发工作中既包括内核网络层模块的开发,又包括应用层程序的开发,所以对于网络数据的通信有那么一些了解.但是对于网络通信过程中,内核和应用层之间接口是如何运作的不是很清楚 ...

  6. tcp/ip 协议栈Linux内核源码分析13 udp套接字发送流程二

    内核版本:3.4.39 继续UDP套接字发送,上一篇讲到了sock_sendmsg,这里继续,下面是sock_sendmsg的相关代码 int sock_sendmsg(struct socket * ...

  7. tcp/ip 协议栈Linux内核源码分析八 路由子系统分析三 路由表

    内核版本:3.4.39 Linux路由子系统代码量虽说不是很多,但是难度还是有的,最近在分析路由子系统这一块,对它的框架有了基本的了解,如果要想掌握的话估计还得再花点时间阅读代码,先把框架记录下来.路 ...

  8. tcp/ip 协议栈Linux内核源码分析七 路由子系统分析二 策略路由

    内核版本:3.4.39 策略路由就是根据配置策略查找路由表,早期的Linux版本是不支持策略路由的,默认的查找策略就是先查找local路由表,找不到再继续查找main表,当支持策略路由功能时,内核最多 ...

  9. tcp/ip 协议栈Linux内核源码分析六 路由子系统分析一路由缓存

    内核版本:3.4.39 收到报文或者发送报文的时候都需要查找路由表,频繁的路由表查找操作时需要耗费一部分CPU的,Linux提供了路由缓存来减少路由表的查询,路由缓存由hash表组织而成,路由缓存的初 ...

最新文章

  1. 数据结构与算法:13 字符串与整数集合
  2. Anaconda | conda常用命令
  3. 业务架构、信息架构、技术架构三位一体,互联网营销
  4. 2021牛客暑期多校训练营1 H Hash Function FFT\NTT
  5. C++|OpenCV-HSV图像
  6. 十分钟-Nginx入门到上线
  7. JavaScript内建对象 (一) ----- Array
  8. vue element序号翻页连续排序
  9. C#实现--单链表(链式)
  10. C语言知识点复习梳理
  11. 安森美的全局快门图像传感器解决机器视觉的成像需求
  12. 典型相关分析(Canonical correlation analysis)(四): 中国城市竞争力与基础设施的相关分析
  13. 基于STM32的卧室智慧监测系统
  14. 双重认证怎么开_facebook bm认证很重要
  15. 系统垂直越权与水平越权漏洞修复记录
  16. “CreateProcess error=206, 文件名或扩展名太长” 错误解决办法
  17. 极线几何关系、极点、极线,三维重构,k-d树的特征匹配方法
  18. 哈工大pyltp安装和使用方法
  19. 集成学习(Ensemble Learning),Bagging、Boosting、Stacking
  20. crc32算法简单理解

热门文章

  1. 【android】错误集锦及解决办法
  2. 做报表时用到的一个存储过程
  3. asp.net mvc中ckeditor+ckfinder的配置方法
  4. java序列化 jar_使用序列化将对象传递给另一个JVM – 相同的Java版本和jar(都运行我们的应用程序)...
  5. 【Paper】2013_基于一致性理论的无人机编队控制器设计_郭伟强
  6. STM32 电机教程 17 - 基于ST MotorControl Workbench的电机调试
  7. python 2021/12/31
  8. BRCM5.02编译七:No package 'uuid' found
  9. 通过PSO实现不同函数的目标值计算和搜索
  10. stm32使用rtc到底用LSI还是LSE