内核是如何接收网络包的

news2024/11/13 9:52:53

1、数据如何从网卡到网络协议栈

1.1内核收包的过程

1、数据帧从外部网络到达网卡
2、网卡把数据帧从自己的缓存DMA(拷贝到)和内核共有的RingBuffer上
3、网卡发出硬中断通知CPU
4、CPU响应硬中断,简单处理后发出软中断
5、k’softirqd线程处理软中断,调用网卡驱动注册的poll函数开始收包
6、帧被从RingBuffer上摘下来被保存为一个skb
7、协议层开始处理网络帧,处理玩后的数据data被放到socket的接收队列中
8、内核唤醒用户进程

1.2 ksoftirqd内核线程的创建

Linux的软中断都是在专门的内核线程(ksoftirqd)中进行的。
该线程的数量等于设备的核数。

系统初始化的时候在kernel/smpboot.c中调用smpboot_register_percpu_thread,该函数进一步会执行到spawn_ksoftirqd(位于kernel/softirq.c)来创建出softirqd线程。

相关代码如下:

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

当ksoftirqd被创建出来以后,就会进入线程循环函数ksoftirqd_should_run和run_ksoftirqd了。接下来判断有没有软中断需要处理。软中断不只有网络软中断,还有其他类型。Linux内核在interrupt.h定义了所有类型的软中断类型:

enum {
    HI_SOFTIRQ = 0,         /* 高优先级软中断,用于紧急任务 */
    TIMER_SOFTIRQ,          /* 定时器软中断,处理定时器事件 */
    NET_TX_SOFTIRQ,         /* 网络传输软中断,处理网络数据包发送 */
    NET_RX_SOFTIRQ,         /* 网络接收软中断,处理网络数据包接收 */
    BLOCK_SOFTIRQ,          /* 块设备软中断,处理块设备I/O操作 */
    IRQ_POLL_SOFTIRQ,       /* IRQ轮询软中断,用于轮询模式下的硬件中断处理 */
    TASKLET_SOFTIRQ,        /* 任务队列软中断,执行任务队列中的工作 */
    SCHED_SOFTIRQ,          /* 调度软中断,处理调度器相关的工作 */
    HRTIMER_SOFTIRQ,        /* 高分辨率定时器软中断(未使用) */
    RCU_SOFTIRQ,            /* 读-复制更新(RCU)软中断,处理RCU更新和同步 */

    NR_SOFTIRQS            /* 软中断的数量 */
};

1.3 网络子系统初始化

在网络子系统的初始化过程中,会为每个CPU初始化softnet_data,也会为RX_SOFTIRQ和TX_SOFTIRQ注册处理函数,流程如下:
1、调用subsys_initcall(net_dev_init)(net/core/dev.c)
2、为每个CPU初始化softnet_data //include.linux/netdevice.h
3、将NET_RX_SOFTIRQ的处理函数注册为net_rx_action,将NET_TX_SOFTIRQ的处理函数注册为net_tx_action
4、将软中断与处理函数的对应关系记录到soft_irq_vec数组

网络子系统初始化会执行net_dev_init函数,在这个函数里,会为每个CPU都申请一个softnet_data数据结构,这个数据结构里的poll_list用于等待驱动程序将其poll函数注册进来;另外open_softirq为每一种软中断都注册一个处理函数,继续跟踪open_softirq后发现这个注册的方式是记录在softirq_vec变量里的,后面ksoftirqd线程收到软中断的时候,也会使用这个变量查找每一种软中断对应的处理函数。

static int __init net_dev_init(void) {
    int i, rc = -ENOMEM;  // 初始化返回码为 -ENOMEM,表示内存不足错误

    BUG_ON(!dev_boot_phase);  // 确保设备启动阶段标志已设置

    // 初始化 /proc/net 设备文件
    if (dev_proc_init())
        goto out;

    // 初始化网络设备相关的 kobject
    if (netdev_kobject_init())
        goto out;

    INIT_LIST_HEAD(&ptype_all);  // 初始化协议类型列表头
    for (i = 0; i < PTYPE_HASH_SIZE; i++) {
        INIT_LIST_HEAD(&ptype_base[i]);  // 初始化协议类型哈希表
    }

    INIT_LIST_HEAD(&offload_base);  // 初始化 offload 列表头

    // 注册网络设备相关的 per-net 子系统
    if (register_pernet_subsys(&netdev_net_ops))
        goto out;

    /*
     * 初始化数据包接收队列。
     */
    for_each_possible_cpu(i) {  // 遍历所有可能的 CPU
        struct work_struct *flush = per_cpu_ptr(&flush_works, i);
        struct softnet_data *sd = &per_cpu(softnet_data, i);

        INIT_WORK(flush, flush_backlog);  // 初始化 backlog 刷新工作

        skb_queue_head_init(&sd->input_pkt_queue);  // 初始化输入数据包队列
        skb_queue_head_init(&sd->process_queue);  // 初始化处理数据包队列
        INIT_LIST_HEAD(&sd->poll_list);  // 初始化轮询列表
        sd->output_queue_tailp = &sd->output_queue;  // 设置输出队列尾指针

#ifdef CONFIG_RPS
        sd->csd.func = rps_trigger_softirq;  // 设置 RPS 触发函数
        sd->csd.info = sd;  // 设置 RPS 信息指针
        sd->cpu = i;  // 设置 CPU 编号
#endif

        sd->backlog.poll = process_backlog;  // 设置 backlog 处理函数
        sd->backlog.weight = weight_p;  // 设置 backlog 权重
    }

    dev_boot_phase = 0;  // 清除设备启动阶段标志

    /*
     * 如果网络命名空间中有其他网络设备,
     * 则环回设备必须存在。
     * 由于我们现在动态分配和释放环回设备,
     * 确保通过将环回设备作为网络设备列表中的第一个设备来维护此不变式。
     * 确保环回设备是第一个出现的设备,并且是最后一个消失的网络设备。
     */
    if (register_pernet_device(&loopback_net_ops))
        goto out;

    if (register_pernet_device(&default_device_ops))
        goto out;

    open_softirq(NET_TX_SOFTIRQ, net_tx_action);  // 打开网络传输软中断
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);  // 打开网络接收软中断

    rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead", NULL, dev_cpu_dead);
    WARN_ON(rc < 0);  // 警告如果 CPUHP 状态设置失败

    dst_subsys_init();  // 初始化目的地子系统
    rc = 0;  // 设置返回码为 0,表示成功
out:
    return rc;  // 返回初始化结果
}
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

1.4 协议栈注册

内核实现了网络层的IP协议,也实现了传输层的TCP协议和UDP协议,这些协议对应的实现函数分别是ip_rcv()、tcp_v4_rcv()和udp_rcv。fs_initcall调用inet_init后开始网络协议栈注册,通过inet_init将这些函数注册到inet_protos和ptype_base数据结构中。
相关代码如下:

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

struct proto tcp_prot = {
    .name                  = "TCP",
    .owner                 = THIS_MODULE,
    .close                 = tcp_close, // net/ipv4/tcp.c
    .connect               = tcp_v4_connect, // net/ipv4/tcp.c
    .disconnect            = tcp_disconnect, // net/ipv4/tcp.c
    .accept                = inet_csk_accept, // net/ipv4/inet_connection_sock.c
    .ioctl                 = tcp_ioctl, // net/ipv4/tcp.c
    .init                  = tcp_v4_init_sock, // net/ipv4/tcp.c
    .destroy               = tcp_v4_destroy_sock, // net/ipv4/tcp.c
    .shutdown              = tcp_shutdown, // net/ipv4/tcp.c
    .setsockopt            = tcp_setsockopt, // net/ipv4/tcp.c
    .getsockopt            = tcp_getsockopt, // net/ipv4/tcp.c
    .keepalive             = tcp_set_keepalive, // net/ipv4/tcp.c
    .recvmsg               = tcp_recvmsg, // net/ipv4/tcp.c
    .sendmsg               = tcp_sendmsg, // net/ipv4/tcp.c
    .sendpage              = tcp_sendpage, // net/ipv4/tcp.c
    .backlog_rcv           = tcp_v4_do_rcv, // net/ipv4/tcp.c
    .release_cb            = tcp_release_cb, // net/ipv4/tcp.c
    .hash                  = inet_hash, // net/ipv4/inet_hashtables.c
    .unhash                = inet_unhash, // net/ipv4/inet_hashtables.c
    .get_port              = inet_csk_get_port, // net/ipv4/inet_hashtables.c
    .enter_memory_pressure  = tcp_enter_memory_pressure, // net/ipv4/tcp.c
    .stream_memory_free     = tcp_stream_memory_free, // net/ipv4/tcp.c
    .sockets_allocated     = &tcp_sockets_allocated, // net/ipv4/tcp.c
    .orphan_count           = &tcp_orphan_count, // net/ipv4/tcp.c
    .memory_allocated      = &tcp_memory_allocated, // net/ipv4/tcp.c
    .memory_pressure        = &tcp_memory_pressure, // net/ipv4/tcp.c
    .sysctl_mem            = sysctl_tcp_mem, // net/ipv4/tcp_sysctl.c
    .sysctl_wmem           = sysctl_tcp_wmem, // net/ipv4/tcp_sysctl.c
    .sysctl_rmem           = sysctl_tcp_rmem, // net/ipv4/tcp_sysctl.c
    .max_header            = MAX_TCP_HEADER,
    .obj_size              = sizeof(struct tcp_sock), // net/ipv4/tcp.h
    .slab_flags            = SLAB_TYPESAFE_BY_RCU,
    .twsk_prot             = &tcp_timewait_sock_ops, // net/ipv4/tcp.c
    .rsk_prot              = &tcp_request_sock_ops, // net/ipv4/tcp.c
    .h.hashinfo            = &tcp_hashinfo, // net/ipv4/tcp_hash.c
    .no_autobind           = true,
#ifdef CONFIG_COMPAT
    .compat_setsockopt      = compat_tcp_setsockopt, // net/ipv4/tcp_compat.c
    .compat_getsockopt      = compat_tcp_getsockopt, // net/ipv4/tcp_compat.c
#endif
    .diag_destroy           = tcp_abort, // net/ipv4/tcp_diag.c
};

struct proto udp_prot = {
    .name                  = "UDP",
    .owner                 = THIS_MODULE,
    .close                 = udp_lib_close,         // net/ipv4/udp.c
    .connect               = ip4_datagram_connect,  // net/ipv4/ip_input.c
    .disconnect            = udp_disconnect,        // net/ipv4/udp.c
    .ioctl                 = udp_ioctl,            // net/ipv4/udp.c
    .init                  = udp_init_sock,         // net/ipv4/udp.c
    .destroy               = udp_destroy_sock,      // net/ipv4/udp.c
    .setsockopt            = udp_setsockopt,       // net/ipv4/udp.c
    .getsockopt            = udp_getsockopt,       // net/ipv4/udp.c
    .sendmsg               = udp_sendmsg,          // net/ipv4/udp.c
    .recvmsg               = udp_recvmsg,          // net/ipv4/udp.c
    .sendpage              = udp_sendpage,         // net/ipv4/udp.c
    .release_cb            = ip4_datagram_release_cb, // net/ipv4/datagram.c
    .hash                  = udp_lib_hash,          // net/ipv4/udp.c
    .unhash                = udp_lib_unhash,       // net/ipv4/udp.c
    .rehash                = udp_v4_rehash,        // net/ipv4/udp.c
    .get_port              = udp_v4_get_port,       // net/ipv4/udp.c
    .memory_allocated      = &udp_memory_allocated, // net/ipv4/udp.c
    .sysctl_mem            = sysctl_udp_mem,       // net/ipv4/udp_sysctl.c
    .sysctl_wmem           = &sysctl_udp_wmem_min,  // net/ipv4/udp_sysctl.c
    .sysctl_rmem           = &sysctl_udp_rmem_min,  // net/ipv4/udp_sysctl.c
    .obj_size              = sizeof(struct udp_sock), // net/ipv4/udp.h
    .h.udp_table           = &udp_table,           // net/ipv4/udp.c
#ifdef CONFIG_COMPAT
    .compat_setsockopt      = compat_udp_setsockopt, // net/ipv4/udp_compat.c
    .compat_getsockopt      = compat_udp_getsockopt, // net/ipv4/udp_compat.c
#endif
    .diag_destroy           = udp_abort,            // net/ipv4/udp_diag.c
};
struct proto raw_prot = {
	.name		   = "RAW",
	.owner		   = THIS_MODULE,
	.close		   = raw_close,
	.destroy	   = raw_destroy,
	.connect	   = ip4_datagram_connect,
	.disconnect	   = __udp_disconnect,
	.ioctl		   = raw_ioctl,
	.init		   = raw_init,
	.setsockopt	   = raw_setsockopt,
	.getsockopt	   = raw_getsockopt,
	.sendmsg	   = raw_sendmsg,
	.recvmsg	   = raw_recvmsg,
	.bind		   = raw_bind,
	.backlog_rcv	   = raw_rcv_skb,
	.release_cb	   = ip4_datagram_release_cb,
	.hash		   = raw_hash_sk,
	.unhash		   = raw_unhash_sk,
	.obj_size	   = sizeof(struct raw_sock),
	.h.raw_hash	   = &raw_v4_hashinfo,
#ifdef CONFIG_COMPAT
	.compat_setsockopt = compat_raw_setsockopt,
	.compat_getsockopt = compat_raw_getsockopt,
	.compat_ioctl	   = compat_raw_ioctl,
#endif
	.diag_destroy	   = raw_abort,
};
struct proto ping_prot = {
    .name                  = "PING",
    .owner                 = THIS_MODULE,
    .init                  = ping_init_sock,    // net/ipv4/icmp.c
    .close                 = ping_close,        // net/ipv4/icmp.c
    .connect               = ip4_datagram_connect, // net/ipv4/ip_input.c
    .disconnect            = __udp_disconnect,   // net/ipv4/udp.c
    .setsockopt            = ip_setsockopt,     // net/ipv4/ip_options.c
    .getsockopt            = ip_getsockopt,     // net/ipv4/ip_options.c
    .sendmsg               = ping_v4_sendmsg,  // net/ipv4/icmp.c
    .recvmsg               = ping_recvmsg,      // net/ipv4/icmp.c
    .bind                  = ping_bind,         // net/ipv4/icmp.c
    .backlog_rcv           = ping_queue_rcv_skb, // net/ipv4/icmp.c
    .release_cb            = ip4_datagram_release_cb, // net/ipv4/datagram.c
    .hash                  = ping_hash,         // net/ipv4/icmp.c
    .unhash                = ping_unhash,      // net/ipv4/icmp.c
    .get_port              = ping_get_port,     // net/ipv4/icmp.c
    .obj_size              = sizeof(struct inet_sock), // include/linux/inet.h
};
static const struct net_proto_family inet_family_ops = {
	.family = PF_INET,
	.create = inet_create,//socket创建函数
	.owner	= THIS_MODULE,
};

static int __init inet_init(void) {
    struct inet_protosw *q; // 定义一个指针,用于遍历所有的 inet_protosw 结构,用于为IP协议注册套接字接口
    struct list_head *r;    // 定义一个指针,用于遍历所有的 inetsw 数组
    int rc = -EINVAL;       // 初始化返回码为 -EINVAL,表示无效参数错误

    // 检查 sock_skb_cb 结构体的大小是否合适
    sock_skb_cb_check_size(sizeof(struct inet_skb_parm));

    // 注册 TCP 协议
    rc = proto_register(&tcp_prot, 1);
    if (rc)
        goto out; // 如果注册失败,跳转到 out 标签进行清理

    // 注册 UDP 协议
    rc = proto_register(&udp_prot, 1);
    if (rc)
        goto out_unregister_tcp_proto; // 如果注册失败,跳转到 out_unregister_tcp_proto 标签进行清理
    // 注册 RAW 协议
    rc = proto_register(&raw_prot, 1);
    if (rc)
        goto out_unregister_udp_proto; // 如果注册失败,跳转到 out_unregister_udp_proto 标签进行清理
    // 注册 ICMP 协议
    rc = proto_register(&ping_prot, 1);
    if (rc)
        goto out_unregister_raw_proto; // 如果注册失败,跳转到 out_unregister_raw_proto 标签进行清理
    // 告诉 SOCKET 子系统我们的存在
    (void)sock_register(&inet_family_ops);

#ifdef CONFIG_SYSCTL
    // 初始化 IP 相关的 sysctl 接口
    ip_static_sysctl_init();
#endif

    // 添加所有基础协议
    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

    // 注册 socket 层的协议交换结构
    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();
    // 初始化 TCP 模块
    tcp_init();
    // 初始化 UDP 内存阈值
    udp_init();
    // 注册 UDP-Lite 协议
    udplite4_register();
    // 初始化 ICMP 模块
    ping_init();

    // 初始化多播路由器
#if defined(CONFIG_IP_MROUTE)
    if (ip_mr_init())
        pr_crit("%s: Cannot init ipv4 mroute\n", __func__);
#endif
    // 初始化 per-net 操作
    if (init_inet_pernet_ops())
        pr_crit("%s: Cannot init ipv4 inet pernet ops\n", __func__);
    // 初始化 IPv4 MIB
    if (init_ipv4_mibs())
        pr_crit("%s: Cannot init ipv4 mibs\n", __func__);
    // 初始化 IPv4 proc 文件系统
    ipv4_proc_init();
    // 初始化 IP 分片处理
    ipfrag_init();
    // 注册 IP 包协议类型
    dev_add_pack(&ip_packet_type);
    // 初始化 IP 隧道核心模块
    ip_tunnel_core_init();
}

inet_add_protocol函数将TCP和UDP对应的处理函数都注册到inet_protos数组中。ip_pack_type结构体中的Type是协议名,func是ip_rcv函数,它们在dev_add_pack中会注册到ptype_base哈希表中。软中断根据ptype_base找到ip_rcv函数地址进而将IP包正确地送到ip_rcv中执行。在ip_rcv中将会通过inet_protos找到TCP或者UDP的处理函数,再把包转发给udp_rcv或tcp_rcv。

1.5 网卡驱动初始化

每一个驱动程序会使用module_init向内核注册一个初始化函数,当驱动程序被加载时,内核就会调用这个函数;比如igb网卡驱动程序位于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,
#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 - version %s\n", igb_driver_string, igb_driver_version);
    // 打印驱动程序的版权信息
    pr_info("%s\n", igb_copyright);

    // 如果配置了 DCA(Direct Cache Access)支持,则注册 DCA 通知
    #ifdef CONFIG_IGB_DCA
    dca_register_notify(&dca_notifier);
    #endif

    // 注册 PCI 驱动程序
    ret = pci_register_driver(&igb_driver);
    // 返回注册操作的结果
    return ret;
}

驱动的pci_register_driver调用完成后,Linux内核就知道了该驱动的相关信息。当网卡设备被识别之后,内核会调用其驱动的probe方法让设备处于ready状态。对于igb网卡,其igb_probe位于drivers/net/ethernet/inetl/igb_main.c下。该函数主要执行操作如下:
1、启动注册到内核
2、调用网卡驱动Probe
3、获取网卡MAC
4、DMA初始化
5、注册ethtool实现函数
6、注册net_device_ops、netdev等变量
7、NAPI初始化,注册poll函数

drivers/net/ethernet/inetl/igb_main.c
static const struct net_device_ops igb_netdev_ops = {
	.ndo_open               = igb_open,             // 打开网络设备
	.ndo_stop               = igb_close,            // 停止网络设备
	.ndo_start_xmit         = igb_xmit_frame,       // 发送数据包
	.ndo_get_stats64        = igb_get_stats64,      // 获取网络设备状态统计信息
	.ndo_set_rx_mode        = igb_set_rx_mode,      // 设置接收模式
	.ndo_set_mac_address    = igb_set_mac,          // 设置MAC地址
	.ndo_change_mtu         = igb_change_mtu,       // 更改MTU大小
	.ndo_do_ioctl           = igb_ioctl,            // 处理控制命令
	.ndo_tx_timeout         = igb_tx_timeout,       // 处理发送超时
	.ndo_validate_addr      = eth_validate_addr,     // 验证MAC地址
	.ndo_vlan_rx_add_vid    = igb_vlan_rx_add_vid,  // 添加VLAN ID
	.ndo_vlan_rx_kill_vid   = igb_vlan_rx_kill_vid, // 移除VLAN ID
	.ndo_set_vf_mac         = igb_ndo_set_vf_mac,   // 设置虚拟功能的MAC地址
	.ndo_set_vf_vlan       = igb_ndo_set_vf_vlan,  // 设置虚拟功能的VLAN标签
	.ndo_set_vf_rate       = igb_ndo_set_vf_bw,   // 设置虚拟功能的带宽限制
	.ndo_set_vf_spoofchk    = igb_ndo_set_vf_spoofchk, // 设置虚拟功能的MAC地址欺骗检查
	.ndo_get_vf_config     = igb_ndo_get_vf_config, // 获取虚拟功能的配置信息
#ifdef CONFIG_NET_POLL_CONTROLLER
	.ndo_poll_controller    = igb_netpoll,         // 网络控制器轮询模式
#endif
	.ndo_fix_features      = igb_fix_features,     // 修正网络设备功能特性
	.ndo_set_features      = igb_set_features,     // 设置网络设备功能特性
	.ndo_fdb_add            = igb_ndo_fdb_add,      // 向FDB添加MAC地址
	.ndo_features_check    = igb_features_check,   // 检查网络设备功能特性
};

第7步在igb_probe初始化过程中,还调用到了igb_alloc_q_vector。它注册了一个NAPI机制必需的poll函数。
igb_probe->igb_sw_init->igb_init_interrupt_scheme->igb_alloc_q_vectors->igb_alloc_q_vector->
netif_napi_add

static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
	...
	/* setup the private structure */
	err = igb_sw_init(adapter);
	...
}
static int igb_sw_init(struct igb_adapter *adapter)
{
	...
	/* set default ring sizes */
	adapter->tx_ring_count = IGB_DEFAULT_TXD;//256
	adapter->rx_ring_count = IGB_DEFAULT_RXD;//256


	adapter->max_frame_size = netdev->mtu + ETH_HLEN + ETH_FCS_LEN +
				  VLAN_HLEN;//mtu+14+4+4
	adapter->min_frame_size = ETH_ZLEN + ETH_FCS_LEN;//60+4

	/* This call may decrease the number of queues */
	if (igb_init_interrupt_scheme(adapter, true)) {
		dev_err(&pdev->dev, "Unable to allocate memory for queues\n");
		return -ENOMEM;
	}
	...
	return 0;
}

static int igb_init_interrupt_scheme(struct igb_adapter *adapter, bool msix)
{
	struct pci_dev *pdev = adapter->pdev; // 获取PCI设备指针
	int err;

	// 根据 msix 参数设置中断类型(MSI-X 或者 非 MSI-X)
	igb_set_interrupt_capability(adapter, msix);

	// 分配队列向量
	err = igb_alloc_q_vectors(adapter);
	if (err) {
		// 如果分配失败,打印错误信息并跳转到错误处理代码
		dev_err(&pdev->dev, "Unable to allocate memory for vectors\n");
		goto err_alloc_q_vectors;
	}

	// 缓存环形注册,这可能涉及到将环形缓冲区的地址缓存到硬件寄存器
	igb_cache_ring_register(adapter);

	return 0; // 成功返回 0

err_alloc_q_vectors:
	// 在错误处理代码中,重置中断能力,释放已分配的资源
	igb_reset_interrupt_capability(adapter);
	return err; // 返回错误代码
}

static int igb_alloc_q_vectors(struct igb_adapter *adapter)
{
	int q_vectors = adapter->num_q_vectors;
	int rxr_remaining = adapter->num_rx_queues;
	int txr_remaining = adapter->num_tx_queues;
	int rxr_idx = 0, txr_idx = 0, v_idx = 0;
	int err;

	if (q_vectors >= (rxr_remaining + txr_remaining)) {
		for (; rxr_remaining; v_idx++) {
			err = igb_alloc_q_vector(adapter, q_vectors, v_idx,
						 0, 0, 1, rxr_idx);

			if (err)
				goto err_out;

			/* update counts and index */
			rxr_remaining--;
			rxr_idx++;
		}
	}

	for (; v_idx < q_vectors; v_idx++) {
		int rqpv = DIV_ROUND_UP(rxr_remaining, q_vectors - v_idx);
		int tqpv = DIV_ROUND_UP(txr_remaining, q_vectors - v_idx);

		err = igb_alloc_q_vector(adapter, q_vectors, v_idx,
					 tqpv, txr_idx, rqpv, rxr_idx);

		if (err)
			goto err_out;

		/* update counts and index */
		rxr_remaining -= rqpv;
		txr_remaining -= tqpv;
		rxr_idx++;
		txr_idx++;
	}
	...
}

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)
{
	struct igb_q_vector *q_vector;
	struct igb_ring *ring;
	int ring_count, size;

	/* igb only supports 1 Tx and/or 1 Rx queue per vector */
	if (txr_count > 1 || rxr_count > 1)
		return -ENOMEM;

	ring_count = txr_count + rxr_count;
	size = sizeof(struct igb_q_vector) +
	       (sizeof(struct igb_ring) * ring_count);

	/* allocate q_vector and rings */
	q_vector = adapter->q_vector[v_idx];
	if (!q_vector) {
		q_vector = kzalloc(size, GFP_KERNEL);
	} else if (size > ksize(q_vector)) {
		kfree_rcu(q_vector, rcu);
		q_vector = kzalloc(size, GFP_KERNEL);
	} else {
		memset(q_vector, 0, size);
	}
	if (!q_vector)
		return -ENOMEM;

	/* initialize NAPI */
	netif_napi_add(adapter->netdev, &q_vector->napi,
		       igb_poll, 64);

	/* tie q_vector and adapter together */
	adapter->q_vector[v_idx] = q_vector;
	q_vector->adapter = adapter;

	/* initialize work limits */
	q_vector->tx.work_limit = adapter->tx_work_limit;

	/* initialize ITR configuration */
	q_vector->itr_register = adapter->io_addr + E1000_EITR(0);
	q_vector->itr_val = IGB_START_ITR;

	/* initialize pointer to rings */
	ring = q_vector->ring;

	/* intialize ITR */
	if (rxr_count) {
		/* rx or rx/tx vector */
		if (!adapter->rx_itr_setting || adapter->rx_itr_setting > 3)
			q_vector->itr_val = adapter->rx_itr_setting;
	} else {
		/* tx only vector */
		if (!adapter->tx_itr_setting || adapter->tx_itr_setting > 3)
			q_vector->itr_val = adapter->tx_itr_setting;
	}

	if (txr_count) {
		/* assign generic ring traits */
		ring->dev = &adapter->pdev->dev;
		ring->netdev = adapter->netdev;

		/* configure backlink on ring */
		ring->q_vector = q_vector;

		/* update q_vector Tx values */
		igb_add_ring(ring, &q_vector->tx);

		/* For 82575, context index must be unique per ring. */
		if (adapter->hw.mac.type == e1000_82575)
			set_bit(IGB_RING_FLAG_TX_CTX_IDX, &ring->flags);

		/* apply Tx specific ring traits */
		ring->count = adapter->tx_ring_count;
		ring->queue_index = txr_idx;

		u64_stats_init(&ring->tx_syncp);
		u64_stats_init(&ring->tx_syncp2);

		/* assign ring to adapter */
		adapter->tx_ring[txr_idx] = ring;

		/* push pointer to next ring */
		ring++;
	}

	if (rxr_count) {
		/* assign generic ring traits */
		ring->dev = &adapter->pdev->dev;
		ring->netdev = adapter->netdev;

		/* configure backlink on ring */
		ring->q_vector = q_vector;

		/* update q_vector Rx values */
		igb_add_ring(ring, &q_vector->rx);

		/* set flag indicating ring supports SCTP checksum offload */
		if (adapter->hw.mac.type >= e1000_82576)
			set_bit(IGB_RING_FLAG_RX_SCTP_CSUM, &ring->flags);

		/* On i350, i354, i210, and i211, loopback VLAN packets
		 * have the tag byte-swapped.
		 */
		if (adapter->hw.mac.type >= e1000_i350)
			set_bit(IGB_RING_FLAG_RX_LB_VLAN_BSWAP, &ring->flags);

		/* apply Rx specific ring traits */
		ring->count = adapter->rx_ring_count;
		ring->queue_index = rxr_idx;

		u64_stats_init(&ring->rx_syncp);

		/* assign ring to adapter */
		adapter->rx_ring[rxr_idx] = ring;
	}

	return 0;
}

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)
{
	...
	// 初始化 NAPI
	netif_napi_add(adapter->netdev, &q_vector->napi,
		       igb_poll, 64);
	...
}

1.5.1 启动网卡

当上面都初始化完成之后,就可以启动网卡。
1、启动网卡
2、调用net_device_ops中注册的open函数,如igb_open
3、分配Rx、Tx队列内存
4、注册中断处理函数
5、打开硬中断,等待包进来

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

static int __igb_open(struct net_device *netdev, bool resuming)
{
	//分配传输描述符数组
	/* allocate transmit descriptors */
	err = igb_setup_all_tx_resources(adapter);
	if (err)
		goto err_setup_tx;
	
	//分配接收描述符数组
	/* allocate receive descriptors */
	err = igb_setup_all_rx_resources(adapter);

	//注册中断处理函数
	err = igb_request_irq(adapter);
	if (err)
		goto err_req_irq;
	
	/* 通知栈实际的队列计数 */
	/* Notify the stack of the actual queue counts. */
	err = netif_set_real_num_tx_queues(adapter->netdev,
					   adapter->num_tx_queues);
	if (err)
		goto err_set_queues;

	err = netif_set_real_num_rx_queues(adapter->netdev,
					   adapter->num_rx_queues);

	//启用NAPI
	for (i = 0; i < adapter->num_q_vectors; i++)
		napi_enable(&(adapter->q_vector[i]->napi));

	//启动中断
	igb_irq_enable(adapter);
}
static int igb_setup_all_tx_resources(struct igb_adapter *adapter)
{
	struct pci_dev *pdev = adapter->pdev;
	int i, err = 0;

	for (i = 0; i < adapter->num_tx_queues; i++) {
		err = igb_setup_tx_resources(adapter->tx_ring[i]);
		if (err) {
			dev_err(&pdev->dev,
				"Allocation for Tx Queue %u failed\n", i);
			for (i--; i >= 0; i--)
				igb_free_tx_resources(adapter->tx_ring[i]);
			break;
		}
	}

	return err;
}

_igb_open函数调用了igb_setup_all_tx_resources和igb_setup_all_rx_resources。在调用igb_setup_all_rx_resources这一步操作中,分配了RingBuffer,并建立了内存和Rx队列的映射关系。

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[i]);
		...
	}
}

在上面的源码中,通过循环创建了若干个接收队列。下面看每一个队列是怎么创建出来的。

int igb_setup_rx_resources(struct igb_ring *rx_ring)
{
	//1.申请igb_rx_buffer数组内存
	size = sizeof(struct igb_rx_buffer) * rx_ring->count;
	rx_ring->rx_buffer_info = vmalloc(size);
	//2.申请e1000_adv_rx_desc DMA数组内存
	rx_ring->size = rx_ring->count * sizeof(union e1000_adv_rx_desc);
	rx_ring->size = ALIGN(rx_ring->size, 4096);
	rx_ring->desc = dma_alloc_coherent(dev, rx_ring->size,
					   &rx_ring->dma, GFP_KERNEL);
	//3.初始化队列成员
	rx_ring->next_to_alloc = 0;
	rx_ring->next_to_clean = 0;
	rx_ring->next_to_use = 0;
}

从上述源码可以看到,实际上Ringbuffer的内部不是仅有一个环形队列数组,而是有两个。
igb_rx_buffer数组:这个数组是内核使用,通过vzalloc申请
e1000_adv_rx_desc数组:这个数组是网卡硬件使用的,通过dma_alloc_coherent分配。

static int igb_request_irq(struct igb_adapter *adapter)
{
	if (adapter->flags & IGB_FLAG_HAS_MSIX) {
		err = igb_request_msix(adapter);
		...
	}
}

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,
				  q_vector);
	}
}

对于多队列网卡,为每一个队列都注册了中断,其对应中断处理函数为igb_msix_ring。在msix方式下,每个RX队列都有队列的MSI-X中断,从网卡硬件中断的层面就可以设置让收到的包被不同的CPU处理。(可以通过irqalance,或者修改/proc/irq/IRQ_NUMBER/smp_affinity,从而修改和CPU的绑定行为)。

1.5 数据接收

当数据帧从网线到达网卡时,第一站是网卡的接收队列,网卡在自己的RingBuffer寻找可用的内存位置,找到DMA引擎会把数据DMA到网卡之前关联的内存里,到这个时候CPU都是无感的;当DMA操作完成之后,网卡向CPU发起一个硬中断,通知CPU有数据到达。过程如下:
1、数据帧从外部网络到达网卡
2、网卡把帧DMA到内存
3、发出IRQ硬中断
4、调用驱动注册的硬中断处理函数
5、启动NAPI,发出软中断
网卡的硬中断处理函数是igb_msix_ring。

static irqreturn_t igb_msix_ring(int irq, void *data)
{
	struct igb_q_vector *q_vector = data; // 获取传递给中断处理函数的数据

	/* 写入 ITR 值,该值根据上一个中断计算得出。 */
	igb_write_itr(q_vector);

	/* 调度 NAPI 来处理接收和发送的数据包。 */
	napi_schedule(&q_vector->napi);

	return IRQ_HANDLED; // 指示中断已被处理
}

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

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

可以看到,这里只是把驱动napi_struct传过来的poll_list添加到CPU变量softnet_data中的poll_list。紧接着__raise_softirq_irqoff触发了一个软中断NET_RX_SOFTIRQ,这个触发过程只是进行了一个或运算。

1.5.1 ksoftirqd内核线程处理软中断

软中断处理流程如下:
在这里插入图片描述
ksoftirqd中两个线程函数ksoftirqd_should_run和run_ksoftirqd。其中ksoftirqd_should_run函数代码如下:

kernel/softirq.c
static int ksoftirqd_should_run(unsigned int cpu)
{
	return local_softirq_pending();
}
#define local_softirq_pending() \
	__IRQ_STAT(smp_processor_id(), __softirq_pending)

从这里可以看到,此函数和硬中断调用了同一个函数local_softirq_pending。
注意:硬中断中的设置软中断标记和ksoftirq中判断是否有软中断到达,都是基于smp_processor_id()。这意味着只要硬中断在哪个CPU上被响应,那么软中断也是在这个CPU上处理的。如果发现Linux软中断的某个CPU消耗都集中在一个核上,正确做法应该是调整硬中断的CPU亲和性。

使用方式的不同之处在于,在硬中断处理中是为了写入标记,这里是读取。如果读取到了NET_RX_SOFTIRQ,则进入内核线程处理函数run_ksoftirqd进行处理:

static void run_ksoftirqd(unsigned int cpu)
{
	local_irq_disable();
	if (local_softirq_pending()) {
		//处理待处理的软中断
		__do_softirq();
		...
	}
	local_irq_enable();
}

在__do_softirq中,判断根据当前CPU的软中断类型,调用个其注册的action方法。

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
	pending = local_softirq_pending(); // 获取待处理的软中断位掩码
	account_irq_enter_time(current); // 账户中断进入时间

	h = softirq_vec; // 获取软中断处理程序数组

	while ((softirq_bit = ffs(pending))) { // 找到第一个待处理的软中断位
		unsigned int vec_nr;
		int prev_count;

		h += softirq_bit - 1; // 获取对应的软中断处理程序

		vec_nr = h - softirq_vec; // 计算软中断向量号
		prev_count = preempt_count(); // 获取当前的抢占计数

		kstat_incr_softirqs_this_cpu(vec_nr); // 增加该 CPU 上的软中断计数

		trace_softirq_entry(vec_nr); // 跟踪软中断进入
		h->action(h); // 调用软中断处理程序
		trace_softirq_exit(vec_nr); // 跟踪软中断退出
		if (unlikely(prev_count != preempt_count())) { // 检查抢占计数是否改变
			pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
			       vec_nr, softirq_to_name[vec_nr], h->action,
			       prev_count, preempt_count());
			preempt_count_set(prev_count); // 还原抢占计数
		}
		h++;
		pending >>= softirq_bit; // 更新待处理的位掩码
	}
}
net/core/dev.c
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
	struct softnet_data *sd = this_cpu_ptr(&softnet_data);
	unsigned long time_limit = jiffies +
		usecs_to_jiffies(netdev_budget_usecs);
	int budget = netdev_budget;
	LIST_HEAD(list);
	LIST_HEAD(repoll);

	//关中断
	local_irq_disable();
	list_splice_init(&sd->poll_list, &list);
	//开中断
	local_irq_enable();

	for (;;) {
		struct napi_struct *n;

		if (list_empty(&list)) {
			if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
				goto out;
			break;
		}

		n = list_first_entry(&list, struct napi_struct, poll_list);
		budget -= napi_poll(n, &repoll);
		if (unlikely(budget <= 0 ||
			     time_after_eq(jiffies, time_limit))) {
			sd->time_squeeze++;
			break;
		}
	}
	...
}

在硬中断中将设备添加到poll_list,会不会重复添加呢?
答案是不会的,在软中断处理函数net_rx_action这里,先调用local_irq_disable把所有的硬中断关闭了,把CPU的netsoft_data的poll_list直接拷贝出来之后再开的中断,不会给硬中断重复添加poll_list的机会。

函数开头的time_limit和budget是用来控制net_rx_action函数主动退出的,目的是保证网络包的接收不霸占CPU不放。这个函数核心逻辑就是获取softnet_data的poll_list进行遍历,然后执行到网卡驱动注册的poll函数。对于igb网卡来说就是igb_poll函数。

static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
	if (test_bit(NAPI_STATE_SCHED, &n->state)) {
		work = n->poll(n, weight);
		trace_napi_poll(n, work, weight);
	}
}

static int igb_poll(struct napi_struct *napi, int budget)
{
	struct igb_q_vector *q_vector = container_of(napi,
						     struct igb_q_vector,
						     napi);

	/* 清理发送队列的中断 */
	if (q_vector->tx.ring)
		clean_complete = igb_clean_tx_irq(q_vector, budget);

	/* 清理接收队列的中断 */
	if (q_vector->rx.ring) {
		int cleaned = igb_clean_rx_irq(q_vector, budget);
	}
	...
}

在读取操作中,igb_poll的重点工作是对igb_clean_rx_igb的调用。

drivers/net/ethernet/inetl/igb_main.c

static int igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget)
{
	struct igb_ring *rx_ring = q_vector->rx.ring; // 获取接收队列
	struct sk_buff *skb = rx_ring->skb; // 获取当前正在处理的 sk_buff
	unsigned int total_bytes = 0, total_packets = 0; // 用于统计接收到的总字节和数据包数
	u16 cleaned_count = igb_desc_unused(rx_ring); // 获取尚未处理的描述符数量

	while (likely(total_packets < budget)) {
		union e1000_adv_rx_desc *rx_desc; // 指向当前接收描述符
		struct igb_rx_buffer *rx_buffer; // 指向当前接收缓冲区
		unsigned int size; // 数据包大小

		// 如果有足够的描述符被清理,重新向硬件返回一些缓冲区
		if (cleaned_count >= IGB_RX_BUFFER_WRITE) {
			igb_alloc_rx_buffers(rx_ring, cleaned_count);
			cleaned_count = 0;
		}

		rx_desc = IGB_RX_DESC(rx_ring, rx_ring->next_to_clean); // 获取下一个待清理的描述符
		size = le16_to_cpu(rx_desc->wb.upper.length); // 获取数据包大小
		if (!size)
			break; // 如果没有数据,退出循环

		// 确保在读取描述符其他字段之前,描述符已经被写回
		dma_rmb();

		rx_buffer = igb_get_rx_buffer(rx_ring, size); // 获取对应的接收缓冲区

		// 从接收队列中检索一个缓冲区
		if (skb)
			igb_add_rx_frag(rx_ring, rx_buffer, skb, size); // 添加到当前数据包的片段
		else if (ring_uses_build_skb(rx_ring))
			skb = igb_build_skb(rx_ring, rx_buffer, rx_desc, size); // 构建一个新的 sk_buff
		else
			skb = igb_construct_skb(rx_ring, rx_buffer, rx_desc, size); // 构造一个新的 sk_buff

		// 如果未能检索到缓冲区,增加分配失败的统计并退出
		if (!skb) {
			rx_ring->rx_stats.alloc_failed++;
			rx_buffer->pagecnt_bias++;
			break;
		}

		igb_put_rx_buffer(rx_ring, rx_buffer); // 将缓冲区放回接收队列
		cleaned_count++; // 增加已清理的描述符计数

		// 如果当前描述符不是帧的结束,继续处理下一个缓冲区
		if (igb_is_non_eop(rx_ring, rx_desc))
			continue;

		// 验证数据包布局是否正确
		if (igb_cleanup_headers(rx_ring, rx_desc, skb)) {
			skb = NULL; // 如果布局不正确,丢弃当前 sk_buff
			continue;
		}

		// 更新接收到的总字节数
		total_bytes += skb->len;

		// 处理数据包的校验和、时间戳、VLAN 标签和协议
		igb_process_skb_fields(rx_ring, rx_desc, skb);

		napi_gro_receive(&q_vector->napi, skb); // 将数据包传递给上层网络堆栈

		// 重置 sk_buff 指针
		skb = NULL;

		// 更新预算统计
		total_packets++;
	}

	// 将未完成的帧重新放回接收队列,以便后续完成
	rx_ring->skb = skb;

	// 更新接收到的总字节和数据包数的统计
	u64_stats_update_begin(&rx_ring->rx_syncp);
	rx_ring->rx_stats.packets += total_packets;
	rx_ring->rx_stats.bytes += total_bytes;
	u64_stats_update_end(&rx_ring->rx_syncp);
	q_vector->rx.total_packets += total_packets;
	q_vector->rx.total_bytes += total_bytes;

	// 如果有清理的描述符,重新为它们分配缓冲区
	if (cleaned_count)
		igb_alloc_rx_buffers(rx_ring, cleaned_count);

	return total_packets; // 返回处理的数据包数量
}

igb_fetch_rx_buffer和igb_is_non_eop的作用就是把数据帧从RingBuffer取下来;Skb被从RingBuffer取下来后,会通过igb_alloc_rx_buffer申请新的skb再重新挂上去。接下来进入napi_gro_receive函数。

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

	skb_gro_reset_offset(skb);

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

dev_gro_receive这个函数代表的是网卡GRO特性,可以简单理解成把相关小包组成一个大包,目的是减少传送给网络栈的包数,有助于降低对CPU的使用量。接下来看napi_skb_finish,这个函数主要是调用了netif_receive_skb_internal,netif_receive_skb_internal又调用了__netif_receive_skb,在__netif_receive_skb中,数据包将被送到协议栈。

static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
{
	switch (ret) {
	case GRO_NORMAL:
		if (netif_receive_skb_internal(skb))
			ret = GRO_DROP;
		break;
	...
}

static int netif_receive_skb_internal(struct sk_buff *skb)
{
	...
	ret = __netif_receive_skb(skb);
	rcu_read_unlock();
	return ret;
}

1.6 网络协议栈处理

__netif_receive_skb函数会根据包的协议进行处理,处理流程如下图所示:
在这里插入图片描述

net/core/dev.c

static int __netif_receive_skb(struct sk_buff *skb)
{
	int ret;

	/* 检查是否有内存分配的套接字和数据包是否使用了页面外内存 */
	if (sk_memalloc_socks() && skb_pfmemalloc(skb)) {
		unsigned int noreclaim_flag;
		noreclaim_flag = memalloc_noreclaim_save(); // 保存当前的 memalloc_noreclaim 状态
		ret = __netif_receive_skb_core(skb, true); // 调用核心函数处理数据包,启用 PF_MEMALLOC 路径
		memalloc_noreclaim_restore(noreclaim_flag); // 恢复之前的 memalloc_noreclaim 状态
	} else {
		ret = __netif_receive_skb_core(skb, false); // 调用核心函数处理数据包,不启用 PF_MEMALLOC 路径
	}

	return ret; // 返回处理结果
}

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
	......
	/* 遍历全局和设备的 packet_type 列表 ,pcap逻辑,这里会将数据送入抓包点,tcpdump就是从这个入口获取包的*/
	list_for_each_entry_rcu(ptype, &ptype_all, list) {
		if (pt_prev)
			ret = deliver_skb(skb, pt_prev, orig_dev);
		pt_prev = ptype;
	}

	list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
		if (pt_prev)
			ret = deliver_skb(skb, pt_prev, orig_dev);
		pt_prev = ptype;
	}

skip_taps:
	
#ifdef CONFIG_NET_INGRESS
	if (static_key_false(&ingress_needed)) {
		skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev);
		if (!skb)
			goto out;
		/* 处理入口过滤,这是netfilter第一个输入挂接点,处理注册的回调函数 */
		if (nf_ingress(skb, &pt_prev, &ret, orig_dev) < 0)
			goto out;
	}
#endif
	......
}

在__netif_receive_skb_core中可以看到tcpdump命令的抓包点和netfilter的输入的第一个挂接点,tcpdump将抓包函数以协议形式挂到ptype_all上,设备层遍历所有协议,就能抓到数据包了,tcpdump会执行packet_create。

static int packet_create(struct net *net, struct socket *sock, int protocol,
			 int kern)
{
	po->prot_hook.func = packet_rcv;

	if (sock->type == SOCK_PACKET)
		po->prot_hook.func = packet_rcv_spkt;

	po->prot_hook.af_packet_priv = sk;

	if (proto) {
		po->prot_hook.type = proto;
		register_prot_hook(sk);
	}
}

register_prot_hook会把tcpdump用到的协议挂到ptype_all上。

接着__netif_receive_skb_core函数取出protocol,它会从数据包中取出协议信息,然后遍历注册在这个协议上的回调函数列表。ptype_base是一个哈希表。ip_rcv函数地址就存在这个哈希表中。

static inline int deliver_skb(struct sk_buff *skb,
			      struct packet_type *pt_prev,
			      struct net_device *orig_dev)
{
	if (unlikely(skb_orphan_frags(skb, GFP_ATOMIC)))
		return -ENOMEM;
	atomic_inc(&skb->users);
	return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}

pt_prev->func调用了协议层注册的处理函数,对于IP包就会进入ip_rcv(如果是arp包就会进入arp_rcv)。

1.7 IP层处理

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)
{
	......
	/* 调用 netfilter 钩子,继续处理数据包 */
	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
		       net, NULL, skb, dev, NULL,
		       ip_rcv_finish);
}

这是第二个iptables_netfilter的挂载点(NF_INET_PRE_ROUTING)。

net/ipv4/arp.c:			NF_HOOK(NFPROTO_ARP, NF_ARP_OUT,dev_net(skb->dev), NULL, skb, NULL, skb->dev,arp_xmit_finish);
net/ipv4/arp.c:			return NF_HOOK(NFPROTO_ARP, NF_ARP_IN,dev_net(dev), NULL, skb, dev, NULL,arp_process);
net/ipv4/ip_input.c:	return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,net, NULL, skb, skb->dev, NULL,ip_local_deliver_finish);
net/ipv4/ip_input.c:	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,net, NULL, skb, dev, NULL,ip_rcv_finish);
net/ipv4/ip_forward.c:		return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,net, NULL, skb, skb->dev, rt->dst.dev,ip_forward_finish);
net/ipv4/xfrm4_output.c		NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,net, sk, skb, NULL, skb_dst(skb)->dev,__xfrm4_output,!(IPCB(skb)->flags & IPSKB_REROUTED));
net/ipv4/ip_output.c		nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,net, sk, skb, NULL, skb_dst(skb)->dev,dst_output);
net/ipv4/ip_output.c		NF_HOOK(NFPROTO_IPV4, NF_INET_POST_ROUTING,net, sk, newskb, NULL, newskb->dev,ip_mc_finish_output);
net/ipv4/ip_output.c		return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
		       net, sk, skb, NULL, skb_dst(skb)->dev,
		       dst_output);

当执行完注册的钩子后就会执行到最后一个参数指向的函数ip_rcv_finish。

net/ipv4/ip_input.c
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	/* 初始化数据包在 Linux 网络中的虚拟路径缓存 */
	if (!skb_valid_dst(skb)) {
		int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
					       iph->tos, dev);
		if (unlikely(err)) {
			if (err == -EXDEV)
				__NET_INC_STATS(net, LINUX_MIB_IPRPFILTER);
			goto drop;
		}
	}
	/* 将数据包传递给目的地 */
	return dst_input(skb);
}

int ip_route_input_noref(struct sk_buff *skb, __be32 daddr, __be32 saddr,
			 u8 tos, struct net_device *dev)
{
	if (ipv4_is_multicast(daddr)) {
	...
		if (our
#ifdef CONFIG_IP_MROUTE
			||
		    (!ipv4_is_local_multicast(daddr) &&
		     IN_DEV_MFORWARD(in_dev))
#endif
		   ) {
			res = ip_route_input_mc(skb, daddr, saddr,
						tos, dev, our);
		}
		...
	}
}

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

	rth = rt_dst_alloc(dev_net(dev)->loopback_dev, flags, RTN_MULTICAST,
			   IN_DEV_CONF_GET(in_dev, NOPOLICY), false, false);
	if (!rth)
		goto e_nobufs;

	rth->dst.output = ip_rt_bug;
	rth->rt_is_input= 1;
	
#ifdef CONFIG_IP_MROUTE
	//如果是多播,赋予多播处理函数
	if (!ipv4_is_local_multicast(daddr) && IN_DEV_MFORWARD(in_dev))
		rth->dst.input = ip_mr_input;
#endif
	RT_CACHE_STAT_INC(in_slow_mc);
	skb_dst_set(skb, &rth->dst);
	...
}
struct rtable *rt_dst_alloc(struct net_device *dev,
			    unsigned int flags, u16 type,
			    bool nopolicy, bool noxfrm, bool will_cache)
{
	...
	if (rt) {
		...
		rt->dst.output = ip_output;
		if (flags & RTCF_LOCAL)
			rt->dst.input = ip_local_deliver;
	}
	return rt;
}

所以回到ip_rcv_finish中的return dst_input(skb)调用的input方法就是路由子系统赋的ip_local_deliver。

net/ipv4/ip_input.c
int ip_local_deliver(struct sk_buff *skb)
{
	struct net *net = dev_net(skb->dev);

	if (ip_is_fragment(ip_hdr(skb))) {
		if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
			return 0;
	}
	return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
		       net, NULL, skb, skb->dev, NULL,
		       ip_local_deliver_finish);
}
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	...
	{
		int protocol = ip_hdr(skb)->protocol;
		...
		ipprot = rcu_dereference(inet_protos[protocol]);
		if (ipprot) {
			ret = ipprot->handler(skb);
		} 
		...
	}
}

inet_protos中保存着tcp_v4_rcv和udp_rcv的函数地址。这里会根据包的协议类型选择分发,在这里skb包派送到更上层的协议中。

1.8 收包小结

准备工作:

  • 创建ksoftirqd线程,为它设置好自己的线程函数
  • 协议栈注册
  • 网卡驱动初始化,每个驱动都有一个初始化函数,内核让驱动也初始化,在初始化过程中,把自己的DMA准备好,把NAPI的poll函数地址告诉内核
  • 启动网卡,分配RX、TX队列,注册中断对应的处理函数

收包过程:

  • 网卡将数据帧DMA到内存的RingBuffer中,然后向CPU发起中断通知
  • CPU响应中断请求,调用网卡启动时注册的中断处理函数
  • 中断函数发起软中断请求
  • 内核线程ksoftirqd发现有软中断请求到来,关闭硬中断
  • ksoftirq线程开始调用驱动的poll函数收包
  • poll函数将收到的包送到协议栈注册的ip_rcv函数中
  • ip_rcv函数将包送到udp_rcv或tcp_rcv函数中

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2146250.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

状态估计算法

目录 前言一、贝叶斯滤波二、卡尔曼滤波2.1 KF简介2.2 基本线性模型2.3 KF公式推导2.3.1 预测值2.3.2 先验误差协方差矩阵2.3.3 卡尔曼增益2.3.4 最优估计值2.3.5 后验误差协方差矩阵 2.4 KF算法使用2.5 MATLAB验证2.5 Python验证 三、扩展卡尔曼滤波3.1 EKF原理3.2 MATLAB实现…

基于vue框架的宠物寻回小程序8g7el(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;发布人,宠物分类,宠物信息,接取人,接取信息,完成信息 开题报告内容 基于Vue框架的宠物寻回小程序开题报告 一、研究背景与意义 随着城市化进程的加快和人们生活水平的提高&#xff0c;宠物已成为许多家庭不可或缺的一员。它们不仅为生…

谷歌导入了自我填充密码,不显示

C:\Users\GZDZ\AppData\Local\Google\Chrome\User Data\Default Login Data Login Data-journal 删除上面两个重启就可以了 https://blog.csdn.net/weixin_30940783/article/details/97552679?utm_mediumdistribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandi…

Windows10电脑右下角时间显示到秒

1、打开注册表 快捷键 WIN R 键&#xff0c;输入 regedit 在注册表中找到如下位置 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced 在空白位置右击&#xff0c;新建--DWORD(32位)值(D) 3、将新建的数值名称设置为 ShowSecondsInSystemCloc…

AI绘画与摄影新纪元:ChatGPT+Midjourney+文心一格 共绘梦幻世界

文章目录 一、AI艺术的新时代二、ChatGPT&#xff1a;创意的引擎与灵感的火花三、Midjourney&#xff1a;图像生成的魔法与技术的奇迹四、文心一格&#xff1a;艺术的升华与情感的共鸣五、融合创新&#xff1a;AI绘画与摄影实战的无限可能六、应用场景与实践案例AI艺术的美好未…

【Delphi】知道控件名称(字符串),访问控件

在 Delphi 中&#xff0c;可以使用 RTTI&#xff08;运行时类型信息&#xff09; 或其他方法通过对象的名称字符串来访问对象。比如&#xff0c;如果你有一个控件的名称字符串&#xff0c;你希望通过该名称找到并访问实际的控件。 以下是通过 RTTI 以及其他技术&#xff08;如…

react之jsx基础(1)概念和本质

文章目录 JSX 的基本概念1. **语法**2. **表达式**3. **属性**4. **子元素** JSX 的编译过程1. **转换成 JavaScript**2. **React 元素** JSX 的实际应用1. **组件定义**2. **组件嵌套** 总结 当然&#xff0c;以下是对 JSX 的详细讲解&#xff0c;包括其基本概念、语法、编译过…

【vue element-ui】关于删除按钮的提示框,可一键复制

实现效果&#xff1a; Delete: function (id) {this.$confirm(此操作将永久删除该文件, 是否继续?, 提示, {confirmButtonText: 确定,cancelButtonText: 取消,type: warning,center: true,}).then(() > {Delete(id).then(() > {this.$message({type: success,message: 删…

基于python+django+vue的图书管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于pythondjangovueMySQL的图…

Modbus_tcp

目录 一&#xff1a;modbus起源 1.起源 2. 分类&#xff1a; 3. 优势&#xff1a; 4. 应用场景&#xff1a; 5.ModbusTCP特点&#xff08;掌握&#xff09;&#xff1a; 二、 ModbusTCP的协议 1. 报文头 2. 寄存器 1. 线圈&#xff08;Coils&#xff09; 2. 离…

数据库———事务及bug的解决

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯&#xff0c;你们的点赞收藏是我前进最大的动力&#xff01;&#xff01;希望本文内容能帮到你&#xff01; 目录 一&#xff1a;事务 1&#xff1a;场景引入 2&#xff1a;“回滚” 3&#xff1a;恢复机制&#xff08;un…

全网最全comfyui工作流保姆级教程来啦!comfyui工作流搭建看这一篇就够了

前言 一、SD主流 UI Stable Diffusion&#xff08;SD&#xff09;因为其开源特性&#xff0c;有着较高的受欢迎程度&#xff0c;并且基于SD的开源社区及教程、插件等&#xff0c;都是所有工具里最多的。基于SD&#xff0c;有不同的操作界面&#xff0c;可以理解为一个工具的不…

vue node node-sass sass-loader 版本 对应 与 兼容

警告&#xff1a; LibSass 和 Node Sass 已弃用。虽然它们将继续无限期地接收维护版本&#xff0c;但没有计划添加其他功能或与任何新的 CSS 或 Sass 功能兼容。仍在使用它的项目应该转移到 Dart Sass。 sass Sass是一种预处理器脚本语言&#xff0c;可以解释或编译成…

【rust】rust条件编译

在c语言中&#xff0c;条件编译是一个非常好用的功能&#xff0c;那么rust中如何实现条件编译呢? rust的条件编译需要两个部分&#xff0c;一个是fratures&#xff0c;另一个是cfg。Cargo feature是一个非常强大的功能&#xff0c;可以提供条件编译和可选依赖项的高级特性&…

如何让大模型更好地进行场景落地?

自ChatGPT模型问世后&#xff0c;在全球范围内掀起了AI新浪潮。 有很多企业和高校也随之开源了一些效果优异的大模型&#xff0c;例如&#xff1a;Qwen系列模型、MiniCPM序列模型、Yi系列模型、ChatGLM系列模型、Llama系列模型、Baichuan系列模型、Deepseek系列模型、Moss模型…

OJ 组合总和

题目&#xff1a; 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 &#xff0c;并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限制…

en造数据结构与算法C# 用Unity实现简单的群组行为算法 之 分散

因为还没写聚集并且材质没有设置摩擦系数&#xff0c;所以出现了这种刚体受力得到初速度却无法减少&#xff0c;从而乱飞的情况 本教程部分代码师承于siki学院siki老师的人工智能编程这一案例&#xff0c;我认为自己的水平有限&#xff0c;老师的写法太过高级&#xff0c;所以就…

Kubernetes从零到精通(11-CNI网络插件)

Kubernetes网络模型 Kubernetes的网络模型&#xff08;Kubernetes Networking Model&#xff09;旨在提供跨所有节点、Pod和服务的统一网络连接。它的核心理念是通过统一的网络通信规则&#xff0c;保证集群中的所有组件能够顺畅地相互通信。Kubernetes网络模型主要有以下几个关…

【html网页制作】传统文化书法主题网页制作html+css(6页面附效果源码)

HTMLCSS传统文化主题书法网页制作 &#x1f354;涉及知识&#x1f964;写在前面&#x1f367;一、网页主题&#x1f333;二、网页效果菜单切换效果PageA、整体页Page1、主页Page2、行书页Page3、楷书页Page4、隶书页Page5、篆书页Page6、草书页 &#x1f40b;三、网页架构与技术…

启动cadence过程中出现cdn_sfl401as.dll缺失问题解决办法_不需要重装软件

有时候&#xff0c;由于OrCAD安装了多了版本或其他原因&#xff0c;原本用的好好地CAD突然无法使用&#xff0c;一般是因为你安装的新软件或者其他操作与原来的CAD环境冲突&#xff0c;出现了cdn_sfl401as.dll等多个dll文件缺失现象&#xff0c;提示需要重装环境。 重装环境&a…