NETDEV 協議 五

這篇是關於IP層協議接收報文時的處理,重點說明了路由表的查找,以及IP分片重組。

ip_rcv進入IP層報文接收函數
      丟棄掉不是發往本機的報文,skb->pkt_type在網卡接收報文處理以太網頭時會根據dst mac設置,協議棧的書會講不是發往本機的廣播報文會在二層被丟棄,實際上丟棄是發生在進入上層之初。

if (skb->pkt_type == PACKET_OTHERHOST)
 goto drop;

      在取IP報頭時要注意可能帶有選項,因此報文長度應當以iph->ihl * 4爲準。這裏就需要嘗試兩次,第一次嘗試sizeof(struct iphdr),只是爲了確保skb還可以容納標準的報頭(即20字節),然後可以ip_hdr(skb)得到報頭;第二次嘗試ihl * 4,這纔是報文的真正長度,然後重新調用ip_hdr(skb)來得到報頭。兩次嘗試pull後要重新調用ip_hdr()的原因是pskb_may_pull()可能會調用__pskb_pull_tail()來改現現有的skb結構。

 
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
 goto inhdr_error;
iph = ip_hdr(skb);
……
if (!pskb_may_pull(skb, iph->ihl*4))
 goto inhdr_error;
iph = ip_hdr(skb);

      獲取到IP報頭後經過一些檢查,獲取到報文的總長度len = iph->tot_len,此時調用pskb_trim_rcsum()去除多餘的字節,即大於len的。

if (pskb_trim_rcsum(skb, len)) {
 IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
 goto drop;
}

      然後調用ip_rcv_finish()繼續IP層的處理,ip_rcv()可以看成是查找路由前的IP層處理,接下來的ip_rcv_finish()會查找路由表,兩者間調用插入的netfilter

return NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);


進入ip_rcv_finish函數
      ip_rcv_finish()主要工作是完成路由表的查詢,決定報文經過IP層處理後,是繼續向上傳遞,還是進行轉發,還是丟棄。
剛開始沒有進行路由表查詢,所以還沒有相應的路由表項:skb_dst(skb) == NULL。則在路由表中查找ip_route_input(),

 
if (skb_dst(skb) == NULL) {
 int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos,
     skb->dev);
 if (unlikely(err)) {
  if (err == -EHOSTUNREACH)
   IP_INC_STATS_BH(dev_net(skb->dev),
     IPSTATS_MIB_INADDRERRORS);
  else if (err == -ENETUNREACH)
   IP_INC_STATS_BH(dev_net(skb->dev),
     IPSTATS_MIB_INNOROUTES);
  goto drop;
 }
}

      通過路由表查找,我們知道:
         - 如果是丟棄的報文,則直接drop;
         - 如果是不能接收或轉發的報文,則input = ip_error
         - 如果是發往本機報文,則input = ip_local_deliver;
         - 如果是廣播報文,則input = ip_local_deliver;
         - 如果是組播報文,則input = ip_local_deliver;
         - 如果是轉發的報文,則input = ip_forward;
      在ip_rcv_finish()最後,會調用查找到的路由項_skb_dst->input()繼續向上傳遞:

return dst_input(skb);

      具體看下各種情況下的報文傳遞,如果是丟棄的報文,則報文被釋放,並從IP協議層返回,完成此次報文傳遞流程。

;  
drop:
 kfree_skb(skb);
 return NET_RX_DROP;

      如果是不能處理的報文,則執行ip_error,根據error類型發送相應的ICMP錯誤報文。

static int ip_error(struct sk_buff *skb)
{
 struct rtable *rt = skb_rtable(skb);
 unsigned long now;
 int code;

 switch (rt->u.dst.error) {
  case EINVAL:
  default:
   goto out;
  case EHOSTUNREACH:
   code = ICMP_HOST_UNREACH;
   break;
  case ENETUNREACH:
   code = ICMP_NET_UNREACH;
   IP_INC_STATS_BH(dev_net(rt->u.dst.dev),
     IPSTATS_MIB_INNOROUTES);
   break;
  case EACCES:
   code = ICMP_PKT_FILTERED;
   break;
 }

 now = jiffies;
 rt->u.dst.rate_tokens += now - rt->u.dst.rate_last;
 if (rt->u.dst.rate_tokens > ip_rt_error_burst)
  rt->u.dst.rate_tokens = ip_rt_error_burst;
 rt->u.dst.rate_last = now;
 if (rt->u.dst.rate_tokens >= ip_rt_error_cost) {
  rt->u.dst.rate_tokens -= ip_rt_error_cost;
  icmp_send(skb, ICMP_DEST_UNREACH, code, 0);
 }

out: kfree_skb(skb);
 return 0;
}

      如果是主機可以接收報文,則執行ip_local_deliver。ip_local_deliver在向上傳遞前,會對分片的IP報文進行組包,因爲IP層協議會對過大的數據包分片,在接收時,就要進行重組,而重組的操作就是在這裏進行的。IP報頭的16位偏移字段frag_off是由3位的標誌(CE,DF,MF)和13的偏移量組成。如果收到了分片的IP報文,如果是最後一片,則MF=0且offset!=0;如果不是最後一片,則MF=1。
      在這種情況下會執行ip_defrag來處理分片的IP報文,如果不是最後一片,則將該報文添加到ip4_frags中保留下來,並return 0,此次數據包接收完成;如果是最後一片,則取出之前收到的分片重組成新的skb,此時ip_defrag返回值爲0,skb被重置爲完整的數據包,然後繼續處理,之後調用ip_local_deliver_finish處理重組後的數據包。

if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {
 if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
  return 0;
}

      下面來看下ip_defrag()函數,主體就是下面的代碼段。它首先用ip_find()查找IP分片,並返回(如果沒有則創建),然後用ip_frag_queue()將新分片加入,關於IP分片的處理,在後面的IP分片中有詳細描述。

if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) {
 int ret;

 spin_lock(&qp->q.lock);

 ret = ip_frag_queue(qp, skb);

 spin_unlock(&qp->q.lock);
 ipq_put(qp);
 return ret;
}

      然後會調用ip_local_deliver_finish()完成IP協議層的傳遞,兩者調用間依然有netfilter,這是查找完路由表繼續向上傳遞的中間點。

NF_HOOK(PF_INET, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);

      在ip_local_deliver_finish()中會完成IP協議層處理,再交由上層協議模塊處理:ICMP、IGMP、UDP、TCP。在ip_local_deliver_finish函數中,由於IP報頭已經處理完,剔除IP報頭,並設置skb->transport_header指向傳輸層協議報頭位置。

 
__skb_pull(skb, ip_hdrlen(skb));
skb_reset_transport_header(skb);

      protocol是IP報頭中的的上層協議號,以它在inet_protos哈希表中查找處理protocol的協議模塊,取出得到ipprot。

hash = protocol & (MAX_INET_PROTOS - 1);
ipprot = rcu_dereference(inet_protos[hash]);

      而關於inet_protos,它的數據結構是哈希表,用來存儲IP層上的協議,包括傳輸層協議和3.5層協議,它在IP協議模塊加載時被添加。

 if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
  printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");
 if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
  printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");
 if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
  printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n");
#ifdef CONFIG_IP_MULTICAST
 if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
  printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n");
#endif

      然後通過調用handler交由上層協議處理,至此,IP層協議處理完成。

 
ret = ipprot->handler(skb);


IP分片
      在收到IP分片時,會暫時存儲到一個哈希表ip4_frags中,它在IP協議模塊加載時初始化,inet_init() -> ipfrag_init()。要留意的是ip4_frag_match用於匹配IP分片是否屬於同一個報文;ip_expire用於在IP分片超時時進行處理。

void __init ipfrag_init(void)
{
 ip4_frags_ctl_register();
 register_pernet_subsys(&ip4_frags_ops);
 ip4_frags.hashfn = ip4_hashfn;
 ip4_frags.constructor = ip4_frag_init;
 ip4_frags.destructor = ip4_frag_free;
 ip4_frags.skb_free = NULL;
 ip4_frags.qsize = sizeof(struct ipq);
 ip4_frags.match = ip4_frag_match;
 ip4_frags.frag_expire = ip_expire;
 ip4_frags.secret_interval = 10 * 60 * HZ;
 inet_frags_init(&ip4_frags);
}

      當收到一個IP分片,首先用ip_find()查找IP分片,實際上就是從ip4_frag表中取出相應項。這裏的哈希值是由IP報頭的(標識,源IP,目的IP,協議號)得到的。

hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol);
q = inet_frag_find(&net->ipv4.frags, &ip4_frags, &arg, hash);


inet_frag_find實現直正的查找
      根據hash值取得ip4_frag->hash[hash]項 – inet_frag_queue,它是一個隊列,然後遍歷該隊列,當net, id, saddr, daddr, protocol, user相匹配時,就是要找的IP分片。如果沒有匹配的,則調用inet_frag_create創建它。

struct inet_frag_queue *inet_frag_find(struct netns_frags *nf,
  struct inet_frags *f, void *key, unsigned int hash)
 __releases(&f->lock)
{
 struct inet_frag_queue *q;
 struct hlist_node *n;

 hlist_for_each_entry(q, n, &f->hash[hash], list) {
  if (q->net == nf && f->match(q, key)) {
   atomic_inc(&q->refcnt);
   read_unlock(&f->lock);
   return q;
  }
 }
 read_unlock(&f->lock);

 return inet_frag_create(nf, f, key);
}


inet_frag_create創建一個IP分片隊列ipq,並插入相應隊列中。
      首先分配空間,真正分配空間的是inet_frag_alloc中的q = kzalloc(f->qsize, GFP_ATOMIC);其中f->qsize = sizeof(struct ipq),也就是說分配了ipq大小空間,但返回的卻是struct inet_frag_queue q結構,原因在於inet_frag_queue是ipq的首個屬性,它們兩者的聯繫如下圖。 

static struct inet_frag_queue *inet_frag_create(struct netns_frags *nf,
  struct inet_frags *f, void *arg)
{
 struct inet_frag_queue *q;

 q = inet_frag_alloc(nf, f, arg);
 if (q == NULL)
  return NULL;

 return inet_frag_intern(nf, q, f, arg);
}

      在分配並初始化空間後,由inet_frag_intern完成插入動作,首先還是根據(標識,源IP,目的IP,協議號)先成hash值,這裏的qp_in即之前的q。

hash = f->hashfn(qp_in);

      然後新創建的隊列qp(即上面的qp_in)插入到hash表(即ip4_frags->hash)和net->ipv4.frags中,並增加隊列qp的引用計數,net中的隊列nqueues統計數。至此,IP分片的創建過程完成。

atomic_inc(&qp->refcnt);
hlist_add_head(&qp->list, &f->hash[hash]);
list_add_tail(&qp->lru_list, &nf->lru_list);
nf->nqueues++;


ip_frag_queue實現將IP分片加入隊列中
      首先獲取該IP分片偏移位置offset,和IP分片偏移結束位置end,其中skb->len – ihl表示IP分片的報文長度,三者間關係即爲end = offset + skb->len – ihl。

offset = ntohs(ip_hdr(skb)->frag_off);
flags = offset & ~IP_OFFSET;
offset &= IP_OFFSET;
offset <<= 3;  /* offset is in 8-byte chunks */
ihl = ip_hdrlen(skb);
/* Determine the position of this fragment. */
end = offset + skb->len - ihl;

      如果該IP分片是最後一片(MF=0,offset!=0),即設置q.last_iin |= INET_FRAG_LAST_IN,表示收到了最後一個分片,qp->q.len = end,此時q.len是整個IP報文的總長度。

if ((flags & IP_MF) == 0) {
 if (end < qp->q.len ||
     ((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len))
  goto err;
 qp->q.last_in |= INET_FRAG_LAST_IN;
 qp->q.len = end;
}

      如果該IP分片不是最後一片(MF=1),當end不是8字節倍數時,通過end &= ~7處理爲8字節整數倍(但此時會忽略掉多出的字節,如end=14 => end=8);然後如果該分片更靠後,則q.len = end。

else {
 if (end&7) {
  end &= ~7;
  if (skb->ip_summed != CHECKSUM_UNNECESSARY)
   skb->ip_summed = CHECKSUM_NONE;
 }
 if (end > qp->q.len) {
  /* Some bits beyond end -> corruption. */
  if (qp->q.last_in & INET_FRAG_LAST_IN)
   goto err;
  qp->q.len = end;
 }
}

      查找q.fragments鏈表,找到該IP分片要插入的位置,這裏的q.fragments就是struct sk_buff類型,即各個IP分片skb都會插入到該鏈表中,插入的位置按偏移順序由小到大排列,prev表示插入的前一個IP分片,next表示插入的後一個IP分片。

prev = NULL;
for (next = qp->q.fragments; next != NULL; next = next->next) {
 if (FRAG_CB(next)->offset >= offset)
  break; /* bingo! */
 prev = next;
}

      然後將skb插入到鏈表中,要注意fragments爲空和不爲空的情形,在下圖中給出。

 
skb->next = next;
if (prev)
 prev->next = skb;
else
 qp->q.fragments = skb;

      增加q.meat計數,表示已收到的IP分片的總長度;如果offset爲0,則表明是第一個IP分片,設置q.last_in |= INET_FRAG_FIRST_IN。

qp->q.meat += skb->len;
if (offset == 0)
 qp->q.last_in |= INET_FRAG_FIRST_IN;

      最後當滿足一定條件時,進行IP重組。當收到了第一個和最後一個IP分片,且收到的IP分片的最大長度等於收到的IP分片的總長度時,表明所有的IP分片已收集齊,調用ip_frag_reasm重組包。具體的,當收到第一個分片(offset=0且MF=1)時設置q.last_in |= INET_FRAG_FIRST_IN;當收到最後一個分片(offset != 0且MF=0)時設置q.last_in |= INET_FRAG_LAST_IN。meat和len的區別在於,IP是不可靠傳輸,到達的IP分片不能保證順序,而meat表示到達IP分片的總長度,len表示到達的IP分片中偏移最大的長度。所以當滿足上述條件時,IP分片一定是收集齊了的。

if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) && qp->q.meat == qp->q.len)
 return ip_frag_reasm(qp, prev, dev);

      以下圖爲例,原始IP報文分成了4片發送,假設收到了1, 3, 4分片,則此時q.last_in = INET_FRGA_FIRST_IN | INET_FRAG_LAST_IN,q.meat = 30,q.len = 50。表明還未收齊IP分片,等待IP分片2的到來。 


      這裏還有一些特殊情況需要處理,它們可能是重新分片或傳輸時錯誤造成的,那就是IP分片互相間有重疊。爲了避免這種情況發生,在插入IP分片前會處理掉這些重疊。
      第一種重疊是與前個分片重疊,即該分片的的偏移是從前個分片的範圍內開始的,這種情況下i表示重疊部分的大小,offset+=i則將該分片偏移後移i個長度,從而與前個分片隔開,而且減少len,pskb_pull(skb, i),見下圖圖示。

if (prev) {
 int i = (FRAG_CB(prev)->offset + prev->len) - offset;

 if (i > 0) {
  offset += i;
  err = -EINVAL;
  if (end <= offset)
   goto err;
  err = -ENOMEM;
  if (!pskb_pull(skb, i))
   goto err;
  if (skb->ip_summed != CHECKSUM_UNNECESSARY)
   skb->ip_summed = CHECKSUM_NONE;
 }
}

      第二種重疊是與後個分片重疊,即該分片的的結束位置在後個分片的範圍內,這種情況下i表示重疊部分的大小。後片重疊稍微複雜點,被i重疊的部分都要刪除掉,如果i比較大,超過了分片長度,則整個分片都被覆蓋,從q.fragments鏈表中刪除。使用while處理i覆蓋多個分片的情況。  

while (next && FRAG_CB(next)->offset < end)

    當整個分片被覆蓋掉,從q.fragments中刪除,並且由於減少了分片總長度,所以q.meat要減去刪除分片的長度。

else {
 struct sk_buff *free_it = next;
 next = next->next;
 if (prev)
  prev->next = next;
 else
  qp->q.fragments = next;
 qp->q.meat -= free_it->len;
 frag_kfree_skb(qp->q.net, free_it, NULL);
} 

      當只覆蓋分片一部分時,offset+=i則將後個分片偏移後移i個長度,從而與該分片隔開,同時這樣相當於減少了IP分片的長度,所以q.meat -= i;見下圖圖示,

if (i < next->len) {
 if (!pskb_pull(next, i))
  goto err;
 FRAG_CB(next)->offset += i;
 qp->q.meat -= i;
 if (next->ip_summed != CHECKSUM_UNNECESSARY)
  next->ip_summed = CHECKSUM_NONE;
 break;
}  

ip_frag_reasm函數實現IP分片的重組
      ip_frag_reasm傳入的參數是prev,而重組完成後ip_defrag會將skb替換成重組後的新的skb,而在之前的操作中,skb插入了qp->q.fragments中,並且prev->next即爲skb,因此第一步就是讓skb變成qp->q.fragments,即IP分片的頭部。

if (prev) {
 head = prev->next;
 fp = skb_clone(head, GFP_ATOMIC);
 if (!fp)
  goto out_nomem;
 fp->next = head->next;
 prev->next = fp;

 skb_morph(head, qp->q.fragments);
 head->next = qp->q.fragments->next;
 kfree_skb(qp->q.fragments);
 qp->q.fragments = head;
}

      下面圖示說明了上面代碼段作用,skb是IP分片3,通過skb_clone拷貝一份3_copy替代之前的分片3,再通過skb_morph拷貝q.fragments到原始IP分片3,替代分片1,並釋放分片1: 


      獲取IP報頭長度ihlen,head就是ip_defrag傳入參數中的skb,並且它已經成爲了IP分片隊列的頭部;len爲整個IP報頭+報文的總長度,qp->q.len是未分片前IP報文的長度。

ihlen = ip_hdrlen(head);
len = ihlen + qp->q.len;

      此時head就是skb,並且它的skb->data存儲了第一個IP分片的內容,其它IP分片的內容將存儲在緊接skb的空間 – frag_list;skb_push將skb->data迴歸原位,即未處理IP報頭前的位置,因爲之前的IP分片處理會調用skb_pull移走IP報頭,將它迴歸原位是因爲skb即將作爲重組後的報文而被處理,那裏會真正的skb_pull移走IP報頭,再交由上層協議處理。

skb_shinfo(head)->frag_list = head->next;
skb_push(head, head->data - skb_network_header(head));

      上面所說的frag_list是struct skb_shared_info的一個屬性,在分配skb時分配在其後空間,通過skb_shinfo(skb)進行引用。下面分配skb大小size和skb_shared_info大小的代碼摘自[net/core/skbuff.c]

size = SKB_DATA_ALIGN(size);
data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info),
  gfp_mask, node);

      這裏要弄清楚sk_buff中線性存儲區和paged buffer的區別,線性存儲區就是存儲報文,如果是分片後的,則只是第一個分片的內容;而paged buffer則存儲其餘分片的內容。而skb->data_len則表示paged buffer中內容長度,而skb->len則是paged buffer + linear buffer。下面這段代碼就是根據餘下的分片增加data_len和len計數。

 
for (fp=head->next; fp; fp = fp->next) {
 head->data_len += fp->len;
 head->len += fp->len;
 ……
}

      IP分片已經重組完成,分片從q.fragments鏈表移到了frag_list上,因此head->next和qp->q.fragments置爲NULL。偏移量frag_off置0,總長度tot_len置爲所有分片的長度和,這樣,skb就相當於沒有分片的完整的大數據包,繼續向上傳遞。

 
head->next = NULL;
head->dev = dev;
……
iph = ip_hdr(head);
iph->frag_off = 0;
iph->tot_len = htons(len);
IP_INC_STATS_BH(net, IPSTATS_MIB_REASMOKS);
qp->q.fragments = NULL;

這部分的重點是三個核心的數據結構-鄰居表、鄰居緩存、代理鄰居表,以及NUD狀態轉移圖。

      總的來說,要成功添加一條鄰居表項,需要滿足兩個條件:1. 本機使用該表項;2. 對方主機進行了確認。同時,表項的添加引入了NUD(Neighbour Unreachability Detection)機制,從創建NUD_NONE到可用NUD_REACHABLE需要經歷一系列狀態轉移,而根據達到兩個條件順序的不同,可以分爲兩條路線:
      先引用再確認- NUD_NONE -> NUD_INCOMPLETE -> NUD_REACHABLE
      先確認再引用- NUD_NONE -> NUD_STALE -> NUD_DELAY -> NUD_PROBE -> NUD_REACHABLE

      下面還是從接收函數入手,當匹配號協議號是0x0806,會調用ARP模塊的接收函數arp_rcv()。
arp_rcv() ARP接收函數
        首先是對arp協議頭進行檢查,比如大小是否足夠,頭部各數值是否正確等,這裏略過代碼,直接向下看。每個協議處理都一樣,如果被多個協議佔有,則拷貝一份。

if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL)
 goto out_of_mem;

        NEIGH_CB(skb)實際就是skb->cb,在skb聲明爲u8 char[48],它用作每個協議模塊的私有數據區(control buffer),每個協議模塊可以根據自身需求在其中存儲私有數據。而arp模塊就利用了它存儲控制結構neighbour_cb,它聲明如下,佔8字節。這個控制結構在代理ARP中使用工作隊列時會發揮作用,sched_next代表下次被調度的時間,flags是標誌。

memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));
struct neighbour_cb {
 unsigned long sched_next;
 unsigned int flags;
};

        函數最後調用arp_process,其間插入netfilter作爲開始處理ARP報文的起點。

 
return NF_HOOK(NFPROTO_ARP, NF_ARP_IN, skb, dev, NULL, arp_process);


arp_process()
    這個函數開始對報文進行處理,首先會從skb中取出arp報頭部分的信息,如sha, sip, tha, tip等,這部分可查閱代碼,這裏略過。ARP不會查詢環路地址和組播地址,因爲它們沒有對應的mac地址,因此遇到這兩類地址,直接退出。

if (ipv4_is_loopback(tip) || ipv4_is_multicast(tip))
 goto out;

       如果收到的是重複地址檢測報文,並且本機佔用了檢測了地址,則調用arp_send發送響應。對於重複地址檢測報文(ARP報文中源IP爲全0),它所帶有的鄰居表項信息還沒通過檢測,此時緩存它顯然沒有意義,也許下一刻就有其它主機聲明它非法,因此,重複地址檢測報文中的信息不會加入鄰居表中。

if (sip == 0) {
 if (arp->ar_op == htons(ARPOP_REQUEST) &&
  inet_addr_type(net, tip) == RTN_LOCAL &&
  !arp_ignore(in_dev, sip, tip))
  arp_send(ARPOP_REPLY, ETH_P_ARP, sip, dev, tip, sha, dev->dev_addr, sha);
 goto out;
}

       下面要處理的地址解析報文,並且要解析的地址在路由表中存在

if (arp->ar_op == htons(ARPOP_REQUEST) &&
 ip_route_input(skb, tip, sip, 0, dev) == 0)

        第一種情況,如果要解析的是本機地址,則調用neigh_event_ns(),並根據查到的鄰居表項n發送ARP響應報文。這裏neigh_event_ns的功能是在arp_tbl中查找是否已含有對方主機的地址信息,如果沒有,則進行創建,然後會調用neigh_update來更新狀態。收到對方主機的請求報文,會導致狀態遷移到NUD_STALE。

if (addr_type == RTN_LOCAL) {
 ……
 if (!dont_send) {
  n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
  if (n) {
   arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha);
   neigh_release(n);
  }
 }
 goto out;
} 

        #NUD_INCOMPLETE也遷移到NUD_STALE,作何解釋?
        第二種情況,如果要解析的不是本機地址,則要判斷是否支持轉發,是否支持代理ARP(代理ARP是陸由器的功能,因此能轉發是先決條件),如果滿足條件,那麼按照代理ARP流程處理。首先無論如何,主機得通了存在這樣一個鄰居,因此要在在arp_tbl中查找並(如果不存在)創建相應鄰居表項;然後,對於代理ARP,這個流程實際上會執行兩遍,第一遍走else部分,第二遍走if部分。第一次的else代碼段會觸發定時器,通過定時器引發報文重新執行arp_process函數,並走if部分。
       -第一遍的else部分:調用pneigh_enqueue()將報文skb加入tbl->proxy_queue隊列,同時設置NEIGH_CB(skb)的值,具體可看後見的代理表項處理。
       -第二遍的if部分,發送ARP響應報文,行使代理ARP的功能。

else if (IN_DEV_FORWARD(in_dev)) {
 if (addr_type == RTN_UNICAST  &&
  (arp_fwd_proxy(in_dev, dev, rt) ||
  arp_fwd_pvlan(in_dev, dev, rt, sip, tip) ||
  pneigh_lookup(&arp_tbl, net, &tip, dev, 0)))
 {
  n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
  if (n)
   neigh_release(n);

  if (NEIGH_CB(skb)->flags & LOCALLY_ENQUEUED ||
   skb->pkt_type == PACKET_HOST ||
   in_dev->arp_parms->proxy_delay == 0) {
   arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha);
  } else {
   pneigh_enqueue(&arp_tbl, in_dev->arp_parms, skb);
   in_dev_put(in_dev);
   return 0;
  }
  goto out;
 }
}

        補充:neigh_event_ns()與neigh_release()配套使用並不代表創建後又被釋放,neigh被釋放的條件是neigh->refcnt==0,但neigh創建時的refcnt=1,而neigh_event_ns會使refcnt+1,neigh_release會使-1,此時refcnt的值還是1,只有當下次單獨調用neigh_release時纔會被釋放。
      查找是否已存在這樣一個鄰居表項。如果ARP報文是發往本機的響應報文,那麼neigh會更新爲NUD_REACHABLE狀態;否則,維持原狀態不變。#個人認爲,這段代碼是處理NUD_INCOMPLETE/NUD_PROBE/NUD_DELAY向NUD_REACHABLE遷移的,但如果一臺主機A發送一個對本機的ARP響應報文,那麼會導致neigh從NUD_NONE直接遷移到NUD_REACHABLE,當然,按照正常流程,一個ARP響應報文肯定是由於本機發送了ARP請求報文,那樣neigh已經處於NUD_INCOMPLETE狀態了。

n = __neigh_lookup(&arp_tbl, &sip, dev, 0);
if (n) {
 int state = NUD_REACHABLE;
 int override;
 override = time_after(jiffies, n->updated + n->parms->locktime);

 if (arp->ar_op != htons(ARPOP_REPLY) ||
  skb->pkt_type != PACKET_HOST)
  state = NUD_STALE;
 neigh_update(n, sha, state, override ? NEIGH_UPDATE_F_OVERRIDE : 0);
 neigh_release(n);
}

        實際上,arp_process是接收到ARP報文的處理函數,它涉及到的是鄰居表項在收到arp請求和響應的情況,下圖反映了arp_process中所涉及的狀態轉移:收到arp請求,NUD_NONE -> NUD_STALE;收到arp響應,NUD_INCOMPLETE/NUD_DELAY/NUD_PROBE -> NUD_REACHABLE。根據之前分析,我認爲還存在NUD_NONE -> NUD_REACHABLE和NUD_INCOMPLETE -> NUD_STALE的轉移,作何解釋?        

NUD狀態
       每個鄰居表項在生效前都要經歷一系列的狀態遷移,每個狀態都有不同的含義,在前面已經多次提到了NUD狀態。要添加一條有效的鄰居表項,有效途徑有兩條:
          先引用再確認- NUD_NONE -> NUD_INCOMPLETE -> NUD_REACHABLE
          先確認再引用- NUD_NONE -> NUD_STALE -> NUD_DELAY -> NUD_PROBE -> NUD_REACHABLE
       其中neigh_timer_handler定時器、neigh_periodic_work工作隊列會異步的更改NUD狀態,neigh_timer_handler用於NUD_INCOMPLETE, NUD_DELAY, NUD_PROBE, NUD_REACHABLE狀態;neigh_periodic_work用於NUD_STALE。注意neigh_timer_handler是每個表項一個的,而neigh_periodic_work是唯一的,NUD_STALE狀態的表項沒必要單獨使用定時器,定期檢查過期就可以了,這樣大大節省了資源。
       neigh_update則專門用於更新表項狀態,neigh_send_event則是解析表項時的狀態更新,能更新表項的函數很多,這裏不一一列出。 

neigh_timer_handler 定時器函數
     當neigh處於NUD_INCOMPLETE, NUD_DELAY, NUD_PEOBE, NUD_REACHABLE時會添加定時器,即neigh_timer_handler,它處理各個狀態在定時器到期時的情況。
     當neigh處於NUD_REACHABLE狀態時,根據NUD的狀態轉移圖,它有三種轉移可能,分別對應下面三個條件語句。neigh->confirmed代表最近收到來自對應鄰居項的報文時間,neigh->used代表最近使用該鄰居項的時間。
         -如果超時,但期間收到對方的報文,不更改狀態,並重置超時時間爲neigh->confirmed+reachable_time;
         -如果超時,期間未收到對方報文,但主機使用過該項,則遷移至NUD_DELAY狀態,並重置超時時間爲neigh->used+delay_probe_time;
         -如果超時,且既未收到對方報文,也未使用過該項,則懷疑該項可能不可用了,遷移至NUD_STALE狀態,而不是立即刪除,neigh_periodic_work()會定時的清除NUD_STALE狀態的表項。

if (state & NUD_REACHABLE) {
 if (time_before_eq(now,
   neigh->confirmed + neigh->parms->reachable_time)) {
  NEIGH_PRINTK2("neigh %p is still alive.\n", neigh);
  next = neigh->confirmed + neigh->parms->reachable_time;
 } else if (time_before_eq(now,
   neigh->used + neigh->parms->delay_probe_time)) {
  NEIGH_PRINTK2("neigh %p is delayed.\n", neigh);
  neigh->nud_state = NUD_DELAY;
  neigh->updated = jiffies;
  neigh_suspect(neigh);
  next = now + neigh->parms->delay_probe_time;
 } else {
  NEIGH_PRINTK2("neigh %p is suspected.\n", neigh);
  neigh->nud_state = NUD_STALE;
  neigh->updated = jiffies;
  neigh_suspect(neigh);
  notify = 1;
 }
}

       下圖是對上面表項處於NUD_REACHABLE狀態時,定時器到期後3種情形的示意圖: 

      當neigh處於NUD_DELAY狀態時,根據NUD的狀態轉移圖,它有二種轉移可能,分別對應下面二個條件語句。
         -如果超時,期間收到對方報文,遷移至NUD_REACHABLE,記錄下次檢查時間到next;
         -如果超時,期間未收到對方的報文,遷移至NUD_PROBE,記錄下次檢查時間到next。
      在NUD_STALE->NUD_PROBE中間還插入NUD_DELAY狀態,是爲了減少ARP包的數目,期望在定時時間內會收到對方的確認報文,而不必再進行地址解析。

else if (state & NUD_DELAY) {
 if (time_before_eq(now,
   neigh->confirmed + neigh->parms->delay_probe_time)) {
  NEIGH_PRINTK2("neigh %p is now reachable.\n", neigh);
  neigh->nud_state = NUD_REACHABLE;
  neigh->updated = jiffies;
  neigh_connect(neigh);
  notify = 1;
  next = neigh->confirmed + neigh->parms->reachable_time;
 } else {
  NEIGH_PRINTK2("neigh %p is probed.\n", neigh);
  neigh->nud_state = NUD_PROBE;
  neigh->updated = jiffies;
  atomic_set(&neigh->probes, 0);
  next = now + neigh->parms->retrans_time;
 }
} 

        當neigh處於NUD_PROBE或NUD_INCOMPLETE狀態時,記錄下次檢查時間到next,因爲這兩種狀態需要發送ARP解析報文,它們過程的遷移依賴於ARP解析的進程。

else {
 /* NUD_PROBE|NUD_INCOMPLETE */
 next = now + neigh->parms->retrans_time;
}

        經過定時器超時後的狀態轉移,如果neigh處於NUD_PROBE或NUD_INCOMPLETE,則會發送ARP報文,先會檢查報文發送的次數,如果超過了限度,表明對方主機沒有迴應,則neigh進入NUD_FAILED,被釋放掉。

  
if ((neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) &&
 atomic_read(&neigh->probes) >= neigh_max_probes(neigh)) {
 neigh->nud_state = NUD_FAILED;
 notify = 1;
 neigh_invalidate(neigh);
}

        檢查完後,如果還未超過限度,則會發送ARP報文,neigh->ops->solicit在創建表項neigh時被賦值,一般是arp_solicit,並且增加探測計算neigh->probes。

if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) {
 struct sk_buff *skb = skb_peek(&neigh->arp_queue);
 /* keep skb alive even if arp_queue overflows */
 if (skb)
  skb = skb_copy(skb, GFP_ATOMIC);
 write_unlock(&neigh->lock);
 neigh->ops->solicit(neigh, skb);
 atomic_inc(&neigh->probes);
 kfree_skb(skb);
}

      實際上,neigh_timer_handler處理啓用了定時器狀態超時的情況,下圖反映了neigh_timer_handler中所涉及的狀態轉移,值得注意的是NUD_DELAY -> NUD_REACHABLE的狀態轉移,在arp_process中也提到過,收到arp reply時會有表項狀態NUD_DELAY -> NUD_REACHABLE。它們兩者的區別在於arp_process處理的是arp的確認報文,而neigh_timer_handler處理的是4層的確認報文。 

       

neigh_periodic_work NUD_STALE狀態的定時函數
     當neigh處於NUD_STALE狀態時,此時它等待一段時間,主機引用到它,從而轉入NUD_DELAY狀態;沒有引用,則轉入NUD_FAIL,被釋放。不同於NUD_INCOMPLETE、NUD_DELAY、NUD_PROBE、NUD_REACHABLE狀態時的定時器,這裏使用的異步機制,通過定期觸發neigh_periodic_work()來檢查NUD_STALE狀態。

tbl->parms.base_reachable_time = 30 HZ

     當初始化鄰居表時,添加了neigh_periodic_work工作
     neigh_table_init() -> neigh_table_init_no_netlink():

INIT_DELAYED_WORK_DEFERRABLE(&tbl->gc_work, neigh_periodic_work);

        當neigh_periodic_work執行時,首先計算到達時間(reachable_time),其中要注意的是

p->reachable_time = neigh_rand_reach_time(p->base_reachable_time);
unsigned long neigh_rand_reach_time(unsigned long base)
{
 return (base ? (net_random() % base) + (base >> 1) : 0);
}

        因此,reachable_time實際取值是1/2 base ~ 2/3 base,而base = base_reachable_time,當表項處於NUD_REACHABLE狀態時,會啓動一個定時器,時長爲reachable_time,即一個表項在不被使用時存活時間是1/2 base_reachable_time ~ 2/3 base_reachable_time。
     然後它會遍歷整個鄰居表,每個hash_buckets的每個表項,如果在gc_staletime內仍未被引用過,則會從鄰居表中清除。

 
for (i = 0 ; i <= tbl->hash_mask; i++) {
 np = &tbl->hash_buckets[i];
 while ((n = *np) != NULL) {
  …..
if (atomic_read(&n->refcnt) == 1 &&
  (state == NUD_FAILED ||
  time_after(jiffies, n->used + n->parms->gc_staletime))) {
  *np = n->next;
  n->dead = 1;
  write_unlock(&n->lock);
  neigh_cleanup_and_release(n);
  continue;
 }
 ……
}

      在工作最後,再次添加該工作到隊列中,並延時1/2 base_reachable_time開始執行,這樣,完成了neigh_periodic_work工作每隔1/2 base_reachable_time執行一次。
schedule_delayed_work(&tbl->gc_work, tbl->parms.base_reachable_time >> 1);
      neigh_periodic_work定期執行,但要保證表項不會剛添加就被neigh_periodic_work清理掉,這裏的策略是:gc_staletime大於1/2 base_reachable_time。默認的,gc_staletime = 30,base_reachable_time = 30。也就是說,neigh_periodic_work會每15HZ執行一次,但表項在NUD_STALE的存活時間是30HZ,這樣,保證了每項在最差情況下也有(30 - 15)HZ的生命週期。

neigh_update 鄰居表項狀態更新
      如果新狀態是非有效(!NUD_VALID),那麼要做的就是刪除該表項:停止定時器neigh_del_timer,設置neigh狀態nud_state爲新狀態new。除此之外,當是NUD_INCOMPLETE或NUD_PROBE狀態時,可能有暫時因爲地址沒有解析而暫存在neigh->arp_queue中的報文,而現在表項更新到NUD_FAILED,即解析無法成功,那麼這麼暫存的報文也只能被丟棄neigh_invalidate。

if (!(new & NUD_VALID)) {
 neigh_del_timer(neigh);
 if (old & NUD_CONNECTED)
  neigh_suspect(neigh);
 neigh->nud_state = new;
 err = 0;
 notify = old & NUD_VALID;
 if ((old & (NUD_INCOMPLETE | NUD_PROBE)) &&
  (new & NUD_FAILED)) {
  neigh_invalidate(neigh);
  notify = 1;
 }
 goto out;
}

         中間這段代碼是對比表項的地址是否發生了變化,略過。#個人認爲NUD_REACHABLE狀態時,新狀態爲NUD_STALE是在下面這段代碼裏面除去了,因爲NUD_REACHABLE狀態更好,不應該回退到NUD_STALE狀態。但是當是NUD_DELAY, NUD_PROBE, NUD_INCOMPLETE時仍會被更新到NUD_STALE狀態,對此很不解???

else {
 if (lladdr == neigh->ha && new == NUD_STALE &&
  ((flags & NEIGH_UPDATE_F_WEAK_OVERRIDE) ||
  (old & NUD_CONNECTED)))
  new = old;
}

        新舊狀態不同時,首先刪除定時器,如果新狀態需要定時器,則重新設置定時器,最後設置表項neigh爲新狀態new。

if (new != old) {
 neigh_del_timer(neigh);
 if (new & NUD_IN_TIMER)
  neigh_add_timer(neigh, (jiffies +
   ((new & NUD_REACHABLE) ?
   neigh->parms->reachable_time :
    0)));
 neigh->nud_state = new;
}

        如果鄰居表項中的地址發生了更新,有了新的地址值lladdr,那麼更新表項地址neigh->ha,並更新與此表項相關的所有緩存表項neigh_update_hhs。

 
if (lladdr != neigh->ha) {
 memcpy(&neigh->ha, lladdr, dev->addr_len);
 neigh_update_hhs(neigh);
 if (!(new & NUD_CONNECTED))
  neigh->confirmed = jiffies -
   (neigh->parms->base_reachable_time << 1);
 notify = 1;
}

        如果表項狀態從非有效(!NUD_VALID)遷移到有效(NUD_VALID),且此表項上的arp_queue上有項,表明之前有報文因爲地址無法解析在暫存在了arp_queue上。此時表項地址解析完成,變爲有效狀態,從arp_queue中取出所有待發送的報文skb,發送出去n1->output(skb),並清空表項的arp_queue。

if (!(old & NUD_VALID)) {
 struct sk_buff *skb;
while (neigh->nud_state & NUD_VALID &&
     (skb = __skb_dequeue(&neigh->arp_queue)) != NULL) {
  struct neighbour *n1 = neigh;
  write_unlock_bh(&neigh->lock);
  /* On shaper/eql skb->dst->neighbour != neigh :( */
  if (skb_dst(skb) && skb_dst(skb)->neighbour)
   n1 = skb_dst(skb)->neighbour;
  n1->output(skb);
  write_lock_bh(&neigh->lock);
 }
 skb_queue_purge(&neigh->arp_queue);
}


neigh_event_send
    當主機需要解析地址,會調用neigh_resolve_output,主機引用表項明顯會涉及到表項的NUD狀態遷移,NUD_NONE->NUD_INCOMPLETE,NUD_STALE->NUD_DELAY。
     neigh_event_send -> __neigh_event_send
    只處理nud_state在NUD_NONE, NUD_STALE, NUD_INCOMPLETE狀態時的情況:

if (neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE))
  goto out_unlock_bh;

       不處於NUD_STALE和NUD_INCOMPLETE狀態,則只能是NUD_NONE。此時主機要用到該鄰居表項(注意是通過neigh_resolve_output進入的),但還沒有,因此要通過ARP進行解析,並且此時沒有收到對方發來的任何報文,要進行的ARP是廣播形式。
    在發送ARP報文時有3個參數- ucast_probes, mcast_probes, app_probes,分別代表單播次數,廣播次數,app_probes比較特殊,一般情況下爲0,當使用了arpd守護進程時纔會設置它的值。如果已經收到過對方的報文,即知道了對方的MAC-IP,ARP解析會使用單播形式,次數由ucast_probes決定;如果未收到過對方報文,此時ARP解析只能使用廣播形式,次數由mcasat_probes決定。
     當mcast_probes有值時,neigh進入NUD_INCOMPLETE狀態,設置定時器,注意此時neigh_probes(表示已經進行探測的次數)初始化爲ucast_probes,目的是隻進行mcast_probes次廣播;當mcast_probes值爲0時(表明當前配置不允許解析),neigh進入NUD_FAILED狀態,被清除。

if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) {
 if (neigh->parms->mcast_probes + neigh->parms->app_probes) {
  atomic_set(&neigh->probes, neigh->parms->ucast_probes);
  neigh->nud_state     = NUD_INCOMPLETE;
  neigh->updated = jiffies;
  neigh_add_timer(neigh, now + 1);
 } else {
  neigh->nud_state = NUD_FAILED;
  neigh->updated = jiffies;
  write_unlock_bh(&neigh->lock);

  kfree_skb(skb);
  return 1;
 }
}

         當neigh處於NUD_STALE狀態時,根據NUD的狀態轉移圖,主機引用到了該鄰居表項,neigh轉移至NUD_DELAY狀態,設置定時器。

else if (neigh->nud_state & NUD_STALE) {
 NEIGH_PRINTK2("neigh %p is delayed.\n", neigh);
 neigh->nud_state = NUD_DELAY;
 neigh->updated = jiffies;
 neigh_add_timer(neigh, jiffies + neigh->parms->delay_probe_time);
}

      當neigh處於NUD_INCOMPLETE狀態時,需要發送ARP報文進行地址解析,__skb_queue_tail(&neigh->arp_queue, skb)的作用就是先把要發送的報文緩存起來,放到neigh->arp_queue鏈表中,當完成地址解析,再從neigh->arp_queue取出報文,併發送出去。

if (neigh->nud_state == NUD_INCOMPLETE) {
 if (skb) {
  if (skb_queue_len(&neigh->arp_queue) >= neigh->parms->queue_len) {
   struct sk_buff *buff;
   buff = __skb_dequeue(&neigh->arp_queue);
   kfree_skb(buff);
   NEIGH_CACHE_STAT_INC(neigh->tbl, unres_discards);
  }
  __skb_queue_tail(&neigh->arp_queue, skb);
 }
 rc = 1;
}


鄰居表的操作
neigh_create 創建鄰居表項
     首先爲新的鄰居表項struct neighbour分配空間,並做一些初始化。傳入的參數tbl就是全局量arp_tbl,分配空間的大小是tbl->entry_size,而這個值在聲明arp_tbl時初始化爲sizeof(struct neighbour) + 4,多出的4個字節就是key值存放的地方。

n = neigh_alloc(tbl);

       拷貝key(即IP地址)到primary_key,而primary_key就是緊接neighbour的4個字節,看下struct neighbor的聲明 - u8 primary_key[0];設置n->dev指向接收到報文的網卡設備dev。

memcpy(n->primary_key, pkey, key_len);
n->dev = dev;

       哈希表是犧牲空間換時間,保證均勻度很重要,一旦某個表項的值過多,鏈表查找會降低性能。因此當表項數目entries大於初始分配大小hash_mask+1時,執行neigh_hash_grow將哈希表空間倍增,這也是內核使用哈希表時常用的方法,可變大小的哈希表。

if (atomic_read(&tbl->entries) > (tbl->hash_mask + 1))
 neigh_hash_grow(tbl, (tbl->hash_mask + 1) << 1);

       通過pkey和dev計算哈希值,決定插入tbl->hash_buckets的表項。

hash_val = tbl->hash(pkey, dev) & tbl->hash_mask;

       搜索tbl->hash_buckets[hash_val]項,如果創建的新ARP表項已存在,則退出;否則將其n插入該項的鏈表頭。

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;
 }
}
n->next = tbl->hash_buckets[hash_val];
tbl->hash_buckets[hash_val] = n;

        附一張創建ARP表項並插入到hash_buckets的圖: 

 neigh_lookup 查找ARP表項
      查找函數很簡單,以IP地址和網卡設備(即pkey和dev)計算哈希值hash_val,然後在tbl->hash_buckets查找相應項。

hash_val = tbl->hash(pkey, dev);
for (n = tbl->hash_buckets[hash_val & tbl->hash_mask]; 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;
 }
}

 

代理ARP
      代理ARP的相關知識查閱google。要明確代理ARP功能是針對陸由器的(或者說是具有轉發功能的主機)。開啓ARP代理後,會對查詢不在本網段的ARP請求包迴應。
      回到之前的arp_process代碼,處理代理ARP的情況,這實際就是進行代理ARP的條件,IN_DEV_FORWARD是支持轉發,RTN_UNICAST是與路由直連,arp_fwd_proxy表示設備支持代理行爲,arp_fwd_pvlan表示支持代理同設備進出,pneigh_lookup表示目的地址的代理。這兩種arp_fwd_proxy和arp_fwd_pvlan都只是網卡設備的一種性質,pneigh_lookup則是一張代理鄰居表,它的內容都是手動添加或刪除的,三種策略任一一種滿足都可以進行代理ARP。

else if (IN_DEV_FORWARD(in_dev)) {
 if (addr_type == RTN_UNICAST  &&
   (arp_fwd_proxy(in_dev, dev, rt) ||
    arp_fwd_pvlan(in_dev, dev, rt, sip, tip) ||
    pneigh_lookup(&arp_tbl, net, &tip, dev, 0)))


pneigh_lookup 查找或添加代理鄰居表項[proxy neighbour]
      以[pkey=tip, key_len=4]計算hash值,執行__pneigh_lookup_1在phash_buckets中查找。

u32 hash_val = pneigh_hash(pkey, key_len);
n = __pneigh_lookup_1(tbl->phash_buckets[hash_val], net, pkey, key_len, dev);

       如果在phash_buckets中查找到,或者不需要創建新表項,則函數返回,此時它的功能僅僅是lookup。

if (n || !creat)
 goto out;

        而當傳入參數create=1時,則它的功能不僅是lookup,還會在表項不存在時create。同neighbour結構一樣,鍵值pkey存儲在pneigh結構的後面,這樣當pkey變化時,修改十分容易。創建操作很直觀,爲pneigh和pkey分配空間,初始化些變量,最後插入phash_buckets。

n = kmalloc(sizeof(*n) + key_len, GFP_KERNEL);
……
write_pnet(&n->net, hold_net(net));
memcpy(n->key, pkey, key_len);
……
n->next = tbl->phash_buckets[hash_val];
tbl->phash_buckets[hash_val] = n;


pneigh_enqueue 將報文加入代理隊列
     首先計算下次調度的時間,這是一個隨機值,記錄到sched_next中;設置flags|=LOCALLY_ENQUEUED表明報文是本地加入的。

unsigned long sched_next = now + (net_random() % p->proxy_delay);
……
NEIGH_CB(skb)->sched_next = sched_next;
NEIGH_CB(skb)->flags |= LOCALLY_ENQUEUED;

        然後將報文加入proxy_queue,並設置定時器proxy_timer,下次超時時間爲剛計算的值sched_next,這樣,下次超時時就會處理proxy_queue隊列中的報文。

__skb_queue_tail(&tbl->proxy_queue, skb);
mod_timer(&tbl->proxy_timer, sched_next);

        這裏的tbl當然是arp_tbl,它的proxy_timer是在初始化時設置的arp_init() -> neigh_table_init_no_netlink()中:

setup_timer(&tbl->proxy_timer, neigh_proxy_process, (unsigned long)tbl);


 neigh_proxy_process 代理ARP的定時器
     skb_queue_walk_safe如同for循環一樣,它遍歷proxy_queue,一個個取出其中的報文skb,查看報文的調度時間sched_next與當前時間now的差值。
      如果tdif<=0則表明調度時間已到或已過,報文要被處理了,從proxy_queue上取出該報文,調用tbl->proxy_redo重新發送報文,tbl->proxy_redo也是在arp初始化時賦值的,實際上就是arp_process()函數。結合上面的分析,它會執行arp_process中代理ARP處理的else部分,發送響應報文。
      如果tdif>0則表明調度時間還未到,else if部分的功能就是記錄下最近要過期的調度時間到sched_next。

skb_queue_walk_safe(&tbl->proxy_queue, skb, n) {
 long tdif = NEIGH_CB(skb)->sched_next - now;

 if (tdif <= 0) {
  struct net_device *dev = skb->dev;
  __skb_unlink(skb, &tbl->proxy_queue);
  if (tbl->proxy_redo && netif_running(dev))
   tbl->proxy_redo(skb);
  else
   kfree_skb(skb);

  dev_put(dev);
 } else if (!sched_next || tdif < sched_next)
  sched_next = tdif;
}

        重新設置proxy_timer的定時器,下次超時時間爲剛剛記錄下的最近要調度的時間sched_next + 當前時間jiffies。

del_timer(&tbl->proxy_timer);
if (sched_next)
 mod_timer(&tbl->proxy_timer, jiffies + sched_next);

        以一張簡單的圖來說明ARP代理的處理過程,過程一是入隊列等待,過程二是出隊列發送。不立即處理ARP代理請求報文的原因是爲了性能,收到報文後會啓動定時器,超時時間是一個隨機變量,保證了在大量主機同時進行此類請求時不會形成太大的負擔。 

       

鄰居表緩存
      鄰居表緩存中存儲的就是二層報頭,如果緩存的報頭正好被用到,那麼直接從鄰居表緩存中取出報文就行了,而不用再額外的構造報頭,加快了協議棧的響應速度。
neigh_hh_init 創建新的鄰居表緩存
     當發送報文時,如果還沒有對方主機MAC地址,則調用neigh_resove_output進行地址解析,此時會判斷dst->hh爲NULL時,就會調用neigh_hh_init創建鄰居表緩存,加速下次的報文發送。
     首先在鄰居表項所鏈的所有鄰居表緩存項n->hh匹配協議號protocol,找到,則說明已有緩存,不必再創建,neigh_hh_init會直接返回;未找到,則會創建新的緩存項hh。

for (hh = n->hh; hh; hh = hh->hh_next)
 if (hh->hh_type == protocol)
  break;

        下面代碼段創建了新的緩存項hh,並初始化了hh的內容,其中dev->header_ops->cache會賦值hh->hh_data,即[SRCMAC, DSTMAC, TYPE]。如果賦值失敗,釋放掉剛纔分配的hh;如果賦值成功,將hh鏈入n->hh的鏈表,並根據NUD狀態賦值hh->hh_output。

if (!hh && (hh = kzalloc(sizeof(*hh), GFP_ATOMIC)) != NULL) {
 seqlock_init(&hh->hh_lock);
 hh->hh_type = protocol;
 atomic_set(&hh->hh_refcnt, 0);
 hh->hh_next = NULL;

 if (dev->header_ops->cache(n, hh)) {
  kfree(hh);
  hh = NULL;
 } else {
  atomic_inc(&hh->hh_refcnt);
  hh->hh_next = n->hh;
  n->hh     = hh;
  if (n->nud_state & NUD_CONNECTED)
   hh->hh_output = n->ops->hh_output;
  else
   hh->hh_output = n->ops->output;
 }
}

      最後,創建成功的hh,陸由緩存dst->hh指向新創建的hh。

if (hh) {
 atomic_inc(&hh->hh_refcnt);
 dst->hh = hh;
}

        從hh的創建過程可以看出,通過鄰居表項neighbour的緩存hh可以遍歷所有的與neighbour相關的緩存(即目的MAC相同,但協議不同);通過dst的緩存hh只能指向相關的一個緩存(儘管dst->hh->hh_next也許有值,但只會使用dst->hh)。
這裏解釋了爲什麼neighbour和dst都有hh指針指向緩存項,可以這麼說,neighbour指向的hh是全部的,dst指向的hh是特定一個。兩者的作用:在發送報文時查找完陸由表找到dst後,會直接用dst->hh,得到以太網頭;而當遠程主機MAC地址變更時,通過dst->neighbour->hh可以遍歷所有緩存項,從而全部更改,而用dst->hh得一個個查找,幾乎是無法完成的。可以這麼說,dst->hh是使用時用的,neigh->hh是管理時用的。 

neigh_update_hhs 更新緩存項
     更新緩存項更新的實際就是緩存項的MAC地址。比如當收到一個報文,以它源IP爲鍵值在鄰居表中查找到的neighbour表項的n->ha與報文源MAC值不同時,說明對方主機的MAC地址發生了變更,此時就需要更新所有以舊MAC生成的hh爲新MAC。
鄰居表項是以IP爲鍵值查找的,因此通過IP可以查找相關的鄰居表項neigh,前面說過neigh->hh可以遍歷所有以之相關的緩存項,所以遍歷它,並調用update函數。以以太網卡爲例,update = neigh->dev->header_ops->cache_update ==> eth_header_cache_update,而eth_header_cache_update函數就是用新的MAC地址覆蓋hh->data中的舊MAC地址。
      neigh_update_hhs函數也說明了neighbour->hh指針的作用。

for (hh = neigh->hh; hh; hh = hh->hh_next) {
 write_seqlock_bh(&hh->hh_lock);
 update(hh, neigh->dev, neigh->ha);
 write_sequnlock_bh(&hh->hh_lock);
}

      補充:緩存項hh的生命期從創建時起,會一直持續到鄰居表項被刪除,也就是調用neigh_destroy時,刪除neigh->hh指向的所有緩存項。

參考:《Understanding Linux Network Internals》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章