网络子系统的实现-驱动与ip层部分

说明

  1. 本文内核基于4.19
  2. 本文内容基于网络子系统,所有中断、数据等均为网络相关,不再单独说明

中断

首先,我知晓的是linux对于数据(包括io\网络数据等)的处理不是采用轮询的方式,而是中断.

同时,由于进入中断时将会关闭中断响应导致系统不响应其他中断,所以为了能尽快完成中断.避免长期占用系统,linux又将中断分硬中断与软中断.

比如系统调用就是一个软中断,操作系统会先将希望执行的系统调用号以及参数传入寄存器或者栈中,然后使用汇编 int 0x80来调用0x80的软中断,从而使自己从用户态进入内核态

其中硬中断用于快速响应,因为其会占用单个cpu,(每个cpu都有自己的一套中断).

软中断用于处理那些不需要立即响应,但需要在内核中执行的复杂任务,如网络数据包的进一步处理、定时器的到期事件等。(软中断处理与其硬中断cpu绑定),软中断能被调度与嵌套

对于网络来说,整个处理逻辑为:

  1. 数据帧从外部到达网卡
  2. 网卡将帧DMA到内存中
  3. 网卡硬中断通知cpu
  4. cpu响应硬中断(仅仅修改一个寄存器)发出软中断
  5. ksoftirp内核线程处理软中断.调用网卡驱动注册的poll函数开始从内存中收包
  6. 数据帧被从ringbuffer中取下,保存在skb中,离开内核态
  7. 进入用户态,协议层开始处理skb中网络帧,处理完后数据被放入sokcet的接受队列中.
  8. 内核唤醒用户进程从socket中接受数据

让我们用更实际的例子来解释这个过程:

  1. 数据的敲门声 - 当数据到达,它先敲响了你的电脑网卡(NIC)的“门”。这就像邮递员把信件投进了邮箱。
  2. 直接内存访问(DMA) - 网卡不会让数据在门口等待,它会直接把数据搬运到电脑的内存里,就像是自动传送带一样,无需CPU插手。
  3. 中断信号响起 - 完成了搬运工作后,网卡会给CPU发个信号,说:“嘿,我刚放下了一些包裹,请查收。”这就是所谓的硬中断,它确保CPU立即注意到新数据的到来。
  4. 快速响应 - CPU听到这个信号后,会迅速做出反应,但它并不会马上停下来处理所有事情。相反,它只是简单地记录下来,并触发一个软中断,告诉系统稍后会有专门的处理程序来处理这些数据。
  5. 软中断处理 - 接下来,系统中的一个特殊助手,我们叫它ksoftirqd线程,会被唤醒来处理这个软中断。它会去检查内存里新来的数据,并开始做进一步的处理。
  6. 数据打包 - 这些数据会被封装进一个叫做skb的结构里,就像给邮件贴上标签,准备送往下一个目的地。
  7. 进入网络协议栈 - skb现在进入了网络协议栈,这里就像邮件中心,会根据不同的协议(比如TCP/IP)对数据进行分类和处理,然后将它们放入特定的队列中,等待被应用程序使用。
  8. 唤醒用户进程 - 最后,当数据准备好后,内核会唤醒正在等待数据的应用程序,告诉它们:“喂,你的数据来了,可以开始干活了。”

通过这种方式,Linux能够高效且有序地处理网络数据,确保你的电脑能快速响应并处理来自网络的每一个请求。

在我们分析网络包处理过程中,我们需要先目标,linux驱动\内核协议栈等模块在能接受网卡数据包之前,是需要做很多准备工作,比如软硬中断的注册,各个协议对应的处理函数,网卡设备子系统的初始化,网卡启动完成,当这些都准备好后,才能开始接受数据包.

内核硬中断注册

由网卡完成,在网卡注册驱动阶段讲解

内核软中断注册

内核启动阶段,软中断将会使用early_initcall(spawn_ksoftirqd);来完成注册,其将会注册thread_should_runthread_fn,前者用于判断当前是否能否允许软中断,后者用于调用具体的软中断函数,也就是run_ksoftirqd

通过,也将自己的进程名称改为ksoftirqd + cpuid

 [root@33-115 kernel]# ps aux  | grep ksoftirqd
root           9  0.0  0.0      0     0 ?        S    701   0:00 [ksoftirqd/0]
root          16  0.0  0.0      0     0 ?        S    701   0:00 [ksoftirqd/1]
root          21  0.0  0.0      0     0 ?        S    701   0:00 [ksoftirqd/2]
root          26  0.0  0.0      0     0 ?        S    701   0:00 [ksoftirqd/3]
...

实现代码如下:

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

顾名思义,smp表示多cpu,也就说明了,每个cpu维护一套独立的中断.换句话说,每个cpu都能单独响应一个中断请求

linux还通过标记来区分了不同的软中断,具体标记含义如下

enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,//用于处理定时器事件
	NET_TX_SOFTIRQ,//用于处理网络发送队列
	NET_RX_SOFTIRQ,//用于处理网络接收队列
	BLOCK_SOFTIRQ,// 用于块设备(如硬盘)的I/O操作
	IRQ_POLL_SOFTIRQ, //用于轮询模式下的中断处理
	TASKLET_SOFTIRQ, // 用于调度tasklets,一种进阶版的软中断,这里不做扩展
	SCHED_SOFTIRQ, //用于调度器的更新
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
			    numbering. Sigh! */
	RCU_SOFTIRQ,   // 用于处理Read-Copy-Update (RCU) 相关的事件,一种并发读写的控制机制 /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS
};

这样系统可以再不同的cpu上处理不同的软中断,但是linux也提供了一定程度上的同一软中断的并行,

例如,如果系统中有多个网络接口,每个网络接口可能在不同的CPU上触发硬中断,因此它们的NET_RX_SOFTIRQ可以在各自的CPU上并行处理。同样,如果多个块设备的I/O操作同时完成,BLOCK_SOFTIRQ也可以在不同的CPU上并行处理。但是同一网络设备,其软中断是无法并行的.

同时,软中断的执行是与硬中断绑定在同一个cpu上. 当一个硬中断触发时,它通常会在触发该中断的CPU上安排相应的软中断处理。

例如,如果一个网络接收中断(NET_RX_SOFTIRQ)在CPU0上触发,那么相关的软中断处理也会在CPU0上进行。这样做的好处是减少了跨CPU的通信和同步成本,提高了处理效率。

到这里,很明显我们需要在系统上注册NET_TX_SOFTIRQNET_RX_SOFTIRQ中断,我们看看内核是如何实现的.

网络子系统初始化

在网络子系统初始化过程中,会为每个CPU初始化softnet_data,也会为NET_TX_SOFTIRQNET_RX_SOFTIRQ注册处理函数.

linux使用subsys_initcall来初始化各个子系统,而网络子系统的初始化就是subsys_initcall(net)`

 *       This is called single threaded during boot, so no need
 *       to take the rtnl semaphore.
 */
static int __init net_dev_init(void)
{
......
	/*
	 *	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); // 为每个cpu创建 softnet_data,将会保存网卡驱动注册的poll函数, poll在NET_RX软中断处理程序中被调用,用于处理网络设备的数据包。

		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);
.......
	}
......
	open_softirq(NET_TX_SOFTIRQ, net_tx_action); // 注册NET_TX_SOFTIRQ 函数
	open_softirq(NET_RX_SOFTIRQ, net_rx_action); // 注册NET_RX_SOFTIRQ 函数
......
}

subsys_initcall(net_dev_init);

open的实现很简单.即为每个cpu下的数组填充内容.

到这里可以发现,每个cpu中每个类型的软中断对应的函数是唯一的,一个类型的软中断无法对应多个函数

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

到这里我们就实现了网络的软中断的注册.现在操作系统已经可以通过软中断来调用网络函数来处理数据包了(但是网络子系统处理函数还没有实现.这部分将在网卡驱动注册部分讲解.我们先继续向上层看.)

协议栈的注册

到这里.结果内核软中断NET_TX_SOFTIRQNET_RX_SOFTIRQ的处理.操作系统已经整理好了数据包.在ISO七层模型中,数据链路层(第二层)和物理层(第一层)的处理已经在NET_RX_SOFTIRQ软中断处理程序中完成。接下来,数据包将通过内核的网络协议栈进行处理,这涉及到第三层(网络层)和更高层的协议。也就是ip协议层

类似网络子系统的注册,内核也使用了fs_initcal->inet_init来完协议栈的注册,

ip协议是有ip_rcv()函数实现,TCP与UDP协议是由tcp_v4_rcv()udp_rcv()来实现.

通过inet_init将会

  1. 通过inet_add_protocoltcp_v4_rcv()udp_rcv()注册到inet_protos
  2. 通过dev_add_packip_rcv()函数注册到ptype_base

icmp igmp也是类似tcp和udp的注册到了inet_protos,这里忽略


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

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

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 int __init inet_init(void)
{
......
	/*
	 *	Add all the base protocols.
	 */

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

protocol为宏定义的协议编号,位于/usr/include/netinet/in.h


int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{
	if (!prot->netns_ok) {
		pr_err("Protocol %u is not namespace aware, cannot register.\n",
			protocol);
		return -EINVAL;
	}

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

dev_add_pack(&ip_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);
	spin_unlock(&ptype_lock);
}
EXPORT_SYMBOL(dev_add_pack);

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

ptype_head主要涉及到网络设备的包类型(packet type)处理。

packet_type结构体表示了一种特定类型的网络包处理方式,比如它可以关联到一个函数,当网络设备接收到符合该类型的包时就会调用这个函数。

在给定的代码中,ptype_head函数的作用是返回一个链表头指针,这个链表包含了所有注册了的packet_type处理程序,根据不同的条件,这个链表可能是一个包含所有类型包处理程序的通用链表,也可能是一个更具体的、只包含特定类型包处理程序的链表。

具体解释如下:

  • pt->type == htons(ETH_P_ALL)检查packet_type是否为ETH_P_ALL类型,这通常意味着它会接收所有类型的包。

    • 如果pt->dev(即网络设备)存在,那么返回&pt->dev->ptype_all,这意味着返回的是该设备上注册的所有包类型的链表头。
    • 如果pt->dev不存在,那么返回全局的&ptype_all链表头,这可能包含了系统级的所有包类型的处理程序。
  • 如果pt->type不是ETH_P_ALL,即它只对特定类型的包感兴趣:

    • 如果pt->dev存在,那么返回&pt->dev->ptype_specific,这意味着返回的是该设备上注册的特定包类型的链表头。
    • 如果pt->dev不存在,那么使用ntohs(pt->type)将网络字节序转换为主机字节序,并与PTYPE_HASH_MASK进行按位与操作来计算哈希值,然后返回&ptype_base[...]中的对应链表头,这里的ptype_base可能是一个数组,其中每个元素都是一个链表头,用于存储特定类型包的处理程序。

这里ip_packet_type的type为ETH_P_IP,表明只对ip包感兴趣,很明显其dev也不存在,所以ip_packet_type将会注册到ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];

当内核需要处理一个特定类型的包时,它会根据包的类型计算出在ptype_base中的位置,然后遍历该位置上的链表,调用链表中的每一个处理器来处理包。这种方法提供了良好的可扩展性和性能,尤其是在处理大量不同类型的网络包时。

看一下ip_rcv和udp_rcv等函数的代码,能看到很多协议的处理过程。例如,ip_rcv中会处理iptable netfilter过滤,udp_rcv中会判断socket接收队列是否满了,对应的相关内核参数是net.core.mmem_max和net.core.rmem_default。

网卡驱动的初始化

这里显卡品牌选择的是igb显卡。其驱动文件位于drivers/net/ethernet/intel/igb/igb_main.c

其操作函数定义位于

static struct pci_driver igb_driver = {
	.name     = igb_driver_name,
	.id_table = igb_pci_tbl,  -> 保存了此驱动支持的dev_id
	.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
};

注册函数igb_init_module,在module_init阶段被调用


/**
 *  igb_init_module - Driver Registration Routine
 *
 *  igb_init_module is the first routine called when the driver is
 *  loaded. All it does is register with the PCI subsystem.
 **/
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);

#ifdef CONFIG_IGB_DCA
	dca_register_notify(&dca_notifier);
#endif
	ret = pci_register_driver(&igb_driver);   <-注册驱动
	return ret;
}

module_init(igb_init_module);

pci_register_driver 执行完成后,linux内核就知道此驱动的相关信息。比如驱动名称,驱动启动probe函数

当linux识别到设备的时候,就会去调用其驱动的probe防范,这里就是igb_probe。从而完成网卡的初始化,使得网卡处于可用状态。

pci如何收到新入设备以及如何给新入设备查找驱动这部分我还没理解,跳过

网卡的probe

待完成,太过于复杂还没理解,目前只知道 网卡probe启动阶段会注册igb_netdev_ops用的是igb_netdev_ops变量,其中包含igb_open等函数,该函数在网卡启动的时候会被调用。

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, /* 执行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, /* 设置虚拟功能(VF)的MAC地址 */
    .ndo_set_vf_vlan = igb_ndo_set_vf_vlan, /* 设置VF的VLAN标签 */
    .ndo_set_vf_rate = igb_ndo_set_vf_bw, /* 设置VF的速率限制 */
    .ndo_set_vf_spoofchk = igb_ndo_set_vf_spoofchk, /* 设置VF的地址欺骗检查 */
    .ndo_set_vf_trust = igb_ndo_set_vf_trust, /* 设置VF的信任级别 */
    .ndo_get_vf_config = igb_ndo_get_vf_config, /* 获取VF配置 */
    .ndo_fix_features = igb_fix_features, /* 固定设备支持的功能 */
    .ndo_set_features = igb_set_features, /* 设置设备的功能 */
    .ndo_fdb_add     = igb_ndo_fdb_add, /* 添加转发数据库(FDB)条目 */
    .ndo_features_check = igb_features_check, /* 检查设备功能是否满足要求 */
    .ndo_setup_tc    = igb_setup_tc, /* 设置流量控制 */
};

还调用到了igb_alloc_q_vector。它注册了一个NAPI机制必需的poll函数,对于igb网卡驱动来说,这个函数就是igb_poll,代码如下所示。

static int igb_alloc_q_vector(struct igb_adapter *adapter,
			      int v_count, int v_idx,
			      int txr_count, int txr_idx,
			      int rxr_count, int rxr_idx)
{
......

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

......
	}

到这里,对于网卡来说,需要的函数基本已经注册完成。可以开始启动网卡了。

网卡的启动

网卡启动函数为igb_open ->__igb_open,我们来看看其实现

static int __igb_open(struct net_device *netdev, bool resuming)
{
	struct igb_adapter *adapter = netdev_priv(netdev);
	struct e1000_hw *hw = &adapter->hw;
	struct pci_dev *pdev = adapter->pdev;
	int err;
	int i;

	/* disallow open during test */
	if (test_bit(__IGB_TESTING, &adapter->state)) {
		WARN_ON(resuming);
		return -EBUSY;
	}

	if (!resuming)
		pm_runtime_get_sync(&pdev->dev);

	netif_carrier_off(netdev);

首先检查是否处于测试模式,如果是则退出

然后netif_carrier_off修改网络标识表明为表明网络连接尚未建立或已经断开,

	/* 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;

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

缓存队列的建立

来看看接受队列缓存是如何建立的

/**
 *  igb_setup_rx_resources - allocate Rx resources (Descriptors)
 *  @rx_ring: Rx descriptor ring (for a specific queue) to setup
 *
 *  Returns 0 on success, negative on failure
 **/
int igb_setup_rx_resources(struct igb_ring *rx_ring)
{
	struct device *dev = rx_ring->dev;
	int size;

	size = sizeof(struct igb_rx_buffer) * rx_ring->count;
	// 首先 建立了一个rx_ring->count * struct igb_rx_buffer 大小的 rx_buffer_info
	rx_ring->rx_buffer_info = vmalloc(size);
	if (!rx_ring->rx_buffer_info)
		goto err;

	/* Round up to nearest 4K */
	rx_ring->size = rx_ring->count * sizeof(union e1000_adv_rx_desc);
		// 然后建立一个dma内存区域,大小为4k对其的 rx_ring->count * sizeof(union e1000_adv_rx_desc);
	rx_ring->size = ALIGN(rx_ring->size, 4096);

   /* 描述符(Packet Descriptor)用来表达一个数据包在缓冲区内的地址以及数据包在NIC中的状态(是否有异常发生)。这是一个硬件相关的数据结构,由NIC去规定
	* 描述符分成了接收描述符 (rx descriptor) 和传输描述符 (tx descriptor) 接收描述符是一个用来描述网卡接收的数据缓冲区首地址和硬件用于存储包信息的数据结构
	* 这些字段在NIC接收到数据包之后通过DMA直接进行修改。
	* 描述符实际存放的位置在驱动分配的DMA内存中,在驱动的Open接口(即启用该设备的时候)进行分配
	* 对于igb网卡来说 描述符信息如下:
	* union e1000_adv_rx_desc {
	* 	struct {
	* 		__le64 pkt_addr;             /* Packet buffer address */	包含了指向数据包存储位置的物理地址,即rx_buffer的物理地址,用于DMA读取操作。
	* 		__le64 hdr_addr;             /* Header buffer address */	包含了指向头包存储位置的物理地址。
	* 	} read;
	* 	struct {
	* 		struct {
	* 			struct {
	* 				__le16 pkt_info;   /* RSS type, Packet type */
	* 				__le16 hdr_info;   /* Split Head, buf len */
	* 			} lo_dword;
	* 			union {
	* 				__le32 rss;          /* RSS Hash */
	* 				struct {
	* 					__le16 ip_id;    /* IP id */
	* 					__le16 csum;     /* Packet Checksum */
	* 				} csum_ip;
	* 			} hi_dword;
	* 		} lower;
	* 		struct {
	* 			__le32 status_error;     /* ext status/error */
	* 			__le16 length;           /* Packet length */
	* 			__le16 vlan;             /* VLAN tag */
	* 		} upper;
	* 	} wb;  /* writeback */
	* }; 
    */
	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;

err:
	vfree(rx_ring->rx_buffer_info);
	rx_ring->rx_buffer_info = NULL;
	dev_err(dev, "Unable to allocate memory for the Rx descriptor ring\n");
	return -ENOMEM;
}

从上述源码可以看到,实际上一个RingBufer的内部不是仅有一个环形队列数组,而是有两个

  1. igb_rx_buffer数组:这个数组是内核使用的,通过vzalloc申请的。
  2. e1000_adv_x_desc数组:这个数组是网卡硬件使用的,通过dma_alloc coheren分配。

这两个队列的作用在报文处理阶段会详细解释,这里略过

接着 igb_open使用igb_power_up_link来初始化硬件igb_configure以及配置(个人推测,这部分还看不懂)

	igb_power_up_link(adapter);

	/* before we allocate an interrupt, we must be ready to handle it.
	 * Setting DEBUG_SHIRQ in the kernel makes it fire an interrupt
	 * as soon as we call pci_request_irq, so we have to setup our
	 * clean_rx handler before we do so.
	 */
	igb_configure(adapter);

硬件中断的注册

然后igb_open开始注册硬中断

	err = igb_request_irq(adapter);
	if (err)
		goto err_req_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 */
		igb_free_all_tx_resources(adapter);
		igb_free_all_rx_resources(adapter);

		igb_clear_interrupt_scheme(adapter);
		err = igb_init_interrupt_scheme(adapter, false);
		if (err)
			goto request_done;

		igb_setup_all_tx_resources(adapter);
		igb_setup_all_rx_resources(adapter);
		igb_configure(adapter);
	}

	igb_assign_vector(adapter->q_vector[0], 0);
	// 是否支持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 */
		igb_reset_interrupt_capability(adapter);
		adapter->flags &= ~IGB_FLAG_HAS_MSI;
	}
	// 都不支持或者设置失败则注册正常中断
	err = request_irq(pdev->irq, igb_intr, IRQF_SHARED,
			  netdev->name, adapter);

	if (err)
		dev_err(&pdev->dev, "Error %d getting interrupt\n",
			err);

request_done:
	return err;
}

整体逻辑比较简单:

  1. 检查MSI-X支持:

    • 如果适配器标志IGB_FLAG_HAS_MSIX被设置,说明适配器支持MSI-X(Message-Signaled Interrupts eXtension)。MSI-X提供了更细粒度的中断处理能力,可以显著提高多队列或多核系统的性能。
  2. 请求MSI-X中断:

    • 调用igb_request_msix尝试设置MSI-X中断。如果成功,函数将返回0,中断设置完成,跳转至request_done标签处结束函数执行。
  3. MSI-X失败后的回退:

    • 如果igb_request_msix失败(即返回非零值),代码将释放所有发送和接收资源,清除当前的中断方案,并重新初始化中断方案,这次不使用MSI-X。
  4. 配置发送和接收资源:

    • 在回退后,重新设置所有发送和接收队列的资源,确保适配器准备好接收中断。
  5. 尝试MSI中断:

    • 如果适配器还支持MSI(但不是MSI-X),尝试请求MSI中断。如果MSI也请求失败,代码将继续回退到传统的共享中断模式。
  6. 请求传统中断:

    • 使用request_irq函数请求传统的PCI中断。如果请求成功,中断将被正确设置;否则,将在设备日志中记录错误。
  7. 错误处理和清理:

    • 如果在任何时候中断请求失败,错误码将被返回给调用者,可能需要进一步的错误处理或资源清理。

对于服务器网卡来说 MSI-X功能几乎是必须支持的。

MSI-X允许网络适配器为不同的队列(发送队列和接收队列)分配独立的中断向量,从而可以将中断处理分担到多个CPU核心上,提高系统整体的性能和响应能力。

我们来看看针对MSI-X中断做了哪些优化

static int igb_request_msix(struct igb_adapter *adapter)
{
	unsigned int num_q_vectors = adapter->num_q_vectors;
	struct net_device *netdev = adapter->netdev;
	int i, err = 0, vector = 0, free_vector = 0;
	// 注册一个其他中断,用于处理一些全局的或非队列特定的事件。这部分中断可能包括但不限于链路状态变化、硬件错误或其他需要全局处理的情况。
	err = request_irq(adapter->msix_entries[vector].vector,
			  igb_msix_other, 0, netdev->name, adapter);
	if (err)
		goto err_out;
	// 判断向量队列数量是否合法,后续可以通过ioctl修改队列大小
	if (num_q_vectors > MAX_Q_VECTORS) {
		num_q_vectors = MAX_Q_VECTORS;
		dev_warn(&adapter->pdev->dev,
			 "The number of queue vectors (%d) is higher than max allowed (%d)\n",
			 adapter->num_q_vectors, MAX_Q_VECTORS);
	}
	for (i = 0; i < num_q_vectors; i++) {
		struct igb_q_vector *q_vector = adapter->q_vector[i];

		vector++;
		// 给每个中断向量的设置其EITR寄存器地址,EITR寄存器允许驱动程序或操作系统设定一个“静默期”,在这个期间内,即使有新的数据包到达,适配器也不会产生中断,直到这个静默期结束后,才会生成下一个中断。
		// 这样一来,中断频率得到了有效控制,避免了不必要的中断,从而减少了CPU的上下文切换次数,提高了系统整体的效率和响应速度
		q_vector->itr_register = adapter->io_addr + E1000_EITR(vector);
		// 通过判断每个队列是否设置了接受缓存和读取缓存,来设置此队列支持的向量类型,同时设置此队列中断名称,可以通过ls /sys/class/net/eno1/queues 查看
		if (q_vector->rx.ring && q_vector->tx.ring)
			sprintf(q_vector->name, "%s-TxRx-%u", netdev->name,
				q_vector->rx.ring->queue_index);
		else if (q_vector->tx.ring)
			sprintf(q_vector->name, "%s-tx-%u", netdev->name,
				q_vector->tx.ring->queue_index);
		else if (q_vector->rx.ring)
			sprintf(q_vector->name, "%s-rx-%u", netdev->name,
				q_vector->rx.ring->queue_index);
		else
			sprintf(q_vector->name, "%s-unused", netdev->name);
		// 注册一个数据处理中断,用于处理数据包的接收和发送事件
		err = request_irq(adapter->msix_entries[vector].vector,
				  igb_msix_ring, 0, q_vector->name,
				  q_vector);
		if (err)
			goto err_free;
	}
	// 一些配置的修改 没看
	igb_configure_msix(adapter);
	return 0;

err_free:
	/* free already assigned IRQs */
	free_irq(adapter->msix_entries[free_vector++].vector, adapter);

	vector--;
	for (i = 0; i < vector; i++) {
		free_irq(adapter->msix_entries[free_vector++].vector,
			 adapter->q_vector[i]);
	}
err_out:
	return err;
}

可以看到,在MSI-X方式下,每个RX 、TX队列有独立的MSI-X中断,从网卡硬件中断的层面就可以设置让收到的包被不同的CPU处理。(可以通过irqbalance绑定中断与cpu

硬中断处理函数的实现

static irqreturn_t igb_msix_ring(int irq, void *data)
{
	struct igb_q_vector *q_vector = data;

	/* Write the ITR value calculated from the previous interrupt. */
	igb_write_itr(q_vector);     //记录硬件中断频率 

	napi_schedule(&q_vector->napi);

	return IRQ_HANDLED;
}

其中的igb_write_itr只记录硬件中断频率("Write the ITR value calculated from the previous interrupt." 这句注释意味着在处理完当前的中断后,函数会更新中断节拍率(Interrupt Throttle Rate, ITR)寄存器的值。这个值通常是基于前一次中断的信息计算得出的,旨在动态调整中断的频率,以优化系统性能。通过调整ITR值,可以减少在数据包处理空闲期的不必要的中断,从而降低CPU的中断处理开销。

然后通过napi_schrdule调度来处理此中断。napi_schrdule首先会判断napi是否可用,然后执行__napi_schedule

napi的实现机制还没看 后续补充

static inline void napi_schedule(struct napi_struct *n)
{
	if (napi_schedule_prep(n))
		__napi_schedule(n);
}

到实际业务处理函数__napi_schedule

/**
 * __napi_schedule - schedule for receive
 * @n: entry to schedule
 *
 * The entry's receive function will be scheduled to run.
 * Consider using __napi_schedule_irqoff() if hard irqs are masked.
 */
void __napi_schedule(struct napi_struct *n)
{
	unsigned long flags;
	// 关闭当前cpu的硬中断响应
	local_irq_save(flags);
	// 调用当前cpu的软中断来处理n,即napi接口提供的信息
	____napi_schedule(this_cpu_ptr(&softnet_data), n);
	// 打开当前cpu硬中断响应
	local_irq_restore(flags);
}
EXPORT_SYMBOL(__napi_schedule);

可以看到网络的硬中断处理逻辑确实很简单,直接填充当前cpu上的软中断所需信息就退出了硬中断

这里可以发现软中断处理和硬中断处理cpu一定在同一个上面

硬中断对软中断的调用逻辑

____napi_schedule中,

/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd,
				     struct napi_struct *napi)
{
	struct task_struct *thread;
	// 检查是否有专用内核线程来处理此中断,如果有则唤醒
	// 此线程由napi_kthread_create 创建 由dev_set_threaded 设置标志位
	if (test_bit(NAPI_STATE_THREADED, &napi->state)) {
		thread = READ_ONCE(napi->thread);
		if (thread) {
			wake_up_process(thread);
			return;
		}
	}
	// 如果没有则使用内核中断线程来处理
	// 首先将内核中,将napi的poll处理函数 添加到此中断注册的poll处理函数链表中,poll函数专门用户处理软中断数据包接收任务
	list_add_tail(&napi->poll_list, &sd->poll_list);
	// 接着,内核在硬中断被禁用的情况下触发了NET_RX_SOFTIRQ 类型的软中断
	__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

而对于NET_RX_SOFTIRQ类型的软中断处理逻辑非常简单,软中断计数器+1,然后使用or_softirq_pending给软中断标志位做或运算,设置软中断类型。

本质是对`irq_stat.__softirq_pending这个变量或运算,`__softirq_pending是一个位图(bitmap),其中的每一位对应于一种软中断类型。当某一种软中断被触发时,相应的位会被置位。这个变量是每个CPU(per-CPU)的,意味着每个CPU都有自己的__softirq_pending变量,这样可以并行处理不同类型的软中断,提高系统的响应能力和效率。

void __raise_softirq_irqoff(unsigned int nr)
{
	trace_softirq_raise(nr);
	or_softirq_pending(1UL << nr);
}

这里内核并没有直接调用软中断,而是修改了软中断的pending标志位,在进程调度的过程或者某些切换上下文阶段(内核的很多地方)都会通过do_softirq函数来拉起软中断,从而实现上下中断的隔离

软中断的处理逻辑

可以看到硬中断中对软中断的调用被是否支持NAPI走了两套不同逻辑:

  • ksoftirqd线程对软中断的处理
  • napi自己创建的线程对软中断的处理。

我们先看下ksoftirqd线程对软中断的处理,

ksoftirqd对软中断的处理

首先在前面《内核软中断注册》章节,我们知晓了内核启动阶段注册了ksoftirqd_should_run run_ksoftirqd,用于专门处理软中断

ksoftirqd_should_run用于判断是否需要运行软中断,而run_ksoftirqd的负责查找和运行对于软中断处理函数。

ksoftirqd_should_run很简单的就是检查被硬中断设置的标志位irq_stat.__softirq_pending的值,如果有则说明有软中断需要处理,则拉起run_ksoftirqd

static int ksoftirqd_should_run(unsigned int cpu)
{
	return local_softirq_pending();
}

run_ksoftirqd代码如下:

static void run_ksoftirqd(unsigned int cpu)
{
	// 关闭本地硬中断
	local_irq_disable();
	// 检查软中断标志位是否被设置
	if (local_softirq_pending()) {
		/*
		 * We can safely run softirq on inline stack, as we are not deep
		 * in the task stack here.
		 */  // 因为软硬中断都在一个cpu上,所以不需要切换stack
		// 执行软中断
		__do_softirq();
		// 打开本地硬中断
		local_irq_enable();
		// 重新调度当前cpu上任务
		cond_resched();
		return;
	}
	local_irq_enable();
}

到了软中断核心处理函数了

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
	unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
	unsigned long old_flags = current->flags;
	int max_restart = MAX_SOFTIRQ_RESTART;
	struct softirq_action *h;
	bool in_hardirq;
	__u32 pending;
	int softirq_bit;

	/*
	 * Mask out PF_MEMALLOC s current task context is borrowed for the
	 * softirq. A softirq handled such as network RX might set PF_MEMALLOC
	 * again if the socket is related to swap
	 */
	// 收回当前进程分配内存权限。避免递归调用内存分配引起的死锁
	current->flags &= ~PF_MEMALLOC;
	
	pending = local_softirq_pending(); 
	account_irq_enter_time(current); // 记录当前进入时间,后面会检查,避免软中断过长占用cpu

	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); // 禁用SOFTIRQ_OFFSET位置的软中断,避免这些软中断运行在本cpu中
	in_hardirq = lockdep_softirq_start();   // 标记正在处理软中断

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0); 				// 清空软中断标志位

	local_irq_enable();						// 打开硬中断

	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软中断数+1

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

	rcu_bh_qs();				// 不知道
	local_irq_disable();		// 禁用硬中断

	pending = local_softirq_pending();  
	if (pending) {                             // 再次判断是否有新的软中断
		if (time_before(jiffies, end) && !need_resched() &&		// 如果有且当前时间片未超时且不需要再次调度且重执行次数没有超标 则重新执行软中断
		    --max_restart)
			goto restart;
												// 否则唤醒ksoftirqd,当进程再次调度,避免软中断长期占用cpu
		wakeup_softirqd();
	}
							
	lockdep_softirq_end(in_hardirq);		// 表示退出软中断
	account_irq_exit_time(current);			// 记录退出时间
	__local_bh_enable(SOFTIRQ_OFFSET);		// 运行执行SOFTIRQ_OFFSET软中断
	WARN_ON_ONCE(in_interrupt());
	current_restore_flags(old_flags, PF_MEMALLOC);	// 运行分配内存
}

而软中断处理函数的注册已经在net_dev_init中实现了

open_softirq(NET_RX_SOFTIRQ, net_rx_action); // 注册NET_RX_SOFTIRQ 函数

对于NET_RX_SOFTIRQ类型的软中断,处理函数为net_rx_action

static __latent_entropy void net_rx_action(struct softirq_action *h)
{
	struct softnet_data *sd = this_cpu_ptr(&softnet_data);       // 当前cpu的softnet_data 记录了poll函数
	unsigned long time_limit = jiffies +
		usecs_to_jiffies(READ_ONCE(netdev_budget_usecs));         // 允许占用当前cpu的最大时间
	int budget = READ_ONCE(netdev_budget);						// 允许处理包数量的最大值
	LIST_HEAD(list);
	LIST_HEAD(repoll);

	local_irq_disable();		//禁用硬中断
	list_splice_init(&sd->poll_list, &list);					// 取出当前注册在当前cpu中的所有poll函数,保存到list中
	local_irq_enable();

	for (;;) {
		struct napi_struct *n;

		skb_defer_free_flush(sd);								// 干啥?

		if (list_empty(&list)) {								// 如果处理完所有数据则退出
			if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
				goto end;
			break;
		}

		n = list_first_entry(&list, struct napi_struct, poll_list);	 // 获取list中第一个napi_struct实例, 为什么不直接(struct napi_struct)list->poll_list
		budget -= napi_poll(n, &repoll);		 // 调用napi_poll处理数据,传入repoll用于可能的重新调度

		/* If softirq window is exhausted then punt.
		 * Allow this to run for 2 jiffies since which will allow
		 * an average latency of 1.5/HZ.
		 */
		// 如果软中断窗口耗尽或达到时间限制,退出循环
		if (unlikely(budget <= 0 ||
			     time_after_eq(jiffies, time_limit))) {
			sd->time_squeeze++;
			break;
		}
	}

	local_irq_disable();

	list_splice_tail_init(&sd->poll_list, &list);
	list_splice_tail(&repoll, &list);
	list_splice(&list, &sd->poll_list);
	if (!list_empty(&sd->poll_list))
		__raise_softirq_irqoff(NET_RX_SOFTIRQ);

	net_rps_action_and_irq_enable(sd);

end:
	__kfree_skb_flush();
}

来看看napi_poll是如何处理数据的

static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
	bool do_repoll = false;
	void *have;
	int work;

	list_del_init(&n->poll_list); // 首先将poll处理函数从当前节点剥离

	have = netpoll_poll_lock(n);	// 标记正在处理n这个数据的poll的owner为当前cpu,如果已经有cpu正在处理n,则将当前cpu进入cpu_relax()忙等待状态,直到可以成功获取锁为止

	work = __napi_poll(n, &do_repoll);  // 处理数据,相关信息已经保存在n中

	if (do_repoll)                 // 如果未执行完,就将当前poll处理函数加入当前节点
		list_add_tail(&n->poll_list, repoll);

	netpoll_poll_unlock(have);

	return work;
}

__napi_poll则会调度n->poll

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

而在网卡的probe阶段,也就是网卡启动阶段,就已经通过netif_napi_addigb_poll注册到了struct napi_struct

	netif_napi_add(adapter->netdev, &q_vector->napi,
		       igb_poll, 64);

自定义进程对软中断的处理逻辑

如果设置NAPI_STATE_THREADED标志为,内核将唤醒napi->thread来处理网络软中断而非唤醒ksoftirpd

napi->threadnapi_kthread_create生成

static int napi_kthread_create(struct napi_struct *n)
{
	int err = 0;

	/* Create and wake up the kthread once to put it in
	 * TASK_INTERRUPTIBLE mode to avoid the blocked task
	 * warning and work with loadavg.
	 */
	n->thread = kthread_run(napi_threaded_poll, n, "napi/%s-%d",
				n->dev->name, n->napi_id);
	if (IS_ERR(n->thread)) {
		err = PTR_ERR(n->thread);
		pr_err("kthread_run failed with err %d\n", err);
		n->thread = NULL;
	}

	return err;
}

而在网卡的probe阶段,也就是网卡启动阶段,就已经通过netif_napi_addigb_poll注册到了struct napi_struct


	netif_napi_add(adapter->netdev, &q_vector->napi,
		       igb_poll, 64);

网卡驱动对网络帧的处理

所以NET_RX_SOFTIRQ软中断中,最终将执行igb_poll

static int igb_poll(struct napi_struct *napi, int budget)
{
	struct igb_q_vector *q_vector = container_of(napi,
						     struct igb_q_vector,
						     napi);
	bool clean_complete = true;
	int work_done = 0;

#ifdef CONFIG_IGB_DCA
	if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED)
		igb_update_dca(q_vector);
#endif
	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);

		work_done += cleaned;
		if (cleaned >= budget)
			clean_complete = false;
	}

	/* If all work not completed, return budget and keep polling */
	if (!clean_complete)
		return budget;

	/* If not enough Rx work done, exit the polling mode */
	napi_complete_done(napi, work_done);
	igb_ring_irq_enable(q_vector);

	return 0;
}

而对于igb_poll来说,重点工作是对tx.ring和rx.ring队列的处理

	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_clean_rx_irq的实现

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结构,即socket buffer,用于存储接收到的数据包。
	unsigned int total_bytes = 0, total_packets = 0;  //累计处理的数据字节数和数据包数
	u16 cleaned_count = igb_desc_unused(rx_ring);  //追踪未使用缓冲队列数量,以便批量重新填充接收缓冲区

#ifdef CONFIG_LOONGARCH
	igb_check_weird_hang(rx_ring);
#endif

	while (likely(total_packets < budget)) {     
		union e1000_adv_rx_desc *rx_desc;		// 接受描述符
		struct igb_rx_buffer *rx_buffer;		//接受缓冲
		unsigned int size;

		/* return some buffers to hardware, one at a time is too slow */
		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)											// 包为0 则说明没有后续包
			break;

		/* This memory barrier is needed to keep us from reading
		 * any other fields out of the rx_desc until we know the
		 * descriptor has been written back
		 */
		dma_rmb();								// dma的读内存屏蔽Read Memory Barrier

		rx_buffer = igb_get_rx_buffer(rx_ring, size);		// 从ex_ring中接受size长度并保存到rx_buffer

		/* retrieve a buffer from the ring */
		if (skb)									// 如果skb(socket buffer)存在,则将rx_buffer保存到skb中
			igb_add_rx_frag(rx_ring, rx_buffer, skb, size);
		else if (ring_uses_build_skb(rx_ring))		// skb不存在  说明为报文头节点,
													// 如果队列使用了build_skb方法,则使用igb_build_skb创建队列
			skb = igb_build_skb(rx_ring, rx_buffer, rx_desc, size);
		else										// 否则使用igb_construct_skb状况skb
			skb = igb_construct_skb(rx_ring, rx_buffer,	rx_desc, size);

		/* exit if we failed to retrieve a buffer */
		if (!skb) {									// 如果skb创建失败
			rx_ring->rx_stats.alloc_failed++;		// skb alloc 失败计数+1
			rx_buffer->pagecnt_bias++;				// pagecnt +1
			break;
		}

		igb_put_rx_buffer(rx_ring, rx_buffer);		// 从rx_ring中摘取rx_buffer并标记对于内存区域为释放或者重用
		cleaned_count++;

		/* fetch next buffer in frame if non-eop */
		if (igb_is_non_eop(rx_ring, rx_desc))		// 如果数据包非结束包则继续
			continue;

		/* verify the packet layout is correct */
		if (igb_cleanup_headers(rx_ring, rx_desc, skb)) {	// 校验数据包,失败则丢弃已经缓存的信息
			skb = NULL;
			continue;
		}

		/* probably a little skewed due to removing CRC */
		total_bytes += skb->len;							// 更新处理的字节总数

		/* populate checksum, timestamp, VLAN, and protocol */
		igb_process_skb_fields(rx_ring, rx_desc, skb);		// 更新skb内容,比如校验码,时间戳,vlanid和协议号等

		napi_gro_receive(&q_vector->napi, skb);				//GRO的处理,搜索了下是数据包的拼接,小包拼接为大包

		/* reset skb pointer */
		skb = NULL;

		/* update budget accounting */
		total_packets++;									// 包处理数+1
	}

	/* place incomplete frames back on ring for completion */
	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_clean_rx_irq主要从通过rx_desc的信息在rx_ring中摘取一定数量的rx_buffer,然后保存到rx_ring->skb中,并保证rx_ring->skb是一个完整的数据包和rx_ring的完整性。

小结igb_clean_rx_irq的处理逻辑:

  1. 处理数据包

    • 函数首先从rx_ring中读取DMA描述符(rx_desc),解析出数据包的大小,然后从接收队列中取出对应的rx_buffer
    • 如果skb(socket buffer)已经存在,意味着这是数据包的一个分片,将rx_buffer的内容添加到skb中;如果skb不存在,这可能是数据包的第一个分片,将创建一个新的skb
    • 通过igb_add_rx_fragigb_build_skbigb_construct_skb等函数,将rx_buffer中的数据附加或构建到skb中,形成完整的数据包。
  2. 资源管理

    • igb_put_rx_buffer函数用于处理rx_buffer,根据情况将其标记为可重用或释放,同时更新cleaned_count,追踪已经处理的描述符数量。
    • cleaned_count达到阈值IGB_RX_BUFFER_WRITE时,调用igb_alloc_rx_buffers函数,批量重新填充接收队列,确保接收队列中有足够的空闲缓冲区来接收新的数据包。
  3. 数据包完整性与统计

    • 通过igb_cleanup_headers函数验证数据包的完整性,如果发现数据包不完整或损坏,将丢弃当前skb,并继续处理下一个数据包。
    • 使用igb_process_skb_fields函数更新skb的元数据,如校验和、时间戳、VLAN标签等。
    • 通过napi_gro_receive函数进行GRO(Generic Receive Offload)处理,可能包括数据包的合并,以减少上层协议栈的处理负担。
    • 更新统计信息,包括处理的数据包数和字节数,这有助于监控网络接口的性能。
  4. 循环与预算

    • 循环处理数据包,直到达到预算budgetrx_ring中没有更多的数据包可处理。预算机制有助于限制软中断处理时间,避免影响系统响应性。

igb_clean_rx_irq的最后napi_gro_receive,

gro_result_t napi_gro_receive	(struct napi_struct *napi, struct sk_buff *skb)
{
	skb_mark_napi_id(skb, napi);  // 在skb上标记napi信息,用于追溯此skb来源,看源码也是给skb-napi指向了napi
	trace_napi_gro_receive_entry(skb);  // 调试相关 没看懂

	skb_gro_reset_offset(skb);		//gro相关

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

dev_gro_receive会尝试将skb中数据合并。便于上层协议处理。

然后将结果传递给napi_skb_finish,

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;

	case GRO_DROP:
		kfree_skb(skb);
		break;

	case GRO_MERGED_FREE:
		if (NAPI_GRO_CB(skb)->free == NAPI_GRO_FREE_STOLEN_HEAD)
			napi_skb_free_stolen_head(skb);
		else
			__kfree_skb(skb);
		break;

	case GRO_HELD:
	case GRO_MERGED:
	case GRO_CONSUMED:
		break;
	}

	return ret;
}

而在netif_receive_skb_internal将对skb做一些调度上的优化

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

	net_timestamp_check(READ_ONCE(netdev_tstamp_prequeue), skb);  // 尝试打上时间戳,但具体是否打上时间戳取决于全局变量 netdev_tstamp_prequeue 的值,这通常用于需要精确时间信息的网络应用程序,如VoIP或实时流媒体。

	if (skb_defer_rx_timestamp(skb))		// 检查是否需要延迟处理时间戳,如果是则标记本次处理完成。由于skb的来源是rx.ring->skb 所以这里并不会丢弃此包。
		return NET_RX_SUCCESS;

	rcu_read_lock();
#ifdef CONFIG_RPS							// RPS是一种优化技术,用于将数据包直接路由到特定的CPU核心进行处理,以提高多核系统的网络处理能力。
	if (static_key_false(&rps_needed)) {
		struct rps_dev_flow voidflow, *rflow = &voidflow;
		int cpu = get_rps_cpu(skb->dev, skb, &rflow);		// 查询使用有是个处理此skb的cpu
		if (cpu >= 0) {
			ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);  // 有则加入目标cpu的队列中
			rcu_read_unlock();
			return ret;
		}
	}
#endif
	ret = __netif_receive_skb(skb);		// 否则使用标准处理函数。
	rcu_read_unlock();
	return ret;
}

``


static int __netif_receive_skb(struct sk_buff *skb)
{
	int ret;
	// 是否使用PFMEMALLOC技术,这里忽略,还没看到这块
	if (sk_memalloc_socks() && skb_pfmemalloc(skb)) {
		unsigned int noreclaim_flag;
		/*
		 * PFMEMALLOC skbs are special, they should
		 * - be delivered to SOCK_MEMALLOC sockets only
		 * - stay away from userspace
		 * - have bounded memory usage
		 *
		 * Use PF_MEMALLOC as this saves us from propagating the allocation
		 * context down to all allocation sites.
		 */
		noreclaim_flag = memalloc_noreclaim_save();
		ret = __netif_receive_skb_one_core(skb, true);
		memalloc_noreclaim_restore(noreclaim_flag);
	} else
		ret = __netif_receive_skb_one_core(skb, false);

	return ret;
}

__netif_receive_skb_one_core将会检查数据包的处理程序,并调用其func处理skb

static int __netif_receive_skb_one_core(struct sk_buff *skb, bool pfmemalloc)
{
	struct net_device *orig_dev = skb->dev;
	struct packet_type *pt_prev = NULL;
	int ret;

	ret = __netif_receive_skb_core(&skb, pfmemalloc, &pt_prev);
	if (pt_prev)
		ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
	return ret;
}

···

static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
				    struct packet_type **ppt_prev)
{
	struct packet_type *ptype, *pt_prev;
	rx_handler_func_t *rx_handler;
	struct sk_buff *skb = *pskb;
	struct net_device *orig_dev;
	bool deliver_exact = false;
	int ret = NET_RX_DROP;
	__be16 type;

	net_timestamp_check(!READ_ONCE(netdev_tstamp_prequeue), skb); //如果前面netif_receive_skb_internal没打上时间戳,这里就打上接收的时间戳了

	trace_netif_receive_skb(skb);

	orig_dev = skb->dev;
	// 重置头部字段内容
	skb_reset_network_header(skb);
	if (!skb_transport_header_was_set(skb))
		skb_reset_transport_header(skb);
	skb_reset_mac_len(skb);

	pt_prev = NULL;

another_round:
	skb->skb_iif = skb->dev->ifindex;
	// 当前cpu 网络软中断计数+1
	__this_cpu_inc(softnet_data.processed);
	// xdp协议处理
	if (static_branch_unlikely(&generic_xdp_needed_key)) {
		int ret2;

		preempt_disable();
		ret2 = do_xdp_generic(rcu_dereference(skb->dev->xdp_prog), skb);
		preempt_enable();

		if (ret2 != XDP_PASS) {
			ret = NET_RX_DROP;
			goto out;
		}
		skb_reset_mac_len(skb);
	}
	// 8021q 或者ad为vlan协议,如果支持则剥离vlan信息
	if (skb->protocol == cpu_to_be16(ETH_P_8021Q) ||
	    skb->protocol == cpu_to_be16(ETH_P_8021AD)) {
		skb = skb_vlan_untag(skb);
		if (unlikely(!skb))
			goto out;
	}
	// tc 流控,是否设置跳过,如果是则跳过流控
	if (skb_skip_tc_classify(skb))
		goto skip_classify;
	// pfmemalloc 内存管理的处理逻辑
	if (pfmemalloc)
		goto skip_taps;
	// 检查注册的对所有包的协议栈处理程序,获得后在下一次遍历中执行
	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_branch_unlikely(&ingress_needed_key)) {
		skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev);
		if (!skb)
			goto out;

		if (nf_ingress(skb, &pt_prev, &ret, orig_dev) < 0)
			goto out;
	}
#endif
	skb_reset_tc(skb); // 清空流控
skip_classify:
	if (pfmemalloc && !skb_pfmemalloc_protocol(skb))
		goto drop;
	// 如果还存在vlan,如果有协议处理支持vlan,则调用(比如vlan vpn?),如果没有则再次回到another_round剥离vlan
	if (skb_vlan_tag_present(skb)) {
		// 是否有未处理的pt_prev则处理,因为的遍历函数list_for_each_entry_rcu最后一个pt_prev不会处理
		if (pt_prev) {
			ret = deliver_skb(skb, pt_prev, orig_dev);
			pt_prev = NULL;
		}
		if (vlan_do_receive(&skb))  // 如果VLAN处理失败,则重新循环处理数据包,将会先剥离掉vlan后继续处理
			goto another_round;
		else if (unlikely(!skb))
			goto out;
	}
	// 检查是否挂载了设备下的rx处理函数
	rx_handler = rcu_dereference(skb->dev->rx_handler);
	if (rx_handler) {
		// 是否有未处理的pt_prev则处理.(因为的遍历函数list_for_each_entry_rcu最后一个pt_prev不会处理)
		if (pt_prev) {
			ret = deliver_skb(skb, pt_prev, orig_dev);
			pt_prev = NULL;
		}
		switch (rx_handler(&skb)) {
		case RX_HANDLER_CONSUMED:	 // 如果数据包被消费,则成功返回
			ret = NET_RX_SUCCESS;
			goto out;
		case RX_HANDLER_ANOTHER:	// 如果需要再次处理,则重新循环
			goto another_round;
		case RX_HANDLER_EXACT:		// 如果需要精确匹配,则标记deliver_exact
			deliver_exact = true;
		case RX_HANDLER_PASS:		// 如果pass,则继续执行
			break;
		default:
			BUG();
		}
	}
	// 如果包依然带有vlan标记(不晓得处理的内容是啥意思
	if (unlikely(skb_vlan_tag_present(skb))) {
		if (skb_vlan_tag_get_id(skb))
			skb->pkt_type = PACKET_OTHERHOST;
		/* Note: we might in the future use prio bits
		 * and set skb->priority like in vlan_do_receive()
		 * For the time being, just ignore Priority Code Point
		 */
		skb->vlan_tci = 0;
	}
	// 拿到协议类型,这个在igb_process_skb_fields的igb_clean_rx_irq被设置
	type = skb->protocol;
	
	// 检查是否需要再次调用协议栈处理函数处理此包  在rx_handler中被标记
	/* deliver only exact match when indicated */
	if (likely(!deliver_exact)) {
		deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
				       &ptype_base[ntohs(type) &
						   PTYPE_HASH_MASK]);
	}

	deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
			       &orig_dev->ptype_specific);

	if (unlikely(skb->dev != orig_dev)) {
		deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
				       &skb->dev->ptype_specific);
	}
	// 如果有最后一个协议栈处理程序,则放入回参中,准备由上层调用
	if (pt_prev) {
		if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))
			goto drop;
		*ppt_prev = pt_prev; //
	} else {
drop:	
		// 如果没有精确匹配标记,则增加丢包计数
		if (!deliver_exact)
			atomic_long_inc(&skb->dev->rx_dropped);
		else
		// 如果有精确匹配,增加无handler计数
			atomic_long_inc(&skb->dev->rx_nohandler);
		kfree_skb(skb);
		/* Jamal, now you will not able to escape explaining
		 * me how you were going to use this. :-)
		 */
		ret = NET_RX_DROP;
	}

out:
	/* The invariant here is that if *ppt_prev is not NULL
	 * then skb should also be non-NULL.
	 *
	 * Apparently *ppt_prev assignment above holds this invariant due to
	 * skb dereferencing near it.
	 */
	*pskb = skb;
	return ret;
}

可以发现__netif_receive_skb_core的核心是遍历ptype_allskb->dev->ptype_all 找到协议栈的处理函数并执行。

ptype_allskb->dev->ptype_all的内容,对于ip\tcp\udp协议来说,其在内核init阶段,被inet_init函数中的dev_add_pack注册到ptype_all或者skb->dev->ptype_all中,也就是上文的<协议栈注册章节>

自此。数据包进入了ip层,也就是OSI的第三层处理了。

有人说这里wireshark工具的抓包函数,就是挂载ptype_all上。所以它在二层就抓取了所有的包

到这里网卡核心内容基本启动完成了。在igb_open后续内容中,主要是打开一些开关、清理标志位的工作。我们这里略过。

结果这一系列初始化处理好,网卡启动成功,可以开始准备接受数据了

IP层的处理逻辑

/*
 * IP receive entry point
 */
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
	   struct net_device *orig_dev)
{
	struct net *net = dev_net(dev);

	skb = ip_rcv_core(skb, net);
	if (skb == NULL)
		return NET_RX_DROP;
	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
		       net, NULL, skb, dev, NULL,
		       ip_rcv_finish);
}

嗯 看到了内核常用机制 HOOK -> NF_HOOK

NF_HOOK

这里的NF_HOOK是一个钩子函数,它就是我们日常工作中经常用到的iptables\netfilter过滤。

如果你有很多或者很复杂的netfilter规则,会在这里消耗过多的CPU资源,加大网络延迟。

另外,使用NF_HOOK在源码中搜索可以搜到很多flter的过滤点,想深入研究netilter 可以从搜索NF HOOK的这些引用处入手。通过搜索结果可以看到,主要是在IP、ARP等层实现的。

可以看到,ip apr层的很多处理都是由其实现的

➜  net git:(linux-next)grep -r 'NF_HOOK' ./*
./bridge/br_forward.c:  return NF_HOOK(NFPROTO_BRIDGE, NF_BR_POST_ROUTING,
./bridge/br_forward.c:  NF_HOOK(NFPROTO_BRIDGE, br_hook,
./bridge/br_netfilter_ipv6.c:   NF_HOOK(NFPROTO_IPV6, NF_INET_PRE_ROUTING, state->net, state->sk, skb,
./bridge/br_stp_bpdu.c: NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_OUT,
./bridge/br_netfilter_hooks.c:  NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, state->net, state->sk, skb,
./bridge/br_netfilter_hooks.c:  NF_HOOK(pf, NF_INET_FORWARD, state->net, NULL, skb,
./bridge/br_netfilter_hooks.c:  NF_HOOK(NFPROTO_ARP, NF_ARP_FORWARD, state->net, state->sk, skb,
./bridge/br_netfilter_hooks.c:  NF_HOOK(pf, NF_INET_POST_ROUTING, state->net, state->sk, skb,
./bridge/br_input.c:    return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN,
./bridge/br_input.c:            if (NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN,
./bridge/br_input.c:            NF_HOOK(NFPROTO_BRIDGE, NF_BR_PRE_ROUTING,
./bridge/br_multicast.c:                NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_OUT,
./decnet/dn_nsp_in.c:   return NF_HOOK(NFPROTO_DECNET, NF_DN_LOCAL_IN,
./decnet/dn_neigh.c:    return NF_HOOK(NFPROTO_DECNET, NF_DN_POST_ROUTING,
./decnet/dn_neigh.c:    return NF_HOOK(NFPROTO_DECNET, NF_DN_POST_ROUTING,
./decnet/dn_neigh.c:    return NF_HOOK(NFPROTO_DECNET, NF_DN_POST_ROUTING,
./decnet/dn_route.c:    return NF_HOOK(NFPROTO_DECNET, NF_DN_PRE_ROUTING,
./decnet/dn_route.c:    return NF_HOOK(NFPROTO_DECNET, NF_DN_PRE_ROUTING,
./decnet/dn_route.c:                    return NF_HOOK(NFPROTO_DECNET, NF_DN_HELLO,
./decnet/dn_route.c:                    return NF_HOOK(NFPROTO_DECNET, NF_DN_ROUTE,
./decnet/dn_route.c:                    return NF_HOOK(NFPROTO_DECNET, NF_DN_HELLO,
./decnet/dn_route.c:                    return NF_HOOK(NFPROTO_DECNET, NF_DN_HELLO,
./decnet/dn_route.c:    return NF_HOOK(NFPROTO_DECNET, NF_DN_LOCAL_OUT,
./decnet/dn_route.c:    return NF_HOOK(NFPROTO_DECNET, NF_DN_FORWARD,
./ipv4/arp.c:   NF_HOOK(NFPROTO_ARP, NF_ARP_OUT,
./ipv4/arp.c:   return NF_HOOK(NFPROTO_ARP, NF_ARP_IN,
./ipv4/raw.c:   err = NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
./ipv4/ip_forward.c:    return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
./ipv4/ipmr.c:  NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
./ipv4/xfrm4_output.c:  return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
./ipv4/ip_input.c:      return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
./ipv4/ip_input.c:      return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
./ipv4/ip_input.c:      NF_HOOK_LIST(NFPROTO_IPV4, NF_INET_PRE_ROUTING, net, NULL,
./ipv4/ip_output.c:                             NF_HOOK(NFPROTO_IPV4, NF_INET_POST_ROUTING,
./ipv4/ip_output.c:                     NF_HOOK(NFPROTO_IPV4, NF_INET_POST_ROUTING,
./ipv4/ip_output.c:     return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
./ipv4/ip_output.c:     return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
./ipv4/xfrm4_input.c:   NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
./ipv6/xfrm6_input.c:   NF_HOOK(NFPROTO_IPV6, NF_INET_PRE_ROUTING,
./ipv6/raw.c:   err = NF_HOOK(NFPROTO_IPV6, NF_INET_LOCAL_OUT, net, sk, skb,
./ipv6/ip6mr.c: return NF_HOOK(NFPROTO_IPV6, NF_INET_FORWARD,
./ipv6/ip6_input.c:     return NF_HOOK(NFPROTO_IPV6, NF_INET_PRE_ROUTING,
./ipv6/ip6_input.c:     NF_HOOK_LIST(NFPROTO_IPV6, NF_INET_PRE_ROUTING, net, NULL,
./ipv6/ip6_input.c:     return NF_HOOK(NFPROTO_IPV6, NF_INET_LOCAL_IN,
./ipv6/ndisc.c: err = NF_HOOK(NFPROTO_IPV6, NF_INET_LOCAL_OUT,
./ipv6/ip6_output.c:                            NF_HOOK(NFPROTO_IPV6, NF_INET_POST_ROUTING,
./ipv6/ip6_output.c:    return NF_HOOK_COND(NFPROTO_IPV6, NF_INET_POST_ROUTING,
./ipv6/ip6_output.c:            return NF_HOOK(NFPROTO_IPV6, NF_INET_LOCAL_OUT,
./ipv6/ip6_output.c:    return NF_HOOK(NFPROTO_IPV6, NF_INET_FORWARD,
./ipv6/xfrm6_output.c:  return NF_HOOK_COND(NFPROTO_IPV6, NF_INET_POST_ROUTING,
./ipv6/mcast.c: err = NF_HOOK(NFPROTO_IPV6, NF_INET_LOCAL_OUT,
./ipv6/mcast.c: err = NF_HOOK(NFPROTO_IPV6, NF_INET_LOCAL_OUT,
./netfilter/ipvs/ip_vs_xmit.c:          NF_HOOK(pf, NF_INET_LOCAL_OUT, cp->ipvs->net, NULL, skb,
./netfilter/ipvs/ip_vs_xmit.c:          NF_HOOK(pf, NF_INET_LOCAL_OUT, cp->ipvs->net, NULL, skb,

ip_rcv最终会调用 ip_rcv_finish


static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct net_device *dev = skb->dev;
	int ret;

	/* if ingress device is enslaved to an L3 master device pass the
	 * skb to its handler for processing
	 */
	skb = l3mdev_ip_rcv(skb);	// 如果本机是绑定的从设备,那么执行这里
	if (!skb)
		return NET_RX_SUCCESS;

	ret = ip_rcv_finish_core(net, sk, skb, dev);	// ip层处理函数
	if (ret != NET_RX_DROP)
		ret = dst_input(skb);	// 如果ip层处理完后还需要继续处理,则交由ip上层处理函数
	return ret;
}

ip层执行完后的上层处理函数dst_input,最终执行的记录在skb中的input函数

/* Input packet from network to transport.  */
static inline int dst_input(struct sk_buff *skb)
{
	return skb_dst(skb)->input(skb);
}

来先看看ip层做了什么

static int ip_rcv_finish_core(struct net *net, struct sock *sk,
			      struct sk_buff *skb, struct net_device *dev)
{
	const struct iphdr *iph = ip_hdr(skb);
	struct rtable *rt;
	int err;
	/* sysctl_ip_early_demux 是否开启早期解复用
	* 早期解复用(Early Demultiplexing)是网络编程和操作系统内核处理网络数据包的一种优化技术,旨在提高数据包处理的效率和性能。在网络通信中,数据包在到达主机后,需要根据其目的端口号或协议类型被分发到正确的应用程序或协议栈进行处理。这个过程称为解复用(Demultiplexing)。

	*传统上,数据包的解复用是在数据包完成整个接收过程后进行的。这意味着数据包首先被接收并存储在内存中,然后根据其头部信息决定如何处理。然而,这种方法可能导致额外的内存复制和处理器开销,特别是在高性能服务器或网络设备中,每毫秒的延迟都可能影响到系统的整体吞吐量。

	*早期解复用的目的是尽可能早地确定数据包的去向,从而减少不必要的数据拷贝和处理步骤。具体来说,当数据包刚刚到达网络接口时,内核就开始解析其头部信息,试图在数据包完全接收之前就识别出其目的协议或端口。一旦识别出数据包的类型,内核可以立即将其直接发送到相应的协议栈或应用程序,而不必等待数据包的其余部分被接收或存储在内存中。
	*/
		// 是否打开了早期解复用
	if (READ_ONCE(net->ipv4.sysctl_ip_early_demux) &&
		// 包没有带目标路由信息
	    !skb_dst(skb) &&
		// 包没有指定套接字
	    !skb->sk &&
		// 包不是一个ip分片
	    !ip_is_fragment(iph)) {
		// 如果都满足则直接解析头部
		switch (iph->protocol) {
		case IPPROTO_TCP:
			if (READ_ONCE(net->ipv4.sysctl_tcp_early_demux)) {
				tcp_v4_early_demux(skb);

				/* must reload iph, skb->head might have changed */
				iph = ip_hdr(skb);
			}
			break;
		case IPPROTO_UDP:
			if (READ_ONCE(net->ipv4.sysctl_udp_early_demux)) {
				err = udp_v4_early_demux(skb);
				if (unlikely(err))
					goto drop_error;

				/* must reload iph, skb->head might have changed */
				iph = ip_hdr(skb);
			}
			break;
		}
	}

	/*
	* 检查skb是否已经有关联的路由信息
	* dst字段是一个指向struct dst_entry结构的指针,这个结构体包含了完整的路由条目信息,包括但不限于:
	* 下一跳地址:在某些情况下,dst结构会包含下一跳的IP地址,但这取决于路由条目的类型和网络配置。
	* 输出网络设备:dst结构会告诉内核应该使用哪个网络设备来发送数据包。
	* 输入/输出函数:dst结构包含input和output函数指针,这些函数会在数据包接收和发送时被调用,执行相应的操作,如地址转换(NAT)、错误处理、队列调度等。
	* 路由类型:dst结构包含了路由条目的类型,如单播(unicast)、广播(broadcast)、多播(multicast)或不可达(unreachable)。
	* 路由缓存信息:dst结构可能是一个路由缓存条目,这样内核就可以快速重用相同的路由信息,避免重复的路由查找。
	* 错误码:如果路由查找失败,dst结构会包含一个错误码,表示为什么路由查找失败。
	*/

	if (!skb_valid_dst(skb)) {
		// 如果没有,它将调用ip_route_input_noref()函数来查找或计算正确的路由信息。
		// 这个函数会根据数据包的目的地址、源地址、tos字段以及接收数据包的网络设备(dev),来确定数据包应该如何被处理。比如 nat,forward等等,提供一个标记(没看实现代码,猜测),并更新skb
		err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
					   iph->tos, dev);
		// 如果路由查找失败(即err不为零),数据包将被丢弃,因为没有有效的路由信息意味着内核不知道如何正确处理这个数据包。
		if (unlikely(err))
			goto drop_error;
	}

#ifdef CONFIG_IP_ROUTE_CLASSID
	if (unlikely(skb_dst(skb)->tclassid)) {	 // 更新分类统计信息
		struct ip_rt_acct *st = this_cpu_ptr(ip_rt_acct);
		u32 idx = skb_dst(skb)->tclassid;
		st[idx&0xFF].o_packets++;  // 输出包计数
		st[idx&0xFF].o_bytes += skb->len; // 输出字节计数
		st[(idx>>16)&0xFF].i_packets++;   // 输入包计数
		st[(idx>>16)&0xFF].i_bytes += skb->len;  // 输入字节计数
	}
#endif
	// 不晓得干啥
	if (iph->ihl > 5 && ip_rcv_options(skb, dev))
		goto drop;
	
	// 获取数据包的路由表项
	rt = skb_rtable(skb);
	if (rt->rt_type == RTN_MULTICAST) {
		__IP_UPD_PO_STATS(net, IPSTATS_MIB_INMCAST, skb->len);	 // 更新多播统计
	} else if (rt->rt_type == RTN_BROADCAST) {
		__IP_UPD_PO_STATS(net, IPSTATS_MIB_INBCAST, skb->len);	 // 更新广播统计
	} else if (skb->pkt_type == PACKET_BROADCAST ||
		   skb->pkt_type == PACKET_MULTICAST) {
		 // 检查是否为不合法的广播包,由 RFC 1122 3.3.6定义
		struct in_device *in_dev = __in_dev_get_rcu(dev);

		/* RFC 1122 3.3.6:
		 *
		 *   When a host sends a datagram to a link-layer broadcast
		 *   address, the IP destination address MUST be a legal IP
		 *   broadcast or IP multicast address.
		 *
		 *   A host SHOULD silently discard a datagram that is received
		 *   via a link-layer broadcast (see Section 2.4) but does not
		 *   specify an IP multicast or broadcast destination address.
		 *
		 * This doesn't explicitly say L2 *broadcast*, but broadcast is
		 * in a way a form of multicast and the most common use case for
		 * this is 802.11 protecting against cross-station spoofing (the
		 * so-called "hole-196" attack) so do it for both.
		 */
		if (in_dev &&
		    IN_DEV_ORCONF(in_dev, DROP_UNICAST_IN_L2_MULTICAST))
			goto drop;
	}

	return NET_RX_SUCCESS;

drop:
	kfree_skb(skb);
	return NET_RX_DROP;

drop_error:
	if (err == -EXDEV)
		__NET_INC_STATS(net, LINUX_MIB_IPRPFILTER);
	goto drop;
}

路由的处理

int ip_route_input_noref(struct sk_buff *skb, __be32 daddr, __be32 saddr,
			 u8 tos, struct net_device *dev)
{
	struct fib_result res;
	int err;

	tos &= IPTOS_RT_MASK;
	rcu_read_lock();
	err = ip_route_input_rcu(skb, daddr, saddr, tos, dev, &res);
	rcu_read_unlock();

	return err;
}
EXPORT_SYMBOL(ip_route_input_noref);

多播

路由层首先检查了对多播的支持

/* called with rcu_read_lock held */
int ip_route_input_rcu(struct sk_buff *skb, __be32 daddr, __be32 saddr,
		       u8 tos, struct net_device *dev, struct fib_result *res)
{
...
	//多播处理
	if (ipv4_is_multicast(daddr)) {
		struct in_device *in_dev = __in_dev_get_rcu(dev);
		int our = 0;
		int err = -EINVAL;

		if (!in_dev)
			return err;
		// 检查自己是否是此多播组的成员 mc = Multicast
		our = ip_check_mc_rcu(in_dev, daddr, saddr,
				      ip_hdr(skb)->protocol);

		/* check l3 master if no match yet */
		// 检查当前网络设备dev是否是第三层(L3)从设备(slave)
		// 如果是,则还需要检查设备组是否是此多播组的成员
		if (!our && netif_is_l3_slave(dev)) {
			struct in_device *l3_in_dev;

			l3_in_dev = __in_dev_get_rcu(skb->dev);
			if (l3_in_dev)
				our = ip_check_mc_rcu(l3_in_dev, daddr, saddr,
						      ip_hdr(skb)->protocol);
		}
		// 如果是成员 则处理
		if (our
#ifdef CONFIG_IP_MROUTE
			||
		    (!ipv4_is_local_multicast(daddr) &&
		     IN_DEV_MFORWARD(in_dev))
#endif
		   ) {
			// 多播数据包的处理代码
			err = ip_route_input_mc(skb, daddr, saddr,
						tos, dev, our);
		}
		return err;
	}

	return ip_route_input_slow(skb, daddr, saddr, tos, dev, res);
}

如果是多播包,这里在ip_route_input_mc就会设置skb的input处理函数

static int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr,
			     u8 tos, struct net_device *dev, int our)
{
	struct in_device *in_dev = __in_dev_get_rcu(dev);
	unsigned int flags = RTCF_MULTICAST;
	struct rtable *rth;
	u32 itag = 0;
	int err;

	err = ip_mc_validate_source(skb, daddr, saddr, tos, dev, in_dev, &itag);
	if (err)
		return err;

	if (our)
		flags |= RTCF_LOCAL;

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

#ifdef CONFIG_IP_ROUTE_CLASSID
	rth->dst.tclassid = itag;
#endif
	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;                <-  这里设置了skb->dst.input
#endif
	RT_CACHE_STAT_INC(in_slow_mc);

	skb_dst_drop(skb);
	skb_dst_set(skb, &rth->dst);
	return 0;
}

而多播结果一些处理后,将会执行ip_local_deliver,

ip_local_deliver函数的实现我们在看完单播处理后来看

/* Multicast packets for forwarding arrive here
 * Called with rcu_read_lock();
 */
int ip_mr_input(struct sk_buff *skb)
{
...
	if (local)
		return ip_local_deliver(skb);

	return 0;

dont_forward:
	if (local)
		return ip_local_deliver(skb);
	kfree_skb(skb);
	return 0;
}

如果不是多播则调用ip_route_input_slow

单播

/*
 *	NOTE. We drop all the packets that has local source
 *	addresses, because every properly looped back packet
 *	must have correct destination already attached by output routine.
 *
 *	Such approach solves two big problems:
 *	1. Not simplex devices are handled properly.
 *	2. IP spoofing attempts are filtered with 100% of guarantee.
 *	called with rcu_read_lock()
 */
// 备注说明ip层将会丢弃所有来源是自身的包,如果来源是自身,应该是使用环回口

static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
			       u8 tos, struct net_device *dev,
			       struct fib_result *res)
{
	struct in_device *in_dev = __in_dev_get_rcu(dev);
	struct flow_keys *flkeys = NULL, _flkeys;
	struct net    *net = dev_net(dev);
	struct ip_tunnel_info *tun_info;
	int		err = -EINVAL;
	unsigned int	flags = 0;
	u32		itag = 0;
	struct rtable	*rth;
	struct flowi4	fl4;
	bool do_cache = true;

	/* IP on this device is disabled. */

	if (!in_dev)
		goto out;

	/* Check for the most weird martians, which can be not detected
	   by fib_lookup.
	 */

	tun_info = skb_tunnel_info(skb);
	if (tun_info && !(tun_info->mode & IP_TUNNEL_INFO_TX))
		fl4.flowi4_tun_key.tun_id = tun_info->key.tun_id;
	else
		fl4.flowi4_tun_key.tun_id = 0;
	skb_dst_drop(skb);
	// // 源地址为多播或本地广播地址,视为非法 
	if (ipv4_is_multicast(saddr) || ipv4_is_lbcast(saddr))
		goto martian_source;

	res->fi = NULL;
	res->table = NULL;
	// 目的地为本地广播或源目的均为零,视为非法
	if (ipv4_is_lbcast(daddr) || (saddr == 0 && daddr == 0))
		goto brd_input;


	// 源目地址为全零,视为非法
	if (ipv4_is_zeronet(saddr))
		goto martian_source;

	if (ipv4_is_zeronet(daddr))
		goto martian_destination;


	// 检查是否为回环地址
	if (ipv4_is_loopback(daddr)) {
		if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))
			goto martian_destination;
	} else if (ipv4_is_loopback(saddr)) {
		if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))
			goto martian_source;
	}

	// 准备进行路由查找
	fl4.flowi4_oif = 0;
	fl4.flowi4_iif = dev->ifindex;
	fl4.flowi4_mark = skb->mark;
	fl4.flowi4_tos = tos;
	fl4.flowi4_scope = RT_SCOPE_UNIVERSE;
	fl4.flowi4_flags = 0;
	fl4.daddr = daddr;
	fl4.saddr = saddr;
	fl4.flowi4_uid = sock_net_uid(net, NULL);
	
	// ????
	if (fib4_rules_early_flow_dissect(net, skb, &fl4, &_flkeys)) {
		flkeys = &_flkeys;
	} else {
		fl4.flowi4_proto = 0;
		fl4.fl4_sport = 0;
		fl4.fl4_dport = 0;
	}

	/*
	* 执行路由查找,它会返回一个fib_result结构,这个结构包含了路由查找的结果,包括路由类型(type)、路由表(table)、路由信息(fi)等信息
	* 路由类型res->type可以是以下几种类型之一:
	* RTN_UNICAST:单播路由,用于直接或间接向单一目标发送数据包。
	* RTN_LOCAL:本地路由,表示数据包的目的地就是本地主机。
	* RTN_BROADCAST:广播路由,用于向网络中的所有主机发送数据包。
	* RTN_MULTICAST:多播路由,用于向特定的多播组发送数据包。
	* RTN_ANYCAST:任播路由,用于向最近的任播组成员发送数据包。
	* RTN_NAT:NAT路由,用于地址转换。
	* RTN_THROW:表示路由查找失败,抛出错误。
	* RTN_UNREACHABLE:表示目的地不可达。
	* RTN_PROHIBIT:表示路由被禁止。
	* RTN_BLACKHOLE:表示数据包应被丢弃。
	*/
	// 其中type字段会设置为路由条目的类型,table字段会指向找到路由条目的路由表,而fi字段则指向具体的struct fib_info实例,包含了详细的路由信息。
	// 然后,这些信息会被用来做出实际的路由决策,如决定数据包应该被转发到哪个下一跳、通过哪个网络接口发送,或是直接交付给本地应用
	err = fib_lookup(net, &fl4, res, 0);
	if (err != 0) {
		if (!IN_DEV_FORWARD(in_dev))
			err = -EHOSTUNREACH;
		goto no_route;
	}

	// 处理广播类型路由
	if (res->type == RTN_BROADCAST) {
		if (IN_DEV_BFORWARD(in_dev))
			goto make_route;
		/* not do cache if bc_forwarding is enabled */
		if (IPV4_DEVCONF_ALL(net, BC_FORWARDING))
			do_cache = false;
		goto brd_input;
	}
	
	// 处理本地类型路由,说明本包目的地是自己
	if (res->type == RTN_LOCAL) {
		// 检查源地址是否合法,比如不能是环回口或广播地址
		err = fib_validate_source(skb, saddr, daddr, tos,
					  0, dev, in_dev, &itag);
		if (err < 0)
			goto martian_source;
		goto local_input;
	}
	// 目的地不是自己的情况下
	// 如果设备不允许转发
	if (!IN_DEV_FORWARD(in_dev)) {
		err = -EHOSTUNREACH;
		goto no_route;
	}
	// 目的地不是自己,且允许转发的情况下
	// 如果路由类型不是单播,视为非法
	if (res->type != RTN_UNICAST)
		goto martian_destination;

make_route:
	// 创建路由条目 干啥用的
	err = ip_mkroute_input(skb, res, in_dev, daddr, saddr, tos, flkeys);
out:	return err;

brd_input:
	if (skb->protocol != htons(ETH_P_IP))
		goto e_inval;

	if (!ipv4_is_zeronet(saddr)) {
		err = fib_validate_source(skb, saddr, 0, tos, 0, dev,
					  in_dev, &itag);
		if (err < 0)
			goto martian_source;
	}
	flags |= RTCF_BROADCAST;
	res->type = RTN_BROADCAST;
	RT_CACHE_STAT_INC(in_brd);

local_input:
	// 处理本地输入
	do_cache &= res->fi && !itag;
	// 如果需要缓存,查找路由条目
	if (do_cache) {
		rth = rcu_dereference(FIB_RES_NH(*res).nh_rth_input);
		if (rt_cache_valid(rth)) {
			skb_dst_set_noref(skb, &rth->dst);
			err = 0;
			goto out;
		}
	}
	// 按照当前路由信息,分配新的dst结构,
	rth = rt_dst_alloc(l3mdev_master_dev_rcu(dev) ? : net->loopback_dev,
			   flags | RTCF_LOCAL, res->type,
			   IN_DEV_CONF_GET(in_dev, NOPOLICY), false, do_cache);
	if (!rth)
		goto e_nobufs;
	
	// 填充out的处理函数
	rth->dst.output= ip_rt_bug;
#ifdef CONFIG_IP_ROUTE_CLASSID
	rth->dst.tclassid = itag;
#endif
	rth->rt_is_input = 1;
	// 统计
	RT_CACHE_STAT_INC(in_slow_tot);
	if (res->type == RTN_UNREACHABLE) {
		// 如果目的地不可达,填充处理函数为ip_error
		rth->dst.input= ip_error;
		rth->dst.error= -err;
		rth->rt_flags 	&= ~RTCF_LOCAL;
	}
	// 如果需要缓存则缓存新的dst结构信息
	if (do_cache) {
		struct fib_nh *nh = &FIB_RES_NH(*res);

		rth->dst.lwtstate = lwtstate_get(nh->nh_lwtstate);
		if (lwtunnel_input_redirect(rth->dst.lwtstate)) {
			WARN_ON(rth->dst.input == lwtunnel_input);
			rth->dst.lwtstate->orig_input = rth->dst.input;
			rth->dst.input = lwtunnel_input;
		}

		if (unlikely(!rt_cache_route(nh, rth)))
			rt_add_uncached_list(rth);
	}
	skb_dst_set(skb, &rth->dst);
	err = 0;
	goto out;

no_route:
	RT_CACHE_STAT_INC(in_no_route);
	res->type = RTN_UNREACHABLE;
	res->fi = NULL;
	res->table = NULL;
	goto local_input;

	/*
	 *	Do not cache martian addresses: they should be logged (RFC1812)
	 */
martian_destination:
	RT_CACHE_STAT_INC(in_martian_dst);
#ifdef CONFIG_IP_ROUTE_VERBOSE
	if (IN_DEV_LOG_MARTIANS(in_dev))
		net_warn_ratelimited("martian destination %pI4 from %pI4, dev %s\n",
				     &daddr, &saddr, dev->name);
#endif

e_inval:
	err = -EINVAL;
	goto out;

e_nobufs:
	err = -ENOBUFS;
	goto out;

martian_source:
	ip_handle_martian_source(dev, in_dev, skb, daddr, saddr);
	goto out;
}

在这里 IP层经过检查路由信息后,可以决定此包是转发还是接受还是丢弃。然后通过rt_dst_alloc填充一个新的dst结构。并缓存

rt_dst_alloc中,将会填充上层的处理函数inputip_local_deliver

struct rtable *rt_dst_alloc(struct net_device *dev,
			    unsigned int flags, u16 type,
			    bool nopolicy, bool noxfrm, bool will_cache)
{
	struct rtable *rt;

	rt = dst_alloc(&ipv4_dst_ops, dev, 1, DST_OBSOLETE_FORCE_CHK,
		       (will_cache ? 0 : DST_HOST) |
		       (nopolicy ? DST_NOPOLICY : 0) |
		       (noxfrm ? DST_NOXFRM : 0));

	if (rt) {
		rt->rt_genid = rt_genid_ipv4(dev_net(dev));
		rt->rt_flags = flags;
		rt->rt_type = type;
		rt->rt_is_input = 0;
		rt->rt_iif = 0;
		rt->rt_pmtu = 0;
		rt->rt_mtu_locked = 0;
		rt->rt_gateway = 0;
		rt->rt_uses_gateway = 0;
		INIT_LIST_HEAD(&rt->rt_uncached);

		rt->dst.output = ip_output;
		if (flags & RTCF_LOCAL)
			rt->dst.input = ip_local_deliver;         <-这里
	}

	return rt;
}
EXPORT_SYMBOL(rt_dst_alloc);

所以可以发现,ip层处理完后,将会都交由ip_local_deliver来由上层继续处理数据包

ip_local_deliver同样是NF_HOOK到了ip_local_deliver_finish

/*
 * 	Deliver IP Packets to the higher protocol layers.
 */
int ip_local_deliver(struct sk_buff *skb)
{
	/*
	 *	Reassemble IP fragments.
	 */
	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);
}

我们看看ip_local_deliver_finish的处理逻辑

static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	__skb_pull(skb, skb_network_header_len(skb)); // 将数据包的网络头部移到数据包的头部,以便更容易访问IP头。

	rcu_read_lock();
	{
		int protocol = ip_hdr(skb)->protocol;	// 获取协议号
		const struct net_protocol *ipprot;
		int raw;

	resubmit:
		// 校验包是否完整
		raw = raw_local_deliver(skb, protocol);

		ipprot = rcu_dereference(inet_protos[protocol]); 	// 通过协议号查找到协议处理器ipprot
		if (ipprot) {
			int ret;

			if (!ipprot->no_policy) {
				if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
					kfree_skb(skb);
					goto out;
				}
				nf_reset(skb);
			}
			ret = ipprot->handler(skb);
			if (ret < 0) {
				protocol = -ret;
				goto resubmit;
			}
			__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
		} else {
			if (!raw) {		// 如果是未知协议且包不完整
				if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
					__IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
					// 内核会生成一个ICMP错误消息回送给发送方,告知其数据包未能送达的原因为 ICMP_DEST_UNREACH的ICMP_PROT_UNREACH
					icmp_send(skb, ICMP_DEST_UNREACH,
						  ICMP_PROT_UNREACH, 0);
				}
				kfree_skb(skb);
			} else {
				__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
				consume_skb(skb);
			}
		}
	}
 out:
	rcu_read_unlock();

	return 0;
}

所以 IP层最后将会通过协议号来调用inet_protos[protocol]->handler(skb)

而同样在inet_init阶段,内核已经将对应此处理函数挂载到对应的inet_protos[protocol]

static int __init inet_init(void)
{
......
	/*
	 *	Add all the base protocols.
	 */
	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__);
....
}

自此,IP层数据处理完成。

### PyCharm 打开文件显示全的解决方案 当遇到PyCharm打开文件显示全的情况时,可以尝试以下几种方法来解决问题。 #### 方法一:清理缓存并重启IDE 有时IDE内部缓存可能导致文件加载异常。通过清除缓存再启动程序能够有效改善此状况。具体操作路径为`File -> Invalidate Caches / Restart...`,之后按照提示完成相应动作即可[^1]。 #### 方法二:调整编辑器字体设置 如果是因为字体原因造成的内容显示问题,则可以通过修改编辑区内的文字样式来进行修复。进入`Settings/Preferences | Editor | Font`选项卡内更改合适的字号大小以及启用抗锯齿功能等参数配置[^2]。 #### 方法三:检查项目结构配置 对于某些特定场景下的源码视图缺失现象,可能是由于当前工作空间未能正确识别全部模块所引起。此时应该核查Project Structure的Content Roots设定项是否涵盖了整个工程根目录;必要时可手动添加遗漏部分,并保存变更生效[^3]。 ```python # 示例代码用于展示如何获取当前项目的根路径,在实际应用中可根据需求调用该函数辅助排查问题 import os def get_project_root(): current_file = os.path.abspath(__file__) project_dir = os.path.dirname(current_file) while not os.path.exists(os.path.join(project_dir, '.idea')): parent_dir = os.path.dirname(project_dir) if parent_dir == project_dir: break project_dir = parent_dir return project_dir print(f"Current Project Root Directory is {get_project_root()}") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值