本文將介紹在Linux系統(tǒng)中,以一個UDP包的接收過程作為示例,介紹數(shù)據(jù)包是如何一步一步從網(wǎng)卡傳到進(jìn)程手中的。
網(wǎng)卡到內(nèi)存
網(wǎng)絡(luò)接口卡必須安裝與之匹配的驅(qū)動程序才能正常工作。這些驅(qū)動程序被視為內(nèi)核模塊,其主要職責(zé)是連接網(wǎng)卡和內(nèi)核中的網(wǎng)絡(luò)模塊。在加載驅(qū)動程序時,驅(qū)動程序?qū)⒆陨碜缘骄W(wǎng)絡(luò)模塊中。當(dāng)相應(yīng)的網(wǎng)卡接收到數(shù)據(jù)包時,網(wǎng)絡(luò)模塊將調(diào)用相應(yīng)的驅(qū)動程序來處理數(shù)據(jù)。
下圖展示了數(shù)據(jù)包(packet)如何進(jìn)入內(nèi)存,并被內(nèi)核的網(wǎng)絡(luò)模塊開始處理:
- 1:外部網(wǎng)絡(luò)傳入的數(shù)據(jù)包會進(jìn)入物理網(wǎng)卡。當(dāng)目的地址不屬于該網(wǎng)卡,且該網(wǎng)卡未啟用混雜模式時,該數(shù)據(jù)包將被網(wǎng)卡丟棄。2:網(wǎng)卡使用直接內(nèi)存訪問(DMA)技術(shù)將數(shù)據(jù)包寫入指定的內(nèi)存地址。這些內(nèi)存地址由網(wǎng)卡驅(qū)動程序進(jìn)行分配和初始化。3:網(wǎng)卡通過硬件中斷請求(IRQ)向CPU發(fā)送通知,以告知數(shù)據(jù)已到達(dá)。4:CPU根據(jù)中斷表的配置,調(diào)用已注冊的中斷處理函數(shù),該函數(shù)會進(jìn)一步調(diào)用網(wǎng)卡驅(qū)動程序(網(wǎng)絡(luò)接口卡驅(qū)動程序)中相應(yīng)的函數(shù)。5:驅(qū)動程序首先禁用網(wǎng)卡的中斷功能,表示驅(qū)動程序已知曉數(shù)據(jù)已存儲在內(nèi)存中,并告知網(wǎng)卡在接收到下一個數(shù)據(jù)包時直接寫入內(nèi)存,而無需再次通知CPU,從而提高效率,并避免CPU被頻繁中斷。6:啟動軟中斷。硬中斷處理函數(shù)執(zhí)行期間不可被中斷,若其執(zhí)行時間過長,則會導(dǎo)致CPU無法響應(yīng)其他硬件的中斷。因此,內(nèi)核引入軟中斷的概念,將硬中斷處理函數(shù)中耗時的部分轉(zhuǎn)移到軟中斷處理函數(shù)中,以便逐步處理。
內(nèi)核的網(wǎng)絡(luò)模塊
軟中斷會觸發(fā)內(nèi)核網(wǎng)絡(luò)模塊中的軟中斷處理函數(shù),后續(xù)流程如下:
- 7:在操作系統(tǒng)內(nèi)核中,存在一個專門處理軟中斷的進(jìn)程,稱為ksoftirqd。當(dāng)ksoftirqd接收到軟中斷時,它會調(diào)用相應(yīng)的軟中斷處理函數(shù),對于上述提到的第6步中由網(wǎng)卡驅(qū)動模塊觸發(fā)的軟中斷,ksoftirqd會調(diào)用網(wǎng)絡(luò)模塊中的net_rx_action函數(shù)。8:net_rx_action函數(shù)會調(diào)用網(wǎng)卡驅(qū)動中的poll函數(shù),逐個處理數(shù)據(jù)包。9:在poll函數(shù)中,驅(qū)動程序會逐個讀取網(wǎng)卡寫入內(nèi)存的數(shù)據(jù)包,該數(shù)據(jù)包的格式只有驅(qū)動程序知道。10:驅(qū)動程序?qū)?nèi)存中的數(shù)據(jù)包轉(zhuǎn)換為內(nèi)核網(wǎng)絡(luò)模塊可識別的skb格式,并調(diào)用napi_gro_receive函數(shù)。11:napi_gro_receive函數(shù)會處理與GRO(通用接收處理)相關(guān)的內(nèi)容,即將可合并的數(shù)據(jù)包進(jìn)行合并,從而只需調(diào)用一次協(xié)議棧。然后檢查是否啟用了RPS(接收包分發(fā)),若啟用,則調(diào)用enqueue_to_backlog函數(shù)。12:在enqueue_to_backlog函數(shù)中,數(shù)據(jù)包將被放入CPU的softnet_data結(jié)構(gòu)體的input_pkt_queue隊(duì)列中,然后返回。如果input_pkt_queue隊(duì)列已滿,則會丟棄該數(shù)據(jù)包,該隊(duì)列的大小可以通過net.core.netdev_max_backlog參數(shù)進(jìn)行配置。13:CPU會在自身的軟中斷上下文中處理input_pkt_queue隊(duì)列中的網(wǎng)絡(luò)數(shù)據(jù)(調(diào)用__netif_receive_skb_core函數(shù))。14:如果未啟用RPS,napi_gro_receive函數(shù)會直接調(diào)用__netif_receive_skb_core函數(shù)。15:首先檢查是否存在AF_PACKET類型的套接字(即原始套接字),如果存在,則將數(shù)據(jù)包復(fù)制給該套接字。例如,tcpdump抓取的數(shù)據(jù)包即是在此處捕獲的。16:調(diào)用相應(yīng)的協(xié)議棧函數(shù),將數(shù)據(jù)包交給協(xié)議棧處理。17:在內(nèi)存中的所有數(shù)據(jù)包處理完成后(即poll函數(shù)執(zhí)行完成),啟用網(wǎng)卡的硬中斷,這樣當(dāng)網(wǎng)卡接收到下一批數(shù)據(jù)時,將會通知CPU。
enqueue_to_backlog函數(shù)也會被netif_rx函數(shù)調(diào)用,而netif_rx正是lo設(shè)備發(fā)送數(shù)據(jù)包時調(diào)用的函數(shù)
協(xié)議棧
IP層
由于是UDP包,所以第一步會進(jìn)入IP層,然后一級一級的函數(shù)往下調(diào):
- ip_rcv:ip_rcv函數(shù)是IP模塊的入口函數(shù),在該函數(shù)里面,第一件事就是將垃圾數(shù)據(jù)包(目的mac地址不是當(dāng)前網(wǎng)卡,但由于網(wǎng)卡設(shè)置了混雜模式而被接收進(jìn)來)直接丟掉,然后調(diào)用注冊在NF_INET_PRE_ROUTING上的函數(shù)NF_INET_PRE_ROUTING:netfilter放在協(xié)議棧中的鉤子,可以通過iptables來注入一些數(shù)據(jù)包處理函數(shù),用來修改或者丟棄數(shù)據(jù)包,如果數(shù)據(jù)包沒被丟棄,將繼續(xù)往下走routing:進(jìn)行路由,如果目的IP不是本地IP,且沒有開啟ip forward功能,那么數(shù)據(jù)包將被丟棄,如果開啟了ip forward功能,那將進(jìn)入ip_forward函數(shù)ip_forward:ip_forward會先調(diào)用netfilter注冊的NF_INET_FORWARD相關(guān)函數(shù),如果數(shù)據(jù)包沒有被丟棄,那么將繼續(xù)往后調(diào)用dst_output_sk函數(shù)dst_output_sk:該函數(shù)會調(diào)用IP層的相應(yīng)函數(shù)將該數(shù)據(jù)包發(fā)送出去。ip_local_deliver:如果上面routing的時候發(fā)現(xiàn)目的IP是本地IP,那么將會調(diào)用該函數(shù),在該函數(shù)中,會先調(diào)用NF_INET_LOCAL_IN相關(guān)的鉤子程序,如果通過,數(shù)據(jù)包將會向下發(fā)送到UDP層
UDP層
- udp_rcv函數(shù)是UDP模塊的入口函數(shù),用于處理接收到的UDP數(shù)據(jù)包。在該函數(shù)中會進(jìn)行一系列檢查,并調(diào)用其他函數(shù)進(jìn)行處理。其中,一個重要的函數(shù)調(diào)用是__udp4_lib_lookup_skb,該函數(shù)根據(jù)目標(biāo)IP和端口查找對應(yīng)的socket。如果找不到相應(yīng)的socket,則該數(shù)據(jù)包將被丟棄;否則,繼續(xù)處理。sock_queue_rcv_skb函數(shù)的主要功能是進(jìn)行兩項(xiàng)檢查。首先,它會檢查socket的接收緩沖區(qū)是否已滿,如果已滿,則會丟棄該數(shù)據(jù)包。然后,它會調(diào)用sk_filter函數(shù)檢查該包是否滿足當(dāng)前socket設(shè)置的過濾條件。如果socket上設(shè)置了過濾條件且該數(shù)據(jù)包不滿足條件,則該數(shù)據(jù)包也會被丟棄。在Linux中,每個socket都可以像tcpdump中一樣定義過濾條件,不滿足條件的數(shù)據(jù)包將被丟棄。__skb_queue_tail函數(shù)用于將數(shù)據(jù)包放入socket的接收隊(duì)列末尾。sk_data_ready函數(shù)用于通知socket數(shù)據(jù)包已準(zhǔn)備就緒,可以進(jìn)行處理。
調(diào)用完sk_data_ready之后,一個數(shù)據(jù)包處理完成,等待應(yīng)用層程序來讀取,上面所有函數(shù)的執(zhí)行過程都在軟中斷的上下文中。
socket
應(yīng)用層一般有兩種方式接收數(shù)據(jù),一種是recvfrom函數(shù)阻塞在那里等著數(shù)據(jù)來,這種情況下當(dāng)socket收到通知后,recvfrom就會被喚醒,然后讀取接收隊(duì)列的數(shù)據(jù);另一種是通過epoll或者select監(jiān)聽相應(yīng)的socket,當(dāng)收到通知后,再調(diào)用recvfrom函數(shù)去讀取接收隊(duì)列的數(shù)據(jù)。兩種情況都能正常的接收到相應(yīng)的數(shù)據(jù)包。
結(jié)束語
了解數(shù)據(jù)包的接收流程有助于幫助我們搞清楚我們可以在哪些地方監(jiān)控和修改數(shù)據(jù)包,哪些情況下數(shù)據(jù)包可能被丟棄,為我們處理網(wǎng)絡(luò)問題提供了一些參考,同時了解netfilter中相應(yīng)鉤子的位置,對于了解iptables的用法有一定的幫助。