本系列文章
Linux 在启动时会进行一系列的初始化工作,以准备好网络数据包的接收和发送。本篇文章我们先来看下 Linux 具体做了哪些初始化工作,后面在具体分析网络包的接收和发送流程。
ksoftirqd 软中断线程初始化#
Linux 使用 ksoftirqd 线程来处理软中断,在系统启动时会为每个 CPU 创建一个对应 ksoftirqd 线程。使用 ps 或 systemd 命令可以查看已经启动的 ksoftirqd 线程及其绑定的 CPU。
1
2
3
4
5
6
7
| ~$ ps aux | grep ksoftirqd
root 16 0.0 0.0 0 0 ? S Jul26 0:01 [ksoftirqd/0]
root 24 0.0 0.0 0 0 ? S Jul26 0:01 [ksoftirqd/1]
~$ systemd-cgls -k | grep softirq
├─ 16 [ksoftirqd/0]
├─ 24 [ksoftirqd/1]
|
Linux 创建 ksoftirqd 线程的过程如下:
softirq.c 下的 spawn_ksoftirqd 函数调用 smpboot.c 的注册函数,为每个 CPU 创建 softirq_threads。
核心代码位于kernel/softirq.c。
1
2
3
4
5
6
7
8
9
| // https://elixir.bootlin.com/linux/v5.15.139/source/kernel/softirq.c#L959
// 软中断线程的结构体定义
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
// 这里是 ksoftirqd 线程的处理函数
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};
|
创建软中断线程的函数调用栈如下:
1
2
3
| |- softirq.c: spawn_ksoftirqd
|- smpboot.c: smpboot_register_percpu_thread
|- smpboot.c __smpboot_create_thread
|
这里我们重点关注 __smpboot_create_thread 函数,其主要负责为每个 CPU 创建一个 ksoftirqd 线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // https://elixir.bootlin.com/linux/v5.15.139/source/kernel/smp
static int
__smpboot_create_thread(struct smp_hotplug_thread *ht, unsigned int cpu)
{
struct task_struct *tsk = *per_cpu_ptr(ht->store, cpu);
struct smpboot_thread_data *td;
if (tsk)
return 0;
// 分配线程数据结构
td = kzalloc_node(sizeof(*td), GFP_KERNEL, cpu_to_node(cpu));
if (!td)
return -ENOMEM;
td->cpu = cpu;
td->ht = ht;
// 创建线程
tsk = kthread_create_on_cpu(smpboot_thread_fn, td, cpu,
ht->thread_comm);
...
return 0;
}
|
- 开启循环,等待处理软中断.
在 __smpboot_create_thread 函数创建 ksoftirqd 线程时有一个 smpboot_thread_fn 函数作为参数。在创建好 ksoftirqd 线程后会执行该函数,开启循环判断是否有软中断触发。如果有软中断就会调用 softirq_threads 的 thread_fn 函数,也就是 run_ksoftirqd() 函数处理软中断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| /**
* smpboot_thread_fn - percpu hotplug thread loop function
* @data: thread data pointer
*
* Checks for thread stop and park conditions. Calls the necessary
* setup, cleanup, park and unpark functions for the registered
* thread.
*
* Returns 1 when the thread should exit, 0 otherwise.
*/
static int smpboot_thread_fn(void *data)
{
struct smpboot_thread_data *td = data;
struct smp_hotplug_thread *ht = td->ht;
while (1) {// 开启循环
set_current_state(TASK_INTERRUPTIBLE);
preempt_disable();
...
if (!ht->thread_should_run(td->cpu)) { // 无 Pending 软中断
preempt_enable_no_resched();
schedule();
} else {
__set_current_state(TASK_RUNNING); // 有 Pending 软中断,执行 run_ksoftirqd
preempt_enable();
ht->thread_fn(td->cpu);
}
}
}
|
- 当有软中断触发时,会调用 run_ksoftirqd 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| static void run_ksoftirqd(unsigned int cpu)
{
ksoftirqd_run_begin();
if (local_softirq_pending()) {
/*
* We can safely run softirq on inline stack, as we are not deep
* in the task stack here.
*/
__do_softirq();
ksoftirqd_run_end();
cond_resched();
return;
}
ksoftirqd_run_end();
}
// https://elixir.bootlin.com/linux/v5.15.139/source/kernel/softirq.c#L405
static inline void ksoftirqd_run_begin(void)
{
local_irq_disable();
}
static inline void ksoftirqd_run_end(void)
{
local_irq_enable();
}
|
其执行逻辑为:
- 关闭所在 CPU 的硬中断。
- 如果有待处理的软中断,则执行 __do_softirq() 函数处理。
- 恢复所在 CPU 的硬中断。
这里最终会调用到 __do_softirq() 函数来执行具体的处理操作,我们在后续分析 Linux 收包流程时在详细研究。
Linux 支持如下类型的软中断:
1
2
3
4
5
6
7
8
9
10
11
12
| $ cat /proc/softirqs
CPU0 CPU1
HI: 1 0
TIMER: 71111915 120166497
NET_TX: 7 11
NET_RX: 19292553 19371990
BLOCK: 1805974 16211728
IRQ_POLL: 0 0
TASKLET: 153626 151427
SCHED: 119862293 170637587
HRTIMER: 482614 371796
RCU: 65165801 66029526
|
其中网络有相关的软中断有两个:
NET_TX 代表网络数据包的发送NET_RX 代表网络数据包的接收
为了能够处理这两类软中断,还需要为其注册处理函数,这一步在初始化网络设备子系统时完成。
初始化内核网络设备子系统#
系统在启动时会执行网络设备子系统的初始化,由 subsys_initcall 调用 net/core/dev.c的 net_dev_init()函数,其主要流程如下。
注册 /proc/net/{dev/softnet_data/ptype}#
首先会注册 /proc/net/ 下的伪文件信息,代码如下。
1
2
3
4
5
6
| static int __init net_dev_init(void)
{
if (dev_proc_init())
goto out;
...
}
|
这样在系统启动后我们可以通过 /proc/dev看到相关的网络统计信息。主要有三种类型:
dev:网卡接口统计信息,包括收发的数据包、字节数、丢包数等。
1
2
3
4
5
6
7
8
| $ cat /proc/net/dev | column -t
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
lo: 2935050070 2601791 0 0 0 0 0 0 2935050070 2601791 0 0 0 0 0 0
eth0: 30656772745 25692220 0 0 0 0 0 0 7836773623 21772082 0 0 0 0 0 0
docker0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
br-f2a5dfa5547a: 1320746372 328165 0 0 0 0 0 0 1308994780 218407 0 0 0 0 0 0
vethd08c5d8: 1325340430 328159 0 0 0 0 0 0 1309054050 219256 0 0 0 0 0 0
|
softnet_stat:每个 CPU 的软中断接收状态。每行代表一个 CPU。
1
2
3
| $ cat /proc/net/softnet_stat | column -t
00c15138 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00e9c405 00000000 00000002 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
|
1
2
3
4
5
6
7
8
| $ cat /proc/net/ptype | column -t
Type Device Function
ALL eth0 tpacket_rcv
ALL eth0 tpacket_rcv
0800 ip_rcv
0004 llc_rcv [llc]
0806 arp_rcv
86dd ipv6_rcv
|
关于以上注册类型更详细的解释可以参考 Monitoring and Tuning the Linux Networking Stack: Receiving Data
创建 softnet_data 数据结构#
这里会为每个 CPU 创建 softnet_data 数据结构。这是每个 CPU 都有的处理网络数据包的上下文结构,包括缓存 skb 的队列、要处理的设备列表等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| static int __init net_dev_init(void)
{ // 注册 /proc/net/ 信息
if (dev_proc_init())
goto out;
...
// 针对每个 CPU,初始化各种数据结构
for_each_possible_cpu(i) {
struct work_struct *flush = per_cpu_ptr(&flush_works, i);
// 1. 为每个 CPU 创建 softnet_data 数据结构
struct softnet_data *sd = &per_cpu(softnet_data, i);
// 初始化接收队列
INIT_WORK(flush, flush_backlog);
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
#ifdef CONFIG_XFRM_OFFLOAD
skb_queue_head_init(&sd->xfrm_backlog);
#endif
INIT_LIST_HEAD(&sd->poll_list);
sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
INIT_CSD(&sd->csd, rps_trigger_softirq, sd);
sd->cpu = i;
#endif
// 初始化 GRO 哈希表
init_gro_hash(&sd->backlog);
sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
...
}
...
out:
return rc;
}
|
softnet_data 内有一个 poll_list 变量,用来保存
网卡驱动
注册的 poll 函数,网卡驱动在初始化时会将其 poll 函数添加到 poll_list 中。
1
2
3
4
5
6
7
8
9
| https://elixir.bootlin.com/linux/v5.15.139/source/include/linux/netdevice.h#L3351
/*
* Incoming packets are placed on per-CPU queues
*/
struct softnet_data {
struct list_head poll_list;
struct sk_buff_head process_queue;
...
}
|
注册软中断函数#
网络收发的软中断分别为 NET_RX 和 NET_TX:
- NET_RX 软中断注册处理函数为 net_rx_action
- NET_TX 注册的软中断处理函数为 net_tx_action
可以看到其注册方式就是中断类型作为 key,处理函数作为 value,保存到 softirq_vec 变量中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| static int __init net_dev_init(void)
{
// 为软中断注册处理函数
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);
rc = 0;
out:
return rc;
}
// https://elixir.bootlin.com/linux/v5.15.139/source/kernel/softirq.c#L703
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
// https://elixir.bootlin.com/linux/v5.15.139/source/kernel/softirq.c#L59
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
const char * const softirq_to_name[NR_SOFTIRQS] = {
"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
"TASKLET", "SCHED", "HRTIMER", "RCU"
};
|
上面我们提到软中断创建完成会执行 _do_softirq 函数来处理软中断,其 h>action(h) 就是调用这里注册的处理函数来处理软中断的。
注册协议栈#
网络设备子系统初始化完成后,Linux 会注册 TCP/IP 网络协议栈。Linux通过 fs_initcall 调用 inet_init 函数来注册协议栈。主要代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| // https://elixir.bootlin.com/linux/v5.15.139/source/net/ipv4/af_inet.c#L1934
static int __init inet_init(void)
{
/*
* Add all the base protocols.
*
* 注册 ICMP、UDP、TCP、IGMP 等协议的处理函数
*/
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
...
// 多种协议的初始化
arp_init();
ip_init();
tcp_init();
udp_init();
udplite4_register();
raw_init();
ping_init();
ipv4_proc_init();
ipfrag_init();
// 注册 IP 协议的处理函数
dev_add_pack(&ip_packet_type);
ip_tunnel_core_init();
rc = 0;
out:
return rc;
...
}
// 注册协议栈
fs_initcall(inet_init);
|
核心代码是下面几行,注册了 IP 协议以及 ICMP、UDP、TCP、IGMP 等协议的处理函数。
1
2
3
4
5
| inet_add_protocol(&icmp_protocol, IPPROTO_ICMP);
inet_add_protocol(&udp_protocol, IPPROTO_UDP);
inet_add_protocol(&tcp_protocol, IPPROTO_TCP);
inet_add_protocol(&igmp_protocol, IPPROTO_IGMP);
dev_add_pack(&ip_packet_type);
|
各种协议的定义信息如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // https://elixir.bootlin.com/linux/v5.15.139/source/net/ipv4/af_inet.c#L1727
#ifdef CONFIG_IP_MULTICAST
static const struct net_protocol igmp_protocol = {
.handler = igmp_rcv,
};
#endif
static const struct net_protocol tcp_protocol = {
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.icmp_strict_tag_validation = 1,
};
static const struct net_protocol udp_protocol = {
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
};
static const struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
.err_handler = icmp_err,
.no_policy = 1,
};
// https://elixir.bootlin.com/linux/v5.15.139/source/net/ipv4/af_inet.c#L1928
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
.list_func = ip_list_rcv,
};
|
可以看到每种协议都注册了相应的处理函数:
- TCP 协议注册了
tcp_v4_rcv 作为处理函数。 - UDP 协议注册了
udp_rcv 作为处理函数。 - IP 协议注册了
ip_rcv 作为处理函数。
最终,ICMP、UDP、TCP、IGMP 协议通过 inet_add_protocol 函数被注册到 inet_protocols 数组中,而 IP 协议通过 dev_add_pack 函数被注册到 ptype_base 哈希表中。
1
2
3
4
5
| // https://elixir.bootlin.com/linux/v5.15.139/source/net/ipv4/protocol.c#L32
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{
return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
NULL, prot) ? 0 : -1;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);
spin_lock(&ptype_lock);
list_add_rcu(&pt->list, head);
spin_unlock(&ptype_lock);
}
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return pt->dev ? &pt->dev->ptype_all : &ptype_all;
else
return pt->dev ? &pt->dev->ptype_specific :
&ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
|
Linux 在执行收包处理时,最终会根据上述注册信息找到 ip_rcv、 tcp_rcv 或 udp_rcv 等函数来处理,我们将在后续文章中分析其流程。
初始化网卡驱动#
网卡需要驱动程序来与操作系统进行交互。驱动程序负责管理网卡的硬件资源,并提供一个抽象接口供上层协议栈使用。
我们可以通过 proc 信息或者 ethtool 命令查看本机网卡使用的驱动程序。可以看到我的腾讯云服务器用的是 virtio_net 驱动,这是为虚拟化环境设计的驱动程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| $ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 8500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:1e:5e:c3 brd ff:ff:ff:ff:ff:ff
altname enp0s5
altname ens5
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether ce:42:63:b6:d8:9c brd ff:ff:ff:ff:ff:ff
4: br-f2a5dfa5547a: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether be:96:58:67:a1:91 brd ff:ff:ff:ff:ff:ff
9: vethd08c5d8@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-f2a5dfa5547a state UP mode DEFAULT group default
link/ether 2a:62:64:aa:47:00 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# 查看 /sys 信息
$ ls -l /sys/class/net/eth0/device/driver
lrwxrwxrwx 1 root root 0 Jul 25 11:11 /sys/class/net/eth0/device/driver -> ../../../../bus/virtio/drivers/virtio_net
# 使用 ethtool 命令查看
$ ethtool -i eth0
driver: virtio_net
version: 1.0.0
firmware-version:
expansion-rom-version:
bus-info: 0000:00:05.0
supports-statistics: yes
supports-test: no
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: no
|
这是另一个数据中心机器的网卡驱动为 i40e,这是 Intel 推出的针对高性能网卡的驱动,带宽可以达到 40 Gbps。
1
2
| $ ls -l /sys/class/net/eno1/device/driver
lrwxrwxrwx 1 root root 0 Jul 29 2024 /sys/class/net/eno1/device/driver -> ../../../../../../bus/pci/drivers/i40e
|
本系列文章我们以 i40e 驱动为例,分析其初始化过程和后续的收发包处理。
注册 PCI 设备列表#
PCI 设备通过 PCI 配置空间 中的一系列寄存器来标识自己。
当驱动程序编译时,使用名为 MODULE_DEVICE_TABLE 的宏会导出一个全局的 PCI 设备 ID 列表,用来标识该驱动支持的设备型号,内核会根据这些设备型号匹配对应的驱动程序。
1
2
3
4
5
6
7
8
9
10
| // https://elixir.bootlin.com/linux/v5.15.139/source/include/linux/module.h#L241
// 宏定义
#ifdef MODULE
/* Creates an alias so file2alias.c can find device table. */
#define MODULE_DEVICE_TABLE(type, name) \
extern typeof(name) __mod_##type##__##name##_device_table \
__attribute__ ((unused, alias(__stringify(name))))
#else /* !MODULE */
#define MODULE_DEVICE_TABLE(type, name)
#endif
|
i40e 支持的设备列表和 PCI 设备 ID 分别定义在 i40e_devids.h 和 i40e_main.c 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
| // https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_devids.h#L14
/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright(c) 2013 - 2018 Intel Corporation. */
#ifndef _I40E_DEVIDS_H_
#define _I40E_DEVIDS_H_
/* Device IDs */
#define I40E_DEV_ID_X710_N3000 0x0CF8
#define I40E_DEV_ID_XXV710_N3000 0x0D58
#define I40E_DEV_ID_SFP_XL710 0x1572
#define I40E_DEV_ID_QEMU 0x1574
#define I40E_DEV_ID_KX_B 0x1580
#define I40E_DEV_ID_KX_C 0x1581
#define I40E_DEV_ID_QSFP_A 0x1583
#define I40E_DEV_ID_QSFP_B 0x1584
#define I40E_DEV_ID_QSFP_C 0x1585
#define I40E_DEV_ID_10G_BASE_T 0x1586
#define I40E_DEV_ID_20G_KR2 0x1587
#define I40E_DEV_ID_20G_KR2_A 0x1588
#define I40E_DEV_ID_10G_BASE_T4 0x1589
#define I40E_DEV_ID_25G_B 0x158A
#define I40E_DEV_ID_25G_SFP28 0x158B
#define I40E_DEV_ID_10G_BASE_T_BC 0x15FF
#define I40E_DEV_ID_10G_B 0x104F
#define I40E_DEV_ID_10G_SFP 0x104E
#define I40E_DEV_ID_5G_BASE_T_BC 0x101F
#define I40E_IS_X710TL_DEVICE(d) \
(((d) == I40E_DEV_ID_5G_BASE_T_BC) || \
((d) == I40E_DEV_ID_10G_BASE_T_BC))
#define I40E_DEV_ID_KX_X722 0x37CE
#define I40E_DEV_ID_QSFP_X722 0x37CF
#define I40E_DEV_ID_SFP_X722 0x37D0
#define I40E_DEV_ID_1G_BASE_T_X722 0x37D1
#define I40E_DEV_ID_10G_BASE_T_X722 0x37D2
#define I40E_DEV_ID_SFP_I_X722 0x37D3
#endif /* _I40E_DEVIDS_H_ */
// https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L61
// 驱动支持的 PCI 设备 ID 列表
/* i40e_pci_tbl - PCI Device ID Table
*
* Last entry must be all 0s
*
* { Vendor ID, Device ID, SubVendor ID, SubDevice ID,
* Class, Class Mask, private data (not used) }
*/
static const struct pci_device_id i40e_pci_tbl[] = {
{PCI_VDEVICE(INTEL, I40E_DEV_ID_SFP_XL710), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_QEMU), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_KX_B), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_KX_C), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_QSFP_A), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_QSFP_B), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_QSFP_C), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_10G_BASE_T), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_10G_BASE_T4), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_10G_BASE_T_BC), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_10G_SFP), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_10G_B), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_KX_X722), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_QSFP_X722), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_SFP_X722), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_1G_BASE_T_X722), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_10G_BASE_T_X722), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_SFP_I_X722), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_20G_KR2), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_20G_KR2_A), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_X710_N3000), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_XXV710_N3000), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_25G_B), 0},
{PCI_VDEVICE(INTEL, I40E_DEV_ID_25G_SFP28), 0},
/* required last entry */
{0, }
};
MODULE_DEVICE_TABLE(pci, igb_pci_tbl);
|
注册驱动:module_init–> pci_register_driver#
所有类型的驱动都需要通过 module_init 宏来向内核注册其初始化函数,当内核加载驱动时会调用该函数进行初始化。i40e 的初始化函数是 i40e_init_module ,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L16660
/**
* i40e_init_module - Driver registration routine
*
* i40e_init_module is the first routine called when the driver is
* loaded. All it does is register with the PCI subsystem.
**/
static int __init i40e_init_module(void)
{
int err;
pr_info("%s: %s\n", i40e_driver_name, i40e_driver_string);
pr_info("%s: %s\n", i40e_driver_name, i40e_copyright);
/* There is no need to throttle the number of active tasks because
* each device limits its own task using a state bit for scheduling
* the service task, and the device tasks do not interfere with each
* other, so we don't set a max task limit. We must set WQ_MEM_RECLAIM
* since we need to be able to guarantee forward progress even under
* memory pressure.
*/
i40e_wq = alloc_workqueue("%s", WQ_MEM_RECLAIM, 0, i40e_driver_name);
if (!i40e_wq) {
pr_err("%s: Failed to create workqueue\n", i40e_driver_name);
return -ENOMEM;
}
i40e_dbg_init();
err = pci_register_driver(&i40e_driver);
if (err) {
destroy_workqueue(i40e_wq);
i40e_dbg_exit();
return err;
}
return 0;
}
module_init(i40e_init_module);
|
具体的注册流程是在 pci_register_driver 函数执行,我们来看下驱动信息的格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L16641
static struct pci_driver i40e_driver = {
.name = i40e_driver_name,
.id_table = i40e_pci_tbl,
.probe = i40e_probe,
.remove = i40e_remove,
.driver = {
.pm = &i40e_pm_ops,
},
.shutdown = i40e_shutdown,
.err_handler = &i40e_err_handler,
.sriov_configure = i40e_pci_sriov_configure,
};
// https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L24
// 驱动基本信息
const char i40e_driver_name[] = "i40e";
static const char i40e_driver_string[] =
"Intel(R) Ethernet Connection XL710 Network Driver";
static const char i40e_copyright[] = "Copyright (c) 2013 - 2019 Intel Corporation.";
|
这里简要解释下几个字段的含义:
- name:标识驱动的字符串名称,在
/sys/bus/pci/drivers/ 目录下会创建一个以该名称命名的目录,里面包含了驱动的相关信息。$ ls /sys/bus/pci/drivers/ 8250_mid i40e lpc_ich serial - id_table:驱动支持的设备 ID 列表。只有匹配到这些设备时才会初始化驱动。
- probe:驱动的初始化函数,当匹配到设备时会调用该函数进行初始化。
- remove:驱动的卸载函数,当设备被移除时会调用该函数进行清理。
加载驱动:i40e_probe#
网卡驱动注册后,内核在启动时会根据其注册的设备 ID 列表去搜索匹配相关网卡硬件,如果匹配则调用驱动的 probe 函数执行网卡驱动的初始化任务,对于 i40e 驱动其初始化函数为 i40e_probe。
i40e_probe 函数比较冗长,这里我们只关注几个最关键的步骤:
- PCI 设备初始化
- 获取 MAC 地址
- 初始化 VSI
- 网络设备初始化
- net_device_ops 注册
- ethtool 注册
- NAPI 初始化
- 网卡启动
我们来详细看下上述几个步骤的实现:
PCI 设备初始化#
这一步会将唤醒 PCI 设备,启用内存资源,然后 dma_set_mask_and_coherent 方法是为设备设置 DMA 掩码,参数是 DMA_BIT_MASK(64),因此网卡可以访问 64 位内存地址。
之后会为网卡申请内存区域,并开启错误报告、DMA 功能等特性。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L15577
static int i40e_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
...
// // 启用PCI设备的内存访问功能
err = pci_enable_device_mem(pdev);
if (err)
return err;
/* set up for high or low dma */
// 设置DMA掩码 - 优先尝试64位DMA,如果不支持则降级为 32 位
err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (err) {
err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
if (err) {
dev_err(&pdev->dev,
"DMA configuration failed: 0x%x\n", err);
goto err_dma;
}
}
/* set up pci connections */
// 请求内存区域
err = pci_request_mem_regions(pdev, i40e_driver_name);
if (err) {
dev_info(&pdev->dev,
"pci_request_selected_regions failed %d\n", err);
goto err_pci_reg;
}
// 启用错误报告
pci_enable_pcie_error_reporting(pdev);
// 启用 DMA 传输
pci_set_master(pdev);
|
获取 MAC 地址#
经过 PCI 设备初始化以及一些其他初始化步骤后,驱动会尝试获取网卡的 MAC 地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L15830
static int i40e_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
...
// 从平台获取 MAC 地址
i40e_get_platform_mac_addr(pdev, pf);
// 检查是否有效
if (!is_valid_ether_addr(hw->mac.addr)) {
dev_info(&pdev->dev, "invalid MAC address %pM\n", hw->mac.addr);
err = -EIO;
goto err_mac_addr;
}
// 打印 MAC 地址
dev_info(&pdev->dev, "MAC address: %pM\n", hw->mac.addr);
// 将当前 MAC 地址复制到永久地址字段
ether_addr_copy(hw->mac.perm_addr, hw->mac.addr);
// 获取端口特定的 MAC 地址(
i40e_get_port_mac_addr(hw, hw->mac.port_addr);
if (is_valid_ether_addr(hw->mac.port_addr))
pf->hw_features |= I40E_HW_PORT_ID_VALID;
...
}
|
以太网初始化#
PCI 设备初始化完成后,会进行以太网的初始化,主要是创建内核抽象的网络设备,并执行一系列的注册操作。
Linux 内核定义了 net_device
结构体
来表示网络设备,驱动程序初始化时会创建该结构体实例,并为其标识硬件特性、注册 net_dev_ops、ethtool_ops 等操作函数,也会为其注册 NAPI。
net_device 的创建是在 i40e_config_netdev( 函数实现的,整体调用栈如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| i40e_probe()
└── i40e_setup_pf_switch()
└── i40e_vsi_setup()
└── i40e_set_num_rings_in_vsi // 设置 VSI 的队列数量
└── i40e_vsi_mem_alloc() // 初始化 VSI
└── i40e_vsi_setup_irqhandler() 初始化 IRQ 处理函数
└── i40e_config_netdev()
└── netdev->netdev_ops = &i40e_netdev_ops; // 注册 net_device_ops
└── i40e_set_ethtool_ops(netdev); // 注册 ethtool_ops
└── i40e_vsi_setup_vectors(vsi);
└── i40e_vsi_alloc_q_vectors()
└── netif_napi_add() // 注册 NAPI poll 方法
└── i40e_alloc_rings(vsi);
└── i40e_vsi_map_rings_to_vectors(vsi); // 分配中断向量和队列,并绑定
|
初始化VSI#
VSI(Virtual Station Interface)是虚拟化环境中用于网络数据包处理的虚拟接口。每个 VSI 对应一个虚拟网络设备,负责处理与该设备相关的网络流量。一张网卡可以支持多个 VSI,每个 VSI 有独立的队列、中断向量、MAC地址等资源。
这一阶段 i40e 会创建 VSI 并为其分配内存,初始化向量、队列、中断处理函数等。
设置队列数量
这里调用 i40e_set_num_rings_in_vsi 函数,根据 VIS 类型、MSIX 特性支持等条件确定缓冲队列,即 RingBuffer 的数量,在网卡启动时会根据这里的数量创建队列。
初始化硬中断处理函数
i40e_vsi_mem_alloc会进行一系列的初始化操作,其中会调用 i40e_vsi_setup_irqhandler 来初始化硬中断处理函数,逻辑如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L11493
static int i40e_vsi_mem_alloc(struct i40e_pf *pf, enum i40e_vsi_type type)
{
...
/* Setup default MSIX irq handler for VSI */
i40e_vsi_setup_irqhandler(vsi, i40e_msix_clean_rings);
...
}
// https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e.h#L1040
static inline void i40e_vsi_setup_irqhandler(struct i40e_vsi *vsi,
irqreturn_t (*irq_handler)(int, void *))
{
vsi->irq_handler = irq_handler;
}
|
可以看到,i40e 驱动使用的 IRQ 处理函数为 i40e_msix_clean_rings(),在网卡启动时,会将这里初始化的 IRQ 处理函数注册到中断向量中。
创建 net_device#
这一步会创建抽象的 net_device 结构体,代表网络设备。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L13681
/**
* i40e_config_netdev - Setup the netdev flags
* @vsi: the VSI being configured
*
* Returns 0 on success, negative value on failure
**/
static int i40e_config_netdev(struct i40e_vsi *vsi)
{
struct i40e_pf *pf = vsi->back;
struct i40e_hw *hw = &pf->hw;
struct i40e_netdev_priv *np;
// net_device 指针
struct net_device *netdev;
u8 broadcast[ETH_ALEN];
u8 mac_addr[ETH_ALEN];
int etherdev_size;
netdev_features_t hw_enc_features;
netdev_features_t hw_features;
etherdev_size = sizeof(struct i40e_netdev_priv);
// 分配 net_device 结构体
netdev = alloc_etherdev_mq(etherdev_size, vsi->alloc_queue_pairs);
if (!netdev)
return -ENOMEM;
// 关联 net_device 和 VSI
vsi->netdev = netdev;
...
|
定义硬件相关特性#
netdev 创建完成后,会为其定义一系列硬件相关特性,这些特性会在网络数据包处理时被用到。下面是一些常见特性的设置,包括:
- 配置 TCP/UDP 校验和、TSO、GSO 等硬件卸载功能。
- 启用 VXLAN、GRE 等隧道协议的硬件处理
- 配置硬件 VLAN 标签处理能力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| // https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L13681
netdev = alloc_etherdev_mq(etherdev_size, vsi->alloc_queue_pairs);
if (!netdev)
return -ENOMEM;
vsi->netdev = netdev;
np = netdev_priv(netdev);
np->vsi = vsi;
// 定义硬件加密卸载功能特性
// 这些特性可以被硬件直接处理,减少 CPU 负载
hw_enc_features = NETIF_F_SG | // 支持分散-聚集 DMA
NETIF_F_IP_CSUM | // IPv4 校验和卸载
NETIF_F_IPV6_CSUM | // IPv6 校验和卸载
NETIF_F_HIGHDMA | // 支持高内存 DMA (>4GB)
NETIF_F_SOFT_FEATURES | // 软件功能特性集合
NETIF_F_TSO | // TCP 分段卸载 (IPv4)
NETIF_F_TSO_ECN | // TSO 支持 ECN 标记
NETIF_F_TSO6 | // TCP 分段卸载 (IPv6)
NETIF_F_GSO_GRE | // GRE 通用分段卸载
NETIF_F_GSO_GRE_CSUM | // GRE 校验和分段卸载
NETIF_F_GSO_PARTIAL | // 部分 GSO 支持
NETIF_F_GSO_IPXIP4 | // IP-in-IP (IPv4) GSO
NETIF_F_GSO_IPXIP6 | // IP-in-IP (IPv6) GSO
NETIF_F_GSO_UDP_TUNNEL | // UDP 隧道 GSO
NETIF_F_GSO_UDP_TUNNEL_CSUM | // UDP 隧道校验和 GSO
NETIF_F_GSO_UDP_L4 | // UDP L4 GSO
NETIF_F_SCTP_CRC | // SCTP CRC 校验和卸载
NETIF_F_RXHASH | // 接收哈希(RSS)支持
NETIF_F_RXCSUM | // 接收校验和验证
0; // 结束标记
if (!(pf->hw_features & I40E_HW_OUTER_UDP_CSUM_CAPABLE))
netdev->gso_partial_features |= NETIF_F_GSO_UDP_TUNNEL_CSUM;
// 设置 UDP 隧道网卡信息,用于 VXLAN、GENEVE 等隧道协议
netdev->udp_tunnel_nic_info = &pf->udp_tunnel_nic;
// 总是将 GRE 校验和作为部分 GSO 功能
netdev->gso_partial_features |= NETIF_F_GSO_GRE_CSUM;
// 将定义的加密功能添加到硬件加密功能列表中
netdev->hw_enc_features |= hw_enc_features;
/* record features VLANs can make use of */
// 设置 VLAN 可以使用的功能特性
netdev->vlan_features |= hw_enc_features | NETIF_F_TSO_MANGLEID;
/* enable macvlan offloads */
// 启用 MACVLAN 卸载功能,允许硬件处理 MACVLAN 转发
netdev->hw_features |= NETIF_F_HW_L2FW_DOFFLOAD;
// 定义基本的硬件功能特性
hw_features = hw_enc_features | // 加密特性
NETIF_F_HW_VLAN_CTAG_TX | // VLAN 发送标记
NETIF_F_HW_VLAN_CTAG_RX; // VLAN 接收标记
if (!(pf->flags & I40E_FLAG_MFP_ENABLED))
hw_features |= NETIF_F_NTUPLE | NETIF_F_HW_TC;
// 将定义的硬件功能添加到网络设备的硬件功能列表
netdev->hw_features |= hw_features;
netdev->features |= hw_features | NETIF_F_HW_VLAN_CTAG_FILTER;
netdev->hw_enc_features |= NETIF_F_TSO_MANGLEID;
|
注册 net_device_ops#
net_device_ops 是 Linux 定义的一个函数指针结构体,里面规定了针对网络设备的所有操作函数,相当于给网络设备驱动程序提供了标准化的操作接口,所有网络驱动都必须实现这一套接口。完整代码参考 struct net_device_ops,下面只列出了一些常用功能对应的处理函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // https://elixir.bootlin.com/linux/v5.15.139/source/include/linux/netdevice.h#L1374
/*
* This structure defines the management hooks for network devices.
* The following hooks can be defined; unless noted otherwise, they are
* optional and can be filled with a null pointer.
*/
struct net_device_ops {
// 启动网卡
int (*ndo_open)(struct net_device *dev);
// 停止网卡
int (*ndo_stop)(struct net_device *dev);
// 发送数据包
netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb, struct net_device *dev);
// 获取网络统计信息
void(*ndo_get_stats64)(struct net_device *dev, struct rtnl_link_stats64 *storage);
// 修改 MTU 大小
int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
// 设置 MAC 地址
int (*ndo_set_mac_address)(struct net_device *dev, void *addr);
...
};
|
i40e 网络驱动实现了上述接口,完整代码如下,可以看到网卡启动函数为 i40e_open,这个我们在网卡启动时还会看到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L13623
static const struct net_device_ops i40e_netdev_ops = {
.ndo_open = i40e_open,
.ndo_stop = i40e_close,
.ndo_start_xmit = i40e_lan_xmit_frame,
.ndo_get_stats64 = i40e_get_netdev_stats_struct,
.ndo_set_rx_mode = i40e_set_rx_mode,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = i40e_set_mac,
.ndo_change_mtu = i40e_change_mtu,
.ndo_eth_ioctl = i40e_ioctl,
.ndo_tx_timeout = i40e_tx_timeout,
.ndo_vlan_rx_add_vid = i40e_vlan_rx_add_vid,
.ndo_vlan_rx_kill_vid = i40e_vlan_rx_kill_vid,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = i40e_netpoll,
#endif
.ndo_setup_tc = __i40e_setup_tc,
.ndo_select_queue = i40e_lan_select_queue,
.ndo_set_features = i40e_set_features,
.ndo_set_vf_mac = i40e_ndo_set_vf_mac,
.ndo_set_vf_vlan = i40e_ndo_set_vf_port_vlan,
.ndo_get_vf_stats = i40e_get_vf_stats,
.ndo_set_vf_rate = i40e_ndo_set_vf_bw,
.ndo_get_vf_config = i40e_ndo_get_vf_config,
.ndo_set_vf_link_state = i40e_ndo_set_vf_link_state,
.ndo_set_vf_spoofchk = i40e_ndo_set_vf_spoofchk,
.ndo_set_vf_trust = i40e_ndo_set_vf_trust,
.ndo_get_phys_port_id = i40e_get_phys_port_id,
.ndo_fdb_add = i40e_ndo_fdb_add,
.ndo_features_check = i40e_features_check,
.ndo_bridge_getlink = i40e_ndo_bridge_getlink,
.ndo_bridge_setlink = i40e_ndo_bridge_setlink,
.ndo_bpf = i40e_xdp,
.ndo_xdp_xmit = i40e_xdp_xmit,
.ndo_xsk_wakeup = i40e_xsk_wakeup,
.ndo_dfwd_add_station = i40e_fwd_add,
.ndo_dfwd_del_station = i40e_fwd_del,
};
|
在 i40e_config_netdev() 函数中会完成的 net_dev_ops 的注册,相关代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| static int i40e_config_netdev(struct i40e_vsi *vsi)
{
...
// 注册 net_device_Ops
netdev->netdev_ops = &i40e_netdev_ops;
netdev->watchdog_timeo = 5 * HZ;
// 注册 net_dev_ethtool_ops
i40e_set_ethtool_ops(netdev);
/* MTU range: 68 - 9706 */
netdev->min_mtu = ETH_MIN_MTU;
netdev->max_mtu = I40E_MAX_RXBUFFER - I40E_PACKET_HDR_PAD;
return 0;
}
|
在 i40e_config_netdev() 函数,还有一行代码 i40e_set_ethtool_ops(netdev);,这是用来 ethtool 的操作函数的。
ethtool 是用来查看和修改网卡配置的命令行工具,和 net_device_ops 一样,内核也定义了一系列的函数,网卡驱动实现这些函数后就可以通过 ethtool 来查看修改网卡的配置。
Linux 的接口定义和 i40e 的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
| // Linux 定义
// https://elixir.bootlin.com/linux/v5.15.139/source/include/linux/ethtool.h#L418
struct ethtool_ops {
u32 cap_link_lanes_supported:1;
u32 supported_coalesce_params;
void (*get_drvinfo)(struct net_device *, struct ethtool_drvinfo *);
int (*get_regs_len)(struct net_device *);
void (*get_regs)(struct net_device *, struct ethtool_regs *, void *);
void (*get_wol)(struct net_device *, struct ethtool_wolinfo *);
int (*set_wol)(struct net_device *, struct ethtool_wolinfo *);
u32 (*get_msglevel)(struct net_device *);
void (*set_msglevel)(struct net_device *, u32);
int (*nway_reset)(struct net_device *);
u32 (*get_link)(struct net_device *);
int (*get_link_ext_state)(struct net_device *,
struct ethtool_link_ext_state_info *);
int (*get_eeprom_len)(struct net_device *);
int (*get_eeprom)(struct net_device *,
struct ethtool_eeprom *, u8 *);
int (*set_eeprom)(struct net_device *,
struct ethtool_eeprom *, u8 *);
...
};
// i40e 实现
// https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_ethtool.c#L5658
static const struct ethtool_ops i40e_ethtool_ops = {
.supported_coalesce_params = ETHTOOL_COALESCE_USECS |
ETHTOOL_COALESCE_MAX_FRAMES_IRQ |
ETHTOOL_COALESCE_USE_ADAPTIVE |
ETHTOOL_COALESCE_RX_USECS_HIGH |
ETHTOOL_COALESCE_TX_USECS_HIGH,
.get_drvinfo = i40e_get_drvinfo,
.get_regs_len = i40e_get_regs_len,
.get_regs = i40e_get_regs,
.nway_reset = i40e_nway_reset,
.get_link = ethtool_op_get_link,
.get_wol = i40e_get_wol,
.set_wol = i40e_set_wol,
.set_eeprom = i40e_set_eeprom,
.get_eeprom_len = i40e_get_eeprom_len,
.get_eeprom = i40e_get_eeprom,
.get_ringparam = i40e_get_ringparam,
.set_ringparam = i40e_set_ringparam,
.get_pauseparam = i40e_get_pauseparam,
.set_pauseparam = i40e_set_pauseparam,
.get_msglevel = i40e_get_msglevel,
.set_msglevel = i40e_set_msglevel,
.get_rxnfc = i40e_get_rxnfc,
.set_rxnfc = i40e_set_rxnfc,
.self_test = i40e_diag_test,
.get_strings = i40e_get_strings,
.get_eee = i40e_get_eee,
.set_eee = i40e_set_eee,
.set_phys_id = i40e_set_phys_id,
.get_sset_count = i40e_get_sset_count,
.get_ethtool_stats = i40e_get_ethtool_stats,
.get_coalesce = i40e_get_coalesce,
.set_coalesce = i40e_set_coalesce,
.get_rxfh_key_size = i40e_get_rxfh_key_size,
.get_rxfh_indir_size = i40e_get_rxfh_indir_size,
.get_rxfh = i40e_get_rxfh,
.set_rxfh = i40e_set_rxfh,
.get_channels = i40e_get_channels,
.set_channels = i40e_set_channels,
.get_module_info = i40e_get_module_info,
.get_module_eeprom = i40e_get_module_eeprom,
.get_ts_info = i40e_get_ts_info,
.get_priv_flags = i40e_get_priv_flags,
.set_priv_flags = i40e_set_priv_flags,
.get_per_queue_coalesce = i40e_get_per_queue_coalesce,
.set_per_queue_coalesce = i40e_set_per_queue_coalesce,
.get_link_ksettings = i40e_get_link_ksettings,
.set_link_ksettings = i40e_set_link_ksettings,
.get_fecparam = i40e_get_fec_param,
.set_fecparam = i40e_set_fec_param,
.flash_device = i40e_ddp_flash,
};
|
最终在 i40e_set_ethtool_ops 函数中完成注册:
1
2
3
4
5
6
7
8
9
10
| void i40e_set_ethtool_ops(struct net_device *netdev)
{
struct i40e_netdev_priv *np = netdev_priv(netdev);
struct i40e_pf *pf = np->vsi->back;
if (!test_bit(__I40E_RECOVERY_MODE, pf->state))
netdev->ethtool_ops = &i40e_ethtool_ops;
else
netdev->ethtool_ops = &i40e_ethtool_recovery_mode_ops;
}
|
这样我们执行命令时就会调用相应的函数,比如执行 ethtool -S eth0 就会调用 i40e 驱动的 i40e_get_ethtool_stats 函数来获取相关统计信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| $ ethtool -S eth0
NIC statistics:
rx_queue_0_packets: 13542096
rx_queue_0_bytes: 3997050818
rx_queue_0_drops: 0
rx_queue_0_xdp_packets: 0
rx_queue_0_xdp_tx: 0
rx_queue_0_xdp_redirects: 0
rx_queue_0_xdp_drops: 0
rx_queue_0_kicks: 5
rx_queue_1_packets: 16117078
rx_queue_1_bytes: 28092443956
rx_queue_1_drops: 0
rx_queue_1_xdp_packets: 0
rx_queue_1_xdp_tx: 0
rx_queue_1_xdp_redirects: 0
rx_queue_1_xdp_drops: 0
rx_queue_1_kicks: 19
tx_queue_0_packets: 12617077
tx_queue_0_bytes: 4545453868
tx_queue_0_xdp_tx: 0
tx_queue_0_xdp_tx_drops: 0
tx_queue_0_kicks: 11321447
tx_queue_1_packets: 12421775
tx_queue_1_bytes: 3816676688
tx_queue_1_xdp_tx: 0
tx_queue_1_xdp_tx_drops: 0
tx_queue_1_kicks: 11235062
|
初始化向量,注册 NAPI poll 方法#
正常情况下,当数据包到来时,网卡通过硬中断的方式通知 CPU 处理数据是没有问题的。但随着网络带宽、吞吐量的不断升高,传统的中断方式逐渐不再适应。假设 MTU 是 1460byte,在千兆以太网下,可能每秒有近 10 万次的中断,这会导致 CPU 一直忙于处理硬中断从而没办法做别的事情。
为了解决该问题,从 Linux 内核 2.6 开始引入了 NAPI 机制,其核心思想是 通过轮询而不是中断的方式去读取数据包,其工作过程大致如下,详细过程我们会在后续章节中介绍。
- 网卡驱动初始化时会注册 poll 函数
- 数据到来时会判断是否有 poll 函数运行
- 如果有,则通过 poll 函数进行数据包的处理,从而避免触发硬中断
- 如果没有,则触发硬中断,CPU 进入中断流程处理,并在后续的处理中唤醒 poll 函数
在 i40e_probe 中,i40e 会注册自己的 poll NAPI 函数,调用栈如下:
1
2
3
4
5
6
7
8
9
| i40e_probe()
└── i40e_setup_pf_switch()
└── i40e_vsi_setup()
└── i40e_vsi_setup_vectors(vsi);
└── i40e_vsi_alloc_q_vectors()
└── netif_napi_add() // 注册 NAPI poll 方法
└── i40e_alloc_rings(vsi);
└── i40e_vsi_map_rings_to_vectors(vsi); // 分配中断向量和队列,并绑定
|
在设置向量时,最终是通过 netif_napi_add 函数完成注册,每个向量都会有自己的 NAPI poll 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
| https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L11945
static int i40e_vsi_alloc_q_vector(struct i40e_vsi *vsi, int v_idx)
{
...
if (vsi->netdev)
netif_napi_add(vsi->netdev, &q_vector->napi,
i40e_napi_poll, NAPI_POLL_WEIGHT);
/* tie q_vector and vsi together */
vsi->q_vectors[v_idx] = q_vector;
return 0;
}
|
这里有两个参数:
1
2
3
4
| /* Default NAPI poll() weight
* Device drivers are strongly advised to not use bigger value
*/
#define NAPI_POLL_WEIGHT 64
|
向量绑定队列#
在完成向量初始化后,驱动会调用 i40e_alloc_rings 初始化队列的结构体,并绑定到向量和 VSI,这里只是创建结构体,具体内存的分配要等到网卡启动时执行。创建完结构体后,会调用 i40e_vsi_map_rings_to_vectors 方法绑定队列和向量的映射,后续还会再网卡启动时,通过对向量设置 CPU 亲和性,最终完成队列-向量-CPU 的关联关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| static void i40e_vsi_map_rings_to_vectors(struct i40e_vsi *vsi)
{
int qp_remaining = vsi->num_queue_pairs;
int q_vectors = vsi->num_q_vectors;
int num_ringpairs;
int v_start = 0;
int qp_idx = 0;
/* If we don't have enough vectors for a 1-to-1 mapping, we'll have to
* group them so there are multiple queues per vector.
* It is also important to go through all the vectors available to be
* sure that if we don't use all the vectors, that the remaining vectors
* are cleared. This is especially important when decreasing the
* number of queues in use.
*/
for (; v_start < q_vectors; v_start++) {
struct i40e_q_vector *q_vector = vsi->q_vectors[v_start];
num_ringpairs = DIV_ROUND_UP(qp_remaining, q_vectors - v_start);
q_vector->num_ringpairs = num_ringpairs;
q_vector->reg_idx = q_vector->v_idx + vsi->base_vector - 1;
q_vector->rx.count = 0;
q_vector->tx.count = 0;
q_vector->rx.ring = NULL;
q_vector->tx.ring = NULL;
while (num_ringpairs--) {
i40e_map_vector_to_qp(vsi, v_start, qp_idx);
qp_idx++;
qp_remaining--;
}
}
}
|
5.启动网卡#
当网络驱动加载完成后,就可以启动网卡进行正常的网络包收发了。根据 Linux 的定义,网卡启动会调用 ndo_open 函数,对应到 i40e 驱动就是 i40e_open() 函数。
i40e_open 会调用 i40e_vsi_open 函数,最主要的操作都在该函数下完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| int i40e_vsi_open(struct i40e_vsi *vsi)
{
struct i40e_pf *pf = vsi->back;
char int_name[I40E_INT_NAME_STR_LEN];
int err;
/* allocate descriptors */
// 创建 TX 队列
err = i40e_vsi_setup_tx_resources(vsi);
if (err)
goto err_setup_tx;
// 创建 RX 队列
err = i40e_vsi_setup_rx_resources(vsi);
if (err)
goto err_setup_rx;
err = i40e_vsi_configure(vsi);
if (err)
goto err_setup_rx;
if (vsi->netdev) {
snprintf(int_name, sizeof(int_name) - 1, "%s-%s",
dev_driver_string(&pf->pdev->dev), vsi->netdev->name);
// 注册硬中断处理函数
err = i40e_vsi_request_irq(vsi, int_name);
if (err)
goto err_setup_rx;
/* Notify the stack of the actual queue counts. */
err = i40e_netif_set_realnum_tx_rx_queues(vsi);
if (err)
goto err_set_queues;
} else if (vsi->type == I40E_VSI_FDIR) {
snprintf(int_name, sizeof(int_name) - 1, "%s-%s:fdir",
dev_driver_string(&pf->pdev->dev),
dev_name(&pf->pdev->dev));
err = i40e_vsi_request_irq(vsi, int_name);
if (err)
goto err_setup_rx;
} else {
err = -EINVAL;
goto err_setup_rx;
}
// 完成后续步骤,启动网卡
err = i40e_up_complete(vsi);
if (err)
goto err_up_complete;
return 0;
err_up_complete:
i40e_down(vsi);
err_set_queues:
i40e_vsi_free_irq(vsi);
err_setup_rx:
i40e_vsi_free_rx_resources(vsi);
err_setup_tx:
i40e_vsi_free_tx_resources(vsi);
if (vsi == pf->vsi[pf->lan_vsi])
i40e_do_reset(pf, I40E_PF_RESET_FLAG, true);
return err;
}
|
这里主要完成几件事情:
- 创建 RX、TX RingBuffer 队列
- 注册硬中断处理函数
- 启用硬中断,等待数据到来
分配 RX、TX 队列#
在 i40e_vsi_open 中,i40e 会调用 i40e_vsi_setup_tx_resources 和 i40e_vsi_setup_rx_resources 函数来分配 TX 和 RX 队列的资源,也就是我们常说的 RingBuffer。代码如下,可以看到其通过循环创建若干个 RingBuffer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L3331
/**
* i40e_vsi_setup_rx_resources - Allocate VSI queues Rx resources
**/
static int i40e_vsi_setup_rx_resources(struct i40e_vsi *vsi)
{
int i, err = 0;
for (i = 0; i < vsi->num_queue_pairs && !err; i++)
err = i40e_setup_rx_descriptors(vsi->rx_rings[i]);
return err;
}
// https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_main.c#L3282
/**
* i40e_vsi_setup_tx_resources - Allocate VSI Tx queue resources
**/
static int i40e_vsi_setup_tx_resources(struct i40e_vsi *vsi)
{
int i, err = 0;
// 普通独队列创建
for (i = 0; i < vsi->num_queue_pairs && !err; i++)
err = i40e_setup_tx_descriptors(vsi->tx_rings[i]);
if (!i40e_enabled_xdp_vsi(vsi))
return err;
// XDP(eXpress Data Path) 专用队列
for (i = 0; i < vsi->num_queue_pairs && !err; i++)
err = i40e_setup_tx_descriptors(vsi->xdp_rings[i]);
return err;
}
|
我们来看下具体的创建流程。
RX 队列创建流程#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_txrx.c#L1559
int i40e_setup_rx_descriptors(struct i40e_ring *rx_ring)
{
struct device *dev = rx_ring->dev;
int err;
u64_stats_init(&rx_ring->syncp);
/* Round up to nearest 4K */
rx_ring->size = rx_ring->count * sizeof(union i40e_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) {
dev_info(dev, "Unable to allocate memory for the Rx descriptor ring, size=%d\n",
rx_ring->size);
return -ENOMEM;
}
rx_ring->next_to_alloc = 0;
rx_ring->next_to_clean = 0;
rx_ring->next_to_use = 0;
/* XDP RX-queue info only needed for RX rings exposed to XDP */
if (rx_ring->vsi->type == I40E_VSI_MAIN) {
err = xdp_rxq_info_reg(&rx_ring->xdp_rxq, rx_ring->netdev,
rx_ring->queue_index, rx_ring->q_vector->napi.napi_id);
if (err < 0)
return err;
}
rx_ring->xdp_prog = rx_ring->vsi->xdp_prog;
rx_ring->rx_bi =
kcalloc(rx_ring->count, sizeof(*rx_ring->rx_bi), GFP_KERNEL);
if (!rx_ring->rx_bi)
return -ENOMEM;
return 0;
}
|
TX 队列创建流程#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| https://elixir.bootlin.com/linux/v5.15.139/source/drivers/net/ethernet/intel/i40e/i40e_txrx.c#L1419
int i40e_setup_tx_descriptors(struct i40e_ring *tx_ring)
{
struct device *dev = tx_ring->dev;
int bi_size;
if (!dev)
return -ENOMEM;
/* warn if we are about to overwrite the pointer */
WARN_ON(tx_ring->tx_bi);
bi_size = sizeof(struct i40e_tx_buffer) * tx_ring->count;
tx_ring->tx_bi = kzalloc(bi_size, GFP_KERNEL);
if (!tx_ring->tx_bi)
goto err;
u64_stats_init(&tx_ring->syncp);
/* round up to nearest 4K */
tx_ring->size = tx_ring->count * sizeof(struct i40e_tx_desc);
/* add u32 for head writeback, align after this takes care of
* guaranteeing this is at least one cache line in size
*/
tx_ring->size += sizeof(u32);
tx_ring->size = ALIGN(tx_ring->size, 4096);
tx_ring->desc = dma_alloc_coherent(dev, tx_ring->size,
&tx_ring->dma, GFP_KERNEL);
if (!tx_ring->desc) {
dev_info(dev, "Unable to allocate memory for the Tx descriptor ring, size=%d\n",
tx_ring->size);
goto err;
}
tx_ring->next_to_use = 0;
tx_ring->next_to_clean = 0;
tx_ring->tx_stats.prev_pkt_ctr = -1;
return 0;
err:
kfree(tx_ring->tx_bi);
tx_ring->tx_bi = NULL;
return -ENOMEM;
}
|
注册硬中断处理函数#
RingBuffer 队列创建完后,会注册硬中断处理函数,整体调用栈如下:
1
2
3
4
5
| i40e_open()
└── i40e_vsi_open()
└── i40e_vsi_request_irq()
└── i40e_vsi_request_irq_msix()
└── request_irq()
|
主要注册逻辑是在 i40e_vsi_request_irq_msix 实现的,我们来看下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| static int i40e_vsi_request_irq_msix(struct i40e_vsi *vsi, char *basename)
{
int q_vectors = vsi->num_q_vectors;
struct i40e_pf *pf = vsi->back;
int base = vsi->base_vector;
int rx_int_idx = 0;
int tx_int_idx = 0;
int vector, err;
int irq_num;
int cpu;
for (vector = 0; vector < q_vectors; vector++) {
struct i40e_q_vector *q_vector = vsi->q_vectors[vector];
irq_num = pf->msix_entries[base + vector].vector;
if (q_vector->tx.ring && q_vector->rx.ring) {
snprintf(q_vector->name, sizeof(q_vector->name) - 1,
"%s-%s-%d", basename, "TxRx", rx_int_idx++);
tx_int_idx++;
} else if (q_vector->rx.ring) {
snprintf(q_vector->name, sizeof(q_vector->name) - 1,
"%s-%s-%d", basename, "rx", rx_int_idx++);
} else if (q_vector->tx.ring) {
snprintf(q_vector->name, sizeof(q_vector->name) - 1,
"%s-%s-%d", basename, "tx", tx_int_idx++);
} else {
/* skip this unused q_vector */
continue;
}
// 注册中断处理函数
err = request_irq(irq_num,
vsi->irq_handler,
0,
q_vector->name,
q_vector);
if (err) {
dev_info(&pf->pdev->dev,
"MSIX request_irq failed, error: %d\n", err);
goto free_queue_irqs;
}
/* register for affinity change notifications */
q_vector->affinity_notify.notify = i40e_irq_affinity_notify;
q_vector->affinity_notify.release = i40e_irq_affinity_release;
irq_set_affinity_notifier(irq_num, &q_vector->affinity_notify);
/* Spread affinity hints out across online CPUs.
*
* get_cpu_mask returns a static constant mask with
* a permanent lifetime so it's ok to pass to
* irq_set_affinity_hint without making a copy.
*/
cpu = cpumask_local_spread(q_vector->v_idx, -1);
irq_set_affinity_hint(irq_num, get_cpu_mask(cpu));
}
vsi->irqs_ready = true;
return 0;
...
}
|
这里会为每个向量注册硬中断处理函数,并且会设置 CPU 亲和性,这样结合之前的队列与向量关系绑定,就形成了队列-向量-CPU 的关联关系,在数据包处理时,根据选择的队列就可以确定处理该数据包的 CPU。
启用硬中断#
最后,i40e_up_complete() 函数注册完 IRQ 驱动函数后网卡驱动会完成一系列的启动工作,做好收发网络包的准备。主要步骤如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| static int i40e_up_complete(struct i40e_vsi *vsi)
{
...
/* start rings */
// 1. 启动VSI的所有TX和RX队列(环形缓冲区)
err = i40e_vsi_start_rings(vsi);
if (err)
return err;
clear_bit(__I40E_VSI_DOWN, vsi->state);
// 2. 为VSI的所有队列向量启用NAPI(New API)轮询机制
i40e_napi_enable_all(vsi);
// 3. 启用VSI的所有硬件中断
i40e_vsi_enable_irq(vsi);
if ((pf->hw.phy.link_info.link_info & I40E_AQ_LINK_UP) &&
(vsi->netdev)) {
i40e_print_link_message(vsi, true);
// 启用发送队列
netif_tx_start_all_queues(vsi->netdev);
// 设置网卡状态为 UP
netif_carrier_on(vsi->netdev);
}
/* replay FDIR SB filters */
if (vsi->type == I40E_VSI_FDIR) {
/* reset fd counters */
pf->fd_add_err = 0;
pf->fd_atr_cnt = 0;
i40e_fdir_filter_restore(vsi);
}
/* On the next run of the service_task, notify any clients of the new
* opened netdev
*/
// 通知其他子系统(如RDMA、存储等)网络接口已经就绪
set_bit(__I40E_CLIENT_SERVICE_REQUESTED, pf->state);
i40e_service_event_schedule(pf);
return 0;
}
|
至此,网络相关的系统初始化工作基本完成,网卡和驱动程序已经准备好接收和发送网络数据包。下一篇我们将分析 Linux 网络收包的具体流程。