Linux內核網絡數據包處理流程


Linux內核網絡數據包處理流程

from kernel-4.9:

0. Linux內核網絡數據包處理流程 - 網絡硬件

網卡工作在物理層和數據鏈路層,主要由PHY/MAC芯片、Tx/Rx FIFO、DMA等組成,其中網線通過變壓器接PHY芯片、PHY芯片通過MII接MAC芯片、MAC芯片接PCI總線

PHY/MAC芯片

PHY芯片主要負責:CSMA/CD、模數轉換、編解碼、串並轉換

MAC芯片主要負責:

  1. 比特流和幀的轉換:7字節的前導碼Preamble和1字節的幀首定界符SFD

  2. CRC校驗

  3. Packet Filtering:L2 Filtering、VLAN Filtering、Manageability / Host Filtering

Intel的千兆網卡以82575/82576為代表、萬兆網卡以82598/82599為代表

1. Linux內核網絡數據包處理流程 - 網卡驅動

網卡驅動ixgbe初始化

網卡驅動為每個新的接口在一個全局的網絡設備列表里插入一個數據結構.每個接口由一個結構 net_device 項來描述, 它在<linux/netdevice.h>里定義。該結構必須動態分配。

每個網卡,無論是物理還是虛擬的網卡,都必須有一個:net_device,這個struct是在網卡驅動中分配創建的,不通的網卡,對應廠商不同的驅動,那么看看ixgbe的驅動初始化; 創建net_device 的函數是: alloc_etherdev , 或者: alloc_etherdev_mq

https://www.cnblogs.com/lidp/archive/2009/05/13/1697981.html

pci設備:

在內核中,一個PCI設備,使用struct pci_driver結構來描述, 因為在系統引導的時候,PCI設備已經被識別,當內核發現一個已經檢測到的設備同驅動注冊的id_table中的信息相匹配時,
它就會觸發驅動的probe函數,

比如,看看ixgbe 驅動:

static struct pci_driver ixgb_driver = {
    .name     = ixgb_driver_name,
    .id_table = ixgb_pci_tbl,
    .probe    = ixgb_probe,
    .remove   = ixgb_remove,
    .err_handler = &ixgb_err_handler
};
#vim drivers/net/ethernet/intel/ixgbe/ixgbe_main.c

module_init
    ixgbe_init_module
        pci_register_driver

probe函數被調用,證明已經發現了我們所支持的網卡,這樣,就可以調用register_netdev函數向內核注冊網絡設備了,注冊之前,一般會調用alloc_etherdev分配一個net_device,然后初始化它的重要成員。

ixgbe_probe  
    struct net_device *netdev;
    struct pci_dev *pdev;
    pci_enable_device_mem(pdev);
    pci_request_mem_regions(pdev, ixgbe_driver_name);
    pci_set_master(pdev);
    pci_save_state(pdev);
    netdev = alloc_etherdev_mq(sizeof(struct ixgbe_adapter), indices);// 這里分配struct net_device
    	alloc_etherdev_mqs
    		alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_UNKNOWN, ether_setup, txqs, rxqs);
    			ether_setup  // Initial struct net_device
    			
    SET_NETDEV_DEV(netdev, &pdev->dev);
    adapter = netdev_priv(netdev);

refs: https://blog.csdn.net/shallnet/article/details/25470775

alloc_etherdev_mqs() -> ether_setup()
void ether_setup(struct net_device *dev)
{
    dev->header_ops     = &eth_header_ops;
    dev->type       = ARPHRD_ETHER;
    dev->hard_header_len    = ETH_HLEN;
    dev->min_header_len = ETH_HLEN;
    dev->mtu        = ETH_DATA_LEN;
    dev->addr_len       = ETH_ALEN;
    dev->tx_queue_len   = 1000; /* Ethernet wants good queues */
    dev->flags      = IFF_BROADCAST|IFF_MULTICAST;
    dev->priv_flags     |= IFF_TX_SKB_SHARING;

    eth_broadcast_addr(dev->broadcast);

}
EXPORT_SYMBOL(ether_setup);

static struct pci_driver ixgbe_driver = {
	.name     = ixgbe_driver_name,
	.id_table = ixgbe_pci_tbl,
	.probe    = ixgbe_probe, // 系統探測到ixgbe網卡后調用ixgbe_probe()
	.remove   = ixgbe_remove,
#ifdef CONFIG_PM
	.suspend  = ixgbe_suspend,
	.resume   = ixgbe_resume,
#endif
	.shutdown = ixgbe_shutdown,
	.sriov_configure = ixgbe_pci_sriov_configure,
	.err_handler = &ixgbe_err_handler
};

static int __init ixgbe_init_module(void)
{
	...
	ret = pci_register_driver(&ixgbe_driver); // 注冊ixgbe_driver
	...
}

module_init(ixgbe_init_module);

static void __exit ixgbe_exit_module(void)
{
	...
	pci_unregister_driver(&ixgbe_driver); // 注銷ixgbe_driver
	...
}

module_exit(ixgbe_exit_module);

2. Linux內核網絡數據包處理流程 - 中斷注冊


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,    /* Preferable RCU should always be the last softirq */
 
    NR_SOFTIRQS
};

內核初始化期間,softirq_init會注冊TASKLET_SOFTIRQ以及HI_SOFTIRQ相關聯的處理函數。


void __init softirq_init(void)
{
    ......
 
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

網絡子系統分兩種soft IRQ。NET_TX_SOFTIRQNET_RX_SOFTIRQ,分別處理發送數據包和接收數據包。這兩個soft IRQ在net_dev_init函數(net/core/dev.c)中注冊:

    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);

收發數據包的軟中斷處理函數被注冊為net_rx_actionnet_tx_action
其中open_softirq實現為:


void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

3. Linux內核網絡數據包處理流程 - 重要結構體初始化

每個cpu都有隊列來處理接收到的幀,都有其數據結構來處理入口和出口流量,因此,不同cpu之間沒有必要使用上鎖機制,。此隊列數據結構為softnet_data(定義在include/linux/netdevice.h中):

/*
 * Incoming packets are placed on per-cpu queues so that
 * no locking is needed.
 */
struct softnet_data
{
struct Qdisc *output_queue; 
struct sk_buff_headinput_pkt_queue;//有數據要傳輸的設備列表
struct list_headpoll_list; //雙向鏈表,其中的設備有輸入幀等着被處理。
struct sk_buff*completion_queue;//緩沖區列表,其中緩沖區已成功傳輸,可以釋放掉

struct napi_structbacklog;
}

softnet_data 是在start_kernel 中創建的, 並且,每個cpu一個 softnet_data 變量, 這個變量中,最重要的是poll_list , 每當收到數據包時,網絡設備驅動會把自己的napi_struct掛到CPU私有變量softnet_data->poll_list上, 這樣在軟中斷時,net_rx_action會遍歷cpu私有變量的softnet_data->poll_list, 執行上面所掛的napi_struct結構的poll鈎子函數,將數據包從驅動傳到網絡協議棧。

內核初始化流程:

start_kernel()
--> rest_init()
        --> do_basic_setup()
            --> do_initcall
               -->net_dev_init

__init  net_dev_init(){
    //每個CPU都有一個CPU私有變量 _get_cpu_var(softnet_data)
    //_get_cpu_var(softnet_data).poll_list很重要,軟中斷中需要遍歷它的
    for_each_possible_cpu(i) {
        struct softnet_data *queue;
        queue = &per_cpu(softnet_data, i);
        skb_queue_head_init(&queue->input_pkt_queue);
        queue->completion_queue = NULL;
        INIT_LIST_HEAD(&queue->poll_list);
        queue->backlog.poll = process_backlog;
        queue->backlog.weight = weight_p;
}
   //在軟中斷上掛網絡發送handler
    open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
//在軟中斷上掛網絡接收handler
    open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
}

4. Linux內核網絡數據包處理流程 - 收發包過程圖

ixgbe_adapter包含ixgbe_q_vector數組(一個ixgbe_q_vector對應一個中斷),ixgbe_q_vector包含napi_struct:

硬中斷函數把napi_struct加入CPU的poll_list,軟中斷函數net_rx_action()遍歷poll_list,執行poll函數

發包過程

1、網卡驅動創建tx descriptor ring(一致性DMA內存),將tx descriptor ring的總線地址寫入網卡寄存器TDBA

2、協議棧通過dev_queue_xmit()sk_buff下送網卡驅動

3、網卡驅動將sk_buff放入tx descriptor ring,更新TDT

4、DMA感知到TDT的改變后,找到tx descriptor ring中下一個將要使用的descriptor

5、DMA通過PCI總線將descriptor的數據緩存區復制到Tx FIFO

6、復制完后,通過MAC芯片將數據包發送出去

7、發送完后,網卡更新TDH,啟動硬中斷通知CPU釋放數據緩存區中的數據包

收包過程

1、網卡驅動創建rx descriptor ring(一致性DMA內存),將rx descriptor ring的總線地址寫入網卡寄存器RDBA

2、網卡驅動為每個descriptor分配sk_buff和數據緩存區,流式DMA映射數據緩存區,將數據緩存區的總線地址保存到descriptor

3、網卡接收數據包,將數據包寫入Rx FIFO

4、DMA找到rx descriptor ring中下一個將要使用的descriptor

5、整個數據包寫入Rx FIFO后,DMA通過PCI總線將Rx FIFO中的數據包復制到descriptor的數據緩存區

6、復制完后,網卡啟動硬中斷通知CPU數據緩存區中已經有新的數據包了,CPU執行硬中斷函數:

  • NAPI(以e1000網卡為例):e1000_intr() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)

  • 非NAPI(以dm9000網卡為例):dm9000_interrupt() -> dm9000_rx() -> netif_rx() -> napi_schedule() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)

7、ksoftirqd執行軟中斷函數net_rx_action()

  • NAPI(以e1000網卡為例):net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()
  • 非NAPI(以dm9000網卡為例):net_rx_action() -> process_backlog() -> netif_receive_skb()

8、網卡驅動通過netif_receive_skb()sk_buff上送協議棧

5. 中斷上下部

硬中斷中的netif_rx()函數:把skb加入CPU的softnet_data-> input_pkt_queue隊列

netif_rx(skb);  // 在 硬中斷中,處理skb
	netif_rx_internal(skb);
		trace_netif_rx(skb);
		preempt_disable();
		rcu_read_lock();
		cpu = get_rps_cpu(skb->dev, skb, &rflow); // 通過rps,獲得cpu id
		enqueue_to_backlog(skb, cpu, &rflow->last_qtail); 
			struct softnet_data *sd;
			sd = &per_cpu(softnet_data, cpu);  // 根據cpu id,獲得sd
			rps_lock(sd);
			__skb_queue_tail(&sd->input_pkt_queue, skb); // enqueue 動作
			input_queue_tail_incr_save(sd, qtail);
			rps_unlock(sd);
			local_irq_restore(flags)
			return NET_RX_SUCCESS
		rcu_read_unlock();
		preempt_enable();
static int netif_rx_internal(struct sk_buff *skb)
{
    int ret;

    net_timestamp_check(netdev_tstamp_prequeue, skb);

    trace_netif_rx(skb);
#ifdef CONFIG_RPS
    if (static_key_false(&rps_needed)) {
        struct rps_dev_flow voidflow, *rflow = &voidflow;
        int cpu;

        preempt_disable(); // 關閉搶占
        rcu_read_lock(); 

        cpu = get_rps_cpu(skb->dev, skb, &rflow);  
        if (cpu < 0)
            cpu = smp_processor_id();

        ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);  // 加入隊列

        rcu_read_unlock();
        preempt_enable();
    } else
#endif
    {
        unsigned int qtail;
        ret = enqueue_to_backlog(skb, get_cpu(), &qtail);
        put_cpu();
    }
    return ret;
}

enqueue_to_backlog()主要工作,就是將skb掛到一個cpu下的softnet_data-> input_pkt_queue隊列里,

static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
                  unsigned int *qtail)
{
    struct softnet_data *sd;
    unsigned long flags;
    unsigned int qlen;

    sd = &per_cpu(softnet_data, cpu);

    local_irq_save(flags);

    rps_lock(sd);
    if (!netif_running(skb->dev))
        goto drop;
    qlen = skb_queue_len(&sd->input_pkt_queue);
    if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
        if (qlen) {
enqueue:
            __skb_queue_tail(&sd->input_pkt_queue, skb);  // 將skb加入到sd-> input_pkt_queue隊列
            input_queue_tail_incr_save(sd, qtail);
            rps_unlock(sd);
            local_irq_restore(flags);
            return NET_RX_SUCCESS; 
        }

        /* Schedule NAPI for backlog device
         * We can use non atomic operation since we own the queue lock
         */
        if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
            if (!rps_ipi_queued(sd))
                ____napi_schedule(sd, &sd->backlog); // napi方式處理skb
        }
        goto enqueue;
    }

drop:
    sd->dropped++;
    rps_unlock(sd);

    local_irq_restore(flags);

    atomic_long_inc(&skb->dev->rx_dropped);
    kfree_skb(skb);
    return NET_RX_DROP;
}
____napi_schedule
	list_add_tail(&napi->poll_list, &sd->poll_list);

上述,就是硬中斷需要做的工作,然后, 軟中斷net_rx_action()會遍歷這個list,進行進一步操作。

中斷處理上,處理skb,包含兩種方式:

硬中斷就是上半部,在上半部,有netif_rx 中對napi進行判斷,在下半部的softirq (net_rx_action()) 中,同樣對napi和非napi進行了判斷 !

  • 非NAPI
    • 非NAPI設備驅動會為其所接收的每一個幀產生一個中斷事件,在高流量負載下,會花掉大量時間處理中斷事件,造成資源浪費。而NAPI驅動混合了中斷事件和輪詢,在高流量負載下其性能會比舊方法要好。
  • NAPI
    • NAPI主要思想是混合使用中斷事件和輪詢,而不是僅僅使用中斷事件驅動模型。當收到新的幀時,關中斷,再一次處理完所有入口隊列。從內核觀點來看,NAPI方法因為中斷事件少了,減少了cpu負載。

默認是napi?還是非napi?

在初始化時,默認是非napi的模式,poll函數默認是: process_backlog ,如下:

net_dev_init
	for_each_possible_cpu(i) {
		sd->backlog.poll = process_backlog;
	}

net_rx_action中將會調用設備的poll函數, 如果沒有, 就是默認的process_backlog函數
process_backlog函數里面將skb出隊列之后, netif_receive_skb處理此skb

軟中斷中,使用net_rx_action(),處理skb:

7、ksoftirqd執行軟中斷函數`net_rx_action()`:

* NAPI(以e1000網卡為例):`net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()`
* 非NAPI(以dm9000網卡為例):`net_rx_action() -> process_backlog() -> netif_receive_skb()`

8、網卡驅動通過`netif_receive_skb()`將`sk_buff`上送協議棧

最后,通過netif_receive_skb(), 將skb送上協議棧;

軟中斷中,對napi和非napi的處理: process_backlog

net_rx_action
	process_backlog
		__netif_receive_skb
			__netif_receive_skb_core

非NAPI vs NAPI

  • (1) 支持NAPI的網卡驅動必須提供輪詢方法poll()
  • (2) 非NAPI的內核接口為netif_rx()
    NAPI的內核接口為napi_schedule()
  • (3) 非NAPI使用共享的CPU隊列softnet_data->input_pkt_queue
    NAPI使用設備內存(或者設備驅動程序的接收環)。

6. 參考:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM