在《linux網絡封包接收發送淺析》一文中介紹了資料鍊路層關于網絡封包的處理。
對于接收到的封包,如果不被丢棄、不被網橋轉發,會調用netif_receive_skb()送出給ip層;
而對于ip層向外發送的封包,則通過調用dev_queue_xmit()送出給資料鍊路層。
本文就以netif_receive_skb()和dev_queue_xmit()為起始,簡要介紹一下封包在ip層的處理過程。
先來一張圖:
封包接收(圖中橙色箭頭所指)
netif_receive_skb()
對每一種已注冊的協定類型調用deliver_skb(),進而調用到其packet_type->func()函數。
對于ip協定,會調用到ip_rcv()。
ip_rcv()
ip報頭檢查,調用ip_fast_csum()檢查校驗和。
netfilter:nf_inet_pre_routing。
調用ip_rcv_finish()。
ip_rcv_finish()
調用ip_route_input_noref()查找路由表,其結果會決定封包的去向。
調用ip_rcv_options()處理ip選項。
調用dst_input(),進而調用到rtable.dst_entry->input()。根據路由不同,主要有ip_forward(轉發)、ip_local_deliver(接收)兩種取值。
ip_local_deliver()
如果存在分片,調用ip_defrag()完成分片重組。如果分片暫未到齊則直接傳回。
netfilter:nf_inet_local_in。
調用ip_local_deliver_finish()。
ip_local_deliver_finish()
調用raw_local_deliver()試圖按raw socket方式(直接收發ip封包的socket)遞交給上層應用,遞交成功則傳回。
按ip_hdr->protocol取出相應l4協定,調用net_protocol.handler(),進而将封包送出給傳輸層。主要有的l4協定handler有icmp_rcv()、udp_rcv()、tcp_v4_rcv()、等。
封包發送(圖中紫色箭頭所指)
傳輸層在發送封包時,會調用到ip層的接口。如:ip_queue_xmit()、ip_append_data()/ip_append_page()+ip_push_pending_frames()、ip_local_out()、等。
這些函數最終會調用到ip_local_out()。
在此之前,封包會被構造好。可能由傳輸層的代碼自己構造、也可能通過調用ip_append_data()這樣的輔助函數來構造。
具體構造封包的細節就不細說了,引用ulni上的一張圖,直接看結果:
struct sock是跟應用建立的socket相對應的結構,其中的sk_write_queue指向待發送的ip封包分片隊列,每個分片由一個struct sk_buff來表示。
由于網絡節點存在mtu,也就是最大傳輸單元,一次送出給資料鍊路層的封包不能太大(如1500位元組),是以過大的ip封包需要分片後發送。
注意,隻有第一個分片有l4的報頭,因為對于l4協定來說,這些分片組裝成的整體才是一個封包。
當然,最好的情況是沒有分片,也就是l4協定總是發送尺寸小于mtu的封包。
struct sk_buff中有一組head、data、tail、end這樣的指針,指向需要發送的封包buffer。
struct sk_buff之後會緊跟一個struct skb_shared_info結構。屬于同一個分片的封包資料可能分散于多個碎片中,其中的第一個碎片由上述head、data等指針指向,後續碎片則由struct skb_shared_info來訓示。
為什麼要有多個碎片呢?
一方面,因為上層發送資料有可能就是一小片一小片的發送的,比如傳送檔案,每次讀128位元組并發送。而這些碎片可以在同一個ip封包中發送,隻要總和不超過mtu。
另一方面,很多硬體支援這樣的由多個碎片構成的緩沖區。如果某些硬體不支援的話,那構造sk_buff的時候就隻能把每次送出的資料都拷貝到同一塊連續的buffer中,這樣可能就會多一次拷貝。
當然,就算硬體支援多個碎片,資料拷貝可能也無法避免。比如說當資料源來自于使用者空間的時候,就必定存在使用者空間到核心空間的一次拷貝(因為使用者指定的位址可能存在錯誤,不能直接送出給網卡)。而在類似于sendfile這樣的系統調用中,資料源本身就在核心空間,則可能做到真正的零拷貝。
ip_append_data()/ip_append_page()就是完成sk_buff構造的輔助函數,調用它往struct sock中塞資料,其内部會控制是否應該配置設定新的struct sk_buff作為分片、或者資料是否應該作為碎片放入struct skb_shared_info結構。
ip_local_out()
netfilter:nf_inet_local_out。
調用dst_output(),進而調用到ip_output()。
ip_output()
netfilter:nf_inet_post_routing。
調用ip_finish_output()。
ip_finish_output()
調用ip_fragment()對封包做分片後發送,或直接調用ip_finish_output2()發送。
ip_finish_output2()
調用鄰居子系統neigh_output(),最終由鄰居子系統調用dev_queue_xmit()發送封包。
封包轉發(圖中綠色箭頭所指)
對于接收到的封包,如果路由子系統認為應該轉發,則dst_input()會調用到ip_forward()。
ip_forward()
處理ip選項、遞減ttl。
netfilter:nf_inet_forward。
調用ip_forward_finish()。
ip_forward_finish()
調用ip_forward_options()處理ip選項。
其他
在上述流程中,有幾個點再額外說明一下:
netfilter:ip封包的處理流程中共有pre_routing、local_in、forward、local_out、post_routing這五個hook點。使用者可以通過配置netfilter,在這幾個節點上添加一些規則,實作對特定ip封包的幹預。
route:路由子系統。決定ip封包下一步應該發送到哪個ip位址上(或者自己接收)。這個目的ip位址所對應的主機必定是與本機直接相連、或通過交換機相連的(也就是說,兩台機器之前的通信不需要路由器轉發,兩台機器是“鄰居”)。
舉個簡單的例子,路由表有如下配置:
destination gateway genmask flags metric ref use iface
10.20.150.0 * 255.255.255.0 u 0 0 0 eth0
default 10.20.150.254 0.0.0.0 ug 0 0 0 eth0
那麼,目的地是10.20.150.0/24這個子網的封包,直接進行轉發(目的主機和本機直接就是鄰居);而目的地是其他位址的封包,發往預設網關10.20.150.254,由它來繼續轉發。(注意,預設網關10.20.150.254也是本機的鄰居。)
neighbour:鄰居子系統。路由子系統确定了封包要發送到的ip位址,而在将封包送出給資料鍊路層之前,還需要知道目的主機的mac位址。這就是鄰居子系統幹的事情。
簡單來說,一台機器通過在子網中廣播一個arp封包,來詢問目的ip位址的mac位址是什麼。比如目的ip位址是10.20.150.133,本機會廣播"who is 10.20.150.133/24"的arp封包。像交換機這樣的資料鍊路層裝置會将arp封包轉發,進而讓每一個鄰居都收到。當10.20.150.133收到詢問後,會向本機回複其mac位址。
然後在此基礎上,鄰居子系統會實作一定的緩存政策,不會對于每個封包件都這麼詢問一下。