天天看點

linux IPv4封包處理淺析

在《linux網絡封包接收發送淺析》一文中介紹了資料鍊路層關于網絡封包的處理。

對于接收到的封包,如果不被丢棄、不被網橋轉發,會調用netif_receive_skb()送出給ip層;

而對于ip層向外發送的封包,則通過調用dev_queue_xmit()送出給資料鍊路層。

本文就以netif_receive_skb()和dev_queue_xmit()為起始,簡要介紹一下封包在ip層的處理過程。

先來一張圖:

linux IPv4封包處理淺析

封包接收(圖中橙色箭頭所指)

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上的一張圖,直接看結果:

linux IPv4封包處理淺析

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位址。

然後在此基礎上,鄰居子系統會實作一定的緩存政策,不會對于每個封包件都這麼詢問一下。

繼續閱讀