Linux協議棧中處理重複地址檢測報文的是arp_process()中的一段代碼,RFC2131是DHCP的草案,相應的sip==0是DHCP服務器用來檢測它所分發的地址是否重複的。
雜談2:NUD狀態轉移的實際理解
根據NUD的狀態轉移,實際測試兩種情況:NUD_INCOMPLETE和NUD_PROBE。
NUD_INCOMPLETE
Linux Host隨便telnet一個沒有使用的IP1,協議棧會爲會IP1創建一個鄰居表項,狀態由NUD_NONE遷移到NUD_INCOMPLETE,具體的協議棧流程在上篇ARP[http://blog.csdn.net/qy532846454/article/details/6806197]中分析的。
在Linux Host上telnet XX.XX.86.198,捕獲到的發包狀況如下:
NUD_PROBE
NUD_PROBE測試複雜一點,先由一臺Host1(IP2)向Linux Host發送arp request請求,協議棧會爲IP2創建一個鄰居表項,狀態由NUD_NONE -> NUD_STALE,然後Linux Host會響應request,狀態由NUD_STALE -> NUD_DELAY -> NUD_PROBE,具體的協議棧流程在上篇ARP[http://blog.csdn.net/qy532846454/article/details/6806197]中分析的。
由Host1構造假arp request(sip=未被使用IP, tip=Linux Host IP)給Linux Host,捕獲到的發包狀況如下:
每一個包是Host1發出的request,每二個包是Linux Host的回覆,後三個包是3次ARP單播嘗試,此時處於NUD_PROBE狀態要嘗試對方是否存活,由於sip使用的是虛假址,因此沒有響應,在嘗試了最大次數3次,對應arp_tbl中的參數ucast_probe=3次數,每次嘗試的間隔時間近似1s,對應arp_tbl中的參數retrans_time=1HZ。
對比下windows這方面的處理可以發現,兩者在這方面的行爲相差很大:比如windows的網絡協議棧會處理RFC826所規定的gratuitous arp報文;windows的arp嘗試只會進行一次。
ICMP模塊比較簡單,要注意的是icmp的速率限制策略,向IP層傳輸數據ip_append_data()和ip_push_pending_frames()。
在net/ipv4/af_inet.c中的inet_init()註冊icmp協議,從這裏也可以看出,ICMP模塊是綁定在IP模塊之上的。inet_add_protocol()會將icmp_protocol加入到全局量inet_protos中。
除了註冊icmp協議,還要對icmp模塊初始化,這部分由icmp_init()完成。
icmp_init()函數做的事很簡單,register_pernet_subsys(&icmp_sk_ops),而註冊icmp網絡子系統過程中會調用icmp_sk_ops.init(即icmp_sk_init函數)來完成它的初始化,下面具體看icmp_sk_init()函數。
首先爲net爲配CPU數目(nr_cpu_ids)個struct sock結構體空間,這裏的net是全局的網絡名,一般是init_inet。
net->ipv4.icmp_sk = kzalloc(nr_cpu_ids * sizeof(struct sock *), GFP_KERNEL);
每個CPU i,它的sock結構體位於net中的icmp_sk[i]。於每個CPU i,初始化剛剛分配的icmp_sk[i]:
-第一步,inet_ctl_sock_create()創建sk,並在net->ipv4.icmp_sk[i] = sk中將其賦值給icmp_sk[i]。
-第二步:ICMP發送緩存區大小sk_sndbuf設置爲128K
忽略發往廣播地址的icmp echo報文;忽略發往廣播地址的錯誤的響應報文;
設置icmp處理速率,這裏的ratelimit和ratemask參數在後面限速處理時會具體用到。
初始化工作完成後,還是從icmp的接收開始,icmp_rcv完成icmp報文的處理。
取得icmp報頭,此時skb->transport_header是在IP模塊處理中的ip_local_deliver_finish()將其設置爲了指向icmp報頭的位置。
根據icmp的類型type交由不同的處理函數去完成。
icmp_pointers是在icmp.c中定義的全局量,部分如下:
比如對於收到的icmp報文type爲0或1(響應答覆或目的不可達),協議棧要做的就是丟棄掉它 – icmp_discard()。下面以icmp echo和icmp timestamp爲例說明。
收到icmp echo報文執行icmp_echo()
icmp_param是回覆時信息,它直接拷貝了echo的ICMP報頭icmp_hdr(skb),僅僅改變了報頭的type = ICMP_ECHO_REPLY,然後調用icmp_reply()處理髮送。
收到icmp timestamp報文後執行icmp_timestamp()
經過IP層處理,skb->data指向icmp報頭的位置,而報頭最小爲4字節,所以這裏判斷skb->len < 4,是則丟棄該報文。從這裏也可以看出,時間戳請求報文可以只有4節字頭部,而沒有時間戳信息。
這段代碼設置時間戳響應的時間戳信息,包括接收時間戳和發送時間戳,兩者分別代表主機收到報文的時間,發送響應報文的時間,而從這部分代碼也可以看出icmp_param.data.times[2] = icmp_param.data.times[1]協議棧簡單的將接收和發送時間戳置爲相同的。時間戳的計算很簡單,格林尼治時間的當天時間的微秒數。最後skb_copy_bits()從skb的ICMP報文內容拷貝4節字的時間到icmp_param_data.times[0],即發起時間戳,所以最後情形如下:
icmp_param_data.times[0] 發起時間戳,從請求報文中拷貝
icmp_param_data.times[0] 接收時間戳,處理ICMP報頭時的時間
icmp_param_data.times[0] 發送時間戳,設置爲與接收時間戳相同
前面已經說過,icmp_param就是要發送ICMP報文的內容,上面設置了內容,接下來設置報頭,同樣是直接拷貝了ICMP請求的報頭,改變type爲ICMP_TIMESTAMPREPLY。注意這裏的data_len設置爲0,因爲它與icmp echo不同,一定是沒有分片的,即沒有paged_data部分。head_len設置爲icmphdrlen+12,這裏是爲了調用icmp_reply()回覆時的統一,實現表示ICMP部分的長度,主要是有分片時會根據head_len來跳過報頭而只拷貝每個分片的內容。
icmp_param.data.icmph = *icmp_hdr(skb);
icmp_param.data.icmph.type = ICMP_TIMESTAMPREPLY;
icmp_param.data.icmph.code = 0;
icmp_param.skb = skb;
icmp_param.offset = 0;
icmp_param.data_len = 0;
icmp_param.head_len = sizeof(struct icmphdr) + 12;
最後調用icmp_reply()回覆,這與icmp_echo()是相同的。
注意兩者設置icmp_param參數時的區別:
icmp_echo()中icmp_param.data_len=skb->len;
icmp_param.head_len=sizeof(struct icmphdr);
icmp_timestamp()中icmp_param.data_len=0。
icmp_param.head_len=sizeof(struct icmphdr)+12;
icmp_reply()
通過ip_route_output_key()查找路由信息,存放在rt中。路由項在這裏有兩個作用:一是限速是針對每個路由項的,在icmpv4_xrlim_allow()中會用到;二是將報文傳遞給IP層需要用到rt。仔細觀察流程可以發現,報文在協議棧傳遞過程中,在IP層會 查找一次路由表獲取到了rt,而在這裏又查找了一次路由表,似乎是重複了。其實不是,IP層查找是在報文接收階段,這裏的查找是在報文的發送階段。
協議棧對於部分ICMP報文進行了限速,但這種限速不是整體的,而是針對每個路由項的,即限制每個地址發送ICMP報文的限率。icmpv4_xrlim_allow()判斷該icmp報文是否需要被限速,如果能接收,則調用icmp_puash_reply()發送響應。
icmpv4_xrlim_allow() -> xrlim_allow() 限速處理
速率有關的參數是在icmp_init() -> icmp_sk_init()創建ICMP的sock時設置的,ratelimit是限制的速率,即TBF代碼段中的timeout,可以理解成一個令牌;ratemask是被限制速率的ICMP的報文類型,(1 << type & retemask) == 1判斷是否限速,type即ICMP類型,可見默認情況下[3]dest unreachable, [4]source quench, [11]time exceeded, [12]parameter problem纔會被限速。
限速使用了Token Bucket Filter(令牌環過濾器)思想,大致是每個到來的令牌從數據隊列中收集一個數據包,然後從桶中刪除。令牌被耗盡時,數據包將停止發送一段時間。
ICMP的限速使用的就是這種思想,不過時間作爲令牌,它的增長是連續的;每來一個報文,拿走一個令牌,則是一個時間段timeout,令牌也限定了最大數目是XRLIM_BURST_FACTOR爲6;簡單來講就是每過timeout時間,令牌數就加1,當令牌數達到6時不再增加;而來一個報文,令牌數就減一,當令牌數爲空時,不再減少,該報文也被丟棄;在這種情況下,在過timeout時間,纔會處理下一個報文。實現的代碼段如下:
dst->rate_tokens記錄上一次的令牌,dst->rate_last記錄上一次訪問時間,now – dst->rate_last爲經過的時間即增加的令牌數;當token>=timeout時即至少還有一個令牌,反回rc=1表示仍有令牌,不用限速;否則返回rc=0,限速。
icmp_push_reply() 發送回覆報文
取出icmp使用的sock sk
if中的ip_append_data()函數表示把數據添加到sk->sk_write_queue,這個函數是用於上層向IP層傳輸報文,它會進行分片的操作,實際是幫IP層做了分片。具體函數調用參見後面的ip_append_data()函數分析。正常情況ip_append_data()返回0,即if的執行語句不會被觸發。
else if進入條件是sk->sk_write_queue中已有數據,顯然在if的判斷語句中已經將報文添加到了sk->sk_write_queue中,所以會進入else if執行語句調用ip_push_pending_frames()將報文傳遞給IP層。而在ip_append_data()函數中可以看到,它只是拷貝了報文內容,並沒有生成ICMP報頭,ICMP報頭生成當然也是在通過ip_push_pending_frames()將報文發給IP層前生成的。取出skb,計算所有分片一起的校驗和,然過通過csum_partial_copy_nocheck()生成新的icmp報頭,最後調用ip_push_pending_frames()發送數據到IP層。函數ip_push_pending_frames()函數分析也參見後文。
ip_append_data() 添加要傳遞到IP層的數據
傳入參數的解釋:
getfrag() – 複製數據,這裏使用函數指針隱藏了複製細節,因爲針對icmp, udp的複製是不同的;
from – 被複制的數據,在icmp模塊中該參數傳入的是struct icmp_bxm;
length – IP報文內容長度
transhdrlen – 傳輸報頭長度,儘管ICMP歸爲網絡層協議,但這裏的transhdrlen也是包括它的,所以更好的解釋是表示IP上一層的報頭,比如ICMP報頭,IGMP報頭,UDP報頭等長度
ip_append_data()函數比較複雜,這裏以兩個例子來解釋這個函數:發送50 Byte的echo報文,發送600 Byte的echo報文。56字節echo報文在IP層不需要分片;600字節echo報文在IP層需要分片。ip_append_data()還可以多次調用來收集數據,而在ICMP模塊中這點並不能體現出來,在以後UDP或TCP時再以解釋多次調用的情況。
example 1:50 Byte echo報文 [假設MTU=520]
如果sk_write_queue爲空,則證明是第一個分片,50字節的報文只需要一個分片。這裏會設置exthdrlen,表示鏈路層額外的報頭長,一般情況下是0,所以此時length和transhdrlen值仍是傳入的值。而sk->sk_sndmsg_page和sk->sk_sndmsg_off與發散/聚合IO有關,這裏先不考慮。
設置各種參數的值,hh_len表示以太網報頭的長度,16字節對齊;fragheaderlen表示分片報頭長度,即IP報頭;maxfraglen表示最大分片長度。各參數值:hh_len = 16, fragheaderlen = 20, maxfraglen = 516,注意要求的節字對齊。
此時sk->sk_write_queue還爲空,跳轉至alloc_new_skb執行分配新的skb。
fraggap在上一個skb沒有8字節對齊時設置爲多餘的字節數,否則的話fraggap=0;datalen表示IP報文長度(不包括IP報頭),fraglen表示以太網幀報文長度(不包括以太網頭),alloclen表示要分配的內容長度,下面代碼省略了一些內容。各參數值: fraggap=0, datalen=50, fraglen=70, alloclen=70。
fraggap = 0;
datalen = length + fraggap;
fraglen = datalen + fragheaderlen;
alloclen = datalen + fragheaderlen;
分配報文skb空間,大小爲alloclen+hh_len+15,alloclen + hh_len就是報文的長度,15個字節爲預留部分。
skb_reserve()保留skb頭的hh_len大小,skb_put()擴展skb大小到fraglen,然後設置network_header和transport_header指向skb的正確位置,data指向ICMP報頭的位置,具體可以看下面的圖示:
skb_reserve(skb, hh_len);
……
data = skb_put(skb, fraglen);
skb_set_network_header(skb, exthdrlen);
skb->transport_header = (skb->network_header + fragheaderlen);
data += fragheaderlen;
copy是要拷貝的長度,爲傳輸層報頭後的內容大小。getfrag()函數實現數據的拷貝,在icmp模塊中,getfrag()指向icmp_glue_bits()函數,它從[from] + offset處拷貝copy個字節到data + transhdrlen處。
偏移offset加上已經拷貝的字節數copy,fraggap=0,length減去的就是IP報文內容長度,由於報文才56字節,一個分片足夠,所以length=0,然後把新生成的skb放入sk->sk_write_queue中,然後執行下次while循環。各參數值:copy=42, offset=42, length=0, 更新transhdrlen=0。
offset += copy;
length -= datalen - fraggap;
transhdrlen = 0;
……
__skb_queue_tail(&sk->sk_write_queue, skb);
continue;
while循環判斷條件是length > 0,因此跳出循環,完成了向IP層發送的數據生成,結果如下,注意,ICMP報頭還是沒有填寫的:
example 2:600 Byte echo 報文[假設MTU=520]
同樣,開始時sk->sk_write_queue()爲空,初始的設置與上述例子完全相同,不同處在於datalen此時比最大分片還要大,因此要設置datalen=maxfraglen-fragheaderlen。
if (datalen > mtu - fragheaderlen)
datalen = maxfraglen - fragheaderlen;
在完全第一個分片後,同樣會將分片skb放入sk_write_queue隊列,並進入下一次while循環。此時各參數的值:datalen=496, fraglen=516, alloclen=516, skb->len=516,
copy=488, offset=488, length=600-496=104, 更新transhdrlen=0。
__skb_queue_tail(&sk->sk_write_queue, skb);
continue;
再次進入while循環,此時不同的是length=104,證明還有數據需要拷貝,此時會對待拷貝的數據進行判斷,下面所指的填充滿是針對maxfraglen而言的。
@copy > 0,表示上個報文未被填充滿,這種情況在多次調用ip_append_data()時會發生,這裏都是一次調用ip_append_data()的情況,所以不會出現,此時會填充數據到上個skb中
@copy = 0,表示上個報文被填充滿,這個例子現在就是這種情況,此時會分配新的skb
@copy < 0,表示上個報文多填充了數據,這時因爲maxfraglen是mtu8字節對齊後的值,所以maxfraglen範圍是[mtu-7, mtu],而在某些特殊情況下,比如上個報文已被填滿(實際還可能有[1, 7]字節的空間),待填充字節數n < 8,這時會把這n個節字補在最後一個報文的尾部。
對這個例子而言,上個skb剛好被填充滿,copy=0,此時分配新的skb。
分配新skb的流程與上個skb的分配過程相同,變化的只是偏移量offset,另外,icmp報頭只存在於第一個分片中,因爲它也屬於IP內容的一部分,在這次拷貝完成後length=0,函數返回,最後結果如下:
ip_push_pending_frames() 將待發送的報文傳遞給網絡層
待發送的報文分片都在sk->sk_write_queue上,這裏要做的就是從sk_write_queue上取出所有分片,合併成一個報文,添加IP報頭信息,使用ip_local_out()傳遞給網絡層處理。
要注意的是這裏的合併並不是真正的合併,只有第一個分片形成了skb,剩下的分片都放到了skb_shinfo(skb)->frag_list上,雖然最後向下傳遞的只是一個skb,並實際上分片工作已經完成了,網絡層並不需要再次分片,由網絡的上層完成分片是出於效率的考慮,雖然與協議標準有所出入。
首先從sk_write_queue上取出第一個分片,skb是最終向下傳遞的報文,tail_skb指向skb的frag_list鏈表尾,即最後一個分片。
if ((skb = __skb_dequeue(&sk->sk_write_queue)) == NULL)
goto out;
tail_skb = &(skb_shinfo(skb)->frag_list);
將skb->data指向ip報頭的位置
tmp_skb表示現在要插入skb的分片,首先通過__skb_pull()除去這些分片的IP報頭,因爲分片共用skb的IP報頭。然後通過tail_skb處理將tmp_skb鏈入frag_list中;最後增加報文長度計數,以前說明過,skb->len代表linear buffer + paged buffer,skb->data_len代表paged_buffer,這裏插入的分片是增加了paged buffer大小,所以對skb->len和skb->data_len都增加分片的長度。
這裏是生成skb的IP報頭,設置其中的值
最終通過ip_local_out()傳遞給IP層