網絡子係統14_鄰居子係統通用接口
//創建一個新的鄰居項 //參考 深入理解linux網絡技術內幕 // 1.鄰居子係統為具體的鄰居協議,提供通用的功能接口 // 2.係統中所有的鄰居協議被鏈接在neigh_tables鏈表中 // 3.neigh_table代表一個具體的鄰居協議 // 4.具體鄰居協議在運行時的行為,可以通過struct neigh_parms調節, // neigh_params與設備關聯,每個鄰居協議neigh_table提供一個默認的neigh_params。 //注冊一個鄰居協議到係統中 // 1.與鄰居協議neigh_table有關的定時器:垃圾回收定時器gc_timer,代理定時器proxy_timer 1.1 void neigh_table_init(struct neigh_table *tbl) { unsigned long now = jiffies; unsigned long phsize; //設置引用計數為1 atomic_set(&tbl->parms.refcnt, 1); //tbl->parms 的類型為 struct neigh_parms,用於調整鄰居協議的行為 INIT_RCU_HEAD(&tbl->parms.rcu_head); //reachable_time表示一個時間間隔,從上一次收到可到達性認證時間t到t+reachable_time之間,都認為鄰居可達 //reachable_time = base_reachable_time/2 + rand(0,base_reachable_time) tbl->parms.reachable_time = neigh_rand_reach_time(tbl->parms.base_reachable_time);// //每個鄰居協議私有的neighbour緩存 if (!tbl->kmem_cachep) tbl->kmem_cachep = kmem_cache_create(tbl->id, tbl->entry_size, 0, SLAB_HWCACHE_ALIGN, NULL, NULL); if (!tbl->kmem_cachep) panic("cannot create neighbour cache"); //per-cpu統計變量 tbl->stats = alloc_percpu(struct neigh_statistics); if (!tbl->stats) panic("cannot create neighbour cache statistics"); //1.鄰居項和代理項的hash_buckets以鏈表指針的形式組織 //2.每一個bucket的鏈表頭為struct neighbour的指針 //3.neighbour通過next域鏈接在同一個bucket //鄰居項hash表,2個bucket tbl->hash_mask = 1; //hash_buckets組織成指針數組的形式,每一個struct neighbour通過next域進行鏈接 tbl->hash_buckets = neigh_hash_alloc(tbl->hash_mask + 1); //代理 phsize = (PNEIGH_HASHMASK + 1) * sizeof(struct pneigh_entry *); tbl->phash_buckets = kmalloc(phsize, GFP_KERNEL); if (!tbl->hash_buckets || !tbl->phash_buckets) panic("cannot allocate neighbour cache hashes"); memset(tbl->phash_buckets, 0, phsize); //hash函數的隨機性 get_random_bytes(&tbl->hash_rnd, sizeof(tbl->hash_rnd)); //初始化協議垃圾回收定時器 rwlock_init(&tbl->lock); init_timer(&tbl->gc_timer); tbl->gc_timer.data = (unsigned long)tbl; tbl->gc_timer.function = neigh_periodic_timer;//異步回收函數 tbl->gc_timer.expires = now + 1; add_timer(&tbl->gc_timer); //代理相關的定時器 init_timer(&tbl->proxy_timer); tbl->proxy_timer.data = (unsigned long)tbl; tbl->proxy_timer.function = neigh_proxy_process; skb_queue_head_init(&tbl->proxy_queue); tbl->last_flush = now;//neigh_forced_gc最近執行一次的時間 tbl->last_rand = now + tbl->parms.reachable_time * 20; write_lock(&neigh_tbl_lock); tbl->next = neigh_tables;//將協議掛接到neigh_tables頭部,所有的鄰居協議通過neigh_tables列表鏈接起來 neigh_tables = tbl; write_unlock(&neigh_tbl_lock); } //為鄰居協議分配hash表 1.2 static struct neighbour **neigh_hash_alloc(unsigned int entries) { unsigned long size = entries * sizeof(struct neighbour *); struct neighbour **ret; //大小少於一頁 if (size <= PAGE_SIZE) { ret = kmalloc(size, GFP_ATOMIC); } else { //多於一頁,分配連續的物理頁 ret = (struct neighbour **) __get_free_pages(GFP_ATOMIC, get_order(size)); } if (ret) memset(ret, 0, size); return ret; } //鄰居項的創建是由事件驅動的,當係統需要一個鄰居項,並且緩存不命中時,就創建一個鄰居項 // 創建新鄰居項的時機: // 1.傳輸請求,當有一個向一台l2地址未知的主機傳輸請求時,就需要對該地址進行解析, // 當目的主機不是與發送方直連時,解析的l2地址就是下一條網關的地址 // 2.收到solicitation請求,由於發送請求的主機在請求封包中有自己的識別信息,收到該 // 請求的主機會假定即將有兩個係統之間的通信 // 3.手工添加,ip neigh add命令創建一個鄰居項 // 創建新鄰居項的過程: // 1.通過slab緩存分配struct neighbour結構 // 2.通過neigh_table->constructor協議提供的初始化函數,初始化新鄰居項 // 3.在neigh_table->constructor會設置neighbour->params,使用協議默認提供,或者驅動 // 提供的協議調整參數(neigh_params),通過neighbour->params->setup更新鄰居項行為。 // 4.更新鄰居項可到達性確認時間戳,使新鄰居項比正常鄰居項提前半個base_reachable_time到期 // ,從而對該鄰居項進行一次主動的可到達性確認 // 5.添加到neigh_table->hash_buckets中,可能會引起hash表的擴容。 2.1 struct neighbour *neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev) { u32 hash_val; int key_len = tbl->key_len; int error; //從鄰居協議的neighbour緩存中分配一個鄰居項 struct neighbour *n1, *rc, *n = neigh_alloc(tbl); if (!n) { rc = ERR_PTR(-ENOBUFS); goto out; } //拷貝hash的key到鄰居項中,arp為ip地址 memcpy(n->primary_key, pkey, key_len); n->dev = dev;//到達該鄰居項使用的接口 dev_hold(dev);//增加該接口的引用計數 //如果鄰居協議提供了鄰居項的初始化函數,則調用 if (tbl->constructor && (error = tbl->constructor(n)) < 0) { rc = ERR_PTR(error); goto out_neigh_release; } //如果設備驅動提供了初始化鄰居項的回調函數,則調用 if (n->parms->neigh_setup && (error = n->parms->neigh_setup(n)) < 0) { rc = ERR_PTR(error); goto out_neigh_release; } //鄰居項可到達性確認的時間戳 //當前時間-base_reachable_time/2,已保證新鄰居項比正常鄰居項提前base_reachable_time/2時間到期,執行可到達性檢測 n->confirmed = jiffies - (n->parms->base_reachable_time << 1); write_lock_bh(&tbl->lock); //如果協議表中的鄰居數大於協議表hash表的長度 if (atomic_read(&tbl->entries) > (tbl->hash_mask + 1)) neigh_hash_grow(tbl, (tbl->hash_mask + 1) << 1);//hash表增加一半當前大小 //使用協議提供的hash函數,對key,dev做hash hash_val = tbl->hash(pkey, dev) & tbl->hash_mask;//保證最終範圍在hash表大小內 if (n->parms->dead) {//如果此鄰居項的配置信息標記為不在使用 rc = ERR_PTR(-EINVAL); goto out_tbl_unlock;//則不添加新鄰居項 } //遍曆hash表中的bucket鏈表 for (n1 = tbl->hash_buckets[hash_val]; n1; n1 = n1->next) { //是否已存在該鄰居 if (dev == n1->dev && !memcmp(n1->primary_key, pkey, key_len)) { neigh_hold(n1);//增加已有鄰居的引用計數 rc = n1; goto out_tbl_unlock; } } //將鄰居添加到bucket的list中 n->next = tbl->hash_buckets[hash_val]; tbl->hash_buckets[hash_val] = n; n->dead = 0;//標記此neighbour為可使用 neigh_hold(n);//增加neighbour的引用計數 write_unlock_bh(&tbl->lock); NEIGH_PRINTK2("neigh %p is created.\n", n); rc = n; out: return rc; out_tbl_unlock: write_unlock_bh(&tbl->lock); out_neigh_release: neigh_release(n); goto out; } //刪除鄰居項 // 鄰居項被刪除的原因: // 1.內核企圖向一個不可到達的主機發送封包,當鄰居子係統察覺到傳輸不成功, // 就將neighbour標記為NUD_FAILED狀態,可以是異步垃圾回收機製清除該neighbour. // 2.與該鄰居結構關聯的主機的l2地址改變了,但它的l3地址還是原來的,一個 // 過時的鄰居項必須進入NUD_FAILED態,然後重新創建一個新鄰居項. // 鄰居項被刪除的過程: // 1.停用與鄰居項有關的定時器 // 2.更新與鄰居項有關的l2幀頭緩存,設置器hh_cache->output為blackhole函數, // 丟棄通過此l2幀頭緩存發送的數據幀,遞減hh_cache的引用計數,如果為0,則釋放 // 3.調用鄰居項提供的刪除回調函數,在neigh_table->constructor或者neigh_table->params->setup中 // 被設置。 // 4.清空該鄰居項的arp_queue,此隊列中緩存的skb為與此鄰居相關的,l2地址待解析的skb。 // 5.更新鄰居協議的鄰居項個數。 3.1 void neigh_destroy(struct neighbour *neigh) { struct hh_cache *hh; //增加協議統計信息,鄰居項被刪除的次數 NEIGH_CACHE_STAT_INC(neigh->tbl, destroys); //鄰居項沒有被標記為不在使用,則說明內核有錯BUG() if (!neigh->dead) { printk(KERN_WARNING "Destroying alive neighbour %p\n", neigh); dump_stack(); return; } //刪除鄰居項的定時器 if (neigh_del_timer(neigh)) printk(KERN_WARNING "Impossible event.\n"); //該鄰居項緩存的l2幀頭鏈表 //從一台主機發往另一台主機的所有封包的l2幀頭都是相同的,當向一個目的地發送第一個封包後, //驅動程序就將其l2幀頭保存在hh_cache的結構中,如果下一次有相同的封包發往 //同一個鄰居,則隻需從緩存中拷貝一個幀頭即可,每個鄰居項可以緩存多個幀頭,但通常隻緩存一個。 while ((hh = neigh->hh) != NULL) { neigh->hh = hh->hh_next; //將l2地址緩存從列表上摘下來 hh->hh_next = NULL; write_lock_bh(&hh->hh_lock); //替換此l2地址的輸出函數為neigh_blackhole,以丟棄發往此目的l2地址的所有skb hh->hh_output = neigh_blackhole; write_unlock_bh(&hh->hh_lock); if (atomic_dec_and_test(&hh->hh_refcnt)) kfree(hh); } //鄰居項提供了刪除函數 if (neigh->ops && neigh->ops->destructor) (neigh->ops->destructor)(neigh); //清空該鄰居項的arp緩存隊列 skb_queue_purge(&neigh->arp_queue); //釋放對dev的引用 dev_put(neigh->dev); //釋放對協議調整參數的引用 neigh_parms_put(neigh->parms); NEIGH_PRINTK2("neigh %p is destroyed.\n", neigh); //遞減協議中鄰居項的個數 atomic_dec(&neigh->tbl->entries); //釋放緩存 kmem_cache_free(neigh->tbl->kmem_cachep, neigh); } // 查詢l3地址對應的鄰居項 // 參數: // tbl,被查詢的協議表,arp為arp_tbl // pkey,查詢的key,arp為ip // dev,到達此鄰居項使用的出口設備 4.1 struct neighbour *neigh_lookup(struct neigh_table *tbl, const void *pkey, struct net_device *dev) { struct neighbour *n; int key_len = tbl->key_len; //hash pkey u32 hash_val = tbl->hash(pkey, dev) & tbl->hash_mask; //更新統計信息,遞增查詢個數 NEIGH_CACHE_STAT_INC(tbl, lookups); read_lock_bh(&tbl->lock); //找到對應的bucket頭鏈表 for (n = tbl->hash_buckets[hash_val]; n; n = n->next) { //遍曆鏈表中的每個元素 if (dev == n->dev && !memcmp(n->primary_key, pkey, key_len)) { //增加鄰居項的引用計數 neigh_hold(n); //更新統計信息,遞增命中率 NEIGH_CACHE_STAT_INC(tbl, hits); break; } } read_unlock_bh(&tbl->lock); return n; } //用於更新一個鄰居項 /* Generic update routine. -- lladdr is new lladdr or NULL, if it is not supplied. -- new is new state. -- flags NEIGH_UPDATE_F_OVERRIDE allows to override existing lladdr, if it is different. NEIGH_UPDATE_F_WEAK_OVERRIDE will suspect existing "connected" lladdr instead of overriding it if it is different. It also allows to retain current state if lladdr is unchanged. NEIGH_UPDATE_F_ADMIN means that the change is administrative. NEIGH_UPDATE_F_OVERRIDE_ISROUTER allows to override existing NTF_ROUTER flag. NEIGH_UPDATE_F_ISROUTER indicates if the neighbour is known as a router. Caller MUST hold reference count on the entry. */ //可能的更新策略: //1.新狀態沒有可用的l2地址,刪除定時器,懷疑鄰居項,退出 //2.新狀態有可用的l2地址: // 2.1舊狀態有可用l2地址, // 2.1.1新地址與舊地址不同並且沒提供更新標誌 // 2.1.1.1 但允許懷疑鄰居項並且就狀態可達,仍然使用舊地址,標示鄰居項需要進行可達性確認 // 2.1.1.2 不允許懷疑鄰居項並且就狀態不可達,退出 // 2.2新舊地址相同,並且新狀態為進行可到達性確認,允許去懷疑或者就狀態為可到達,依然保持就狀態 //3.新舊狀態不同,刪除定時器,如果新狀態需要定時器,更新定時器。更新鄰居狀態 //4.新地址與舊地址不同,拷貝新l2地址到鄰居項的ha字段,更新l2緩存 //5.如果新舊狀態相同,退出 //6.更新鄰居項的輸出回調函數 //7.如果舊狀態沒有可用的l2地址 // 7.1 當前狀態有可用的l2地址,將arp_queue中的所有skb發送 // 7.2 否則,清空arp_queue 5.1 int neigh_update(struct neighbour *neigh, const u8 *lladdr, u8 new, u32 flags) { u8 old; int err; ... struct net_device *dev; int update_isrouter = 0; //獲取此鄰居項的鎖 write_lock_bh(&neigh->lock); //此鄰居項的狀態 dev = neigh->dev; old = neigh->nud_state; err = -EPERM; //如果沒有admin,並且就狀態為手動設置 if (!(flags & NEIGH_UPDATE_F_ADMIN) && (old & (NUD_NOARP | NUD_PERMANENT))) goto out;//退出 //新狀態沒有可用的l2地址 if (!(new & NUD_VALID)) { neigh_del_timer(neigh);//刪除鄰居項的定時器 if (old & NUD_CONNECTED)//舊狀態為可達的 neigh_suspect(neigh);//懷疑鄰居項 neigh->nud_state = new;//更新狀態為新狀態 err = 0; goto out; } //運行到此處,說明有新狀態有可用的l2地址,開始檢查此地址 //設備沒有提供l2地址 if (!dev->addr_len) { //設置提供的l2地址為鄰居項的l2地址 lladdr = neigh->ha; } else if (lladdr) {//提供了l2地址 //如果舊狀態有l2地址 if ((old & NUD_VALID) && !memcmp(lladdr, neigh->ha, dev->addr_len))//比較新舊地址 lladdr = neigh->ha;//如果一樣的話,lladdr指向鄰居的l2的地址 } else { err = -EINVAL;//沒有提供地址 if (!(old & NUD_VALID))//並且舊狀態沒有可用的l2地址 goto out;//返回錯誤 lladdr = neigh->ha; } //新狀態為可達狀態 if (new & NUD_CONNECTED) neigh->confirmed = jiffies;//更新鄰居項上一次被確認的時間為當前 neigh->updated = jiffies;//上一次更新時間為當前 err = 0; update_isrouter = flags & NEIGH_UPDATE_F_OVERRIDE_ISROUTER;//是否允許去更新NTF_ROUTER if (old & NUD_VALID) {//舊狀態有l2地址 if (lladdr != neigh->ha && !(flags & NEIGH_UPDATE_F_OVERRIDE)) {//(設備具有l2地址,或者新地址與舊地址不相同) && 沒有提供更新地址的指示標誌 update_isrouter = 0; if ((flags & NEIGH_UPDATE_F_WEAK_OVERRIDE) &&//允許懷疑 (old & NUD_CONNECTED)) {//舊狀態可達 lladdr = neigh->ha;//改變提供的地址為鄰居項的舊地址 new = NUD_STALE;//指示鄰居項需要進行可達性確認 } else//新舊地址不同,但是不允許更新,則直接退出 goto out; } else { if (lladdr == neigh->ha && new == NUD_STALE &&//新地址等於舊地址,並且新狀態要求進行可達性確認 ((flags & NEIGH_UPDATE_F_WEAK_OVERRIDE) ||//允許去懷疑就狀態 (old & NUD_CONNECTED))//舊狀態為可達的 ) new = old;//新狀態改變為舊狀態 } } if (new != old) {//如果新舊狀態不同 neigh_del_timer(neigh);//刪除定時器 if (new & NUD_IN_TIMER) { neigh_hold(neigh);//添加新的定時器 neigh->timer.expires = jiffies + ((new & NUD_REACHABLE) ? neigh->parms->reachable_time : 0); add_timer(&neigh->timer); } neigh->nud_state = new; } if (lladdr != neigh->ha) {//新地址與舊地址不同 memcpy(&neigh->ha, lladdr, dev->addr_len);//拷貝新l2地址到鄰居項的ha字段 neigh_update_hhs(neigh);//更新l2地址緩存 if (!(new & NUD_CONNECTED)) neigh->confirmed = jiffies - (neigh->parms->base_reachable_time << 1); } if (new == old)//如果新舊狀態相同 goto out; if (new & NUD_CONNECTED)//新狀態表示鄰居可達 neigh_connect(neigh);// else neigh_suspect(neigh);//否則懷疑鄰居項 if (!(old & NUD_VALID)) {//如果舊狀態沒有可用的l2地址 struct sk_buff *skb; while (neigh->nud_state & NUD_VALID &&//並且新地址為有可用的l2地址 (skb = __skb_dequeue(&neigh->arp_queue)) != NULL) {//從arp_queue中取出所有等待l2地址解析的skb struct neighbour *n1 = neigh; write_unlock_bh(&neigh->lock); if (skb->dst && skb->dst->neighbour)//如果skb有路由緩存 n1 = skb->dst->neighbour; n1->output(skb);//則通過鄰居項提供的output完成傳輸 write_lock_bh(&neigh->lock); } skb_queue_purge(&neigh->arp_queue);//否則清空所有待解析l2地址的skb } out: if (update_isrouter) { neigh->flags = (flags & NEIGH_UPDATE_F_ISROUTER) ? (neigh->flags | NTF_ROUTER) : (neigh->flags & ~NTF_ROUTER); } write_unlock_bh(&neigh->lock); #ifdef CONFIG_ARPD if (notify && neigh->parms->app_probes) neigh_app_notify(neigh); #endif return err; } //鄰居狀態為懷疑狀態,即需要可達性確認 5.2 static void neigh_suspect(struct neighbour *neigh) { struct hh_cache *hh; NEIGH_PRINTK2("neigh %p is suspected.\n", neigh); //更新鄰居的輸出函數為最通用的輸出函數 neigh->output = neigh->ops->output; //更新所有l2緩存的輸出函數為最通用的輸出函數 for (hh = neigh->hh; hh; hh = hh->hh_next) hh->hh_output = neigh->ops->output; } //鄰居狀態為可達狀態 5.3 static void neigh_connect(struct neighbour *neigh) { struct hh_cache *hh; NEIGH_PRINTK2("neigh %p is connected.\n", neigh); //使用設備驅動程序填充l2頭的輸出方式 neigh->output = neigh->ops->connected_output; //使用幀頭緩存的方式,快速發送報文 for (hh = neigh->hh; hh; hh = hh->hh_next) hh->hh_output = neigh->ops->hh_output; } //當設備的硬件地址發生改變時,調用此函數 //標記使用此設備的鄰居項不在使用,並且停用此鄰居項的所有定時器 6.1 void neigh_changeaddr(struct neigh_table *tbl, struct net_device *dev) { int i; write_lock_bh(&tbl->lock); //鄰居協議表hash表的長度 for (i=0; i <= tbl->hash_mask; i++) { struct neighbour *n, **np; //遍曆每一個bucket np = &tbl->hash_buckets[i]; while ((n = *np) != NULL) { //遍曆直到傳入設備等於鄰居協議所在的設備 if (dev && n->dev != dev) { np = &n->next; continue; } *np = n->next; write_lock_bh(&n->lock); //將鄰居標記為dead n->dead = 1; //刪除鄰居的定時器 neigh_del_timer(n);//垃圾回收進程負責處理停用項 write_unlock_bh(&n->lock); //遞減引用計數 neigh_release(n); } } write_unlock_bh(&tbl->lock); } //查找代理地址數據庫 //pkey為目的ip //dev為用於代理ip的設備 //creat指示在查找失敗時,是否創建 7.1 struct pneigh_entry * pneigh_lookup(struct neigh_table *tbl, const void *pkey, struct net_device *dev, int creat) { struct pneigh_entry *n; int key_len = tbl->key_len; u32 hash_val = *(u32 *)(pkey + key_len - 4);//通用性,對於arp,hash_val=ip //hash計算 hash_val ^= (hash_val >> 16); hash_val ^= hash_val >> 8; hash_val ^= hash_val >> 4; hash_val &= PNEIGH_HASHMASK; read_lock_bh(&tbl->lock); //代理hash表 for (n = tbl->phash_buckets[hash_val]; n; n = n->next) { //被代理的鄰居的l3地址和提供的目標l3地址相同 if (!memcmp(n->key, pkey, key_len) && (n->dev == dev || !n->dev)) {//當鄰居項放在表示代理hash表中,neighbour->dev的語義就發生了變化 read_unlock_bh(&tbl->lock);//在一般情況下,其表示通過此設備可以訪問到這個鄰居,轉義後,表示,由哪個設備代理此目的ip,到達此目的 goto out;//ip的設備,通過路由表查找得到 } } read_unlock_bh(&tbl->lock); n = NULL; //沒有找到代理鄰居項 if (!creat) goto out; n = kmalloc(sizeof(*n) + key_len, GFP_KERNEL); if (!n) goto out; //創建鄰居項,並添加到hash表中 memcpy(n->key, pkey, key_len); n->dev = dev; if (dev) dev_hold(dev); if (tbl->pconstructor && tbl->pconstructor(n)) { if (dev) dev_put(dev); kfree(n); n = NULL; goto out; } write_lock_bh(&tbl->lock); n->next = tbl->phash_buckets[hash_val]; tbl->phash_buckets[hash_val] = n; write_unlock_bh(&tbl->lock); out: return n; } //將代理的solicitation放入延遲隊列,延遲處理 8.1 void pneigh_enqueue(struct neigh_table *tbl, struct neigh_parms *p, struct sk_buff *skb) { unsigned long now = jiffies; unsigned long sched_next = now + (net_random() % p->proxy_delay);//延時時間 if (tbl->proxy_queue.qlen > p->proxy_qlen) {//延遲隊列超過限製 kfree_skb(skb);//直接丟棄封包 return; } skb->stamp.tv_sec = LOCALLY_ENQUEUED;//標示入隊時間,在出隊後,通過此字段可以判斷出此skb是否曾經被入過隊,防止循環入隊 skb->stamp.tv_usec = sched_next; spin_lock(&tbl->proxy_queue.lock); if (del_timer(&tbl->proxy_timer)) {// if (time_before(tbl->proxy_timer.expires, sched_next)) sched_next = tbl->proxy_timer.expires;//設置定時器的到期時間為需要最早被處理solicitation的時間,因此通過一個定時器就可以管理整個隊列 } dst_release(skb->dst); skb->dst = NULL; dev_hold(skb->dev); __skb_queue_tail(&tbl->proxy_queue, skb);//放入tbl->proxy_queue中 mod_timer(&tbl->proxy_timer, sched_next); spin_unlock(&tbl->proxy_queue.lock); } //被動學習 //接收到ARP_REQUEST封包的目的主機從請求封包中也知道了發送方的地址 //該函數會負責檢查是否有一個鄰居項和請求者關聯,然後就會更新這個存在的鄰居項,如果不存在,則創建一個新的鄰居項 9.1 struct neighbour *neigh_event_ns(struct neigh_table *tbl, u8 *lladdr, void *saddr, struct net_device *dev) { //查找鄰居項 struct neighbour *neigh = __neigh_lookup(tbl, saddr, dev, lladdr || !dev->addr_len); //如果存在,則更新為NUD_STALE狀態,表示有l2地址可用 if (neigh) neigh_update(neigh, lladdr, NUD_STALE, NEIGH_UPDATE_F_OVERRIDE); return neigh; }
最後更新:2017-04-03 15:21:56