tcp/ip 协议栈Linux内核源码分析十 邻居子系统分析一 概述通用邻居框架
內核版本: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内核源码分析十 邻居子系统分析一 概述通用邻居框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tcp/ip 协议栈Linux内核源码分
- 下一篇: 天壮营养丽爱花颜是做什么的?