Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 506, comments - 13, trackbacks - 0, articles - 1

          本文作者張彥飛,原題“圖解Linux網(wǎng)絡(luò)包接收過(guò)程”,內(nèi)容有少許改動(dòng)。

          1、引言

          因?yàn)橐獙?duì)百萬(wàn)、千萬(wàn)、甚至是過(guò)億的用戶提供各種網(wǎng)絡(luò)服務(wù),所以在一線互聯(lián)網(wǎng)企業(yè)里面試和晉升后端開發(fā)同學(xué)的其中一個(gè)重點(diǎn)要求就是要能支撐高并發(fā),要理解性能開銷,會(huì)進(jìn)行性能優(yōu)化。而很多時(shí)候,如果你對(duì)網(wǎng)絡(luò)底層的理解不深的話,遇到很多線上性能瓶頸你會(huì)覺(jué)得狗拿刺猬,無(wú)從下手。

          這篇文章將用圖解的方式,從操作系統(tǒng)這一層來(lái)深度理解一下網(wǎng)絡(luò)包的接收過(guò)程(因?yàn)槟苤苯涌吹絻?nèi)核源碼,本文以Linux為例)。

          按照慣例來(lái)借用一段最簡(jiǎn)單的代碼開始思考。

          為了簡(jiǎn)單起見,我們用udp來(lái)舉例,如下:

          int main(){

              intserverSocketFd = socket(AF_INET, SOCK_DGRAM, 0);

              bind(serverSocketFd, ...);

           

              char buff[BUFFSIZE];

              int readCount = recvfrom(serverSocketFd, buff, BUFFSIZE, 0, ...);

              buff[readCount] = '\0';

              printf("Receive from client:%s\n", buff);

          }

          上面代碼是一段udp server接收收據(jù)的邏輯。當(dāng)在開發(fā)視角看的時(shí)候,只要客戶端有對(duì)應(yīng)的數(shù)據(jù)發(fā)送過(guò)來(lái),服務(wù)器端執(zhí)行recv_from后就能收到它,并把它打印出來(lái)。

          我們現(xiàn)在想知道的是:當(dāng)網(wǎng)絡(luò)包達(dá)到網(wǎng)卡,直到我們的recvfrom收到數(shù)據(jù),這中間,究竟都發(fā)生過(guò)什么?

          通過(guò)本文,你將從操作系統(tǒng)內(nèi)部這一層深入理解網(wǎng)絡(luò)是如何實(shí)現(xiàn)的,以及各個(gè)部分之間是如何交互的。相信這對(duì)你的工作將會(huì)有非常大的幫助(本文將以Linux為例,源碼基于Linux 3.10,源代碼參見:https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/,網(wǎng)卡驅(qū)動(dòng)采用Intel的igb網(wǎng)卡舉例)。

          友情提示:本文略長(zhǎng),可以先Mark后看!

          (本文同步發(fā)布于:http://www.52im.net/thread-3247-1-1.html

          2、系列文章

          本文是系列文章中的第10篇,本系列文章的大綱如下:

          不為人知的網(wǎng)絡(luò)編程(一):淺析TCP協(xié)議中的疑難雜癥(上篇)

          不為人知的網(wǎng)絡(luò)編程(二):淺析TCP協(xié)議中的疑難雜癥(下篇)

          不為人知的網(wǎng)絡(luò)編程(三):關(guān)閉TCP連接時(shí)為什么會(huì)TIME_WAIT、CLOSE_WAIT

          不為人知的網(wǎng)絡(luò)編程(四):深入研究分析TCP的異常關(guān)閉

          不為人知的網(wǎng)絡(luò)編程(五):UDP的連接性和負(fù)載均衡

          不為人知的網(wǎng)絡(luò)編程(六):深入地理解UDP協(xié)議并用好它

          不為人知的網(wǎng)絡(luò)編程(七):如何讓不可靠的UDP變的可靠?

          不為人知的網(wǎng)絡(luò)編程(八):從數(shù)據(jù)傳輸層深度解密HTTP

          不為人知的網(wǎng)絡(luò)編程(九):理論聯(lián)系實(shí)際,全方位深入理解DNS

          不為人知的網(wǎng)絡(luò)編程(十):深入操作系統(tǒng),從內(nèi)核理解網(wǎng)絡(luò)包的接收過(guò)程(Linux篇)》(本文

          3、網(wǎng)絡(luò)收包總覽

          在TCP/IP網(wǎng)絡(luò)分層模型里,整個(gè)協(xié)議棧被分成了:物理層、鏈路層、網(wǎng)絡(luò)層,傳輸層和應(yīng)用層。

          物理層對(duì)應(yīng)的是網(wǎng)卡和網(wǎng)線,應(yīng)用層對(duì)應(yīng)的是我們常見的Nginx,F(xiàn)TP等等各種應(yīng)用。對(duì)于Linux來(lái)說(shuō),它實(shí)現(xiàn)的是鏈路層、網(wǎng)絡(luò)層和傳輸層這三層。

          在Linux內(nèi)核實(shí)現(xiàn)中,鏈路層協(xié)議靠網(wǎng)卡驅(qū)動(dòng)來(lái)實(shí)現(xiàn),內(nèi)核協(xié)議棧來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)層和傳輸層。內(nèi)核對(duì)更上層的應(yīng)用層提供socket接口來(lái)供用戶進(jìn)程訪問(wèn)。

          我們用Linux的視角來(lái)看到的TCP/IP網(wǎng)絡(luò)分層模型應(yīng)該是下面這個(gè)樣子的:

          在Linux的源代碼中,網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)對(duì)應(yīng)的邏輯位于driver/net/ethernet。

          其中:

          1)intel系列網(wǎng)卡的驅(qū)動(dòng)在driver/net/ethernet/intel目錄下;

          2)協(xié)議棧模塊代碼位于kernel和net目錄。

          內(nèi)核和網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)是通過(guò)中斷的方式來(lái)處理的。

          當(dāng)設(shè)備上有數(shù)據(jù)到達(dá)的時(shí)候:會(huì)給CPU的相關(guān)引腳上觸發(fā)一個(gè)電壓變化,以通知CPU來(lái)處理數(shù)據(jù)。

          對(duì)于網(wǎng)絡(luò)模塊來(lái)說(shuō):由于處理過(guò)程比較復(fù)雜和耗時(shí),如果在中斷函數(shù)中完成所有的處理,將會(huì)導(dǎo)致中斷處理函數(shù)(優(yōu)先級(jí)過(guò)高)將過(guò)度占據(jù)CPU,將導(dǎo)致CPU無(wú)法響應(yīng)其它設(shè)備,例如鼠標(biāo)和鍵盤的消息。

          因此Linux中斷處理函數(shù)是分上半部和下半部的。上半部是只進(jìn)行最簡(jiǎn)單的工作,快速處理然后釋放CPU,接著CPU就可以允許其它中斷進(jìn)來(lái)。剩下將絕大部分的工作都放到下半部中,可以慢慢從容處理。Linux 2.4以后的內(nèi)核版本采用的下半部實(shí)現(xiàn)方式是軟中斷,由ksoftirqd內(nèi)核線程全權(quán)處理。和硬中斷不同的是,硬中斷是通過(guò)給CPU物理引腳施加電壓變化,而軟中斷是通過(guò)給內(nèi)存中的一個(gè)變量的二進(jìn)制值以通知軟中斷處理程序。

          好了,大概了解了網(wǎng)卡驅(qū)動(dòng)、硬中斷、軟中斷和ksoftirqd線程之后,我們?cè)谶@幾個(gè)概念的基礎(chǔ)上給出一個(gè)內(nèi)核收包的路徑示意。

          Linux內(nèi)核網(wǎng)絡(luò)收包總覽:

          如上圖所示:當(dāng)網(wǎng)卡上收到數(shù)據(jù)以后,Linux中第一個(gè)工作的模塊是網(wǎng)絡(luò)驅(qū)動(dòng)。網(wǎng)絡(luò)驅(qū)動(dòng)會(huì)以DMA的方式把網(wǎng)卡上收到的幀寫到內(nèi)存里。再向CPU發(fā)起一個(gè)中斷,以通知CPU有數(shù)據(jù)到達(dá)。第二,當(dāng)CPU收到中斷請(qǐng)求后,會(huì)去調(diào)用網(wǎng)絡(luò)驅(qū)動(dòng)注冊(cè)的中斷處理函數(shù)。網(wǎng)卡的中斷處理函數(shù)并不做過(guò)多工作,發(fā)出軟中斷請(qǐng)求,然后盡快釋放CPU。ksoftirqd檢測(cè)到有軟中斷請(qǐng)求到達(dá),調(diào)用poll開始輪詢收包,收到后交由各級(jí)協(xié)議棧處理。對(duì)于UDP包來(lái)說(shuō),會(huì)被放到用戶socket的接收隊(duì)列中。

          我們從上面這張圖中已經(jīng)從整體上把握到了操作系統(tǒng)對(duì)數(shù)據(jù)包的處理過(guò)程。但是要想了解更多網(wǎng)絡(luò)模塊工作的細(xì)節(jié),我們還得往下看。

          4、網(wǎng)絡(luò)數(shù)據(jù)到來(lái)前操作系統(tǒng)的準(zhǔn)備

          Linux驅(qū)動(dòng)、內(nèi)核協(xié)議棧等等模塊在具備接收網(wǎng)卡數(shù)據(jù)包之前,要做很多的準(zhǔn)備工作才行。

          比如:要提前創(chuàng)建好ksoftirqd內(nèi)核線程,要注冊(cè)好各個(gè)協(xié)議對(duì)應(yīng)的處理函數(shù),網(wǎng)絡(luò)設(shè)備子系統(tǒng)要提前初始化好,網(wǎng)卡要啟動(dòng)好。只有這些都Ready之后,我們才能真正開始接收數(shù)據(jù)包。

          那么我們現(xiàn)在來(lái)看看這些準(zhǔn)備工作都是怎么做的。

          4.1 創(chuàng)建ksoftirqd內(nèi)核線程

          Linux的軟中斷都是在專門的內(nèi)核線程(ksoftirqd)中進(jìn)行的,因此我們非常有必要看一下這些進(jìn)程是怎么初始化的,這樣我們才能在后面更準(zhǔn)確地了解收包過(guò)程。該進(jìn)程數(shù)量不是1個(gè),而是N個(gè),其中N等于你的機(jī)器的核數(shù)。

          系統(tǒng)初始化的時(shí)候在kernel/smpboot.c中調(diào)用了smpboot_register_percpu_thread, 該函數(shù)進(jìn)一步會(huì)執(zhí)行到spawn_ksoftirqd(位于kernel/softirq.c)來(lái)創(chuàng)建出softirqd進(jìn)程。

          創(chuàng)建ksoftirqd內(nèi)核線程: 

          相關(guān)代碼如下:

          //file: kernel/softirq.c

          static struct smp_hotplug_thread softirq_threads = {

              .store          = &ksoftirqd,

              .thread_should_run  = ksoftirqd_should_run,

              .thread_fn      = run_ksoftirqd,

              .thread_comm        = "ksoftirqd/%u",};

          static__init intspawn_ksoftirqd(void){

              register_cpu_notifier(&cpu_nfb);

           

              BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

              return0;

          }

          early_initcall(spawn_ksoftirqd);

          當(dāng)ksoftirqd被創(chuàng)建出來(lái)以后,它就會(huì)進(jìn)入自己的線程循環(huán)函數(shù)ksoftirqd_should_run和run_ksoftirqd了。不停地判斷有沒(méi)有軟中斷需要被處理。

          這里需要注意的一點(diǎn)是,軟中斷不僅僅只有網(wǎng)絡(luò)軟中斷,還有其它類型:

          //file: include/linux/interrupt.h

          enum{

              HI_SOFTIRQ=0,

              TIMER_SOFTIRQ,

              NET_TX_SOFTIRQ,

              NET_RX_SOFTIRQ,

              BLOCK_SOFTIRQ,

              BLOCK_IOPOLL_SOFTIRQ,

              TASKLET_SOFTIRQ,

              SCHED_SOFTIRQ,

              HRTIMER_SOFTIRQ,

              RCU_SOFTIRQ, 

          };

          4.2 網(wǎng)絡(luò)子系統(tǒng)初始化

          網(wǎng)絡(luò)子系統(tǒng)初始化: 

          linux內(nèi)核通過(guò)調(diào)用subsys_initcall來(lái)初始化各個(gè)子系統(tǒng),在源代碼目錄里你可以grep出許多對(duì)這個(gè)函數(shù)的調(diào)用。

          這里我們要說(shuō)的是網(wǎng)絡(luò)子系統(tǒng)的初始化,會(huì)執(zhí)行到net_dev_init函數(shù):

          //file: net/core/dev.c

          static int __init net_dev_init(void){

              ......

              for_each_possible_cpu(i) {

                  structsoftnet_data *sd = &per_cpu(softnet_data, i);

           

                  memset(sd, 0, sizeof(*sd));

                  skb_queue_head_init(&sd->input_pkt_queue);

                  skb_queue_head_init(&sd->process_queue);

                  sd->completion_queue = NULL;

                  INIT_LIST_HEAD(&sd->poll_list);

                  ......

              }

              ......

              open_softirq(NET_TX_SOFTIRQ, net_tx_action);

              open_softirq(NET_RX_SOFTIRQ, net_rx_action);

          }

          subsys_initcall(net_dev_init);

          在這個(gè)函數(shù)里,會(huì)為每個(gè)CPU都申請(qǐng)一個(gè)softnet_data數(shù)據(jù)結(jié)構(gòu),在這個(gè)數(shù)據(jù)結(jié)構(gòu)里的poll_list是等待驅(qū)動(dòng)程序?qū)⑵鋚oll函數(shù)注冊(cè)進(jìn)來(lái),稍后網(wǎng)卡驅(qū)動(dòng)初始化的時(shí)候我們可以看到這一過(guò)程。

          另外open_softirq注冊(cè)了每一種軟中斷都注冊(cè)一個(gè)處理函數(shù)。NET_TX_SOFTIRQ的處理函數(shù)為net_tx_action,NET_RX_SOFTIRQ的為net_rx_action。繼續(xù)跟蹤open_softirq后發(fā)現(xiàn)這個(gè)注冊(cè)的方式是記錄在softirq_vec變量里的。后面ksoftirqd線程收到軟中斷的時(shí)候,也會(huì)使用這個(gè)變量來(lái)找到每一種軟中斷對(duì)應(yīng)的處理函數(shù)。

          //file: kernel/softirq.c

          void open_softirq(int nr, void(*action)(struct softirq_action *)){

              softirq_vec[nr].action = action;

          }

          4.3 協(xié)議棧注冊(cè)

          操作系統(tǒng)內(nèi)核實(shí)現(xiàn)了網(wǎng)絡(luò)層的ip協(xié)議,也實(shí)現(xiàn)了傳輸層的tcp協(xié)議和udp協(xié)議。這些協(xié)議對(duì)應(yīng)的實(shí)現(xiàn)函數(shù)分別是ip_rcv(),tcp_v4_rcv()和udp_rcv()。和我們平時(shí)寫代碼的方式不一樣的是,內(nèi)核是通過(guò)注冊(cè)的方式來(lái)實(shí)現(xiàn)的。

          Linux內(nèi)核中的fs_initcall和subsys_initcall類似,也是初始化模塊的入口。fs_initcall調(diào)用inet_init后開始網(wǎng)絡(luò)協(xié)議棧注冊(cè)。通過(guò)inet_init,將這些函數(shù)注冊(cè)到了inet_protos和ptype_base數(shù)據(jù)結(jié)構(gòu)中了。

          如下圖:

          相關(guān)代碼如下:

          //file: net/ipv4/af_inet.c

          static struct packet_type ip_packet_type __read_mostly = {

              .type = cpu_to_be16(ETH_P_IP),

              .func = ip_rcv,};static const struct net_protocol udp_protocol = {

              .handler =  udp_rcv,

              .err_handler =  udp_err,

              .no_policy =    1,

              .netns_ok = 1,};static const struct net_protocol tcp_protocol = {

              .early_demux    =   tcp_v4_early_demux,

              .handler    =   tcp_v4_rcv,

              .err_handler    =   tcp_v4_err,

              .no_policy  =   1,

              .netns_ok   =   1,

          };

          static int __init inet_init(void){

              ......

              if(inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)

                  pr_crit("%s: Cannot add ICMP protocol\n", __func__);

              if(inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)

                  pr_crit("%s: Cannot add UDP protocol\n", __func__);

              if(inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)

                  pr_crit("%s: Cannot add TCP protocol\n", __func__);

              ......

              dev_add_pack(&ip_packet_type);

          }

          上面的代碼中我們可以看到,udp_protocol結(jié)構(gòu)體中的handler是udp_rcv,tcp_protocol結(jié)構(gòu)體中的handler是tcp_v4_rcv,通過(guò)inet_add_protocol被初始化了進(jìn)來(lái)。

          int inet_add_protocol(const struct net_protocol *prot, unsigned charprotocol){

              if(!prot->netns_ok) {

                  pr_err("Protocol %u is not namespace aware, cannot register.\n",

                      protocol);

                  return-EINVAL;

              }

           

              return !cmpxchg((conststructnet_protocol **)&inet_protos[protocol],

                      NULL, prot) ? 0 : -1;

          }

          inet_add_protocol函數(shù)將tcp和udp對(duì)應(yīng)的處理函數(shù)都注冊(cè)到了inet_protos數(shù)組中了。再看dev_add_pack(&ip_packet_type);這一行,ip_packet_type結(jié)構(gòu)體中的type是協(xié)議名,func是ip_rcv函數(shù),在dev_add_pack中會(huì)被注冊(cè)到ptype_base哈希表中。

          //file: net/core/dev.c

          void dev_add_pack(struct packet_type *pt){

              struct list_head *head = ptype_head(pt);

              ......

          }

          static inline struct list_head *ptype_head(const struct packet_type *pt){

              if(pt->type == htons(ETH_P_ALL))

                  return &ptype_all;

              else

                  return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];

          }

          這里我們需要記住inet_protos記錄著udp,tcp的處理函數(shù)地址,ptype_base存儲(chǔ)著ip_rcv()函數(shù)的處理地址。后面我們會(huì)看到軟中斷中會(huì)通過(guò)ptype_base找到ip_rcv函數(shù)地址,進(jìn)而將ip包正確地送到ip_rcv()中執(zhí)行。在ip_rcv中將會(huì)通過(guò)inet_protos找到tcp或者udp的處理函數(shù),再而把包轉(zhuǎn)發(fā)給udp_rcv()或tcp_v4_rcv()函數(shù)。

          擴(kuò)展一下,如果看一下ip_rcv和udp_rcv等函數(shù)的代碼能看到很多協(xié)議的處理過(guò)程。

          例如:ip_rcv中會(huì)處理netfilter和iptable過(guò)濾,如果你有很多或者很復(fù)雜的 netfilter 或 iptables 規(guī)則,這些規(guī)則都是在軟中斷的上下文中執(zhí)行的,會(huì)加大網(wǎng)絡(luò)延遲。

          再例如:udp_rcv中會(huì)判斷socket接收隊(duì)列是否滿了。對(duì)應(yīng)的相關(guān)內(nèi)核參數(shù)是net.core.rmem_max和net.core.rmem_default。如果有興趣,建議大家好好讀一下inet_init這個(gè)函數(shù)的代碼。

          4.4 網(wǎng)卡驅(qū)動(dòng)初始化

          每一個(gè)驅(qū)動(dòng)程序(不僅僅只是網(wǎng)卡驅(qū)動(dòng))會(huì)使用 module_init 向內(nèi)核注冊(cè)一個(gè)初始化函數(shù),當(dāng)驅(qū)動(dòng)被加載時(shí),內(nèi)核會(huì)調(diào)用這個(gè)函數(shù)。

          比如igb網(wǎng)卡驅(qū)動(dòng)的代碼位于drivers/net/ethernet/intel/igb/igb_main.c:

          //file: drivers/net/ethernet/intel/igb/igb_main.c

          static struct pci_driver igb_driver = {

              .name     = igb_driver_name,

              .id_table = igb_pci_tbl,

              .probe    = igb_probe,

              .remove= igb_remove,

              ......

          };

          static int __init igb_init_module(void){

              ......

              ret = pci_register_driver(&igb_driver);

              return ret;

          }

          驅(qū)動(dòng)的pci_register_driver調(diào)用完成后,Linux內(nèi)核就知道了該驅(qū)動(dòng)的相關(guān)信息,比如igb網(wǎng)卡驅(qū)動(dòng)的igb_driver_name和igb_probe函數(shù)地址等等。當(dāng)網(wǎng)卡設(shè)備被識(shí)別以后,內(nèi)核會(huì)調(diào)用其驅(qū)動(dòng)的probe方法(igb_driver的probe方法是igb_probe)。驅(qū)動(dòng)probe方法執(zhí)行的目的就是讓設(shè)備ready,對(duì)于igb網(wǎng)卡,其igb_probe位于drivers/net/ethernet/intel/igb/igb_main.c下。

          主要執(zhí)行的操作如下: 

          第5步中我們看到:網(wǎng)卡驅(qū)動(dòng)實(shí)現(xiàn)了ethtool所需要的接口,也在這里注冊(cè)完成函數(shù)地址的注冊(cè)。當(dāng) ethtool 發(fā)起一個(gè)系統(tǒng)調(diào)用之后,內(nèi)核會(huì)找到對(duì)應(yīng)操作的回調(diào)函數(shù)。對(duì)于igb網(wǎng)卡來(lái)說(shuō),其實(shí)現(xiàn)函數(shù)都在drivers/net/ethernet/intel/igb/igb_ethtool.c下。

          相信你這次能徹底理解ethtool的工作原理了吧?這個(gè)命令之所以能查看網(wǎng)卡收發(fā)包統(tǒng)計(jì)、能修改網(wǎng)卡自適應(yīng)模式、能調(diào)整RX 隊(duì)列的數(shù)量和大小,是因?yàn)閑thtool命令最終調(diào)用到了網(wǎng)卡驅(qū)動(dòng)的相應(yīng)方法,而不是ethtool本身有這個(gè)超能力。

          第6步:注冊(cè)的igb_netdev_ops中包含的是igb_open等函數(shù),該函數(shù)在網(wǎng)卡被啟動(dòng)的時(shí)候會(huì)被調(diào)用。

          //file: drivers/net/ethernet/intel/igb/igb_main.c

          static const struct net_device_ops igb_netdev_ops = {

            .ndo_open               = igb_open,

            .ndo_stop               = igb_close,

            .ndo_start_xmit         = igb_xmit_frame,

            .ndo_get_stats64        = igb_get_stats64,

            .ndo_set_rx_mode        = igb_set_rx_mode,

            .ndo_set_mac_address    = igb_set_mac,

            .ndo_change_mtu         = igb_change_mtu,

            .ndo_do_ioctl           = igb_ioctl,

           ......

          第7步:在igb_probe初始化過(guò)程中,還調(diào)用到了igb_alloc_q_vector。他注冊(cè)了一個(gè)NAPI機(jī)制所必須的poll函數(shù),對(duì)于igb網(wǎng)卡驅(qū)動(dòng)來(lái)說(shuō),這個(gè)函數(shù)就是igb_poll,如下代碼所示。

          static int igb_alloc_q_vector(struct igb_adapter *adapter,

                            int v_count, int v_idx,

                            int txr_count, int txr_idx,

                            int rxr_count, int rxr_idx){

              ......

              /* initialize NAPI */

              netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);

          }

          4.5 啟動(dòng)網(wǎng)卡

          當(dāng)上面的初始化都完成以后,就可以啟動(dòng)網(wǎng)卡了。

          回憶前面網(wǎng)卡驅(qū)動(dòng)初始化時(shí),我們提到了驅(qū)動(dòng)向內(nèi)核注冊(cè)了 structure net_device_ops 變量,它包含著網(wǎng)卡啟用、發(fā)包、設(shè)置mac 地址等回調(diào)函數(shù)(函數(shù)指針)。當(dāng)啟用一個(gè)網(wǎng)卡時(shí)(例如,通過(guò) ifconfig eth0 up),net_device_ops 中的 igb_open方法會(huì)被調(diào)用。

          它通常會(huì)做以下事情:

          //file: drivers/net/ethernet/intel/igb/igb_main.c

          static int __igb_open(struct net_device *netdev, bool resuming){

              /* allocate transmit descriptors */

              err = igb_setup_all_tx_resources(adapter);

           

              /* allocate receive descriptors */

              err = igb_setup_all_rx_resources(adapter);

           

              /* 注冊(cè)中斷處理函數(shù) */

              err = igb_request_irq(adapter);

              if(err)

                  goto err_req_irq;

           

              /* 啟用NAPI */

              for(i = 0; i < adapter->num_q_vectors; i++)

                  napi_enable(&(adapter->q_vector[I ]->napi));

              ......

          }

          在上面__igb_open函數(shù)調(diào)用了igb_setup_all_tx_resources,和igb_setup_all_rx_resources。在igb_setup_all_rx_resources這一步操作中,分配了RingBuffer,并建立內(nèi)存和Rx隊(duì)列的映射關(guān)系。(Rx Tx 隊(duì)列的數(shù)量和大小可以通過(guò) ethtool 進(jìn)行配置)。

          我們?cè)俳又粗袛嗪瘮?shù)注冊(cè)igb_request_irq:

          static int igb_request_irq(struct igb_adapter *adapter){

              if(adapter->msix_entries) {

                  err = igb_request_msix(adapter);

                  if(!err)

                      goto request_done;

                  ......

              }

          }

          static int igb_request_msix(struct igb_adapter *adapter){

              ......

              for(i = 0; i < adapter->num_q_vectors; i++) {

                  ...

                  err = request_irq(adapter->msix_entries[vector].vector,

                            igb_msix_ring, 0, q_vector->name,

              }

          在上面的代碼中跟蹤函數(shù)調(diào)用, __igb_open => igb_request_irq => igb_request_msix, 在igb_request_msix中我們看到了,對(duì)于多隊(duì)列的網(wǎng)卡,為每一個(gè)隊(duì)列都注冊(cè)了中斷,其對(duì)應(yīng)的中斷處理函數(shù)是igb_msix_ring(該函數(shù)也在drivers/net/ethernet/intel/igb/igb_main.c下)。

          我們也可以看到,msix方式下,每個(gè) RX 隊(duì)列有獨(dú)立的MSI-X 中斷,從網(wǎng)卡硬件中斷的層面就可以設(shè)置讓收到的包被不同的 CPU處理。(可以通過(guò) irqbalance ,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity能夠修改和CPU的綁定行為)。

          當(dāng)做好以上準(zhǔn)備工作以后,就可以開門迎客(數(shù)據(jù)包)了!

          5、開始迎接數(shù)據(jù)的到來(lái)

          5.1 硬中斷處理

          首先:當(dāng)數(shù)據(jù)幀從網(wǎng)線到達(dá)網(wǎng)卡上的時(shí)候,第一站是網(wǎng)卡的接收隊(duì)列。

          網(wǎng)卡在分配給自己的RingBuffer中尋找可用的內(nèi)存位置,找到后DMA引擎會(huì)把數(shù)據(jù)DMA到網(wǎng)卡之前關(guān)聯(lián)的內(nèi)存里,這個(gè)時(shí)候CPU都是無(wú)感的。當(dāng)DMA操作完成以后,網(wǎng)卡會(huì)像CPU發(fā)起一個(gè)硬中斷,通知CPU有數(shù)據(jù)到達(dá)。

          網(wǎng)卡數(shù)據(jù)硬中斷處理過(guò)程:

          注意:當(dāng)RingBuffer滿的時(shí)候,新來(lái)的數(shù)據(jù)包將給丟棄。ifconfig查看網(wǎng)卡的時(shí)候,可以里面有個(gè)overruns,表示因?yàn)榄h(huán)形隊(duì)列滿被丟棄的包。如果發(fā)現(xiàn)有丟包,可能需要通過(guò)ethtool命令來(lái)加大環(huán)形隊(duì)列的長(zhǎng)度。

          在啟動(dòng)網(wǎng)卡一節(jié),我們說(shuō)到了網(wǎng)卡的硬中斷注冊(cè)的處理函數(shù)是igb_msix_ring:

          //file: drivers/net/ethernet/intel/igb/igb_main.c

          static irqreturn_t igb_msix_ring(intirq, void *data){

              struct igb_q_vector *q_vector = data;

           

              /* Write the ITR value calculated from the previous interrupt. */

              igb_write_itr(q_vector);

           

              napi_schedule(&q_vector->napi);

              return IRQ_HANDLED;

          }

          igb_write_itr只是記錄一下硬件中斷頻率(據(jù)說(shuō)目的是在減少對(duì)CPU的中斷頻率時(shí)用到)。

          順著napi_schedule調(diào)用一路跟蹤下去,__napi_schedule=>____napi_schedule:

          /* Called with irq disabled */

          static inline void____napi_schedule(struct softnet_data *sd,

                               struct napi_struct *napi){

              list_add_tail(&napi->poll_list, &sd->poll_list);

              __raise_softirq_irqoff(NET_RX_SOFTIRQ);

          }

          這里我們看到:list_add_tail修改了CPU變量softnet_data里的poll_list,將驅(qū)動(dòng)napi_struct傳過(guò)來(lái)的poll_list添加了進(jìn)來(lái)。

          其中:softnet_data中的poll_list是一個(gè)雙向列表,其中的設(shè)備都帶有輸入幀等著被處理。緊接著__raise_softirq_irqoff觸發(fā)了一個(gè)軟中斷NET_RX_SOFTIRQ, 這個(gè)所謂的觸發(fā)過(guò)程只是對(duì)一個(gè)變量進(jìn)行了一次或運(yùn)算而已。

          void __raise_softirq_irqoff(unsigned int nr){

              trace_softirq_raise(nr);

              or_softirq_pending(1UL << nr);

          }

          //file: include/linux/irq_cpustat.h

          #define or_softirq_pending(x)  (local_softirq_pending() |= (x))

          我們說(shuō)過(guò):Linux在硬中斷里只完成簡(jiǎn)單必要的工作,剩下的大部分的處理都是轉(zhuǎn)交給軟中斷的。

          通過(guò)上面代碼可以看到:硬中斷處理過(guò)程真的是非常短。只是記錄了一個(gè)寄存器,修改了一下下CPU的poll_list,然后發(fā)出個(gè)軟中斷。就這么簡(jiǎn)單,硬中斷工作就算是完成了。

          5.2 ksoftirqd內(nèi)核線程處理軟中斷

          ksoftirqd內(nèi)核線程:

          內(nèi)核線程初始化的時(shí)候,我們介紹了ksoftirqd中兩個(gè)線程函數(shù)ksoftirqd_should_run和run_ksoftirqd。

          其中ksoftirqd_should_run代碼如下:

          static int ksoftirqd_should_run(unsigned int cpu){

              return local_softirq_pending();

          }

          #define local_softirq_pending() \    __IRQ_STAT(smp_processor_id(), __softirq_pending)

          這里看到和硬中斷中調(diào)用了同一個(gè)函數(shù)local_softirq_pending。使用方式不同的是硬中斷位置是為了寫入標(biāo)記,這里僅僅只是讀取。如果硬中斷中設(shè)置了NET_RX_SOFTIRQ,這里自然能讀取的到。

          接下來(lái)會(huì)真正進(jìn)入線程函數(shù)中run_ksoftirqd處理:

          static void run_ksoftirqd(unsigned int cpu){

              local_irq_disable();

              if(local_softirq_pending()) {

                  __do_softirq();

                  rcu_note_context_switch(cpu);

                  local_irq_enable();

                  cond_resched();

                  return;

              }

              local_irq_enable();

          }

          在__do_softirq中,判斷根據(jù)當(dāng)前CPU的軟中斷類型,調(diào)用其注冊(cè)的action方法。

          asmlinkage void__do_softirq(void){

              do{

                  if(pending & 1) {

                      unsigned int vec_nr = h - softirq_vec;

                      int prev_count = preempt_count();

                      ...

                      trace_softirq_entry(vec_nr);

                      h->action(h);

                      trace_softirq_exit(vec_nr);

                      ...

                  }

                  h++;

                  pending >>= 1;

              } while(pending);

          }

          在網(wǎng)絡(luò)子系統(tǒng)初始化小節(jié), 我們看到我們?yōu)镹ET_RX_SOFTIRQ注冊(cè)了處理函數(shù)net_rx_action。所以net_rx_action函數(shù)就會(huì)被執(zhí)行到了。

          這里需要注意一個(gè)細(xì)節(jié),硬中斷中設(shè)置軟中斷標(biāo)記,和ksoftirq的判斷是否有軟中斷到達(dá),都是基于smp_processor_id()的。這意味著只要硬中斷在哪個(gè)CPU上被響應(yīng),那么軟中斷也是在這個(gè)CPU上處理的。所以說(shuō),如果你發(fā)現(xiàn)你的Linux軟中斷CPU消耗都集中在一個(gè)核上的話,做法是要把調(diào)整硬中斷的CPU親和性,來(lái)將硬中斷打散到不同的CPU核上去。

          我們?cè)賮?lái)把精力集中到這個(gè)核心函數(shù)net_rx_action上來(lái):

          static void net_rx_action(struct softirq_action *h){

              struct softnet_data *sd = &__get_cpu_var(softnet_data);

              unsigned long time_limit = jiffies + 2;

              int budget = netdev_budget;

              void *have;

           

              local_irq_disable();

              while(!list_empty(&sd->poll_list)) {

                  ......

                  n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);

           

                  work = 0;

                  if(test_bit(NAPI_STATE_SCHED, &n->state)) {

                      work = n->poll(n, weight);

                      trace_napi_poll(n);

                  }

                  budget -= work;

              }

          }

          函數(shù)開頭的time_limit和budget是用來(lái)控制net_rx_action函數(shù)主動(dòng)退出的,目的是保證網(wǎng)絡(luò)包的接收不霸占CPU不放。等下次網(wǎng)卡再有硬中斷過(guò)來(lái)的時(shí)候再處理剩下的接收數(shù)據(jù)包。其中budget可以通過(guò)內(nèi)核參數(shù)調(diào)整。這個(gè)函數(shù)中剩下的核心邏輯是獲取到當(dāng)前CPU變量softnet_data,對(duì)其poll_list進(jìn)行遍歷, 然后執(zhí)行到網(wǎng)卡驅(qū)動(dòng)注冊(cè)到的poll函數(shù)。

          對(duì)于igb網(wǎng)卡來(lái)說(shuō),就是igb驅(qū)動(dòng)力的igb_poll函數(shù)了:

          static int igb_poll(struct napi_struct *napi, int budget){

              ...

              if(q_vector->tx.ring)

                  clean_complete = igb_clean_tx_irq(q_vector);

           

              if(q_vector->rx.ring)

                  clean_complete &= igb_clean_rx_irq(q_vector, budget);

              ...

          }

          在讀取操作中,igb_poll的重點(diǎn)工作是對(duì)igb_clean_rx_irq的調(diào)用:

          static bool igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget){

              ...

              do{

                  /* retrieve a buffer from the ring */

                  skb = igb_fetch_rx_buffer(rx_ring, rx_desc, skb);

           

                  /* fetch next buffer in frame if non-eop */

                  if(igb_is_non_eop(rx_ring, rx_desc))

                      continue;

                  }

           

                  /* verify the packet layout is correct */

                  if(igb_cleanup_headers(rx_ring, rx_desc, skb)) {

                      skb = NULL;

                      continue;

                  }

           

                  /* populate checksum, timestamp, VLAN, and protocol */

                  igb_process_skb_fields(rx_ring, rx_desc, skb);

           

                  napi_gro_receive(&q_vector->napi, skb);

          }

          igb_fetch_rx_buffer和igb_is_non_eop的作用就是把數(shù)據(jù)幀從RingBuffer上取下來(lái)。

          為什么需要兩個(gè)函數(shù)呢?因?yàn)橛锌赡軒级喽鄠€(gè)RingBuffer,所以是在一個(gè)循環(huán)中獲取的,直到幀尾部。獲取下來(lái)的一個(gè)數(shù)據(jù)幀用一個(gè)sk_buff來(lái)表示。收取完數(shù)據(jù)以后,對(duì)其進(jìn)行一些校驗(yàn),然后開始設(shè)置sbk變量的timestamp, VLAN id, protocol等字段。

          接下來(lái)進(jìn)入到napi_gro_receive中:

          //file: net/core/dev.c

          gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb){

              skb_gro_reset_offset(skb);

              return napi_skb_finish(dev_gro_receive(napi, skb), skb);

          }

          dev_gro_receive這個(gè)函數(shù)代表的是網(wǎng)卡GRO特性,可以簡(jiǎn)單理解成把相關(guān)的小包合并成一個(gè)大包就行,目的是減少傳送給網(wǎng)絡(luò)棧的包數(shù),這有助于減少 CPU 的使用量。我們暫且忽略,直接看napi_skb_finish。

          這個(gè)函數(shù)主要就是調(diào)用了netif_receive_skb:

          //file: net/core/dev.c

          static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb){

              switch(ret) {

              case GRO_NORMAL:

                  if(netif_receive_skb(skb))

                      ret = GRO_DROP;

                  break;

              ......

          }

          在netif_receive_skb中,數(shù)據(jù)包將被送到協(xié)議棧中。聲明,以下的5.3、5.4、5.5也都屬于軟中斷的處理過(guò)程,只不過(guò)由于篇幅太長(zhǎng),單獨(dú)拿出來(lái)成小節(jié)。

          5.3 網(wǎng)絡(luò)協(xié)議棧處理

          netif_receive_skb函數(shù)會(huì)根據(jù)包的協(xié)議,假如是udp包,會(huì)將包依次送到ip_rcv(),udp_rcv()協(xié)議處理函數(shù)中進(jìn)行處理。

          網(wǎng)絡(luò)協(xié)議棧處理:

          //file: net/core/dev.c

          int netif_receive_skb(struct sk_buff *skb){

              //RPS處理邏輯,先忽略    ......

              return __netif_receive_skb(skb);

          }

          static int __netif_receive_skb(struct sk_buff *skb){

              ...... 

              ret = __netif_receive_skb_core(skb, false);}static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc){

              ......

           

              //pcap邏輯,這里會(huì)將數(shù)據(jù)送入抓包點(diǎn)。tcpdump就是從這個(gè)入口獲取包的    list_for_each_entry_rcu(ptype, &ptype_all, list) {

                  if(!ptype->dev || ptype->dev == skb->dev) {

                      if(pt_prev)

                          ret = deliver_skb(skb, pt_prev, orig_dev);

                      pt_prev = ptype;

                  }

              }

              ......

              list_for_each_entry_rcu(ptype,

                      &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {

                  if(ptype->type == type &&

                      (ptype->dev == null_or_dev || ptype->dev == skb->dev ||

                       ptype->dev == orig_dev)) {

                      if(pt_prev)

                          ret = deliver_skb(skb, pt_prev, orig_dev);

                      pt_prev = ptype;

                  }

              }

          }

          在__netif_receive_skb_core中,我看著原來(lái)經(jīng)常使用的tcpdump的抓包點(diǎn),很是激動(dòng),看來(lái)讀一遍源代碼時(shí)間真的沒(méi)白浪費(fèi)。

          接著__netif_receive_skb_core取出protocol,它會(huì)從數(shù)據(jù)包中取出協(xié)議信息,然后遍歷注冊(cè)在這個(gè)協(xié)議上的回調(diào)函數(shù)列表。ptype_base 是一個(gè) hash table,在協(xié)議注冊(cè)小節(jié)我們提到過(guò)。ip_rcv 函數(shù)地址就是存在這個(gè) hash table中的。

          //file: net/core/dev.c

          static inline int deliver_skb(struct sk_buff *skb,

                            struct packet_type *pt_prev,

                            struct net_device *orig_dev){

              ......

              return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);

          }

          pt_prev->func這一行就調(diào)用到了協(xié)議層注冊(cè)的處理函數(shù)了。對(duì)于ip包來(lái)講,就會(huì)進(jìn)入到ip_rcv(如果是arp包的話,會(huì)進(jìn)入到arp_rcv)。

          5.4 IP協(xié)議層處理

          我們?cè)賮?lái)大致看一下linux在ip協(xié)議層都做了什么,包又是怎么樣進(jìn)一步被送到udp或tcp協(xié)議處理函數(shù)中的。

          //file: net/ipv4/ip_input.c

          int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev){

              ......

              return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);

          }

          這里NF_HOOK是一個(gè)鉤子函數(shù),當(dāng)執(zhí)行完注冊(cè)的鉤子后就會(huì)執(zhí)行到最后一個(gè)參數(shù)指向的函數(shù)ip_rcv_finish。

          static int ip_rcv_finish(struct sk_buff *skb){

              ......

              if(!skb_dst(skb)) {

                  int err = ip_route_input_noref(skb, iph->daddr, iph->saddr, iph->tos, skb->dev);

                  ...

              }

              ......

              return dst_input(skb);

          }

          跟蹤ip_route_input_noref 后看到它又調(diào)用了 ip_route_input_mc。

          在ip_route_input_mc中,函數(shù)ip_local_deliver被賦值給了dst.input, 如下:

          //file: net/ipv4/route.c

          static int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr,u8 tos, struct net_device *dev, int our){

              if(our) {

                  rth->dst.input= ip_local_deliver;

                  rth->rt_flags |= RTCF_LOCAL;

              }

          }

          所以回到ip_rcv_finish中的return dst_input(skb):

          /* Input packet from network to transport.  */

          static inline intdst_input(struct sk_buff *skb){

              return skb_dst(skb)->input(skb);

          }

          skb_dst(skb)->input調(diào)用的input方法就是路由子系統(tǒng)賦的ip_local_deliver:

          //file: net/ipv4/ip_input.c

          int ip_local_deliver(struct sk_buff *skb){

              /*     *  Reassemble IP fragments.     */

              if(ip_is_fragment(ip_hdr(skb))) {

                  if(ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))

                      return 0;

              }

           

              return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);

          }

          static int ip_local_deliver_finish(struct sk_buff *skb){

              ......

              int protocol = ip_hdr(skb)->protocol;

              const struct net_protocol *ipprot;

           

              ipprot = rcu_dereference(inet_protos[protocol]);

              if(ipprot != NULL) {

                  ret = ipprot->handler(skb);

              }

          }

          如協(xié)議注冊(cè)小節(jié)看到inet_protos中保存著tcp_rcv()和udp_rcv()的函數(shù)地址。這里將會(huì)根據(jù)包中的協(xié)議類型選擇進(jìn)行分發(fā),在這里skb包將會(huì)進(jìn)一步被派送到更上層的協(xié)議中,udp和tcp。

          5.5 UDP協(xié)議層處理

          在協(xié)議注冊(cè)小節(jié)的時(shí)候我們說(shuō)過(guò),udp協(xié)議的處理函數(shù)是udp_rcv。

          //file: net/ipv4/udp.c

          int udp_rcv(struct sk_buff *skb){

              return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);

          }

          int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,

                     int proto){

              sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);

           

              if(sk != NULL) {

                  intret = udp_queue_rcv_skb(sk, skb

              }

              icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

          }

          __udp4_lib_lookup_skb是根據(jù)skb來(lái)尋找對(duì)應(yīng)的socket,當(dāng)找到以后將數(shù)據(jù)包放到socket的緩存隊(duì)列里。如果沒(méi)有找到,則發(fā)送一個(gè)目標(biāo)不可達(dá)的icmp包。

          //file: net/ipv4/udp.c

          int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb){ 

              ......

              if(sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf))

                  goto drop;

           

              rc = 0;

           

              ipv4_pktinfo_prepare(skb);

              bh_lock_sock(sk);

              if(!sock_owned_by_user(sk))

                  rc = __udp_queue_rcv_skb(sk, skb);

              else if(sk_add_backlog(sk, skb, sk->sk_rcvbuf)) {

                  bh_unlock_sock(sk);

                  goto drop;

              }

              bh_unlock_sock(sk);

              return rc;

          }

          sock_owned_by_user判斷的是用戶是不是正在這個(gè)socker上進(jìn)行系統(tǒng)調(diào)用(socket被占用),如果沒(méi)有,那就可以直接放到socket的接收隊(duì)列中。如果有,那就通過(guò)sk_add_backlog把數(shù)據(jù)包添加到backlog隊(duì)列。

          當(dāng)用戶釋放的socket的時(shí)候,內(nèi)核會(huì)檢查backlog隊(duì)列,如果有數(shù)據(jù)再移動(dòng)到接收隊(duì)列中。

          sk_rcvqueues_full接收隊(duì)列如果滿了的話,將直接把包丟棄。接收隊(duì)列大小受內(nèi)核參數(shù)net.core.rmem_max和net.core.rmem_default影響。

          6、recvfrom系統(tǒng)調(diào)用

          花開兩朵,各表一枝。上面我們說(shuō)完了整個(gè)Linux內(nèi)核對(duì)數(shù)據(jù)包的接收和處理過(guò)程,最后把數(shù)據(jù)包放到socket的接收隊(duì)列中了。那么我們?cè)倩仡^看用戶進(jìn)程調(diào)用recvfrom后是發(fā)生了什么。

          我們?cè)诖a里調(diào)用的recvfrom是一個(gè)glibc的庫(kù)函數(shù),該函數(shù)在執(zhí)行后會(huì)將用戶進(jìn)行陷入到內(nèi)核態(tài),進(jìn)入到Linux實(shí)現(xiàn)的系統(tǒng)調(diào)用sys_recvfrom。

          在理解Linux對(duì)sys_revvfrom之前,我們先來(lái)簡(jiǎn)單看一下socket這個(gè)核心數(shù)據(jù)結(jié)構(gòu)。這個(gè)數(shù)據(jù)結(jié)構(gòu)太大了,我們只把對(duì)和我們今天主題相關(guān)的內(nèi)容畫出來(lái)。

          如下(socket內(nèi)核數(shù)據(jù)機(jī)構(gòu)):

          socket數(shù)據(jù)結(jié)構(gòu)中的const struct proto_ops對(duì)應(yīng)的是協(xié)議的方法集合。每個(gè)協(xié)議都會(huì)實(shí)現(xiàn)不同的方法集,對(duì)于IPv4 Internet協(xié)議族來(lái)說(shuō),每種協(xié)議都有對(duì)應(yīng)的處理方法,如下。對(duì)于udp來(lái)說(shuō),是通過(guò)inet_dgram_ops來(lái)定義的,其中注冊(cè)了inet_recvmsg方法。

          //file: net/ipv4/af_inet.c

          const struct proto_ops inet_stream_ops = {

              ......

              .recvmsg       = inet_recvmsg,

              .mmap          = sock_no_mmap,

              ......

          }

          const struct proto_ops inet_dgram_ops = {

              ......

              .sendmsg       = inet_sendmsg,

              .recvmsg       = inet_recvmsg,

              ......

          }

          socket數(shù)據(jù)結(jié)構(gòu)中的另一個(gè)數(shù)據(jù)結(jié)構(gòu)struct sock *sk是一個(gè)非常大,非常重要的子結(jié)構(gòu)體。其中的sk_prot又定義了二級(jí)處理函數(shù)。對(duì)于UDP協(xié)議來(lái)說(shuō),會(huì)被設(shè)置成UDP協(xié)議實(shí)現(xiàn)的方法集udp_prot。

          //file: net/ipv4/udp.c

          struct proto udp_prot = {

              .name          = "UDP",

              .owner         = THIS_MODULE,

              .close         = udp_lib_close,

              .connect       = ip4_datagram_connect,

              ......

              .sendmsg       = udp_sendmsg,

              .recvmsg       = udp_recvmsg,

              .sendpage      = udp_sendpage,

              ......

          }

          看完了socket變量之后,我們?cè)賮?lái)看sys_revvfrom的實(shí)現(xiàn)過(guò)程。

          recvfrom函數(shù)內(nèi)部實(shí)現(xiàn)過(guò)程:

          在inet_recvmsg調(diào)用了sk->sk_prot->recvmsg:

          //file: net/ipv4/af_inet.c

          int inet_recvmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,size_tsize, int flags){ 

              ......

              err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,

                             flags & ~MSG_DONTWAIT, &addr_len);

              if(err >= 0)

                  msg->msg_namelen = addr_len;

              return err;

          }

          上面我們說(shuō)過(guò)這個(gè)對(duì)于udp協(xié)議的socket來(lái)說(shuō),這個(gè)sk_prot就是net/ipv4/udp.c下的struct proto udp_prot。由此我們找到了udp_recvmsg方法。

          //file:net/core/datagram.c:EXPORT_SYMBOL(__skb_recv_datagram);

          struct sk_buff *__skb_recv_datagram(struct sock *sk, unsigned int flags,int*peeked, int *off, int *err){

              ......

              do{

                  struct sk_buff_head *queue = &sk->sk_receive_queue;

                  skb_queue_walk(queue, skb) {

                      ......

                  }

           

                  /* User doesn't want to wait */

                  error = -EAGAIN;

                  if(!timeo)

                      goto no_packet;

              } while(!wait_for_more_packets(sk, err, &timeo, last));

          }

          終于:我們找到了我們想要看的重點(diǎn),在上面我們看到了所謂的讀取過(guò)程,就是訪問(wèn)sk->sk_receive_queue。如果沒(méi)有數(shù)據(jù),且用戶也允許等待,則將調(diào)用wait_for_more_packets()執(zhí)行等待操作,它加入會(huì)讓用戶進(jìn)程進(jìn)入睡眠狀態(tài)。

          7、本文小結(jié)

          網(wǎng)絡(luò)模塊是操作系統(tǒng)內(nèi)核中最復(fù)雜的模塊了,看起來(lái)一個(gè)簡(jiǎn)簡(jiǎn)單單的收包過(guò)程就涉及到許多內(nèi)核組件之間的交互,如網(wǎng)卡驅(qū)動(dòng)、協(xié)議棧、內(nèi)核ksoftirqd線程等,看起來(lái)很復(fù)雜。本文想通過(guò)圖示的方式,盡量以容易理解的方式來(lái)將內(nèi)核收包過(guò)程講清楚。

          現(xiàn)在讓我們?cè)俅淮麄€(gè)收包過(guò)程:當(dāng)用戶執(zhí)行完recvfrom調(diào)用后,用戶進(jìn)程就通過(guò)系統(tǒng)調(diào)用進(jìn)行到內(nèi)核態(tài)工作了。如果接收隊(duì)列沒(méi)有數(shù)據(jù),進(jìn)程就進(jìn)入睡眠狀態(tài)被操作系統(tǒng)掛起。這塊相對(duì)比較簡(jiǎn)單,剩下大部分的戲份都是由Linux內(nèi)核其它模塊來(lái)表演了。

          首先在開始收包之前,操作系統(tǒng)要做許多的準(zhǔn)備工作(以Linux為例):

          • 1)創(chuàng)建ksoftirqd線程,為它設(shè)置好它自己的線程函數(shù),后面指望著它來(lái)處理軟中斷呢;
          • 2)協(xié)議棧注冊(cè),linux要實(shí)現(xiàn)許多協(xié)議,比如arp,icmp,ip,udp,tcp,每一個(gè)協(xié)議都會(huì)將自己的處理函數(shù)注冊(cè)一下,方便包來(lái)了迅速找到對(duì)應(yīng)的處理函數(shù);
          • 3)網(wǎng)卡驅(qū)動(dòng)初始化,每個(gè)驅(qū)動(dòng)都有一個(gè)初始化函數(shù),內(nèi)核會(huì)讓驅(qū)動(dòng)也初始化一下。在這個(gè)初始化過(guò)程中,把自己的DMA準(zhǔn)備好,把NAPI的poll函數(shù)地址告訴內(nèi)核;
          • 4)啟動(dòng)網(wǎng)卡,分配RX,TX隊(duì)列,注冊(cè)中斷對(duì)應(yīng)的處理函數(shù)。

          以上是內(nèi)核準(zhǔn)備收包之前的重要工作,當(dāng)上面都ready之后,就可以打開硬中斷,等待數(shù)據(jù)包的到來(lái)了。

          當(dāng)數(shù)據(jù)到來(lái)了以后,第一個(gè)迎接它的是網(wǎng)卡(我去,這不是廢話么):

          • 1)網(wǎng)卡將數(shù)據(jù)幀DMA到內(nèi)存的RingBuffer中,然后向CPU發(fā)起中斷通知;
          • 2)CPU響應(yīng)中斷請(qǐng)求,調(diào)用網(wǎng)卡啟動(dòng)時(shí)注冊(cè)的中斷處理函數(shù);
          • 3)中斷處理函數(shù)幾乎沒(méi)干啥,就發(fā)起了軟中斷請(qǐng)求;
          • 4)內(nèi)核線程ksoftirqd線程發(fā)現(xiàn)有軟中斷請(qǐng)求到來(lái),先關(guān)閉硬中斷;
          • 5)ksoftirqd線程開始調(diào)用驅(qū)動(dòng)的poll函數(shù)收包;
          • 6)poll函數(shù)將收到的包送到協(xié)議棧注冊(cè)的ip_rcv函數(shù)中;
          • 7)ip_rcv函數(shù)再講包送到udp_rcv函數(shù)中(對(duì)于tcp包就送到tcp_rcv)。

          現(xiàn)在,我們可以回到開篇的問(wèn)題了:我們?cè)谟脩魧涌吹降暮?jiǎn)單一行recvfrom,Linux內(nèi)核要替我們做如此之多的工作,才能讓我們順利收到數(shù)據(jù)。

          這還是簡(jiǎn)簡(jiǎn)單單的UDP,如果是TCP,內(nèi)核要做的工作更多,不由得感嘆內(nèi)核的開發(fā)者們真的是用心良苦。

          理解了整個(gè)收包過(guò)程以后,我們就能明確知道Linux收一個(gè)包的CPU開銷了:

          • 1)首先第一塊是用戶進(jìn)程調(diào)用系統(tǒng)調(diào)用陷入內(nèi)核態(tài)的開銷;
          • 2)其次第二塊是CPU響應(yīng)包的硬中斷的CPU開銷;
          • 3)接著第三塊是ksoftirqd內(nèi)核線程的軟中斷上下文花費(fèi)的。

          后面我們?cè)賹iT發(fā)一篇文章實(shí)際觀察一下這些開銷。

          另外:網(wǎng)絡(luò)收發(fā)中有很多末支細(xì)節(jié)咱們并沒(méi)有展開了說(shuō),比如說(shuō):no NAPI, GRO,RPS等。因?yàn)槲矣X(jué)得說(shuō)的太對(duì)了反而會(huì)影響大家對(duì)整個(gè)流程的把握,所以盡量只保留主框架了,少即是多!

          附錄:更多網(wǎng)絡(luò)編程精華文章

          如果您覺(jué)得本系列文章過(guò)于專業(yè),可先閱讀《網(wǎng)絡(luò)編程懶人入門》系列,目錄如下:

          網(wǎng)絡(luò)編程懶人入門(一):快速理解網(wǎng)絡(luò)通信協(xié)議(上篇)

          網(wǎng)絡(luò)編程懶人入門(二):快速理解網(wǎng)絡(luò)通信協(xié)議(下篇)

          網(wǎng)絡(luò)編程懶人入門(三):快速理解TCP協(xié)議一篇就夠

          網(wǎng)絡(luò)編程懶人入門(四):快速理解TCP和UDP的差異

          網(wǎng)絡(luò)編程懶人入門(五):快速理解為什么說(shuō)UDP有時(shí)比TCP更有優(yōu)勢(shì)

          網(wǎng)絡(luò)編程懶人入門(六):史上最通俗的集線器、交換機(jī)、路由器功能原理入門

          網(wǎng)絡(luò)編程懶人入門(七):深入淺出,全面理解HTTP協(xié)議

          網(wǎng)絡(luò)編程懶人入門(八):手把手教你寫基于TCP的Socket長(zhǎng)連接

          網(wǎng)絡(luò)編程懶人入門(九):通俗講解,有了IP地址,為何還要用MAC地址?

          本文已同步發(fā)布于“即時(shí)通訊技術(shù)圈”公眾號(hào)。

          ▲ 本文在公眾號(hào)上的鏈接是:點(diǎn)此進(jìn)入,原文鏈接是:http://www.52im.net/thread-3247-1-1.html



          作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時(shí)通訊開發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時(shí)是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級(jí)移動(dòng)端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處(也可前往 我的52im.net 找到我)。


          只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 剑阁县| 通渭县| 阜南县| 泾川县| 辽宁省| 唐海县| 奉化市| 万盛区| 巴东县| 杨浦区| 安国市| 昭觉县| 徐州市| 日喀则市| 丰原市| 焦作市| 昌宁县| 即墨市| 临泽县| 康平县| 偏关县| 顺昌县| 永福县| 大冶市| 万山特区| 麟游县| 仁布县| 阜新| 大埔区| 湟中县| 澳门| 伊春市| 金堂县| 曲靖市| 仪征市| 抚顺县| 策勒县| 金川县| 青浦区| 萝北县| 盈江县|