linux 网络协议栈变化,ZZ Linux网络协议栈学习
最近學習linux內核網絡協議棧,把數據包接收流程大致理了一下,
前面也看了瀚海書香兄的總結,感覺總結的比我精煉,抓住了主干,是一目了然的那種
我的這篇本來是自己看得,因此把我自己學習中一些遇到的問題寫了出來,可能其他人會覺得廢話比較多,呵呵
另外,因為我看的書Understanding Linux Network Internal只講了ip層及以下,
因此L4層的流程是我自己在代碼中找的,不保證100%正確,
如果有錯誤,還希望大蝦及時指出,防止誤人子弟
NAPI驅動流程:
中斷發生
-->確定中斷原因是數據接收完畢(中斷原因也可能是發送完畢,DMA完畢,甚至是中斷通道上的其他設備中斷)
-->通過netif_rx_schedule將驅動自己的napi結構加入softnet_data的poll_list鏈表,禁用網卡中斷,并發出軟中斷
-->中斷返回時觸發軟中斷net_rx_action,從softnet_data的poll_list上取下剛掛入的napi結構,并且調用其
poll函數,這個poll函數也是驅動自己提供的,比如rtl8139網卡驅動中的rtl8139_poll等。
-->在poll函數中進行輪詢,直到接受完所有的數據或者預算(budget)耗盡。每接收一個報文要分配skb,用eth_type_trans處理并交給netif_receive_skb。
-->如果數據全部接收完(預算沒有用完),則重新使能中斷并將napi從鏈表中取下。如果數據沒接收完,則什么也不作,等待下一次poll函數被調度。
非NAPI流程:
中斷發生
-->確定中斷發生的原因是接收完畢。分配skb,讀入數據,用eth_type_trans處理并且將skb交給netif_rx
-->在netif_rx中,將packet加入到softnet_data的input_pkt_queue末尾(NAPI驅動不使用這個
input_pkt_queue),再通過napi_schedule將softnet_data中的backlog(這也是個napi結構)加入
softnet_data的poll_list,最后發出軟中斷
-->軟中斷net_rx_action從poll_list上取下softnet_data的backlog,調用其poll函數,這個poll函數是內核提供的process_backlog
-->函數process_backlog從softnet_data的input_pkt_queue末尾取下skb,并且直接交給netif_receive_skb處理。
-->如果input_pkt_queue中所有skb都處理完則將backlog從隊列中除去(注意input_pkt_queue中可能有多個網卡加入的報文,因為它是每cpu公用的)并退出循環;如果預算用完后也跳出循環。最后返回接受到的包數
總結:
NAPI和非NAPI的區別
1.NAPI使用中斷+輪詢的方式,中斷產生之后暫時關閉中斷然后輪詢接收完所有的數據包,接著再開中斷。而非NAPI采用純粹中斷的方式,一個中斷接收一個數據包
2.NAPI都有自己的struct napi結構,非NAPI沒有
3.NAPI有自己的poll函數,而且接收數據都是在軟中斷調用poll函數時做的,而非NAPI使用公共的process_backlog函數作為其poll函數,接收數據是在硬件中斷中做的
4.NAPI在poll函數中接收完數據之后直接把skb發給netif_receive_skb,而非NAPI在硬件中斷中接收了數據通過
netif_rx把skb掛到公共的input_pkt_queue上,最后由軟中斷調用的process_backlog函數來將其發送給
netif_receive_skb
驅動以及軟中斷這塊對skb僅僅做了以下簡單處理:
1.調用skb_reserve預留出2個字節的空間,這是為了讓ip首部對齊,因為以太網首部是14字節
2.調用skb_put將tail指向數據末尾
3.調用eth_type_trans進行如下處理:
(1)將skb->dev指向接收設備
(2)將skb->mac_header指向data(此時data就是指向mac起始地址)
(3)調用skb_pull(skb, ETH_HLEN)將skb->data后移14字節指向ip首部
(4)通過比較目的mac地址判斷包的類型,并將skb->pkt_type賦值PACKET_BROADCAST或PACKET_MULTICAST或者PACKET_OTHERHOST,因為PACKET_HOST為0,所以是默認值
(5)最后判斷協議類型,并返回(大部分情況下直接返回eth首部的protocol字段的值),這個返回值被存在skb->protocol字段中
總結,結束后,skb->data指向ip首部,skb->mac_header指向
mac首部,skb->protocol儲存L3的協議代碼,skb->pkt_type已被設置,skb->len等于接收到的報文
長度減去eth首部長度,也就是整個ip報文的總長。其余字段基本上還是默認值。
netif_receive_skb
1.將skb->iif賦值為skb->dev->ifindex,將skb->network_header和
skb->transport_header都指向skb->data,也就是ip首部,然后skb->mac_len=skb-&
gt;network_header-skb->mac_header,正常情況下應該等于ETH_HLEN吧
2.向ptype_all中注冊(通過dev_add_pack)的每一個packet_type調用一次deliver_skb,這里沒有拷貝skb,只是先增加了一下skb->users
3.調用handle_bridge處理橋報文,如果該dev不是一個橋端口則直接返回
4.調用handle_macvlan處理vlan
5.對于每一個在ptype_base中注冊的packet_type(也是用dev_add_pack),調用deliver_skb
6.如果沒有任何一個注冊的packet_type接受skb則直接kfree_skb并且返回NET_RX_DROP。否則返回最后一個pkt_type->func返回的值
總結,需要說一下dev_add_pack,這個函數根據傳入的packet_type的type字
段決定加入哪個隊列,如果是ETH_P_ALL就加入ptype_all,否則計算哈希值并加入ptype_base,通過這個函數注冊的都是L3層的協
議,比如ip,arp,rarp,bootp等,其實還有packet協議族套接字的監聽函數(除了ETH_P_ALL之外都加入ptype_base,
它們對應的接收函數是packet_rcv),這里對于ip來說,接受函數就是ip_rcv。
經過這個函數,又有幾個字段發生變化:
network_header和transport_header都指向ip首部,mac_len為mac首部長度
ip_rcv:
1.丟棄所有pkt_type為OTHER_HOST的包,注意對于將網卡設為混雜模式的監聽進程來說,這個包已經在netif_receive_skb中給它們發送了一份拷貝
2.檢查skb是否被共享,如果被共享需要用skb_clone拷貝一份,因為后面要對skb的內容進行變更
3.常規檢測:如果報文的長度小于ip首部最小長度,丟棄;如果ip協議字段不等于4丟棄;若ip首部長度字段小于5,丟棄;若ip首部長度小于ip首部
長度字段*4,丟棄;如果ip首部校驗和出錯,丟棄;如果skb->len(此時len為整個ip報文長度)小于ip首部總長字段,丟棄;如果ip
首部總長字段小于ip首部長度字段,丟棄;
4.注意第三步中skb->len是可以小于ip首部的總長字段的,因為根據代碼注釋,傳輸介質有可能在末尾添加了padding,在這種情況下,
會調用pskb_trim_rcsum將多余的結尾部分砍掉(通過把skb->tail往前移),并且還要將檢查和無效化
5.此處調用NF_INET_PRE_ROUTING鉤子函數
總結,ip_rcv主要進行的常規檢查,唯一對skb進行操作的就是將結尾的填充字段砍掉。
ip_rcv_finish:
1.首先,如果skb->dst為空,說明還不確定這個ip報文的目的地是本機還是別的機器,這時通過ip_route_input來找到rtable并且賦給skb->rtable
2.如果ip首部長度字段大于5則調用ip_rcv_options處理ip選項。該函數調用ip_options_compile將選項全部處理放在
skb的cb字段中,作為一個struct
ip_options(還要詳細看ip_options_compile)。如果有源站路由選項則檢查設備是否支持源站路由(軟件支持,可配置),則調用
ip_options_rcv_srr(此函數也還需認真看)填寫源站路由。
3.添加統計信息并調用dst_input,dst_input只是調用skb->dst->input函數,這個skb->dst就
是前面用ip_route_input確定的,而根據dst類型的不同,這個input函數可能是ip_local_deliver或者
ip_forward,這里我們看ip_local_deliver。
總結,ip_rcv_finish改變了skb->dst字段(如果本來
skb->dst字段已經有值則不改變)和skb->cb字段(在ip_rcv_options中將ip首部選項編譯之后放入cb)。
ip_options_compile可以改變報文內容,比如填寫路由記錄選項,填寫時間戳選項等
ip_local_deliver
1.如果ip首部offset或者MF不為0,則調用ip_defrag進行ip分片的重組,ip_defrag只在成功完全重組了一個報文之后才會返回
0,其他情況都是返回非0,如果返回非0就會從ip_local_deliver返回。ip_defrag也比較復雜,需要細看,總體來說就是將分片放在
一個哈希表中,開啟定時器,來一個分片就與前面屬于同一ip報文的分片合并(兩個分片是否屬于同一個ip報文是通過ip的id字段,源目的地址,L4協議
等多個參數確定的,可參考ip4_frag_match)
2.鉤子NF_INET_LOCAL_IN,并調用ip_local_deliver_finish
總結,從上兩個函數可以看出NF_INET_PRE_ROUTING和
NF_INET_LOCAL_IN之間的區別,前者還沒有經過路由處理,即skb->dst一般還沒有確定,而后者是已經確定了
skb->dst且dst為本地地址,假如skb->dst不是本地地址則會調用ip_forward,這就不會觸發
NF_INET_LOCAL_IN了。另外NF_INET_PRE_ROUTING尚未對ip分片進行合并處理,而NF_INET_LOCAL_IN抓到
的數據包是已經合并成的ip報文了
ip_local_deliver_finish
1.將skb->data繼續移動指向傳輸層首部,并且將skb->transport_header也指向傳輸層首部,接下來開始處理
2.首先從ip首部取得傳輸層協議號,然后用這個協議號調用raw_local_deliver將skb傳給raw_v4_hashinfo哈希表中的原始套接字協議
3.再利用protocol值作為下標取得inet_protos全局數組中的注冊協議(對于tcp,udp,icmp分別是
tcp_protocol,udp_protocol,icmp_protocol)。如果找到了對應的協議處理結構,就把skb交給該結構的
handler函數處理(對于tcp,udp,icmp分別是tcp_v4_rcv,udp_rcv,icmp_rcv)。如果沒找到對應的處理結構,則
回發一個icmp協議不可達的目的不可達報文,并釋放skb。
總結:這里又一次移動了skb->data指針,將其指向傳輸層首部,同時設置了
transport_header也指向傳輸層首部。raw_v4_hashinfo和inet_protos都是一個256項的全局數組,以協議號為下
標保存了各個協議的處理結構。這兩個數組就像是L4層的ptype_base,根據本層的協議號來決定處理函數
注意區別raw_v4_hashinfo和上面的ptype_all,前者是AF_INET的SOCK_RAW套接字注冊的接收結構,而后者是
AF_PACKET套接字注冊的接收結構,可見raw套接字是經過了ip層處理的而packet是在netif_receive_skb中接收的,尚未經
過任何處理,其中一個顯著區別就是raw經過了ip_defrag而packet沒有
對于udp來說,inet_protos中的結構是全局變量udp_protocol,它的handler函數是udp_rcv
udp_rcv所做的就是直接調用__udp4_lib_rcv(skb, udp_hash, IPPROTO_UDP);
__udp4_lib_rcv
此函數中會調用__udp4_lib_lookup_skb-->()__udp4_lib_lookup()來查找此udp包對應的socket,主要是查找源目的地址和端口號都符合的socket。
如果查找到了對應的socket,則調用udp_queue_rcv_skb將skb放入udp的接收隊列,然后返回0
如果沒有查找到對應的socket,要向源地址發送一個ICMP端口不可達消息
udp_queue_rcv_skb
它經過__udp_queue_rcv_skb(sk,
skb)-->__udp_queue_rcv_skb-->skb_queue_tail一系列調用過程將skb加入socket的接收隊
列sk->sk_receive_queuek末尾。其中還要檢測接收緩沖區是否已經滿。
接著調用sk->sk_data_ready(sk, skb_len)通知socket有數據就緒,可以讀了。一般情況下這個函數對應sock_def_readable,這個函數的功能就是喚醒在sk->sk_sleep上睡眠的進程
那么是誰在這里睡眠呢?在調用recvfrom系統調用接收報文的時候,會經過這樣一個流程
sys_socketcall
-->sys_recvfrom
-->sock_recvmsg
-->__sock_recvmsg
-->sock->ops->recvmsg,這個sock->ops對應全局變量inet_dgram_ops,里面的recvmsg對應sock_common_recvmsg
-->sock_common_recvmsg
-->sk->sk_prot->recvmsg,這個sk->sk_prot對應全局變量udp_prot,里面的recvmsg對應udp_recvmsg
-->udp_recvmsg
-->__skb_recv_datagram
在__skb_recv_datagram中,會首先嘗試從sk->sk_receive_queue上取下數據包,如果發現隊列中沒有數據包,則
開始在sk->sk_skeep上睡眠。而上面sock_def_readable喚醒的就是這里睡眠的進程。
可以看到,在__skb_recv_datagram中被喚醒后,函數又嘗試從sk->sk_receive_queue上取下數據包,這時當然會
成功,成功之后返回到udp_recvmsg。udp_recvmsg再進行一些簡單的檢測之后就調用copy_to_user將數據拷貝到用戶空間了
(其實這里并不是簡單調用copy_to_user,還要處理很多情況,比如用戶使用的msghdr可能包含多個iovec,skb可能有多個frags
等等)
這樣,一個udp數據包就從網卡到達了用戶的緩沖區
閱讀(713) | 評論(0) | 轉發(0) |
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的linux 网络协议栈变化,ZZ Linux网络协议栈学习的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaEE规范与系统结构
- 下一篇: linux中输入ls出现蓝色的点,lin