当前位置: 首页 > news >正文

深入理解Linux网络随笔(一):内核是如何接收网络包的(上篇)

深入理解Linux网络随笔(一):内核是如何接收网络包的(上篇)

1、TCP/IP模型概述

从Linux视角看,TCP/IP网络分层模型包括用户空间和内核空间。用户空间(应用层)负责HTTP、FTP等协议的网络服务。内核空间则实现了各个协议层:

  • 传输层:TCP确保可靠传输,UDP提供无连接快速传输。
  • 网络层:IP负责数据包路由,ICMP用于网络诊断。
  • 链路层:通过网络设备驱动处理数据帧和硬件交互。
  • 物理层:网卡实现物理信号收发。

各层通过封装与解封装协作完成数据传输,内核通过socket接口支持用户进程与网络交互。
在这里插入图片描述

2、内核收包过程

内核收包路径如下图所示,其核心步骤小结如下:

1、网卡NIC接收到网络数据包

由于网卡位于物理层,此时接收的数据以电信号形式存在,网卡将电信号转换为数字信号,并进行数据帧的封装和预处理。

2、以DMA方式将数据帧写放入内存

采用直接内存访问(DMA)技术,将数据帧直接写入内核中预定的缓冲区,减少CPU负担并提高数据传输效率。

3、网卡触发硬中断通知CPU

4、软中断处理

CPU收到中断请求,调用网络设备驱动注册的中断处理函数,将中断请求转为软中断,提交给软中断处理机制。硬中断的目的是尽快响应硬件,而软中断则允许系统进行更复杂的处理。

5、ksoftirqd内核线程处理,poll轮询收包

  • 软中断请求由ksoftirqd内核线程处理,通过poll轮询的方式处理所有网络相关的软中断任务。批量处理多个网络数据包,减少中断上下文切换的开销。
  • 网卡的接收缓冲区(Ring Buffer)中取出数据,将其封装为Socket缓冲区(skb)对象。skb是Linux内核中用于描述网络数据包的数据结构。

6、协议栈处理网络帧

  • 协议栈开始处理网路帧,处理完成的data存于socket的接收队列(每个进程通过socket与内核网络栈通信,接收到的数据会被放入该进程对应的接收队列)。
  • socket充当用户进程和内核通信桥梁

7、内核唤醒用户进程

  • 调用如recv()read()等系统调用读取数据,数据会从接收队列中取出并交给用户空间。

在这里插入图片描述

2.1Linux启动

2.1.1 创建ksoftirqd内核线程

ksoftirqd 是一种 per-CPU 内核线程,意味着每个 CPU 核心都会有一个独立的 ksoftirqd 线程来处理该核心的软中断。在 Linux 内核中,ksoftirqd 的优先级相对较低。当系统中有更高优先级的任务(如硬中断、实时任务或其他重要进程)需要处理时,ksoftirqd 线程会被调度器推迟执行,等待 CPU 资源空闲。只有当 CPU 处于空闲状态,或者软中断的处理已经无法继续推迟时,ksoftirqd 线程才会被调度执行。这种延迟处理机制,使得ksoftirqd 只会在 CPU 空闲或者软中断处理不能再推迟时才会被调度执行,最大化 CPU 的利用率,避免不必要的资源浪费。
ps命令查看**ksoftirq内核线程, **ps -eLf | grep ksoftirqd 命令查看到了 [ksoftirqd/0][ksoftirqd/1][ksoftirqd/2][ksoftirqd/3] 这 4 个 ksoftirq 内核线程,使用 nproc 命令查看到当前机器的 CPU 核心数为 4 。ksoftirqd线程数量等于机器的CPU核心数

在这里插入图片描述
Linux内核启动过程中,系统初始化的时候在kernel/smpboot.c中调用了smpboot_register_percpu_thread为每个CPU核心启动一个内核线程,该函数会进一步执行到spawn_ksoftirqd(位于kernel/ksoftirqd.c)来创建softirqd线程。

//kernel/softirq.c
static __init int spawn_ksoftirqd(void)
{
	cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
				  takeover_tasklets);//注册CPU的热插拔状态,确保在CPU下线时能够正确处理软中断
	BUG_ON(smpboot_register_percpu_thread(&softirq_threads));//负责在每一个CPU上注册并启动软中断线程softirqd

	return 0;
}
early_initcall(spawn_ksoftirqd);

spawn_ksoftirqd会在内核启动时创建并初始化每个CPU上的软中断线程,并为每个CPU设置适当的热插拔状态。通过这种方式,系统可以在多核环境下并行处理软中断。

static struct smp_hotplug_thread softirq_threads = {
	.store			= &ksoftirqd,
	.thread_should_run	= ksoftirqd_should_run,
	.thread_fn		= run_ksoftirqd,
	.thread_comm		= "ksoftirqd/%u",
};

smp_hotplug_thread结构体描述热插拔线程(hotplug thread),即在系统中随着 CPU 的启停而创建或销毁的线程。softirq_threads 专门用来描述软中断线程,设置回调函数ksoftirqd_should_run,决定线程是否需要继续执行;设置回调函数run_ksoftirqd处理软中断任务。(软中断核心流程不做解释)

// include/linux/interrupt.h
enum
{
    HI_SOFTIRQ = 0,               // 高优先级软中断,通常用于处理紧急任务
    TIMER_SOFTIRQ,                // 定时器软中断,处理内核定时器相关的任务
    NET_TX_SOFTIRQ,               // 网络传输发送软中断,用于处理网络发送操作
    NET_RX_SOFTIRQ,               // 网络传输接收软中断,用于处理网络接收操作
    BLOCK_SOFTIRQ,                // 块设备软中断,处理与块设备(如硬盘)相关的任务
    BLOCK_IOPOLL_SOFTIRQ,         // 块 I/O 轮询软中断,负责检查 I/O 操作是否完成
    TASKLET_SOFTIRQ,              // tasklet 软中断,处理延迟任务
    SCHED_SOFTIRQ,                // 调度软中断,处理任务调度相关的任务
    HRTIMER_SOFTIRQ,              // 高分辨率定时器软中断,处理高精度定时器任务
    RCPU_SOFTIRQ,                 // RCPU 同步软中断,涉及到 RCPU 的更新和回调处理
    NR_SOFTIRQS                   // 软中断的总数,表示系统中所有软中断类型的数量
};

软中断类型中,网络传输发送软中断NET_TX_SOFTIRQ,网络传输接收软中断NET_RX_SOFTIRQ

2.1.2网络子系统初始化

Linux内核通过调用subsys_initcall来初始化各个子系统,使用subsys_initcall(net_dev_init)初始化网络设备。

在这里插入图片描述

static int __init net_dev_init(void)
{
......
	/*
	 *	Initialise the packet receive queues.
	 */
    
    // *初始化每个 CPU 上的网络接收队列和软中断相关的数据结构。
	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);//处理队列
......

		init_gro_hash(&sd->backlog);//哈希表
		sd->backlog.poll = process_backlog;//回调
		sd->backlog.weight = weight_p;//Gro权值
	}
    //初始化阶段结束
	dev_boot_phase = 0;

	/* The loopback device is special if any other network devices
	 * is present in a network namespace the loopback device must
	 * be present. Since we now dynamically allocate and free the
	 * loopback device ensure this invariant is maintained by
	 * keeping the loopback device as the first device on the
	 * list of network devices.  Ensuring the loopback devices
	 * is the first device that appears and the last network device
	 * that disappears.
	 */
    //注册回环设备
	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);
    // 设置 CPU 热插拔状态,注册 CPU 下线时的清理函数
	rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead",
				       NULL, dev_cpu_dead);
	WARN_ON(rc < 0);
	rc = 0;
out:
	return rc;
}

网络子系统初始化过程,为每个CPU初始化softnet_data结构体,进行软中断处理和管理网络数据包队列,为软中断注册处理函数使用方法open_softirq,网络传输发送软中断NET_TX_SOFTIRQ处理注册回调处理函数net_tx_action,网络传输接收软中断NET_RX_SOFTIRQ处理注册回调处理函数net_rx_action

struct softnet_data {
	struct list_head	poll_list;
	struct sk_buff_head	process_queue;

	/* stats */
	unsigned int		processed;
	unsigned int		time_squeeze;
#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		received_rps;
	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;
};

在每个 softnet_data 结构体中,poll_list 用来链接需要轮询的网络设备接口。每个 CPU 上的 softnet_data 结构体都有一个 poll_list,记录了当前需要处理的网络设备。这样可以通过轮询这些设备的接口来处理网络数据包。

在 Linux 网络栈的工作流程中,NAPI 会定期轮询设备(而不是每次包都中断),有效地减少中断处理的负载。每个网络设备的 NAPI 结构体都会被放入这个链表中,表示这个设备正在进行轮询,等待数据包的处理。

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

软中断处理函数绑定软中断号通过softirq_vec数组实现,每一个元素对应一种软中断类型。

2.1.3协议栈注册

Linux内核实现了网络层的IP协议,同样也实现了TCP、UDP协议,协议栈初始化入口函数为fs_initcall(inet_init),调用inet_init进行网络协议栈注册。

在这里插入图片描述

static int __init inet_init(void)
{
    ......
    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__);  
    ......
    dev_add_pack(&ip_packet_type);
}

使用inet_add_protocol向协议栈注册协议,协议定义结构体如下:

static struct packet_type ip_packet_type __read_mostly = {
	.type = cpu_to_be16(ETH_P_IP),
	.func = ip_rcv,
	.list_func = ip_list_rcv,
};
static const struct net_protocol icmp_protocol = {
	.handler =	icmp_rcv,
	.err_handler =	icmp_err,
	.no_policy =	1,
};
static const struct net_protocol udp_protocol = {
	.handler =	udp_rcv,
	.err_handler =	udp_err,
	.no_policy =	1,
};
static const struct net_protocol tcp_protocol = {
	.handler	=	tcp_v4_rcv,//回调处理函数
	.err_handler	=	tcp_v4_err,
	.no_policy	=	1,
	.icmp_strict_tag_validation = 1,
};

dev_add_pack负责将新的packet_type结构体添加于协议类型链表。

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);// 将新的 packet_type 结构体添加到链表中
	spin_unlock(&ptype_lock);
}

ptype_head 是一个内联函数,根据传入的 packet_type 结构体 (pt) 获取对应协议类型的链表头。这个链表存储了与该协议类型相关的处理程序。它根据协议类型和设备信息决定返回哪个链表的头部。

static inline struct list_head *ptype_head(const struct packet_type *pt)
{
    if (pt->type == htons(ETH_P_ALL))  // 如果协议类型是 ETH_P_ALL
        return pt->dev ? &pt->dev->ptype_all : &ptype_all;  // 返回设备特定的链表或全局链表
    else  // 如果协议类型不是 ETH_P_ALL
        return pt->dev ? &pt->dev->ptype_specific : 
                         &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];  // 返回设备特定的链表或根据协议类型哈希的链表
}

协议类型通过 ntohs(pt->type) & PTYPE_HASH_MASK 进行哈希计算,决定将其对应的 packet_type 结构体插入到 ptype_base 数组中的哪个位置,在这里IP协议会注册到ptype_base 哈希表,存储ip_rcv函数处理地址。

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;
}
EXPORT_SYMBOL(inet_add_protocol);

TCP、UDP、ICMP协议注册方式与IP协议不同,通过inet_add_protocol函数调用完成协议注册,将协议类型protocol对应的处理程序添加于inet_protos数组。

2.1.4网卡驱动初始化

每一个驱动程序(不仅仅包括网卡驱动程序)会使用module_init向内核注册一个初始化函数,当驱动程序被加载时,内核会调用这个函数,以igb网卡驱动程序为例,其初始化函数如下。

在这里插入图片描述

static int __init igb_init_module(void)
{
......
	ret = pci_register_driver(&igb_driver);
	return ret;
}
module_init(igb_init_module);
//驱动相关信息
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
};

注册PCI驱动程序在pci_register_driver函数完成,函数调用关系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;          // 绑定到 PCI 总线类型
	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);        // 初始化锁,用于动态分配的设备 ID
	INIT_LIST_HEAD(&drv->dynids.list);        // 初始化动态设备列表

	/* register with core */
	return driver_register(&drv->driver);    // 注册驱动到内核的核心驱动模型中
}
EXPORT_SYMBOL(__pci_register_driver);

__pci_register_driver调用完成后,Linux内核就会知道驱动相关信息(如igb网卡驱动名称、igb_probe地址等),当网卡设备被识别以后,Linux内核会调用.probe方法(igb_probe方法)。

static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
    ......
    //获取MAC地址
    if (eth_platform_get_mac_address(&pdev->dev, hw->mac.addr)) {
		/* copy the MAC address out of the NVM */
		if (hw->mac.ops.read_mac_addr(hw))
			dev_err(&pdev->dev, "NVM Read Error\n");
	}
    //DMA初始化
    err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    //注册ethtool函数
    igb_set_ethtool_ops(netdev);
    //注册net_device_ops、netdev等
    netdev = alloc_etherdev_mq(sizeof(struct igb_adapter),
				   IGB_MAX_TX_QUEUES);
	if (!netdev)
		goto err_alloc_etherdev;

	SET_NETDEV_DEV(netdev, &pdev->dev);

	pci_set_drvdata(pdev, netdev);
	adapter = netdev_priv(netdev);
	adapter->netdev = netdev;
	adapter->pdev = pdev;
	hw = &adapter->hw;
	hw->back = adapter;
   ......
	netdev->netdev_ops = &igb_netdev_ops;
    // 注册NAPI相关的资源分配
    err = igb_alloc_q_vector(adapter);
    if (err)
        goto err_alloc_q_vector;
    ......	
}

igb_probe初始化过程中,调用igb_alloc_q_vector–>netif_napi_add,初始化NAPI机制,对于igb网卡来说,NAPI机制的poll函数是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);
    ....
}
2.1.5启动网卡
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,
};

igb_netdev_ops结构体定义了网络设备常见的执行操作,当网卡被启动时调用回调函数igb_open。函数调用关系igb_open–>_igb_open
在这里插入图片描述

static int __igb_open(struct net_device *netdev, bool resuming)
{
    ......
	netif_carrier_off(netdev);

	/* 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);
	if (err)
		goto err_setup_rx;
    
	err = igb_request_irq(adapter);
	if (err)
		goto err_req_irq;
    ......
    igb_irq_enable(adapter);
	for (i = 0; i < adapter->num_q_vectors; i++)
		napi_enable(&(adapter->q_vector[i]->napi));
    ......
    
}

__igb_open中调用igb_setup_all_tx_resources分配TX队列内存,调用igb_setup_all_rx_resources分配RX队列内存,调用igb_request_irq注册中断处理函数,igb_irq_enable启用硬中断, napi_enable启用NAPI机制。

static int igb_setup_all_rx_resources(struct igb_adapter *adapter)
{
	struct pci_dev *pdev = adapter->pdev;
	int i, err = 0;
	for (i = 0; i < adapter->num_rx_queues; i++) {
		err = igb_setup_rx_resources(adapter->rx_ring[i]);
		......
		}
	}
	return err;
}

循环创建若干个接收队列,调用igb_setup_rx_resources为RX队分配资源,前期准备工作已完成,等待网络数据包到来~~。

int igb_setup_rx_resources(struct igb_ring *rx_ring)
{
......
    //申请igb_rx_buffer数组内存供内核使用
	size = sizeof(struct igb_rx_buffer) * rx_ring->count;

	rx_ring->rx_buffer_info = vmalloc(size);
	if (!rx_ring->rx_buffer_info)
		goto err;
    //申请e1000_adv_rx_desc数组内存供网卡使用
	/* 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);

	rx_ring->desc = dma_alloc_coherent(dev, rx_ring->size,
					   &rx_ring->dma, GFP_KERNEL);
	if (!rx_ring->desc)
		goto err;
    //初始化
	rx_ring->next_to_alloc = 0;
	rx_ring->next_to_clean = 0;
	rx_ring->next_to_use = 0;
......
	return 0;
}

参考资料:《深入理解linux网络》

http://www.dtcms.com/a/20170.html

相关文章:

  • 【Linux】Ubuntu Linux 系统 ——PHP开发环境
  • Linux探秘坊-------5.git
  • 一种面向车载时间敏感网络的联合路由与时隙调度负载均衡算法
  • 我是农场主之在Linux中养鱼、喂牛、开火车
  • DeepSeek指导手册从入门到精通
  • langchain学习笔记之langserve服务部署
  • 科技快讯 | 目标2030年登月 登月服和载人月球车全面进入初样研制阶段;字节豆包大模型团队提出稀疏模型架构UltraMem,推理成本最高可降低83%
  • 20240914 天翼物联 笔试
  • fun-transformer学习笔记-Task1——Transformer、Seq2Seq、Encoder-Decoder、Attention之间的关系
  • 数据结构------单向链表。
  • 从Sora到有言:3D视频生成技术的突破与应用
  • Docker中安装MySql方法
  • springboot和vue项目中加入支付宝沙盒
  • Pytest自动化测试框架关联/参数化实战
  • SQL Server 导入Excel数据
  • 基于单片机的常规肺活量SVC简单计算
  • LeetCode 热题 100
  • Synchronized 原理
  • DeepSeek R1完全本地部署实战教程01-课程大纲
  • 【Java 面试 八股文】Spring Cloud 篇
  • 鸿蒙中,UIAbility组件启动模式(3种分别是Singleton(单实例模式)Multiton(多实例模式)Specified(指定实例模式))
  • 如何学习Elasticsearch(ES):从入门到精通的完整指南
  • Java短信验证功能简单使用
  • vscode/cursor+godot C#中使用socketIO
  • SpringMVC新版本踩坑[已解决]
  • AUGUST的深度学习笔记(四,现代循环神经网络与注意力机制)
  • $符(前端)
  • 神经网络常见激活函数 9-CELU函数
  • CAS单点登录(第7版)10.多因素身份验证
  • 02.01、移除重复节点