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