0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

深度理解在Linux下網(wǎng)絡(luò)包的接收過程

Linux愛好者 ? 來源:張彥飛allen ? 作者:張彥飛allen ? 2020-11-10 14:43 ? 次閱讀

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

我們今天用圖解的方式,來深度理解一下在Linux下網(wǎng)絡(luò)包的接收過程。還是按照慣例來借用一段最簡單的代碼開始思考。為了簡單起見,我們用udp來舉例,如下:

int main(){ int serverSocketFd = socket(AF_INET, SOCK_DGRAM, 0); bind(serverSocketFd, ...); char buff[BUFFSIZE]; int readCount = recvfrom(serverSocketFd, buff, BUFFSIZE, 0, ...); buff[readCount] = '';

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

}

上面代碼是一段udp server接收收據(jù)的邏輯。當在開發(fā)視角看的時候,只要客戶端有對應的數(shù)據(jù)發(fā)送過來,服務(wù)器端執(zhí)行recv_from后就能收到它,并把它打印出來。我們現(xiàn)在想知道的是,當網(wǎng)絡(luò)包達到網(wǎng)卡,直到我們的recvfrom收到數(shù)據(jù),這中間,究竟都發(fā)生過什么?

通過本文,你將深入理解Linux網(wǎng)絡(luò)系統(tǒng)內(nèi)部是如何實現(xiàn)的,以及各個部分之間如何交互。相信這對你的工作將會有非常大的幫助。本文基于Linux 3.10,網(wǎng)卡驅(qū)動采用Intel的igb網(wǎng)卡舉例。

友情提示,本文略長,可以先Mark后看!

一 Linux網(wǎng)絡(luò)收包總覽

在TCP/IP網(wǎng)絡(luò)分層模型里,整個協(xié)議棧被分成了物理層、鏈路層、網(wǎng)絡(luò)層,傳輸層和應用層。物理層對應的是網(wǎng)卡和網(wǎng)線,應用層對應的是我們常見的Nginx,F(xiàn)TP等等各種應用。Linux實現(xiàn)的是鏈路層、網(wǎng)絡(luò)層和傳輸層這三層。

在Linux內(nèi)核實現(xiàn)中,鏈路層協(xié)議靠網(wǎng)卡驅(qū)動來實現(xiàn),內(nèi)核協(xié)議棧來實現(xiàn)網(wǎng)絡(luò)層和傳輸層。內(nèi)核對更上層的應用層提供socket接口來供用戶進程訪問。我們用Linux的視角來看到的TCP/IP網(wǎng)絡(luò)分層模型應該是下面這個樣子的。

圖1 Linux視角的網(wǎng)絡(luò)協(xié)議棧

在Linux的源代碼中,網(wǎng)絡(luò)設(shè)備驅(qū)動對應的邏輯位于driver/net/ethernet, 其中intel系列網(wǎng)卡的驅(qū)動在driver/net/ethernet/intel目錄下。協(xié)議棧模塊代碼位于kernel和net目錄。

內(nèi)核和網(wǎng)絡(luò)設(shè)備驅(qū)動是通過中斷的方式來處理的。當設(shè)備上有數(shù)據(jù)到達的時候,會給CPU的相關(guān)引腳上觸發(fā)一個電壓變化,以通知CPU來處理數(shù)據(jù)。對于網(wǎng)絡(luò)模塊來說,由于處理過程比較復雜和耗時,如果在中斷函數(shù)中完成所有的處理,將會導致中斷處理函數(shù)(優(yōu)先級過高)將過度占據(jù)CPU,將導致CPU無法響應其它設(shè)備,例如鼠標和鍵盤的消息。因此Linux中斷處理函數(shù)是分上半部和下半部的。上半部是只進行最簡單的工作,快速處理然后釋放CPU,接著CPU就可以允許其它中斷進來。剩下將絕大部分的工作都放到下半部中,可以慢慢從容處理。2.4以后的內(nèi)核版本采用的下半部實現(xiàn)方式是軟中斷,由ksoftirqd內(nèi)核線程全權(quán)處理。和硬中斷不同的是,硬中斷是通過給CPU物理引腳施加電壓變化,而軟中斷是通過給內(nèi)存中的一個變量的二進制值以通知軟中斷處理程序。

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

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

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

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

二 Linux啟動

Linux驅(qū)動,內(nèi)核協(xié)議棧等等模塊在具備接收網(wǎng)卡數(shù)據(jù)包之前,要做很多的準備工作才行。比如要提前創(chuàng)建好ksoftirqd內(nèi)核線程,要注冊好各個協(xié)議對應的處理函數(shù),網(wǎng)絡(luò)設(shè)備子系統(tǒng)要提前初始化好,網(wǎng)卡要啟動好。只有這些都Ready之后,我們才能真正開始接收數(shù)據(jù)包。那么我們現(xiàn)在來看看這些準備工作都是怎么做的。

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

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

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

圖3 創(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 int spawn_ksoftirqd(void){ register_cpu_notifier(&cpu_nfb); BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

return 0;

}

early_initcall(spawn_ksoftirqd);

當ksoftirqd被創(chuàng)建出來以后,它就會進入自己的線程循環(huán)函數(shù)ksoftirqd_should_run和run_ksoftirqd了。不停地判斷有沒有軟中斷需要被處理。這里需要注意的一點是,軟中斷不僅僅只有網(wǎng)絡(luò)軟中斷,還有其它類型。

//file: include/linux/interrupt.henum{ HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ,

};

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

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

linux內(nèi)核通過調(diào)用subsys_initcall來初始化各個子系統(tǒng),在源代碼目錄里你可以grep出許多對這個函數(shù)的調(diào)用。這里我們要說的是網(wǎng)絡(luò)子系統(tǒng)的初始化,會執(zhí)行到net_dev_init函數(shù)。

//file: net/core/dev.c

static int __init net_dev_init(void){

...... for_each_possible_cpu(i) { struct softnet_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);

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

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

//file: kernel/softirq.c

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

softirq_vec[nr].action = action;

}

2.3 協(xié)議棧注冊

內(nèi)核實現(xiàn)了網(wǎng)絡(luò)層的ip協(xié)議,也實現(xiàn)了傳輸層的tcp協(xié)議和udp協(xié)議。這些協(xié)議對應的實現(xiàn)函數(shù)分別是ip_rcv(),tcp_v4_rcv()和udp_rcv()。和我們平時寫代碼的方式不一樣的是,內(nèi)核是通過注冊的方式來實現(xiàn)的。Linux內(nèi)核中的fs_initcall和subsys_initcall類似,也是初始化模塊的入口。fs_initcall調(diào)用inet_init后開始網(wǎng)絡(luò)協(xié)議棧注冊。通過inet_init,將這些函數(shù)注冊到了inet_protos和ptype_base數(shù)據(jù)結(jié)構(gòu)中了。如下圖:

圖5 AF_INET協(xié)議棧注冊

相關(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,

};

staticint __init inet_init(void){

...... if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0) ? ? ? ?pr_crit("%s: Cannot add ICMP protocol ", __func__); ? ?if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0) ? ? ? ?pr_crit("%s: Cannot add UDP protocol ", __func__); ? ?if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0) ? ? ? ?pr_crit("%s: Cannot add TCP protocol ", __func__); ? ?......

dev_add_pack(&ip_packet_type);

}

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

int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol){ if (!prot->netns_ok) { pr_err("Protocol %u is not namespace aware, cannot register. ", protocol); return -EINVAL; } return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],

NULL, prot) ? 0 : -1;

}

inet_add_protocol函數(shù)將tcp和udp對應的處理函數(shù)都注冊到了inet_protos數(shù)組中了。再看dev_add_pack(&ip_packet_type);這一行,ip_packet_type結(jié)構(gòu)體中的type是協(xié)議名,func是ip_rcv函數(shù),在dev_add_pack中會被注冊到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存儲著ip_rcv()函數(shù)的處理地址。后面我們會看到軟中斷中會通過ptype_base找到ip_rcv函數(shù)地址,進而將ip包正確地送到ip_rcv()中執(zhí)行。在ip_rcv中將會通過inet_protos找到tcp或者udp的處理函數(shù),再而把包轉(zhuǎn)發(fā)給udp_rcv()或tcp_v4_rcv()函數(shù)。

擴展一下,如果看一下ip_rcv和udp_rcv等函數(shù)的代碼能看到很多協(xié)議的處理過程。例如,ip_rcv中會處理netfilter和iptable過濾,如果你有很多或者很復雜的 netfilter 或 iptables 規(guī)則,這些規(guī)則都是在軟中斷的上下文中執(zhí)行的,會加大網(wǎng)絡(luò)延遲。再例如,udp_rcv中會判斷socket接收隊列是否滿了。對應的相關(guān)內(nèi)核參數(shù)是net.core.rmem_max和net.core.rmem_default。如果有興趣,建議大家好好讀一下inet_init這個函數(shù)的代碼。

2.4 網(wǎng)卡驅(qū)動初始化

每一個驅(qū)動程序(不僅僅只是網(wǎng)卡驅(qū)動)會使用 module_init 向內(nèi)核注冊一個初始化函數(shù),當驅(qū)動被加載時,內(nèi)核會調(diào)用這個函數(shù)。比如igb網(wǎng)卡驅(qū)動的代碼位于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,

......

};

staticint __init igb_init_module(void){

...... ret = pci_register_driver(&igb_driver);

return ret;

}

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

圖6 網(wǎng)卡驅(qū)動初始化

第5步中我們看到,網(wǎng)卡驅(qū)動實現(xiàn)了ethtool所需要的接口,也在這里注冊完成函數(shù)地址的注冊。當 ethtool 發(fā)起一個系統(tǒng)調(diào)用之后,內(nèi)核會找到對應操作的回調(diào)函數(shù)。對于igb網(wǎng)卡來說,其實現(xiàn)函數(shù)都在drivers/net/ethernet/intel/igb/igb_ethtool.c下。相信你這次能徹底理解ethtool的工作原理了吧?這個命令之所以能查看網(wǎng)卡收發(fā)包統(tǒng)計、能修改網(wǎng)卡自適應模式、能調(diào)整RX 隊列的數(shù)量和大小,是因為ethtool命令最終調(diào)用到了網(wǎng)卡驅(qū)動的相應方法,而不是ethtool本身有這個超能力。

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

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

staticconststruct 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初始化過程中,還調(diào)用到了igb_alloc_q_vector。他注冊了一個NAPI機制所必須的poll函數(shù),對于igb網(wǎng)卡驅(qū)動來說,這個函數(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);

}

2.5 啟動網(wǎng)卡

當上面的初始化都完成以后,就可以啟動網(wǎng)卡了?;貞浨懊婢W(wǎng)卡驅(qū)動初始化時,我們提到了驅(qū)動向內(nèi)核注冊了 structure net_device_ops 變量,它包含著網(wǎng)卡啟用、發(fā)包、設(shè)置mac 地址等回調(diào)函數(shù)(函數(shù)指針)。當啟用一個網(wǎng)卡時(例如,通過 ifconfig eth0 up),net_device_ops 中的 igb_open方法會被調(diào)用。它通常會做以下事情:

圖7 啟動網(wǎng)卡

//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); /* 注冊中斷處理函數(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隊列的映射關(guān)系。(Rx Tx 隊列的數(shù)量和大小可以通過 ethtool 進行配置)。我們再接著看中斷函數(shù)注冊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; ......

}

}

staticintigb_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中我們看到了,對于多隊列的網(wǎng)卡,為每一個隊列都注冊了中斷,其對應的中斷處理函數(shù)是igb_msix_ring(該函數(shù)也在drivers/net/ethernet/intel/igb/igb_main.c下)。我們也可以看到,msix方式下,每個 RX 隊列有獨立的MSI-X 中斷,從網(wǎng)卡硬件中斷的層面就可以設(shè)置讓收到的包被不同的 CPU處理。(可以通過 irqbalance ,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity能夠修改和CPU的綁定行為)。

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

三 迎接數(shù)據(jù)的到來

3.1 硬中斷處理

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

圖8 網(wǎng)卡數(shù)據(jù)硬中斷處理過程

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

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

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

static irqreturn_t igb_msix_ring(int irq, 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ù)說目的是在減少對CPU的中斷頻率時用到)。順著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ū)動napi_struct傳過來的poll_list添加了進來。其中softnet_data中的poll_list是一個雙向列表,其中的設(shè)備都帶有輸入幀等著被處理。緊接著__raise_softirq_irqoff觸發(fā)了一個軟中斷NET_RX_SOFTIRQ, 這個所謂的觸發(fā)過程只是對一個變量進行了一次或運算而已。

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))

我們說過,Linux在硬中斷里只完成簡單必要的工作,剩下的大部分的處理都是轉(zhuǎn)交給軟中斷的。通過上面代碼可以看到,硬中斷處理過程真的是非常短。只是記錄了一個寄存器,修改了一下下CPU的poll_list,然后發(fā)出個軟中斷。就這么簡單,硬中斷工作就算是完成了。

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

圖9 ksoftirqd內(nèi)核線程

內(nèi)核線程初始化的時候,我們介紹了ksoftirqd中兩個線程函數(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)用了同一個函數(shù)local_softirq_pending。使用方式不同的是硬中斷位置是為了寫入標記,這里僅僅只是讀取。如果硬中斷中設(shè)置了NET_RX_SOFTIRQ,這里自然能讀取的到。接下來會真正進入線程函數(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ù)當前CPU的軟中斷類型,調(diào)用其注冊的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注冊了處理函數(shù)net_rx_action。所以net_rx_action函數(shù)就會被執(zhí)行到了。

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

我們再來把精力集中到這個核心函數(shù)net_rx_action上來。

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是用來控制net_rx_action函數(shù)主動退出的,目的是保證網(wǎng)絡(luò)包的接收不霸占CPU不放。等下次網(wǎng)卡再有硬中斷過來的時候再處理剩下的接收數(shù)據(jù)包。其中budget可以通過內(nèi)核參數(shù)調(diào)整。這個函數(shù)中剩下的核心邏輯是獲取到當前CPU變量softnet_data,對其poll_list進行遍歷, 然后執(zhí)行到網(wǎng)卡驅(qū)動注冊到的poll函數(shù)。對于igb網(wǎng)卡來說,就是igb驅(qū)動力的igb_poll函數(shù)了。

staticintigb_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的重點工作是對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上取下來。為什么需要兩個函數(shù)呢?因為有可能幀要占多多個RingBuffer,所以是在一個循環(huán)中獲取的,直到幀尾部。獲取下來的一個數(shù)據(jù)幀用一個sk_buff來表示。收取完數(shù)據(jù)以后,對其進行一些校驗,然后開始設(shè)置sbk變量的timestamp, VLAN id, protocol等字段。接下來進入到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這個函數(shù)代表的是網(wǎng)卡GRO特性,可以簡單理解成把相關(guān)的小包合并成一個大包就行,目的是減少傳送給網(wǎng)絡(luò)棧的包數(shù),這有助于減少 CPU 的使用量。我們暫且忽略,直接看napi_skb_finish, 這個函數(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é)議棧中。聲明,以下的3.3, 3.4, 3.5也都屬于軟中斷的處理過程,只不過由于篇幅太長,單獨拿出來成小節(jié)。

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

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

圖10 網(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邏輯,這里會將數(shù)據(jù)送入抓包點。tcpdump就是從這個入口獲取包的 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中,我看著原來經(jīng)常使用的tcpdump的抓包點,很是激動,看來讀一遍源代碼時間真的沒白浪費。接著__netif_receive_skb_core取出protocol,它會從數(shù)據(jù)包中取出協(xié)議信息,然后遍歷注冊在這個協(xié)議上的回調(diào)函數(shù)列表。ptype_base是一個 hash table,在協(xié)議注冊小節(jié)我們提到過。ip_rcv 函數(shù)地址就是存在這個 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é)議層注冊的處理函數(shù)了。對于ip包來講,就會進入到ip_rcv(如果是arp包的話,會進入到arp_rcv)。

3.4 IP協(xié)議層處理

我們再來大致看一下linux在ip協(xié)議層都做了什么,包又是怎么樣進一步被送到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是一個鉤子函數(shù),當執(zhí)行完注冊的鉤子后就會執(zhí)行到最后一個參數(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 int dst_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);

}

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

3.5 UDP協(xié)議層處理

在協(xié)議注冊小節(jié)的時候我們說過,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) { int ret = udp_queue_rcv_skb(sk, skb }

icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

}

__udp4_lib_lookup_skb是根據(jù)skb來尋找對應的socket,當找到以后將數(shù)據(jù)包放到socket的緩存隊列里。如果沒有找到,則發(fā)送一個目標不可達的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判斷的是用戶是不是正在這個socker上進行系統(tǒng)調(diào)用(socket被占用),如果沒有,那就可以直接放到socket的接收隊列中。如果有,那就通過sk_add_backlog把數(shù)據(jù)包添加到backlog隊列。當用戶釋放的socket的時候,內(nèi)核會檢查backlog隊列,如果有數(shù)據(jù)再移動到接收隊列中。

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

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

花開兩朵,各表一枝。上面我們說完了整個Linux內(nèi)核對數(shù)據(jù)包的接收和處理過程,最后把數(shù)據(jù)包放到socket的接收隊列中了。那么我們再回頭看用戶進程調(diào)用recvfrom后是發(fā)生了什么。我們在代碼里調(diào)用的recvfrom是一個glibc的庫函數(shù),該函數(shù)在執(zhí)行后會將用戶進行陷入到內(nèi)核態(tài),進入到Linux實現(xiàn)的系統(tǒng)調(diào)用sys_recvfrom。在理解Linux對sys_revvfrom之前,我們先來簡單看一下socket這個核心數(shù)據(jù)結(jié)構(gòu)。這個數(shù)據(jù)結(jié)構(gòu)太大了,我們只把對和我們今天主題相關(guān)的內(nèi)容畫出來,如下:

圖11 socket內(nèi)核數(shù)據(jù)機構(gòu)

socket數(shù)據(jù)結(jié)構(gòu)中的const struct proto_ops對應的是協(xié)議的方法集合。每個協(xié)議都會實現(xiàn)不同的方法集,對于IPv4 Internet協(xié)議族來說,每種協(xié)議都有對應的處理方法,如下。對于udp來說,是通過inet_dgram_ops來定義的,其中注冊了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)中的另一個數(shù)據(jù)結(jié)構(gòu)struct sock *sk是一個非常大,非常重要的子結(jié)構(gòu)體。其中的sk_prot又定義了二級處理函數(shù)。對于UDP協(xié)議來說,會被設(shè)置成UDP協(xié)議實現(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變量之后,我們再來看sys_revvfrom的實現(xiàn)過程。

圖12 recvfrom函數(shù)內(nèi)部實現(xiàn)過程

在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_t size, 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;

}

上面我們說過這個對于udp協(xié)議的socket來說,這個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));

}

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

五 總結(jié)

網(wǎng)絡(luò)模塊是Linux內(nèi)核中最復雜的模塊了,看起來一個簡簡單單的收包過程就涉及到許多內(nèi)核組件之間的交互,如網(wǎng)卡驅(qū)動、協(xié)議棧,內(nèi)核ksoftirqd線程等。看起來很復雜,本文想通過圖示的方式,盡量以容易理解的方式來將內(nèi)核收包過程講清楚?,F(xiàn)在讓我們再串一串整個收包過程。

當用戶執(zhí)行完recvfrom調(diào)用后,用戶進程就通過系統(tǒng)調(diào)用進行到內(nèi)核態(tài)工作了。如果接收隊列沒有數(shù)據(jù),進程就進入睡眠狀態(tài)被操作系統(tǒng)掛起。這塊相對比較簡單,剩下大部分的戲份都是由Linux內(nèi)核其它模塊來表演了。

首先在開始收包之前,Linux要做許多的準備工作:

1. 創(chuàng)建ksoftirqd線程,為它設(shè)置好它自己的線程函數(shù),后面指望著它來處理軟中斷呢

2. 協(xié)議棧注冊,linux要實現(xiàn)許多協(xié)議,比如arp,icmp,ip,udp,tcp,每一個協(xié)議都會將自己的處理函數(shù)注冊一下,方便包來了迅速找到對應的處理函數(shù)

3. 網(wǎng)卡驅(qū)動初始化,每個驅(qū)動都有一個初始化函數(shù),內(nèi)核會讓驅(qū)動也初始化一下。在這個初始化過程中,把自己的DMA準備好,把NAPI的poll函數(shù)地址告訴內(nèi)核

4. 啟動網(wǎng)卡,分配RX,TX隊列,注冊中斷對應的處理函數(shù)

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

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

1. 網(wǎng)卡將數(shù)據(jù)幀DMA到內(nèi)存的RingBuffer中,然后向CPU發(fā)起中斷通知

2. CPU響應中斷請求,調(diào)用網(wǎng)卡啟動時注冊的中斷處理函數(shù)

3. 中斷處理函數(shù)幾乎沒干啥,就發(fā)起了軟中斷請求

4. 內(nèi)核線程ksoftirqd線程發(fā)現(xiàn)有軟中斷請求到來,先關(guān)閉硬中斷

5. ksoftirqd線程開始調(diào)用驅(qū)動的poll函數(shù)收包

6. poll函數(shù)將收到的包送到協(xié)議棧注冊的ip_rcv函數(shù)中

7. ip_rcv函數(shù)再講包送到udp_rcv函數(shù)中(對于tcp包就送到tcp_rcv)

現(xiàn)在我們可以回到開篇的問題了,我們在用戶層看到的簡單一行recvfrom,Linux內(nèi)核要替我們做如此之多的工作,才能讓我們順利收到數(shù)據(jù)。這還是簡簡單單的UDP,如果是TCP,內(nèi)核要做的工作更多,不由得感嘆內(nèi)核的開發(fā)者們真的是用心良苦。

理解了整個收包過程以后,我們就能明確知道Linux收一個包的CPU開銷了。首先第一塊是用戶進程調(diào)用系統(tǒng)調(diào)用陷入內(nèi)核態(tài)的開銷。第二塊是CPU響應包的硬中斷的CPU開銷。第三塊是ksoftirqd內(nèi)核線程的軟中斷上下文花費的。后面我們再專門發(fā)一篇文章實際觀察一下這些開銷。

另外網(wǎng)絡(luò)收發(fā)中有很多末支細節(jié)咱們并沒有展開了說,比如說no NAPI, GRO,RPS等。因為我覺得說的太對了反而會影響大家對整個流程的把握,所以盡量只保留主框架了,少即是多!

原文標題:圖解 Linux 網(wǎng)絡(luò)包接收過程

文章出處:【微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

責任編輯:haq

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • cpu
    cpu
    +關(guān)注

    關(guān)注

    68

    文章

    10702

    瀏覽量

    209356
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11123

    瀏覽量

    207908

原文標題:圖解 Linux 網(wǎng)絡(luò)包接收過程

文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    Linux網(wǎng)絡(luò)協(xié)議棧的實現(xiàn)

    網(wǎng)絡(luò)協(xié)議棧是操作系統(tǒng)核心的一個重要組成部分,負責管理網(wǎng)絡(luò)通信中的數(shù)據(jù)處理。 Linux 操作系統(tǒng)中,
    的頭像 發(fā)表于 09-10 09:51 ?121次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>網(wǎng)絡(luò)</b>協(xié)議棧的實現(xiàn)

    FPGA深度神經(jīng)網(wǎng)絡(luò)中的應用

    隨著人工智能技術(shù)的飛速發(fā)展,深度神經(jīng)網(wǎng)絡(luò)(Deep Neural Network, DNN)作為其核心算法之一,圖像識別、語音識別、自然語言處理等領(lǐng)域取得了顯著成果。然而,傳統(tǒng)的深度
    的頭像 發(fā)表于 07-24 10:42 ?383次閱讀

    深度神經(jīng)網(wǎng)絡(luò)中的前饋過程

    深度神經(jīng)網(wǎng)絡(luò)(Deep Neural Networks,DNNs)中的前饋過程是其核心操作之一,它描述了數(shù)據(jù)從輸入層通過隱藏層最終到達輸出層的過程,期間不涉及任何反向傳播或權(quán)重調(diào)整。這
    的頭像 發(fā)表于 07-08 17:29 ?217次閱讀

    深度學習的典型模型和訓練過程

    深度學習作為人工智能領(lǐng)域的一個重要分支,近年來圖像識別、語音識別、自然語言處理等多個領(lǐng)域取得了顯著進展。其核心在于通過構(gòu)建復雜的神經(jīng)網(wǎng)絡(luò)模型,從大規(guī)模數(shù)據(jù)中自動學習并提取特征,進而實現(xiàn)高效準確的預測和分類。本文將深入解讀
    的頭像 發(fā)表于 07-03 16:06 ?659次閱讀

    如何解決Linux系統(tǒng)中的網(wǎng)絡(luò)連接問題?

    Linux系統(tǒng)中的網(wǎng)絡(luò)連接問題。 首先,讓我們了解一下網(wǎng)絡(luò)連接問題的常見原因。這些原因包括但不限于錯誤的網(wǎng)絡(luò)配置、網(wǎng)絡(luò)故障、防火墻設(shè)置、D
    的頭像 發(fā)表于 01-12 15:17 ?625次閱讀

    網(wǎng)絡(luò)率正常范圍及其影響因素

    網(wǎng)絡(luò)率正常范圍及其影響因素 網(wǎng)絡(luò)率是評估網(wǎng)絡(luò)性能和穩(wěn)定性的重要指標之一。 一、網(wǎng)絡(luò)
    的頭像 發(fā)表于 12-29 14:45 ?4432次閱讀

    Linux網(wǎng)絡(luò)基本配置與管理

    Linux是一種開源操作系統(tǒng),被廣泛用于服務(wù)器和網(wǎng)絡(luò)設(shè)備中。Linux中,網(wǎng)絡(luò)配置和管理是一個重要且復雜的任務(wù)。本篇文章將詳細介紹
    的頭像 發(fā)表于 11-27 16:51 ?692次閱讀

    linux手動設(shè)置網(wǎng)絡(luò)參數(shù)

    Linux 是一種廣泛使用的操作系統(tǒng),提供了豐富的網(wǎng)絡(luò)配置選項,允許用戶手動設(shè)置網(wǎng)絡(luò)參數(shù),以滿足各種網(wǎng)絡(luò)需求。本文將詳盡、詳實、細致地介紹 Linu
    的頭像 發(fā)表于 11-27 15:20 ?560次閱讀

    linux重啟網(wǎng)絡(luò)服務(wù)

    網(wǎng)絡(luò)服務(wù)是現(xiàn)代計算機系統(tǒng)中不可或缺的一部分,而Linux作為開源的操作系統(tǒng),其網(wǎng)絡(luò)服務(wù)的管理和維護也成為系統(tǒng)管理員的一項重要工作。本文將詳細介紹Linux
    的頭像 發(fā)表于 11-17 09:53 ?1076次閱讀

    網(wǎng)絡(luò)問題分析

    所謂丟,是指在網(wǎng)絡(luò)數(shù)據(jù)的收發(fā)過程中,由于種種原因,數(shù)據(jù)還沒傳輸?shù)綉贸绦蛑?,就被丟棄了。這些被丟棄的數(shù)量,除以總的傳輸
    的頭像 發(fā)表于 11-13 11:24 ?794次閱讀
    <b class='flag-5'>網(wǎng)絡(luò)</b>丟<b class='flag-5'>包</b>問題分析

    Linux內(nèi)核UDP收為什么效率低

    現(xiàn)在很多人都在詬病Linux內(nèi)核協(xié)議棧收效率低,不管他們是真的懂還是一點都不懂只是聽別人說的,反正就是一味地懟Linux內(nèi)核協(xié)議棧,他們的武器貌似只有DPDK。 但是,即便
    的頭像 發(fā)表于 11-13 10:38 ?385次閱讀
    <b class='flag-5'>Linux</b>內(nèi)核UDP收<b class='flag-5'>包</b>為什么效率低

    Linux場景下數(shù)據(jù)是如何在協(xié)議層傳輸?shù)?/a>

    數(shù)據(jù) 應用層發(fā)送數(shù)據(jù)的過程大致如下: 我們把上述處理過程的區(qū)域大致分為: User區(qū)域 Kernel 區(qū)域 Device區(qū)域 user和kernel區(qū)域的任務(wù)都是由本機cpu執(zhí)行,這兩個區(qū)域合并稱為host區(qū)域,以區(qū)分devi
    的頭像 發(fā)表于 11-11 11:33 ?962次閱讀
    <b class='flag-5'>Linux</b>場景下數(shù)據(jù)<b class='flag-5'>包</b>是如何在協(xié)議層傳輸?shù)? />    </a>
</div>                            <div   id=

    接收UDP報文的過程

    最近工作中遇到某個服務(wù)器應用程序 UDP 丟,排查過程中查閱了很多資料,總結(jié)出來這篇文章,供更多人參考。 開始之前,我們先用一張圖解釋 lin
    的頭像 發(fā)表于 11-11 11:22 ?752次閱讀
    <b class='flag-5'>接收</b>UDP報文的<b class='flag-5'>過程</b>

    如何優(yōu)化Linux內(nèi)核UDP收效率低

    很多人都在詬病Linux內(nèi)核協(xié)議棧收效率低,不管他們是真的懂還是一點都不懂只是聽別人說的,反正就是一味地懟Linux內(nèi)核協(xié)議棧,他們的武器貌似只有DPDK。 但是,
    的頭像 發(fā)表于 11-10 10:51 ?466次閱讀
    如何優(yōu)化<b class='flag-5'>Linux</b>內(nèi)核UDP收<b class='flag-5'>包</b>效率低

    網(wǎng)絡(luò)問題解析

    什么是丟 數(shù)據(jù)Internet上是以數(shù)據(jù)為單位傳輸?shù)模瑔挝粸樽止?jié),數(shù)據(jù)在網(wǎng)絡(luò)上傳輸,受網(wǎng)絡(luò)設(shè)備,網(wǎng)
    的頭像 發(fā)表于 11-09 15:10 ?760次閱讀
    <b class='flag-5'>網(wǎng)絡(luò)</b>丟<b class='flag-5'>包</b>問題解析