Linux网络子系统
今天分享一篇經(jīng)典Linux協(xié)議棧文章,主要講解Linux網(wǎng)絡子系統(tǒng),看完相信大家對協(xié)議棧又會加深不少,不光可以了解協(xié)議棧處理流程,方便定位問題,還可以學習一下怎么去設計一個可擴展的子系統(tǒng),屏蔽不同層次的差異。
目錄
1.Linux網(wǎng)絡子系統(tǒng)的分層
2.TCP/IP分層模型
3.Linux?網(wǎng)絡協(xié)議棧
4.Linux?網(wǎng)卡收包時的中斷處理問題
5.Linux?網(wǎng)絡啟動的準備工作
6.Linux網(wǎng)絡包:中斷到網(wǎng)絡層接收
7.總結(jié)
Linux網(wǎng)絡子系統(tǒng)的分層
Linux網(wǎng)絡子系統(tǒng)實現(xiàn)需要:
支持不同的協(xié)議族 ( INET, INET6, UNIX, NETLINK...)
支持不同的網(wǎng)絡設備
支持統(tǒng)一的BSD socket API
需要屏蔽協(xié)議、硬件、平臺(API)的差異,因而采用分層結(jié)構(gòu):
?
?
系統(tǒng)調(diào)用提供用戶的應用程序訪問內(nèi)核的唯一途徑。協(xié)議無關(guān)接口由socket layer來實現(xiàn)的,其提供一組通用功能,以支持各種不同的協(xié)議。網(wǎng)絡協(xié)議層為socket層提供具體協(xié)議接口——proto{},實現(xiàn)具體的協(xié)議細節(jié)。設備無關(guān)接口,提供一組通用函數(shù)供底層網(wǎng)絡設備驅(qū)動程序使用。設備驅(qū)動與特定網(wǎng)卡設備相關(guān),定義了具體的協(xié)議細節(jié),會分配一個net_device結(jié)構(gòu),然后用其必需的例程進行初始化。
TCP/IP分層模型
在TCP/IP網(wǎng)絡分層模型里,整個協(xié)議棧被分成了物理層、鏈路層、網(wǎng)絡層,傳輸層和應用層。物理層對應的是網(wǎng)卡和網(wǎng)線,應用層對應的是我們常見的Nginx,FTP等等各種應用。Linux實現(xiàn)的是鏈路層、網(wǎng)絡層和傳輸層這三層。
在Linux內(nèi)核實現(xiàn)中,鏈路層協(xié)議靠網(wǎng)卡驅(qū)動來實現(xiàn),內(nèi)核協(xié)議棧來實現(xiàn)網(wǎng)絡層和傳輸層。內(nèi)核對更上層的應用層提供socket接口來供用戶進程訪問。我們用Linux的視角來看到的TCP/IP網(wǎng)絡分層模型應該是下面這個樣子的。
?
?
?
首先我們梳理一下每層模型的職責:
鏈路層:對0和1進行分組,定義數(shù)據(jù)幀,確認主機的物理地址,傳輸數(shù)據(jù);
網(wǎng)絡層:定義IP地址,確認主機所在的網(wǎng)絡位置,并通過IP進行MAC尋址,對外網(wǎng)數(shù)據(jù)包進行路由轉(zhuǎn)發(fā);
傳輸層:定義端口,確認主機上應用程序的身份,并將數(shù)據(jù)包交給對應的應用程序;
應用層:定義數(shù)據(jù)格式,并按照對應的格式解讀數(shù)據(jù)。
?
然后再把每層模型的職責串聯(lián)起來,用一句通俗易懂的話講就是:
當你輸入一個網(wǎng)址并按下回車鍵的時候,首先,應用層協(xié)議對該請求包做了格式定義;緊接著傳輸層協(xié)議加上了雙方的端口號,確認了雙方通信的應用程序;然后網(wǎng)絡協(xié)議加上了雙方的IP地址,確認了雙方的網(wǎng)絡位置;最后鏈路層協(xié)議加上了雙方的MAC地址,確認了雙方的物理位置,同時將數(shù)據(jù)進行分組,形成數(shù)據(jù)幀,采用廣播方式,通過傳輸介質(zhì)發(fā)送給對方主機。而對于不同網(wǎng)段,該數(shù)據(jù)包首先會轉(zhuǎn)發(fā)給網(wǎng)關(guān)路由器,經(jīng)過多次轉(zhuǎn)發(fā)后,最終被發(fā)送到目標主機。目標機接收到數(shù)據(jù)包后,采用對應的協(xié)議,對幀數(shù)據(jù)進行組裝,然后再通過一層一層的協(xié)議進行解析,最終被應用層的協(xié)議解析并交給服務器處理。
Linux 網(wǎng)絡協(xié)議棧
基于TCP/IP協(xié)議棧的send/recv在應用層,傳輸層,網(wǎng)絡層和鏈路層中具體函數(shù)調(diào)用過程已經(jīng)有很多人研究,本文引用一張比較完善的圖如下:
?
?
以上說明基本大致說明了TCP/IP中TCP,UDP協(xié)議包在網(wǎng)絡子系統(tǒng)中的實現(xiàn)流程。本文主要在鏈路層中,即關(guān)于網(wǎng)卡收報觸發(fā)中斷到進入網(wǎng)絡層之間的過程探究。
Linux 網(wǎng)卡收包時的中斷處理問題
中斷,一般指硬件中斷,多由系統(tǒng)自身或與之鏈接的外設(如鍵盤、鼠標、網(wǎng)卡等)產(chǎn)生。中斷首先是處理器提供的一種響應外設請求的機制,是處理器硬件支持的特性。一個外設通過產(chǎn)生一種電信號通知中斷控制器,中斷控制器再向處理器發(fā)送相應的信號。處理器檢測到了這個信號后就會打斷自己當前正在做的工作,轉(zhuǎn)而去處理這次中斷(所以才叫中斷)。當然在轉(zhuǎn)去處理中斷和中斷返回時都有保護現(xiàn)場和返回現(xiàn)場的操作,這里不贅述。
那軟中斷又是什么呢?我們知道在中斷處理時CPU沒法處理其它事物,對于網(wǎng)卡來說,如果每次網(wǎng)卡收包時中斷的時間都過長,那很可能造成丟包的可能性。當然我們不能完全避免丟包的可能性,以太包的傳輸是沒有100%保證的,所以網(wǎng)絡才有協(xié)議棧,通過高層的協(xié)議來保證連續(xù)數(shù)據(jù)傳輸?shù)臄?shù)據(jù)完整性(比如在協(xié)議發(fā)現(xiàn)丟包時要求重傳)。但是即使有協(xié)議保證,那我們也不能肆無忌憚的使用中斷,中斷的時間越短越好,盡快放開處理器,讓它可以去響應下次中斷甚至進行調(diào)度工作。基于這樣的考慮,我們將中斷分成了上下兩部分,上半部分就是上面說的中斷部分,需要快速及時響應,同時需要越快結(jié)束越好。而下半部分就是完成一些可以推后執(zhí)行的工作。對于網(wǎng)卡收包來說,網(wǎng)卡收到數(shù)據(jù)包,通知內(nèi)核數(shù)據(jù)包到了,中斷處理將數(shù)據(jù)包存入內(nèi)存這些都是急切需要完成的工作,放到上半部完成。而解析處理數(shù)據(jù)包的工作則可以放到下半部去執(zhí)行。
軟中斷就是下半部使用的一種機制,它通過軟件模仿硬件中斷的處理過程,但是和硬件沒有關(guān)系,單純的通過軟件達到一種異步處理的方式。其它下半部的處理機制還包括tasklet,工作隊列等。依據(jù)所處理的場合不同,選擇不同的機制,網(wǎng)卡收包一般使用軟中斷。對應NET_RX_SOFTIRQ這個軟中斷,軟中斷的類型如下:
enum {HI_SOFTIRQ=0,TIMER_SOFTIRQ,NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,BLOCK_SOFTIRQ,IRQ_POLL_SOFTIRQ,TASKLET_SOFTIRQ,SCHED_SOFTIRQ,HRTIMER_SOFTIRQ,RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */NR_SOFTIRQS };通過以上可以了解到,Linux中斷注冊顯然應該包括網(wǎng)卡的硬中斷,包處理的軟中斷兩個步驟。
?
注冊網(wǎng)卡中斷
我們以一個具體的網(wǎng)卡驅(qū)動為例,比如e1000。其模塊初始化函數(shù)就是:
static int __init e1000_init_module(void) {int ret;pr_info("%s - version %s\n", e1000_driver_string, e1000_driver_version);pr_info("%s\n", e1000_copyright);ret = pci_register_driver(&e1000_driver); ...return ret;}其中e1000_driver這個結(jié)構(gòu)體是一個關(guān)鍵,這個結(jié)構(gòu)體中很主要的一個方法就是.probe方法,也就是e1000_probe():
/** * e1000_probe - Device Initialization Routine * @pdev: PCI device information struct * @ent: entry in e1000_pci_tbl * * Returns 0 on success, negative on failure * * e1000_probe initializes an adapter identified by a pci_dev structure. * The OS initialization, configuring of the adapter private structure, * and a hardware reset occur. **/ static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { ... ...netdev->netdev_ops = &e1000_netdev_ops;e1000_set_ethtool_ops(netdev); ... ... }這個函數(shù)很長,我們不都列出來,這是e1000主要的初始化函數(shù),即使從注釋都能看出來。我們留意其注冊了netdev的netdev_ops,用的是e1000_netdev_ops這個結(jié)構(gòu)體:
static const struct net_device_ops e1000_netdev_ops = {.ndo_open = e1000_open,.ndo_stop = e1000_close,.ndo_start_xmit = e1000_xmit_frame,.ndo_set_rx_mode = e1000_set_rx_mode,.ndo_set_mac_address = e1000_set_mac,.ndo_tx_timeout = e1000_tx_timeout, ... ... };這個e1000的方法集里有一個重要的方法,e1000_open,我們要說的中斷的注冊就從這里開始:
/** * e1000_open - Called when a network interface is made active * @netdev: network interface device structure * * Returns 0 on success, negative value on failure * * The open entry point is called when a network interface is made * active by the system (IFF_UP). At this point all resources needed * for transmit and receive operations are allocated, the interrupt * handler is registered with the OS, the watchdog task is started, * and the stack is notified that the interface is ready. **/ int e1000_open(struct net_device *netdev) {struct e1000_adapter *adapter = netdev_priv(netdev);struct e1000_hw *hw = &adapter->hw; ... ...err = e1000_request_irq(adapter); ... }e1000在這里注冊了中斷:
如上所示,這個被注冊的中斷處理函數(shù),也就是handler,就是e1000_intr()。我們不展開這個中斷處理函數(shù)看了,我們知道中斷處理函數(shù)在這里被注冊了,在網(wǎng)絡包來的時候會觸發(fā)這個中斷函數(shù)。
注冊軟中斷
內(nèi)核初始化期間,softirq_init會注冊TASKLET_SOFTIRQ以及HI_SOFTIRQ相關(guān)聯(lián)的處理函數(shù)。
void __init softirq_init(void) {......open_softirq(TASKLET_SOFTIRQ, tasklet_action);open_softirq(HI_SOFTIRQ, tasklet_hi_action);} 網(wǎng)絡子系統(tǒng)分兩種soft IRQ。NET_TX_SOFTIRQ和NET_RX_SOFTIRQ,分別處理發(fā)送數(shù)據(jù)包和接收數(shù)據(jù)包。這兩個soft IQ在net_dev_init函數(shù)(net/core/dev.c)中注冊: open_softirq(NET_TX_SOFTIRQ, net_tx_action);open_softirq(NET_RX_SOFTIRQ,?net_rx_action);收發(fā)數(shù)據(jù)包的軟中斷處理函數(shù)被注冊為net_rx_action和net_tx_action。其中open_softirq實現(xiàn)為:
void open_softirq(int nr, void (*action)(struct softirq_action *)) {softirq_vec[nr].action = action;}?
?
從硬中斷到軟中斷
?
?
?
Linux 網(wǎng)絡啟動的準備工作
首先在開始收包之前,Linux要做許多的準備工作:
1. 創(chuàng)建ksoftirqd線程,為它設置好它自己的線程函數(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ù)
?
創(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進程。
?
相關(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", };當ksoftirqd被創(chuàng)建出來以后,它就會進入自己的線程循環(huán)函數(shù)ksoftirqd_should_run和run_ksoftirqd了。不停地判斷有沒有軟中斷需要被處理。這里需要注意的一點是,軟中斷不僅僅只有網(wǎng)絡軟中斷,還有其它類型。
?
創(chuàng)建ksoftirqd內(nèi)核線程
?
linux內(nèi)核通過調(diào)用subsys_initcall來初始化各個子系統(tǒng),在源代碼目錄里你可以grep出許多對這個函數(shù)的調(diào)用。這里我們要說的是網(wǎng)絡子系統(tǒng)的初始化,會執(zhí)行到net_dev_init函數(shù)。
?
在這個函數(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ù)。
?
協(xié)議棧注冊
內(nèi)核實現(xiàn)了網(wǎng)絡層的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)絡協(xié)議棧注冊。通過inet_init,將這些函數(shù)注冊到了inet_protos和ptype_base數(shù)據(jù)結(jié)構(gòu)中
相關(guān)代碼如下
//file: net/ipv4/af_inet.cstatic 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, };擴展一下,如果看一下ip_rcv和udp_rcv等函數(shù)的代碼能看到很多協(xié)議的處理過程。例如,ip_rcv中會處理netfilter和iptable過濾,如果你有很多或者很復雜的 netfilter 或 iptables 規(guī)則,這些規(guī)則都是在軟中斷的上下文中執(zhí)行的,會加大網(wǎng)絡延遲。再例如,udp_rcv中會判斷socket接收隊列是否滿了。對應的相關(guān)內(nèi)核參數(shù)是net.core.rmem_max和net.core.rmem_default。如果有興趣,建議大家好好讀一下inet_init這個函數(shù)的代碼。
網(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
驅(qū)動的pci_register_driver調(diào)用完成后,Linux內(nèi)核就知道了該驅(qū)動的相關(guān)信息,比如igb網(wǎng)卡驅(qū)動的igb_driver_name和igb_probe函數(shù)地址等等。當網(wǎng)卡設備被識別以后,內(nèi)核會調(diào)用其驅(qū)動的probe方法(igb_driver的probe方法是igb_probe)。驅(qū)動probe方法執(zhí)行的目的就是讓設備ready,對于igb網(wǎng)卡,其igb_probe位于drivers/net/ethernet/intel/igb/igb_main.c下。主要執(zhí)行的操作如下:
?
第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. ...... 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初始化過程中,還調(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);} 啟動網(wǎng)卡當上面的初始化都完成以后,就可以啟動網(wǎng)卡了。回憶前面網(wǎng)卡驅(qū)動初始化時,我們提到了驅(qū)動向內(nèi)核注冊了 structure net_device_ops 變量,它包含著網(wǎng)卡啟用、發(fā)包、設置mac 地址等回調(diào)函數(shù)(函數(shù)指針)。當啟用一個網(wǎng)卡時(例如,通過 ifconfig eth0 up),net_device_ops 中的 igb_open方法會被調(diào)用。它通常會做以下事情:
?
//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;......} }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中我們看到了,對于多隊列的網(wǎng)卡,為每一個隊列都注冊了中斷,其對應的中斷處理函數(shù)是igb_msix_ring(該函數(shù)也在drivers/net/ethernet/intel/igb/igb_main.c下)。我們也可以看到,msix方式下,每個 RX 隊列有獨立的MSI-X 中斷,從網(wǎng)卡硬件中斷的層面就可以設置讓收到的包被不同的 CPU處理。(可以通過 irqbalance ,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity能夠修改和CPU的綁定行為)。
到此準備工作完成。
Linux網(wǎng)絡包:中斷到網(wǎng)絡層接收
網(wǎng)卡收包從整體上是網(wǎng)線中的高低電平轉(zhuǎn)換到網(wǎng)卡FIFO存儲再拷貝到系統(tǒng)主內(nèi)存(DDR3)的過程,其中涉及到網(wǎng)卡控制器,CPU,DMA,驅(qū)動程序,在OSI模型中屬于物理層和鏈路層,如下圖所示。
中斷處理
物理網(wǎng)卡收到數(shù)據(jù)包的處理流程如上圖左半部分所示,詳細步驟如下:
1. 網(wǎng)卡收到數(shù)據(jù)包,先將高低電平轉(zhuǎn)換到網(wǎng)卡fifo存儲,網(wǎng)卡申請ring buffer的描述,根據(jù)描述找到具體的物理地址,從fifo隊列物理網(wǎng)卡會使用DMA將數(shù)據(jù)包寫到了該物理地址,,其實就是skb_buffer中.
2. 這個時候數(shù)據(jù)包已經(jīng)被轉(zhuǎn)移到skb_buffer中,因為是DMA寫入,內(nèi)核并沒有監(jiān)控數(shù)據(jù)包寫入情況,這時候NIC觸發(fā)一個硬中斷,每一個硬件中斷會對應一個中斷號,且指定一個vCPU來處理,如上圖vcpu2收到了該硬件中斷.
3. 硬件中斷的中斷處理程序,調(diào)用驅(qū)動程序完成,a.啟動軟中斷
4. 硬中斷觸發(fā)的驅(qū)動程序會禁用網(wǎng)卡硬中斷,其實這時候意思是告訴NIC,再來數(shù)據(jù)不用觸發(fā)硬中斷了,把數(shù)據(jù)DMA拷入系統(tǒng)內(nèi)存即可
5. 硬中斷觸發(fā)的驅(qū)動程序會啟動軟中斷,啟用軟中斷目的是將數(shù)據(jù)包后續(xù)處理流程交給軟中斷慢慢處理,這個時候退出硬件中斷了,但是注意和網(wǎng)絡有關(guān)的硬中斷,要等到后續(xù)開啟硬中斷后,才有機會再次被觸發(fā)
6. NAPI觸發(fā)軟中斷,觸發(fā)napi系統(tǒng)
7. 消耗ringbuffer指向的skb_buffer
8. NAPI循環(huán)處理ringbuffer數(shù)據(jù),處理完成
9. 啟動網(wǎng)絡硬件中斷,有數(shù)據(jù)來時候就可以繼續(xù)觸發(fā)硬件中斷,繼續(xù)通知CPU來消耗數(shù)據(jù)包.
其實上述過程過程簡單描述為:網(wǎng)卡收到數(shù)據(jù)包,DMA到內(nèi)核內(nèi)存,中斷通知內(nèi)核數(shù)據(jù)有了,內(nèi)核按輪次處理消耗數(shù)據(jù)包,一輪處理完成后,開啟硬中斷。其核心就是網(wǎng)卡和內(nèi)核其實是生產(chǎn)和消費模型,網(wǎng)卡生產(chǎn),內(nèi)核負責消費,生產(chǎn)者需要通知消費者消費;如果生產(chǎn)過快會產(chǎn)生丟包,如果消費過慢也會產(chǎn)生問題。也就說在高流量壓力情況下,只有生產(chǎn)消費優(yōu)化后,消費能力夠快,此生產(chǎn)消費關(guān)系才可以正常維持,所以如果物理接口有丟包計數(shù)時候,未必是網(wǎng)卡存在問題,也可能是內(nèi)核消費的太慢。
關(guān)于CPU與ksoftirqd的關(guān)系可以描述如下:
?
?
?
網(wǎng)卡收到的數(shù)據(jù)寫入到內(nèi)核內(nèi)存
NIC在接收到數(shù)據(jù)包之后,首先需要將數(shù)據(jù)同步到內(nèi)核中,這中間的橋梁是rx ring buffer。它是由NIC和驅(qū)動程序共享的一片區(qū)域,事實上,rx ring buffer存儲的并不是實際的packet數(shù)據(jù),而是一個描述符,這個描述符指向了它真正的存儲地址,具體流程如下:
1. 驅(qū)動在內(nèi)存中分配一片緩沖區(qū)用來接收數(shù)據(jù)包,叫做sk_buffer;
2. 將上述緩沖區(qū)的地址和大小(即接收描述符),加入到rx ring buffer。描述符中的緩沖區(qū)地址是DMA使用的物理地址;
3. 驅(qū)動通知網(wǎng)卡有一個新的描述符;
4. 網(wǎng)卡從rx ring buffer中取出描述符,從而獲知緩沖區(qū)的地址和大小;
5. 網(wǎng)卡收到新的數(shù)據(jù)包;
6. 網(wǎng)卡將新數(shù)據(jù)包通過DMA直接寫到sk_buffer中。
當驅(qū)動處理速度跟不上網(wǎng)卡收包速度時,驅(qū)動來不及分配緩沖區(qū),NIC接收到的數(shù)據(jù)包無法及時寫到sk_buffer,就會產(chǎn)生堆積,當NIC內(nèi)部緩沖區(qū)寫滿后,就會丟棄部分數(shù)據(jù),引起丟包。這部分丟包為rx_fifo_errors,在 /proc/net/dev中體現(xiàn)為fifo字段增長,在ifconfig中體現(xiàn)為overruns指標增長。
中斷下半部分
ksoftirqd內(nèi)核線程處理軟中斷,即中斷下半部分軟中斷處理過程:
1.NAPI(以e1000網(wǎng)卡為例):net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()
2.非NAPI(以dm9000網(wǎng)卡為例):net_rx_action() -> process_backlog() -> netif_receive_skb()
最后網(wǎng)卡驅(qū)動通過netif_receive_skb()將sk_buff上送協(xié)議棧。
?
內(nèi)核線程初始化的時候,我們介紹了ksoftirqd中兩個線程函數(shù)ksoftirqd_should_run和run_ksoftirqd。其中ksoftirqd_should_run代碼如下:
?
#define local_softirq_pending() \ __IRQ_STAT(smp_processor_id(), __softirq_pending)這里看到和硬中斷中調(diào)用了同一個函數(shù)local_softirq_pending。使用方式不同的是硬中斷位置是為了寫入標記,這里僅僅只是讀取。如果硬中斷中設置了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)
?
在網(wǎng)絡子系統(tǒng)初始化小節(jié),我們看到我們?yōu)镹ET_RX_SOFTIRQ注冊了處理函數(shù)net_rx_action。所以net_rx_action函數(shù)就會被執(zhí)行到了。
這里需要注意一個細節(jié),硬中斷中設置軟中斷標記,和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)絡包的接收不霸占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ù)了。
/*** igb_poll - NAPI Rx polling callback* @napi: napi polling structure* @budget: count of how many packets we should handle**/ 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的重點工作是對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ù)以后,對其進行一些校驗,然后開始設置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)絡棧的包數(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é)議棧中,接下來在網(wǎng)絡層協(xié)議層的處理流程便不再贅述。
總結(jié)
send發(fā)包過程
1、網(wǎng)卡驅(qū)動創(chuàng)建tx descriptor ring(一致性DMA內(nèi)存),將tx descriptor ring的總線地址寫入網(wǎng)卡寄存器TDBA
2、協(xié)議棧通過dev_queue_xmit()將sk_buff下送網(wǎng)卡驅(qū)動
3、網(wǎng)卡驅(qū)動將sk_buff放入tx descriptor ring,更新TDT
4、DMA感知到TDT的改變后,找到tx descriptor ring中下一個將要使用的descriptor
5、DMA通過PCI總線將descriptor的數(shù)據(jù)緩存區(qū)復制到Tx FIFO
6、復制完后,通過MAC芯片將數(shù)據(jù)包發(fā)送出去
7、發(fā)送完后,網(wǎng)卡更新TDH,啟動硬中斷通知CPU釋放數(shù)據(jù)緩存區(qū)中的數(shù)據(jù)包
recv收包過程
1、網(wǎng)卡驅(qū)動創(chuàng)建rx descriptor ring(一致性DMA內(nèi)存),將rx descriptor ring的總線地址寫入網(wǎng)卡寄存器RDBA
2、網(wǎng)卡驅(qū)動為每個descriptor分配sk_buff和數(shù)據(jù)緩存區(qū),流式DMA映射數(shù)據(jù)緩存區(qū),將數(shù)據(jù)緩存區(qū)的總線地址保存到descriptor
3、網(wǎng)卡接收數(shù)據(jù)包,將數(shù)據(jù)包寫入Rx FIFO
4、DMA找到rx descriptor ring中下一個將要使用的descriptor
5、整個數(shù)據(jù)包寫入Rx FIFO后,DMA通過PCI總線將Rx FIFO中的數(shù)據(jù)包復制到descriptor的數(shù)據(jù)緩存區(qū)
6、復制完后,網(wǎng)卡啟動硬中斷通知CPU數(shù)據(jù)緩存區(qū)中已經(jīng)有新的數(shù)據(jù)包了,CPU執(zhí)行硬中斷函數(shù):
NAPI(以e1000網(wǎng)卡為例):e1000_intr() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)
非NAPI(以dm9000網(wǎng)卡為例):dm9000_interrupt() -> dm9000_rx() -> netif_rx() -> napi_schedule() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)
7、ksoftirqd執(zhí)行軟中斷函數(shù)net_rx_action():
NAPI(以e1000網(wǎng)卡為例):net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()
非NAPI(以dm9000網(wǎng)卡為例):net_rx_action() -> process_backlog() -> netif_receive_skb()
8、網(wǎng)卡驅(qū)動通過netif_receive_skb()將sk_buff上送協(xié)議棧
?
?
Linux網(wǎng)絡子系統(tǒng)的分層
Linux網(wǎng)絡子系統(tǒng)實現(xiàn)需要:
支持不同的協(xié)議族 ( INET, INET6, UNIX, NETLINK...)
支持不同的網(wǎng)絡設備
支持統(tǒng)一的BSD socket API
需要屏蔽協(xié)議、硬件、平臺(API)的差異,因而采用分層結(jié)構(gòu)
系統(tǒng)調(diào)用
系統(tǒng)調(diào)用提供用戶的應用程序訪問內(nèi)核的唯一途徑。協(xié)議無關(guān)接口由socket layer來實現(xiàn)的,其提供一組通用功能,以支持各種不同的協(xié)議。網(wǎng)絡協(xié)議層為socket層提供具體協(xié)議接口——proto{},實現(xiàn)具體的協(xié)議細節(jié)。設備無關(guān)接口,提供一組通用函數(shù)供底層網(wǎng)絡設備驅(qū)動程序使用。設備驅(qū)動與特定網(wǎng)卡設備相關(guān),定義了具體的協(xié)議細節(jié),會分配一個net_device結(jié)構(gòu),然后用其必需的例程進行初始化。
?
來源:https://www.cnblogs.com/ypholic/p/14337328.html
- END -
看完一鍵三連在看,轉(zhuǎn)發(fā),點贊
是對文章最大的贊賞,極客重生感謝你
推薦閱讀
圖解Linux 內(nèi)核TCP/IP 協(xié)議棧實現(xiàn)|Linux網(wǎng)絡硬核系列
網(wǎng)絡排障全景指南手冊v1.0精簡版pdf
一個奇葩的網(wǎng)絡問題
總結(jié)
以上是生活随笔為你收集整理的Linux网络子系统的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 为什么字节跳动选择使用 Go 语言?
- 下一篇: 硬核致敬Linux !30岁生日快乐!