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