首页 > 其他分享 >网络收包讲解

网络收包讲解

时间:2024-01-30 16:14:48浏览次数:23  
标签:struct rx 网络 tcp init igb 讲解 inet 收包

 

图1 整体流程图

一、系统启yjqe动 1.1 概述
  • 网卡驱动的加载
  • 网卡驱动的初始化(probe)
  • 网卡设备的启用(ndo_open)
  • 软中断进程初始化(ksoftirqd)
  • 网络子系统初始化(net)
  • 网络协议栈初始化
1.2 网卡驱动的加载 网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核。当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。网卡驱动程序 igb 向 Linux 内核通过 module_init 宏注册一个初始化函数 igb_init_module,当驱动加载的时候,该函数被内核调用。

static struct pci_driver igb_driver = {
    .name     = igb_driver_name, //igb
    .id_table = igb_pci_tbl,
    .probe    = igb_probe,
    .remove   = igb_remove,
#ifdef CONFIG_PM
    .driver.pm = &igb_pm_ops,
#endif
    .shutdown = igb_shutdown,
    .sriov_configure = igb_pci_sriov_configure,
    .err_handler = &igb_err_handler
};

static int __init igb_init_module(void)
{
    int ret;
    pr_info("%s\n", igb_driver_string);
    pr_info("%s\n", igb_copyright);
#ifdef CONFIG_IGB_DCA
    dca_register_notify(&dca_notifier);
#endif
    ret = pci_register_driver(&igb_driver);
    return ret;
}

module_init(igb_init_module);

igb_init_module 函数的大部分工作是通过 pci_register_driver 内 __pci_register_driver 函数来完成的。

int __pci_register_driver(struct pci_driver *drv, struct module *owner, const char *mod_name)
{
    /* initialize common driver fields */
    drv->driver.name = drv->name;
    drv->driver.bus = &pci_bus_type;
    drv->driver.owner = owner;
    drv->driver.mod_name = mod_name;
    drv->driver.groups = drv->groups;
    drv->driver.dev_groups = drv->dev_groups;

    spin_lock_init(&drv->dynids.lock);
    INIT_LIST_HEAD(&drv->dynids.list);

    /* register with core */
    return driver_register(&drv->driver);
}

最后 driver_register 函数把网卡的驱动(driver)加载到内核 PCI 子系统。 1.3 网卡驱动的初始化(probe) 一个驱动程序可以支持一个或多个设备,而一个设备只会绑定一个驱动程序。驱动程序将其支持的所有设备保存在一个列表 struct pci_device_id 中。igb 驱动程序所支持的 PCI 设备列表部分如下:

static const struct pci_device_id igb_pci_tbl[] = {
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_1GBPS) },
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_SGMII) },
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_2_5GBPS) },
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I211_COPPER), board_82575 },
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER), board_82575 },
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_FIBER), board_82575 },
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES), board_82575 },
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SGMII), board_82575 },
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER_FLASHLESS), board_82575 },
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES_FLASHLESS), board_82575 },
    /* required last entry */
    {0, }
};

内核通过设备 ID 与驱动支持的设备列表匹配,选择合适的驱动控制网卡,然后调用之前注册到内核 PCI 子系统的探测函数(probe)完成初始化。例如 igb 驱动程序的 igb_probe 函数,其处理流程包括:
  • 设置 DMA 寻址限制和缓存一致性;
  • 申请内核内存;
  • struct net_device 结构体的创建、初始化和注册;
  • 注册 struct net_device_ops (里面有 igb_open ) 到 net_device;
  • 注册驱动支持的 ethtool 调用函数;
  • 注册 poll 函数到 NAPI 子系统;
igb 驱动程序中 igb_probe 函数的部分代码如下:

static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
    /* 设置 DMA 寻址限制和缓存一致性 */
    err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    /* 申请内存 */
    err = pci_request_mem_regions(pdev, igb_driver_name);
    /* 网络设备 */
    netdev = alloc_etherdev_mq(sizeof(struct igb_adapter), IGB_MAX_TX_QUEUES);
    /* net_device_ops 结构体,代表一个网络设备 */
    netdev->netdev_ops = &igb_netdev_ops;
    /* 注册驱动支持的 ethtool 调用函数 */
    igb_set_ethtool_ops(netdev);
    /* 函数里面注册了 poll 函数 */
    err = igb_sw_init(adapter);
}

DMA(Direct Memory Access)顾名思义就是「直接内存访问」,是指一个设备和 CPU 共享内存总线。DMA 主要优点:通过和 CPU 共享内存总线,DMA 可以实现 IO 设备和内存之间快速的数据复制(不论内存到设备还是设备到内存,都能够加速数据传输)。
1.3.1 注册 net_device_ops 到 net_device net_device_ops 结构体包含了指向打开设备、发送数据和设置 MAC 地址等操作函数的指针,代码如下:

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_eth_ioctl      = igb_ioctl,
    .ndo_tx_timeout     = igb_tx_timeout,
    .ndo_validate_addr  = eth_validate_addr,
    .ndo_vlan_rx_add_vid= igb_vlan_rx_add_vid,
    .ndo_vlan_rx_kill_vid= igb_vlan_rx_kill_vid,
    .ndo_set_vf_mac     = igb_ndo_set_vf_mac,
    .ndo_set_vf_vlan    = igb_ndo_set_vf_vlan,
    .ndo_set_vf_rate    = igb_ndo_set_vf_bw,
    .ndo_set_vf_spoofchk= igb_ndo_set_vf_spoofchk,
    .ndo_set_vf_trust   = igb_ndo_set_vf_trust,
    .ndo_get_vf_config  = igb_ndo_get_vf_config,
    .ndo_fix_features   = igb_fix_features,
    .ndo_set_features   = igb_set_features,
    .ndo_fdb_add        = igb_ndo_fdb_add,
    .ndo_features_check = igb_features_check,
    .ndo_setup_tc       = igb_setup_tc,
    .ndo_bpf            = igb_xdp,
    .ndo_xdp_xmit       = igb_xdp_xmit,
};

1.3.2 注册 poll 函数到 NAPI 子系统 网卡驱动程序都会实现poll函数,igb驱动程序实现的poll函数是 igb_poll 函数,通过调用netif_napi_add函数将其注册到 NAPI 子系统。调用链:igb_sw_init -> igb_init_interrupt_scheme -> igb_alloc_q_vectors -> igb_alloc_q_vector。 igb_alloc_q_vector 函数部分代码如下:

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)
{
    /* allocate q_vector and rings */
    q_vector = adapter->q_vector[v_idx];
    /* 初始化 NAPI */
    netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);
}

其中,weight代表 RX 队列的处理权重,budget表示一种惩罚措施,用于多 CPU 多队列之间的公平性调度 1.4 网卡设备的启用(ndo_open) 当网络设备被启用时(比如使用 ifconfig eth0 up 命令)net_device_ops 中的 ndo_open 所指向的函数将会被调用。完成以下处理:
  • 分配多 TX/RX 队列的内核内存空间;
  • 给网卡配置 RX/TX 队列,给 RX 申请 DMA 空间;
  • 注册硬中断处理函数;
  • 打开 NAPI;
  • 打开网卡硬中断;

 

igb 驱动程序中 ndo_open 指向的是 igb_open,部分代码如下:

static int __igb_open(struct net_device *netdev, bool resuming)
{
    /* 分配多 TX 队列的内存空间 */
    err = igb_setup_all_tx_resources(adapter);
    /* 分配多 RX 队列的内存空间 */
    err = igb_setup_all_rx_resources(adapter);
    /* 给网卡配置 RX/TX 队列,给 RX 申请 DMA 空间 */
    igb_configure(adapter);
    /* 注册中断处理函数 */
    err = igb_request_irq(adapter);
    /* 打开 NAPI */
    for (i = 0; i < adapter->num_q_vectors; i++)
        napi_enable(&(adapter->q_vector->napi));
    /* 打开硬中断 */
    igb_irq_enable(adapter);
    /* 启动所有 TX 队列 */
    netif_tx_start_all_queues(netdev);
}

int igb_open(struct net_device *netdev)
{
    return __igb_open(netdev, false);
}

1.4.1 分配 TX/RX 多队列(Ring Buffer)内存空间 目前大部分网络都采用基于环形缓冲区的队列来进行 DMA 的收发数据包。igb_open 代码中 igb_setup_all_rx_resources 会循环调用 igb_setup_rx_resources 函数 num_rx_queues 次,每次申请一个 Ring Buffer 内存空间,元素是 struct igb_rx_buffer,并且通过 DMA 申请连续内核空间用来存放 Ring Buffer 对应的网络数据。部分代码如下:

static int igb_setup_all_rx_resources(struct igb_adapter *adapter)
{
    for (i = 0; i < adapter->num_rx_queues; i++)
        err = igb_setup_rx_resources(adapter->rx_ring);
}

int igb_setup_rx_resources(struct igb_ring *rx_ring)
{
    /* Ring Buffer 的元素是 struct igb_rx_buffer */
    size = sizeof(struct igb_rx_buffer) * rx_ring->count;
    /* 申请 Ring Buffer 内存空间 */
    rx_ring->rx_buffer_info = vmalloc(size);
    /* Round up to nearest 4K */
    rx_ring->size = rx_ring->count * sizeof(union e1000_adv_rx_desc);
    rx_ring->size = ALIGN(rx_ring->size, 4096);
    /* 通过 DMA 申请连续内核空间,数量与 Ring Buffer 长度一致 */
    rx_ring->desc = dma_alloc_coherent(dev, rx_ring->size, &rx_ring->dma, GFP_KERNEL);
    /* 复位 */
    rx_ring->next_to_alloc = 0;
    rx_ring->next_to_clean = 0;
    rx_ring->next_to_use = 0;
}

struct igb_rx_buffer {
    dma_addr_t dma; /* DMA 内核空间地址 */
    struct page *page;
    __u16 page_offset;
    __u16 pagecnt_bias;
};

1.4.2 网卡配置 TX/RX 队列 创建完 RX 和 TX 队列后,需要把他们关联到网卡硬件,关联方式是通过把 RX/TX 的首元素写入网卡寄存器等操作,最后需要申请 RX 队列内长度 - 1个 igb_rx_buffer 元素的 DMA 地址(总线地址)空间,便于网卡收到数据好有地方存。igb_configure 和 igb_alloc_rx_buffers 函数部分代码如下:

static void igb_configure(struct igb_adapter *adapter)
{
    struct net_device *netdev = adapter->netdev;
    int i;
    /* 给网卡配置 TX/RX 队列,收发数据均从一个元素开始 */
    igb_configure_tx(adapter);
    igb_configure_rx(adapter);
    /* 清空网卡内的 RX FIFO */
    igb_rx_fifo_flush_82575(&adapter->hw);
    /* 给每个 RX 队列分配 DMA 空间,便于网卡硬件接收数据写入其中 */
    for (i = 0; i < adapter->num_rx_queues; i++) {
        struct igb_ring *ring = adapter->rx_ring;
        igb_alloc_rx_buffers(ring, igb_desc_unused(ring));
    }
}

void igb_alloc_rx_buffers(struct igb_ring *rx_ring, u16 cleaned_count)
{
    union e1000_adv_rx_desc *rx_desc;
    struct igb_rx_buffer *bi;
    u16 i = rx_ring->next_to_use;
    u16 bufsz;

    rx_desc = IGB_RX_DESC(rx_ring, i);
    bi = &rx_ring->rx_buffer_info;
    i -= rx_ring->count;
    bufsz = igb_rx_bufsz(rx_ring);

    do {
        /* 申请 DMA 地址(总线地址)空间供网卡写入接收的数据,sync the buffer for use by the device */
        dma_sync_single_range_for_device(rx_ring->dev, bi->dma, bi->page_offset, bufsz, DMA_FROM_DEVICE);
       /* Refresh the desc even if buffer_addrs didn't change
        * because each write-back erases this info.
        */
        rx_desc->read.pkt_addr = cpu_to_le64(bi->dma + bi->page_offset);

        rx_desc++;
        bi++;
        i++;
        if (unlikely(!i)) {
            rx_desc = IGB_RX_DESC(rx_ring, 0);
            bi = rx_ring->rx_buffer_info;
            i -= rx_ring->count;
        }
        /* clear the length for the next_to_use descriptor */
        rx_desc->wb.upper.length = 0;
        cleaned_count--;
    } while (cleaned_count);
}

1.4.3 注册中断函数 通常设备可以采用不同的中断方式:MSI-X、MSI 和 legacy 模式的中断方式。MSI-X 中断是较好的方法,特别是对于支持多 RX 队列的网卡,每个 RX 队列都有其分配的特定硬中断号,可以绑定固定的 CPU 处理。根据设备所支持的中断方式,驱动程序采用最合适的中断方式注册处理函数。 在 igb 驱动中,igb_msix_ring、igb_intr_msi 和 igb_intr 分别是 MSI-X、MSI 和 legacy 模式的中断处理函数。igb 按照 MSI-X -> MSI -> legacy 的顺序尝试注册中断处理函数。igb_request_irq 部分代码如下:

static int igb_request_irq(struct igb_adapter *adapter)
{
    struct net_device *netdev = adapter->netdev;
    struct pci_dev *pdev = adapter->pdev;
    int err = 0;
    /* MSI-X */
    if (adapter->flags & IGB_FLAG_HAS_MSIX) {
        err = igb_request_msix(adapter);
        if (!err)
            goto request_done;
        /* fall back to MSI */
    }
    /* MSI */
    if (adapter->flags & IGB_FLAG_HAS_MSI) {
        err = request_irq(pdev->irq, igb_intr_msi, 0, netdev->name, adapter);
        if (!err)
            goto request_done;
        /* fall back to legacy interrupts */
    }
    /* legacy interrupts */
    err = request_irq(pdev->irq, igb_intr, IRQF_SHARED, netdev->name, adapter);
}

多数情况下网卡驱动会选择 MSI-X 中断方式,调用 igb_request_msix 函数,然后注册 igb_msix_ring 函数为中断处理函数,部分代码如下:

static int igb_request_msix(struct igb_adapter *adapter)
{
    /* 注册 igb_msix_ring 硬中断函数 */
    err = request_irq(adapter->msix_entries[vector].vector, igb_msix_ring, 0, q_vector->name, q_vector);
}

当 NIC 收到数据后发出一个硬件中断信号时,上面注册的中断函数将会执行,具体执行到收包过程再讲。 1.4.4 打开 NAPI NAPI 的核心概念是不采用频繁硬中断的方式读取数据,而是首先采用硬中断唤醒 NAPI 子系统,然后触发软中断,网络子系统处理软中断,然后循环调用 poll_list 中的 NAPI 实例的 poll 函数来循环接收数据包,这样可以防止高频硬中断影响系统的运行效率。当然,NAPI 也有缺陷,系统不能及时接收每一个包,而是多个包一起处理,进而增加了部分数据包的延时。 前面驱动程序介绍了如何将 poll 函数注册到 NAPI 子系统,但是 NAPI 通常会等到设备被打开之后才会开始工作。打开 NAPI 比较简单。在 igb 驱动中,调用 napi_enable 实现。

/* 打开 NAPI */
for (i = 0; i < adapter->num_q_vectors; i++)
    napi_enable(&(adapter->q_vector->napi));

1.4.5 打开硬中断 打开 NIC 硬中断,等待数据包的到来。打开中断是一个硬件操作,igb 驱动通过函数 igb_irq_enable 写寄存器实现。

static void igb_irq_enable(struct igb_adapter *adapter)
{
    struct e1000_hw *hw = &adapter->hw;

    if (adapter->flags & IGB_FLAG_HAS_MSIX) {
        u32 ims = E1000_IMS_LSC | E1000_IMS_DOUTSYNC | E1000_IMS_DRSTA;
        u32 regval = rd32(E1000_EIAC);

        wr32(E1000_EIAC, regval | adapter->eims_enable_mask);
        regval = rd32(E1000_EIAM);
        wr32(E1000_EIAM, regval | adapter->eims_enable_mask);
        wr32(E1000_EIMS, adapter->eims_enable_mask);
        if (adapter->vfs_allocated_count) {
            wr32(E1000_MBVFIMR, 0xFF);
            ims |= E1000_IMS_VMMB;
    }
    wr32(E1000_IMS, ims);
    } else {
        wr32(E1000_IMS, IMS_ENABLE_MASK | E1000_IMS_DRSTA);
        wr32(E1000_IAM, IMS_ENABLE_MASK | E1000_IMS_DRSTA);
    }
}

1.5 软中断 ksoftirqd 内核进程 CPU 在执行硬中断处理函数时可能会短暂的关闭硬中断,中断处理函数处理时间越长,关闭时间越长,丢掉其他硬中断事件的机会就越大。因此,硬中断处理函数处理的事情越少越好,这样可以尽快完成中断处理函数并且重新打开硬中断。 中断处理函数的总工作量不变的情况下,还得减少硬中断的工作量,就引入了软中断,复杂的事情交给软中断来处理。软中断在 ksoftirq 内核进程来处理,与硬中断不在一个层面,其可以被硬中断打断。每个 CPU 负责执行一个 ksoftirq 内核进程,比如 ksoftirqd/0 运行在 CPU 0上,这些内核进程执行不同软中断注册的中断处理函数。内核通过 open_softirq 函数来注册软中断处理函数。 一个重要知识点:执行硬中断的处理函数的 CPU 核心,也会执行该硬中断后续的软中断处理函数,也就是同一中断事件的软/硬中断处理函数会被同一个 CPU 核心执行。 ksoftirqd 内核进程通过 spawn_ksoftirqd 函数初始化,代码如下:

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) {
    cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL, takeover_tasklets);
    BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

    return 0;
}

early_initcall(spawn_ksoftirqd);

1.6 网络子系统 上面讲解了网络驱动和软中断的初始化流程,下面介绍下「网络子系统」的初始化。「网络子系统」通过 net_dev_init 函数进行初始化,部分代码如下:

static int __init net_dev_init(void)
{
    int i, rc = -ENOMEM;
    /* Initialise the packet receive queues. */
    for_each_possible_cpu(i) {
        struct work_struct *flush = per_cpu_ptr(&flush_works, i);
        struct softnet_data *sd = &per_cpu(softnet_data, i);

        INIT_WORK(flush, flush_backlog);

        skb_queue_head_init(&sd->input_pkt_queue);
        skb_queue_head_init(&sd->process_queue);
#ifdef CONFIG_XFRM_OFFLOAD
        skb_queue_head_init(&sd->xfrm_backlog);
#endif
        INIT_LIST_HEAD(&sd->poll_list);
        sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
        /* 注册 IPI 信号的处理函数,然后发出 NET_RX_SOFTIRQ 软中断信号 */
        INIT_CSD(&sd->csd, rps_trigger_softirq, sd);
        sd->cpu = i;
#endif
        INIT_CSD(&sd->defer_csd, trigger_rx_softirq, sd);
        spin_lock_init(&sd->defer_lock);

        init_gro_hash(&sd->backlog);
        /* 软中断中通过调用 backlog(napi_struct)的 poll 处理 cpu 的 sd 的 input_pkt_queue(skb) 队列 */
        sd->backlog.poll = process_backlog;
        /* weight_p 可以调整,网卡的 poll 权重是 hardcode 64 */
        sd->backlog.weight = weight_p;
    }

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

/* 内核通过调用 subsys_initcall 初始化各个子系统 */
subsys_initcall(net_dev_init);

该函数会给每个 CPU 创建一个 softnet_data 结构体,该结构体包含了很多数据,其中包括:
  • 注册到该 CPU 的 NAPI 结构体列表(poll_list);
  • 接收和发送队列;
  • backlog(napi_struct)初始化;
  • RPS 相关的指针;

struct softnet_data {
    struct list_head poll_list;
    struct sk_buff_head process_queue;

    /* stats */
    unsigned int processed;
    unsigned int time_squeeze;
    unsigned int received_rps;
#ifdef CONFIG_RPS
    struct softnet_data *rps_ipi_list;
#endif
#ifdef CONFIG_NET_FLOW_LIMIT
    struct sd_flow_limit __rcu *flow_limit;
#endif
    struct Qdisc *output_queue;
    struct Qdisc **output_queue_tailp;
    struct sk_buff *completion_queue;
#ifdef CONFIG_XFRM_OFFLOAD
    struct sk_buff_head xfrm_backlog;
#endif
    /* written and read only by owning cpu: */
    struct {
        u16 recursion;
        u8 more;
#ifdef CONFIG_NET_EGRESS
        u8 skip_txqueue;
#endif
    } xmit;
#ifdef CONFIG_RPS
    /* input_queue_head should be written by cpu owning this struct,
     * and only read by other cpus. Worth using a cache line.
     */
    unsigned int input_queue_head ____cacheline_aligned_in_smp;

    /* Elements below can be accessed between CPUs for RPS/RFS */
    call_single_data_t csd ____cacheline_aligned_in_smp;
    struct softnet_data *rps_ipi_next;
    unsigned int cpu;
    unsigned int input_queue_tail;
#endif
    unsigned int dropped;
    struct sk_buff_head input_pkt_queue;
    struct napi_struct backlog;

    /* Another possibly contended cache line */
    spinlock_t defer_lock ____cacheline_aligned_in_smp;
    int defer_count;
    int defer_ipi_scheduled;
    struct sk_buff *defer_list;
    call_single_data_t defer_csd;
};

此外,net_dev_init 注册了两个软中断处理函数,分别用 net_rx_action 和 net_tc_action 中断函数处理接收和发送的数据包。

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

open_softirq 函数将软中断类型和处理函数对存在 softirq_vec 里,当有软中断到来时,通过查此表来找对应的中断处理函数,代码如下:

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    /* softirq_vec 是静态变量 */
    softirq_vec[nr].action = action;
}

1.7 协议栈初始化 内核中的fs_initcall和subsys_initcall类似,也是模块初始化的入口。fs_initcall调用 inet_init 完成网络协议栈模块初始化,主要流程有:
  • 将 TCP、UDP 和 ICMP 的接收函数注册到 inet_protos 数组中;
  • 注册 Socket 相关的信息到 inetsw 链表数组中,便于 inet_create 函数创建套接字;
  • 将 IP 的接收函数注册到 ptype_base 哈希表中。
inet_init 部分代码如下:

static int __init inet_init(void) {
    struct inet_protosw *q;
    struct list_head *r;
    int rc;
    sock_skb_cb_check_size(sizeof(struct inet_skb_parm));
    raw_hashinfo_init(&raw_v4_hashinfo);
    /* 注册各种协议的各种处理函数 */
    rc = proto_register(&tcp_prot, 1);
    rc = proto_register(&udp_prot, 1);
    rc = proto_register(&raw_prot, 1);
    rc = proto_register(&ping_prot, 1);
    /* Tell SOCKET that we are alive... */
    (void)sock_register(&inet_family_ops);
#ifdef CONFIG_SYSCTL
    ip_static_sysctl_init();
#endif
    /* 添加所有基础网络协议,eg. 添加到 inet_protos[IPPROTO_ICMP] = icmp_protocol 数组里 */
    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__);
#ifdef CONFIG_IP_MULTICAST
    if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
        pr_crit("%s: Cannot add IGMP protocol\n", __func__);
#endif
    /* Register the socket-side information for inet_create. */
    for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
        INIT_LIST_HEAD(r);
    for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
        inet_register_protosw(q);
    /* 加载 arp 模块 */
    arp_init();
    /* 加载 ip 模块 */
    ip_init();
    /* Initialise per-cpu ipv4 mibs */
    if (init_ipv4_mibs())
        panic("%s: Cannot init ipv4 mibs\n", __func__);
    /* Setup TCP slab cache for open requests. */
    tcp_init();
    /* Setup UDP memory threshold */
    udp_init();
    /* Add UDP-Lite (RFC 3828) */
    udplite4_register();
    /* RAW 类型数据包 */
    raw_init();
    ping_init();
   /* 加载 icmp 模块 */
    if (icmp_init() < 0)
        panic("Failed to create the ICMP control socket.\n");
   /* Initialise the multicast router */
#if defined(CONFIG_IP_MROUTE)
    if (ip_mr_init())
        pr_crit("%s: Cannot init ipv4 mroute\n", __func__);
#endif
    if (init_inet_pernet_ops())
        pr_crit("%s: Cannot init ipv4 inet pernet ops\n", __func__);

    ipv4_proc_init();

    ipfrag_init();
    /* 将 IP 的接收函数 ip_rcv 注册到 ptype_base 列表里 */
    dev_add_pack(&ip_packet_type);

    ip_tunnel_core_init();

    rc = 0;
}
fs_initcall(inet_init);

1.7.1 inet_protos 数组 TCP、UDP 和 ICMP 的net_protocol结构体定义如下,其中有 tcp_v4_rcv、udp_rcv 和 icmp_rcv 函数用来接收数据。

static const struct net_protocol tcp_protocol = {
    .handler = tcp_v4_rcv,
    .err_handler = tcp_v4_err,
    .no_policy = 1,
    .icmp_strict_tag_validation = 1,
};

static const struct net_protocol udp_protocol = {
    .handler = udp_rcv,
    .err_handler = udp_err,
    .no_policy = 1,
};

static const struct net_protocol icmp_protocol = {
    .handler = icmp_rcv,
    .err_handler = icmp_err,
    .no_policy = 1,
};

inet_init 函数中调用 inet_add_protocol 函数将上面三个结构体注册到 inet_protos 数组里:

struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS] __read_mostly;

int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol) {
    return !cmpxchg((const struct net_protocol **)&inet_protos[protocol], NULL, prot) ? 0 : -1;
}

1.7.2 inetsw 链表数组

/* The inetsw table contains everything that inet_create needs to
* build a new socket.
*/
static struct list_head inetsw[SOCK_MAX];

/* Upon startup we insert all the elements in inetsw_array[] into
* the linked list inetsw.
*/
static struct inet_protosw inetsw_array[] = {
    {
        .type = SOCK_STREAM,
        .protocol = IPPROTO_TCP,
        .prot = &tcp_prot,
        .ops = &inet_stream_ops,
        .flags = INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK,
    },

    {
        .type = SOCK_DGRAM,
        .protocol = IPPROTO_UDP,
        .prot = &udp_prot,
        .ops = &inet_dgram_ops,
        .flags = INET_PROTOSW_PERMANENT,
    },

    {
        .type = SOCK_DGRAM,
        .protocol = IPPROTO_ICMP,
        .prot = &ping_prot,
        .ops = &inet_sockraw_ops,
        .flags = INET_PROTOSW_REUSE,
    },

    {
        .type = SOCK_RAW,
        .protocol = IPPROTO_IP, /* wild card */
        .prot = &raw_prot,
        .ops = &inet_sockraw_ops,
        .flags = INET_PROTOSW_REUSE,
    }
};

inet_init 函数遍历所有协议,循环调用 inet_register_protosw 函数将 inetsw_array 数组中各个协议的操作注册到 inetsw 链表数组中,便于 inet_create 函数根据具体协议类型创建套接字。

/* Register the socket-side information for inet_create. */
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
    inet_register_protosw(q);

inet_register_protosw 函数部分代码如下:

void inet_register_protosw(struct inet_protosw *p)
{
    struct list_head *lh;
    struct inet_protosw *answer;
    int protocol = p->protocol;
    struct list_head *last_perm;
    ...
    last_perm = &inetsw[p->type];
    list_for_each(lh, &inetsw[p->type]) {
        answer = list_entry(lh, struct inet_protosw, list);
        /* Check only the non-wild match. */
        if ((INET_PROTOSW_PERMANENT & answer->flags) == 0)
            break;
        if (protocol == answer->protocol)
            goto out_permanent;
        last_perm = lh;
    }
    ...
}

TCP 和 UDP Scoket 使用的ops结构体如下:

const struct proto_ops inet_stream_ops = {
    .family         = PF_INET,
    .owner          = THIS_MODULE,
    .release        = inet_release,
    .bind           = inet_bind,
    .connect        = inet_stream_connect,
    .socketpair     = sock_no_socketpair,
    .accept         = inet_accept,
    .getname        = inet_getname,
    .poll           = tcp_poll,
    .ioctl          = inet_ioctl,
    .gettstamp      = sock_gettstamp,
    .listen         = inet_listen,
    .shutdown       = inet_shutdown,
    .setsockopt     = sock_common_setsockopt,
    .getsockopt     = sock_common_getsockopt,
    .sendmsg        = inet_sendmsg,
    .recvmsg        = inet_recvmsg,// 接收数据
#ifdef CONFIG_MMU
    .mmap           = tcp_mmap,
#endif
    .sendpage       = inet_sendpage,
    .splice_read    = tcp_splice_read,
    .read_sock      = tcp_read_sock,
    .read_skb       = tcp_read_skb,
    .sendmsg_locked = tcp_sendmsg_locked,
    .sendpage_locked= tcp_sendpage_locked,
    .peek_len       = tcp_peek_len,
#ifdef CONFIG_COMPAT
    .compat_ioctl   = inet_compat_ioctl,
#endif
    .set_rcvlowat   = tcp_set_rcvlowat,
};

const struct proto_ops inet_dgram_ops = {
    .family         = PF_INET,
    .owner          = THIS_MODULE,
    .release        = inet_release,
    .bind           = inet_bind,
    .connect        = inet_dgram_connect,
    .socketpair     = sock_no_socketpair,
    .accept         = sock_no_accept,
    .getname        = inet_getname,
    .poll           = udp_poll,
    .ioctl          = inet_ioctl,
    .gettstamp      = sock_gettstamp,
    .listen         = sock_no_listen,
    .shutdown       = inet_shutdown,
    .setsockopt     = sock_common_setsockopt,
    .getsockopt     = sock_common_getsockopt,
    .sendmsg        = inet_sendmsg,
    .read_skb       = udp_read_skb,
    .recvmsg        = inet_recvmsg,// 接收数据
    .mmap           = sock_no_mmap,
    .sendpage       = inet_sendpage,
    .set_peek_off   = sk_set_peek_off,
#ifdef CONFIG_COMPAT
    .compat_ioctl   = inet_compat_ioctl,
#endif
};

TCP 和 UDP Scoket 使用的prot结构体如下:

struct proto tcp_prot = {
    .name           = "TCP",
    .owner          = THIS_MODULE,
    .close          = tcp_close,
    .pre_connect    = tcp_v4_pre_connect,
    .connect        = tcp_v4_connect,
    .disconnect     = tcp_disconnect,
    .accept         = inet_csk_accept,
    .ioctl          = tcp_ioctl,
    .init           = tcp_v4_init_sock,
    .destroy        = tcp_v4_destroy_sock,
    .shutdown       = tcp_shutdown,
    .setsockopt     = tcp_setsockopt,
    .getsockopt     = tcp_getsockopt,
    .bpf_bypass_getsockopt = tcp_bpf_bypass_getsockopt,
    .keepalive      = tcp_set_keepalive,
    .recvmsg        = tcp_recvmsg,// 接收数据
    .sendmsg        = tcp_sendmsg,
    .sendpage       = tcp_sendpage,
    .backlog_rcv    = tcp_v4_do_rcv,
    .release_cb     = tcp_release_cb,
    .hash           = inet_hash,
    .unhash         = inet_unhash,
    .get_port       = inet_csk_get_port,
    .put_port       = inet_put_port,
#ifdef CONFIG_BPF_SYSCALL
    .psock_update_sk_prot   = tcp_bpf_update_proto,
#endif
    .enter_memory_pressure  = tcp_enter_memory_pressure,
    .leave_memory_pressure  = tcp_leave_memory_pressure,
    .stream_memory_free     = tcp_stream_memory_free,
    .sockets_allocated      = &tcp_sockets_allocated,
    .orphan_count           = &tcp_orphan_count,

    .memory_allocated       = &tcp_memory_allocated,
    .per_cpu_fw_alloc       = &tcp_memory_per_cpu_fw_alloc,

    .memory_pressure        = &tcp_memory_pressure,
    .sysctl_mem             = sysctl_tcp_mem,
    .sysctl_wmem_offset     = offsetof(struct net, ipv4.sysctl_tcp_wmem),
    .sysctl_rmem_offset     = offsetof(struct net, ipv4.sysctl_tcp_rmem),
    .max_header             = MAX_TCP_HEADER,
    .obj_size               = sizeof(struct tcp_sock),
    .slab_flags             = SLAB_TYPESAFE_BY_RCU,
    .twsk_prot              = &tcp_timewait_sock_ops,
    .rsk_prot               = &tcp_request_sock_ops,
    .h.hashinfo             = &tcp_hashinfo,
    .no_autobind            = true,
    .diag_destroy           = tcp_abort,
};

struct proto udp_prot = {
    .name           = "UDP",
    .owner          = THIS_MODULE,
    .close          = udp_lib_close,
    .pre_connect    = udp_pre_connect,
    .connect        = ip4_datagram_connect,
    .disconnect     = udp_disconnect,
    .ioctl          = udp_ioctl,
    .init           = udp_init_sock,
    .destroy        = udp_destroy_sock,
    .setsockopt     = udp_setsockopt,
    .getsockopt     = udp_getsockopt,
    .sendmsg        = udp_sendmsg,
    .recvmsg        = udp_recvmsg,// 接收数据
    .sendpage       = udp_sendpage,
    .release_cb     = ip4_datagram_release_cb,
    .hash           = udp_lib_hash,
    .unhash         = udp_lib_unhash,
    .rehash         = udp_v4_rehash,
    .get_port       = udp_v4_get_port,
    .put_port       = udp_lib_unhash,
#ifdef CONFIG_BPF_SYSCALL
    .psock_update_sk_prot = udp_bpf_update_proto,
#endif
    .memory_allocated   = &udp_memory_allocated,
    .per_cpu_fw_alloc   = &udp_memory_per_cpu_fw_alloc,

    .sysctl_mem         = sysctl_udp_mem,
    .sysctl_wmem_offset = offsetof(struct net, ipv4.sysctl_udp_wmem_min),
    .sysctl_rmem_offset = offsetof(struct net, ipv4.sysctl_udp_rmem_min),
    .obj_size = sizeof(struct udp_sock),
    .h.udp_table = &udp_table,
    .diag_destroy = udp_abort,
};

1.7.3 ptype_base 哈希表 后面调用 dev_add_pack 函数将 ip_packet_type 结构体注册到 ptype_base 哈希表中,type 是 ETH_P_IP(0x0800),func 是 ip_rcv 函数。

static struct packet_type ip_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,
    .list_func = ip_list_rcv,
};

extern struct list_head ptype_all            __read_mostly;
struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly;

void dev_add_pack(struct packet_type *pt) {
    struct list_head *head = ptype_head(pt);
    spin_lock(&ptype_lock);
    list_add_rcu(&pt->list, head);
    spin_unlock(&ptype_lock);
}

static inline struct list_head *ptype_head(const struct packet_type *pt) {
    if (pt->type == htons(ETH_P_ALL))
        return pt->dev ? &pt->dev->ptype_all : &ptype_all;
    else
        return pt->dev ? &pt->dev->ptype_specific : &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}

除了 IP 还有下面协议:

$ grep -R dev_add_pack net/{ipv4,packet}/*
net/ipv4/af_inet.c:         dev_add_pack(&ip_packet_type);   //IP
net/ipv4/arp.c:             dev_add_pack(&arp_packet_type);  //ARP
net/ipv4/ipconfig.c:        dev_add_pack(&rarp_packet_type);
net/ipv4/ipconfig.c:        dev_add_pack(&bootp_packet_type);
net/packet/af_packet.c:     dev_add_pack(&po->prot_hook);    //用于抓包
net/packet/af_packet.c:     dev_add_pack(&f->prot_hook);     //用于抓包

包的类型可以通过下面命令查看:

$ cat /proc/net/ptype     # packet type (skb->protocol)
Type Device      Function
0800             ip_rcv
0806             arp_rcv
86dd             ipv6_rcv

1.7.4 小结 好了,inet_protos 存储着 TCP、UDP 和 ICMP 接收数据的 udp_rcv 和 icmp_rcv 函数地址,ptype_base 存储着接收数据的 ip_rcv 函数地址。后面会看到软中断中会通过 ptype_base 找到 ip_rcv 函数地址,进而将 IP 包正确地送到 ip_rcv 中执行。在 ip_rcv 中将会通过 inet_protos 找到 TCP 或者 UDP 的处理函数,再而把包转发给 tcp_v4_rcv 或者 udp_rcv 函数。 ip_rcv、tcp_v4_rcv、udp_rcv 和 icmp_rcv 函数已经注册好了,就等待数据包的到来。最后通过 inet_create 函数根据具体协议类型和 inetsw 链表数组创建套接字来完成接收数据。 二、网络收包概述 前面主要介绍了系统启动时的初始化操作,接下来开始正式介绍网络的详细收包过程,从网络接口层(L1)、网络层(L2)、传输层(L3)、套接字(L3.5)再到应用层(L4)的整个过程。

 

图2 整体流程图
从硬中断到协议栈的调用链:    

 

                 

 

标签:struct,rx,网络,tcp,init,igb,讲解,inet,收包
From: https://www.cnblogs.com/yipianchuyun/p/17997315

相关文章

  • 收包
     整体流程图三、网络接口层3.1概述数据包在本层主要处理流程有五:网卡收到数据包,DMA方式写入RingBuffer,发出硬中断;内核收到硬中断,NAPI加入本CPU的轮询列表,发出软中断;内核收到软中断,轮询NAPI并执行poll函数从RingBuffer取数据;GRO操作(默认开启),合并多个数......
  • 行业认可!Coremail入选CCSIP2023中国网络安全行业全景册(第六版)
    2024年1月24日,FreeBuf咨询正式发布《CCSIP(ChinaCyberSecurityIndustryPanorama)2023中国网络安全行业全景册(第六版)》,旨在为企业安全建设及产品选型提供参考。Coremail凭借多年的专业技术积累和产品创新能力,入选该全景册的“邮件安全”、“恶意内容检测”、“钓鱼检测”、“数据......
  • 使用Java编写HTTP客户端和服务器:一场与网络的欢乐共舞
    你是否曾经想过,如果有一天你可以和网络对话,那会是怎样的场景?好消息,Java给了你这个机会!今天,我们要一起探讨如何使用Java编写HTTP客户端和服务器,让你和网络的互动变得更加有趣和欢乐。首先,我们需要了解HTTP是什么。简单来说,HTTP就是“超文本传输协议”,它就像是我们与网络交流的语言。......
  • 卷积神经网络理解(3)
    1、定义LeNet是深度学习领域的一个经典卷积神经网络模型,由YannLeCun等人于1998年提出,被广泛应用于手写数字识别和其他图像识别任务。LeNet的网络结构相对简单,包含两个卷积层和三个全连接层,是卷积神经网络的基础。LeNet对于现代的图像识别任务来说可能过于简单,但其对于深度学习......
  • 卷积神经网络理解(二)
    1、卷积神经网络的特点卷积神经网络相对于普通神经网络在于以下四个特点:局部感知域:CNN的神经元只与输入数据的一小部分区域相连接,这使得CNN对数据的局部结构具有强大的敏感性,可以自动学习到图像的特征。参数共享:在CNN中,同一个卷积核(filter)在整个输入图像上滑动,共享权重和偏置......
  • 网络管理 SNMP Qos
    一、网络管理基础网络管理五大功能:故障管理、配置管理、计费管理、性能管理、安全管理;关键词:安配能计障。故障管理的目的:尽快发现故障,找出故障原因,以便采取补救措施。网管系统中代理与监视器两种通信方式:轮询和事件报告。 二、网络管理系统的组成网络管理......
  • 【面试突击】计算机网络面试实战(下)
    欢迎关注公众号【11来了】,及时收到AI前沿项目工具及新技术的推送!在我后台回复「资料」可领取编程高频电子书!在我后台回复「面试」可领取硬核面试笔记!Https的工作原理Http的内容是明文传输的,铭文数据经过中间代理服务器、路由器、wifi热点等多个物理节点,如果被劫持会导致传输......
  • 网络流最大流
    最大流问题有向图G中,有两个特殊的点,源点和汇点,每条边有指定的容量,求S到T的最大流。就像从源点放水,水量无穷大,汇点的水量是多少?定义c为容量,f为流量流量守恒\(f(x,y)\leqc(x,y)\)容量性质\(\sumf(u,x)=\sumf(x,u)\)斜对称性\(f(x,y)=-f(y,x)\)容量网络,流量网络......
  • 常见的八种网络攻击和防范措施
    在数字化时代,网络安全成为了一个重要的议题。保护个人和组织的网络安全是至关重要的。网络攻击又是一种针对我们日常使用的计算机或信息系统的行为,其目的是篡改、破坏我们的数据,甚至直接窃取,或者利用我们的网络进行不法行为。你可能已经注意到,随着我们生活中越来越多的业务进行数字......
  • Cisco Catalyst Center 2.3.7.4-VA - 网络管理和自动化
    CiscoCatalystCenter2.3.7.4-VA-网络管理和自动化CiscoCatalystCenter-NetworkManagementandAutomation请访问原文链接:https://sysin.org/blog/cisco-catalyst-center/,查看最新版。原创作品,转载请保留出处。作者主页:sysin.orgCiscoCatalystCenter节约时间,不再......