歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核分析 - 網絡[十三]:校驗和

Linux內核分析 - 網絡[十三]:校驗和

日期:2017/3/3 16:38:07   编辑:Linux內核

內核版本:2.6.34

報文的IP校驗和、ICMP校驗和、TCP/UDP校驗和使用相同的算法,在RFC1071中定義,網上這方面的 資料和例子很多,就不解釋算法流程了,而是側重於在實現的變化和技巧。

The checksum algorithm is simply to add up all the 16-bit words in one's complement and then to take the one's complement of the sum.

校驗和的計算可以分為兩步:累加、取反。這個劃分很重要,它大大減少了校驗和計算的消耗。校驗和計 算首要要明確一點:校驗和計算是很耗時的!原因並不在於算法復雜,而是在於輸入數據的龐大,試想傳送500M文件,則內核要 校驗500M字節的數據,並且對於每個報文,都是要進行校驗和。所以協議棧的校驗和實現並不是簡單明了的,使用了很多方法來 規避這種開銷。

第一:推遲校驗和計算

按照協議的規定,報文到達每一層,首先驗證校驗 和是否正確,丟棄掉不正確的報文,再才會進行後續操作。對於傳輸層下的協議,內核是這樣做的,因為IP只需要校驗IP報頭, 最多60字節;而對於網絡層上的協議,內核就不是這樣做的,ICMP/TCP/UDP都需要校驗報文的內容,而這部分消耗是很大的。

以UDP為例,在報文傳遞到UDP處理時,它並不會去驗證校驗和是否正確,而是直接將報文skb插入 到相應socket的接收隊列sk_receive_queue中。等到真正有程序要接收這個報文,從接收隊列中取出時,內核才去計算校驗和。 考量下這種做法,由於推遲了校驗和計算,因此很多錯誤的報文都被接收了,它們會占用處理報文的流程,直到報文准備進入用 戶空間時,這時候才計算了校驗和,發現錯誤並丟棄掉。這樣看似乎平白無故增加了開銷,必竟校驗和的計算是一定要進行的。 但這樣做,將校驗和計算推遲到了拷貝報文到用戶空間時,這兩個操作的綁定是很關鍵的。本來,校驗和計算要遍歷一次報文, 而拷貝又要遍歷一次報文,這樣就是兩次遍歷操作,合並後用一次遍歷搞定,它所節約的開銷是遠遠多於額外支付的。

第二:分離校驗和計算步驟

開始提到校驗和的計算分為兩步:累加、取反,將這兩步分開後,會 發現校驗和是可以一部分一部分計算的,最後再用每部分計算的值求和取反。這個特性在另一方面對拷貝和校驗和計算同時進行 提供了支持。並且,由於報文可能是分片重組的,這樣報文內容並不是一起存儲在線性地址空間中,而是將分片掛在第一個分片 skb的frag_list上,這部分內容是存儲在非線性地址空間的。因此,拷貝會一個分片一個分片的進行,由於校驗和計算的劃分, 它也可以一個分片一個分片的計算。csum_partial()和csum_fold()就是為此而生的,前者計算累加,後者計算取反。

所以一般校驗和會這樣計算,skb_checksum()計算skb的累加和,並和之前已經計算出的累加和skb- >csum相加,然後csum_fold()對最後結果取反,就是得到的校驗和。s

um = csum_fold(skb_checksum(skb, 0, len, skb->csum));

第三:改進校驗和計算

RFC1071中校驗和計算是每16bit為 單位的,但實際在累加這一步是可以調整的,內核計算是每32bit計算的,單位越大,循環就少,效率也自然會高。下面要說明 的是32bit累加與16bit累加結果是一致的。

假設要計算8個字節的校驗和,這8字節按每16bit分成 4份:1,2,3,4。左邊是每16bit累加的過程,右邊是每32bit累加的過程:

會出現疑惑的地方就是累加的進位問題,左邊16bit累加進位加到sum中,右邊32bit累加進位也要加到sum中,至於2,4相 加產生的進位,和16bit累加進位的結果是一樣的。下面就是32bit累加的代碼段,w>result判斷是否產生了進位,假設X+Y=Z 產生了進位溢出,則X&lt;Z且Y<Z,否則Z>X且Z>Y。

unsigned int 

carry = 0;     
do {     
 unsigned int w = *(unsigned int *) buff;     
 count--;     
 buff += 4;     
 result += carry;     
 result += w;     
 carry = (w > result);     
} while (count);     
result += carry;     
result = (result & 0xffff) + (result >> 16);

第四:校驗和計算技巧

節 省校驗和最好的辦法就是不計算校驗和,這在某些情況下是可行的,比如大流量發包時或局域網中,這時效率比正確性更為重要 。skb->ip_summed參數就是為此目的,CHECKSUM_UNNECESSARY就跳過校驗和計算。或者用戶在發包時設置校驗和字段 checksum=0,也會跳過校驗和計算。

skb->ip_summed = CHECKSUM_UNNECESSARY;

另外為了加速校驗和計算,很多網卡都提供了硬件計算校驗和,特別的,linux使用了skb->ip_summed和skb->csum 來使用硬件計算能力來幫助校驗TCP/UDP報文。CHECKSUM_COMPLETE表示硬件進行了計算,計算結果存儲在skb->csum中。

skb->ip_summed == CHECKSUM_COMPLETE;

在很多芯片的實現上,校驗和的計 算代碼都是用匯編來實現了,這也是出於校驗和計算的效率考慮。

最後,簡單分析下校驗和計算的兩個核心函數 。
do_csum() 校驗和累加

校驗和計算的主體部分是32bit為單位計算的,並假設buff起始地 址是對齊過的,長度也是對齊過的。因此,傳入的buff要進行處理以滿足假設。

保證計算的起始地址 是字節對齊

這裡的對齊有16bit對齊和32bit對齊。起始地址是對齊是為了效率,比如起始地址是 奇數,那麼累加時用16bit或32bit就很可能跨越一個int范圍,即讀一個數要兩次內存操作;對齊後讀一個數都只用一次內存操 作。

如果不是偶數字節,則odd=1,處理掉第一個字節,使超地址變成偶數。

odd = 1 & (unsigned long) buff;     
if (odd) {     
#ifdef __LITTLE_ENDIAN     
 result += (*buff << 8);     
#else     
 result = *buff;     
#endif     
 len--;     
 buff++;     
}

當然處理掉第一個字節後,從buff計算校驗和與從buf+1計算校驗和結果顯然是不同的,下面這步在校驗和計算完成 後,就是為了處理這種差異的。

if (odd)     
 result = ((result >> 8) & 0xff) | ((result & 0xff) << 8);

還是以例子說明,一個5字節 的buff,起始地址addr(1)=0x1,下面是傳統計算和從偶數地址開始計算的對比,要注意的是累加進程中是循環進位的,即溢出 的進位會加到最低位。因此,無論哪種方法,1,3,5累加進位會加到2+4中,而2,4累加進位會加到1+3+5中,這也是最後調換前後 8bit的值就可以保證兩者相等原因。

保證計算的長度是偶數字節

長度對齊理由很簡單,累加是 以16bit為單位的,因此主體部分只計算偶數字節,如果有多余的一個字節len & 1,則進行如下處理。

if (len & 1)     
#ifdef __LITTLE_ENDIAN
  result += *buff;
#else
  result += (*buff << 8);
#endif

最後是計算的主體部分,可以看到,它並不是單純的16bit累加,而是用32bit累加do-while循環。當然,為了 進行32bit累加,要將起始地址處理成32bit對齊,長度也要處理成32bit對齊。

count = len >> 1;  /* nr of 16-bit words.. */ 
if (count) {
 if (2 & (unsigned long) buff) {
  result += *(unsigned short *) buff;
  count--;
  len -= 2;
  buff += 2;
 }
 count >>= 1;  /* nr of 32-bit words.. */ 
 if (count) {     
  unsigned int carry = 0;     
  do {     
   unsigned int w = *(unsigned int *) buff;     
   count--;     
   buff += 4;     
   result += carry;     
   result += w;     
   carry = (w > result);     
  } while (count);     
  result += carry;     
  result = (result & 0xffff) + (result >> 16);     
 }     
 if (len & 2) {     
  result += *(unsigned short *) buff;     
  buff += 2;     
 }     
}

csum_fold() 校驗和取反

取反操作很簡單,~sum

static inline __sum16 csum_fold(__wsum csum)     
{
 u32 sum = (__force u32)csum;     
 sum = (sum & 0xffff) + (sum >> 16);     
 sum = (sum & 0xffff) + (sum >> 16);     
 return (__force __sum16)~sum;     
}
Copyright © Linux教程網 All Rights Reserved