目录
源码版本
- Linux Kernel v3.10-rc7 版本。
- Intel I350 IGB 网卡驱动程序。
1、内核启动流程
-
起电:开启主机硬件电源。
-
固件:主板固件加载 BIOS 或 UEFI,进行硬件自检和初始化,检查系统配置是否正确。
-
BIOS/UEFI:BIOS 或 UEFI 开始寻找可启动介质,读取磁盘的 MBR 或 GBT 引导分区,启动 Bootloader。
-
Bootloader:Bootloader 执行 GRUB2 引导程序,GRUB2 通过 /boot/grub2/grub.cfg 配置文件的内容,从 /boot 目录中读取 /boot/vmlinuz-3.10.0-1160.83.1.el7.x86_64 内核文件,并加载到内存中。管理员可以通过 /boot/grub2/grub.cfg 配置文件,设置系统启动选项。
-
Initramfs:Kernel 启动过程中,首先加载 /boot/initramfs-3.10.0-1160.83.1.el7.x86_64.img 镜像文件,这是 initramfs(initial RAM filesystem),作为临时文件系统用于进行基本的系统初始化工作。包括加载 /usr/lib/modules/3.10.0-1160.83.1.el7.x86_64/kernel/fs/xfs 驱动程序。有了 xfs 驱动程序之后,Kernel 才可以挂载 xfs 格式的 / 根分区并访问文件。
-
Init 系统:Kernel 挂载根分区后,开始运行 init 或 systemd 进程,这是第一个 User Process。init 进程会读取 /etc/inittab 配置文件,根据不同的运行级别,开始启动相应的各种程序和服务。包括:各种设备驱动程序、进程管理、内存管理等系统服务。
Init 系统的入口在 linux/init/main.c start_kernel(),相当于 Kernel 的 main 函数,是 Kernel 真正的初始化流程入口,start_kerenl() 将会调用一系列的初始化函数,包括:CPU 初始化,Main Memory 初始化,Interrupt 初始化,Process Scheduling 初始化,TCP/IP Stack 初始化等,目的是最终建立起基本完整的 Linux Kernel ENV。
asmlinkage __visible void __init start_kernel(void)
{
...
rest_init(); // 最后一个函数。
}
rest_init() 的最后会进入 0 号进程,并开始永循环。
2、内核协议栈初始化流程
start_kernel() 过程中调用 linux/net/socket.c sock_init() 进入协议栈初始化流程。
static int __init sock_init(void)
{
...
err = net_sysctl_init();
...
skb_init();
...
init_inodecache();
...
err = register_filesystem(&sock_fs_type);
...
sock_mnt = kern_mount(&sock_fs_type);
...
netfilter_init();
...
skb_timestamping_init();
...
}
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
-
sock_init() Socket 初始化:使用 Slab 内存分配算法创建 sk_buff 的 Cache 空间,并注册 Socket filesystem。
-
proto_init() 协议栈初始化:在 /proc/net/ 目录下创建各类协议文件,注册相关的协议文件操作函数。
-
dev_init() 网络设备初始化:
- 在 /proc/sys/net/ 目录下创建 Ethernet Device 和 TCP/IP Protocols 相关的数据结构文件;
- 开启 Device 的 Hardware Rx/Tx Interrupt;
- 为每个 CPU 初始化一个 Rx Queues 并绑定硬中断号,同时注册接收报文的软中断回调函数;
- 注册 loopback 本地回环操作函数;
-
inet_proto_init INET Socket 初始化:注册 INET Socket 接口函数,例如:TCP、UDP、ICMP、IGMP 等协议类型的基本收包处理函数。
-
unix_proto_init UNIX Socket 初始化:注册 UNIX Socket 接口函数。
3、网卡驱动程序注册流程
Driver 会调用 module_init() 向 Kernel 注册 init() 函数,然后在 dev_init() 过程中初始化 Device 和 Driver 时,Kernel 就会调用它。以 Intel I350 网卡的 IGB Driver(Intel Gigabit Ethernet)为例,它的初始化函数为 linux/drivers/net/ethernet/intel/igb/igb_main.c igb_init_module()。
/**
* 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\n", igb_driver_string);
pr_info("%s\n", igb_copyright);
#ifdef CONFIG_IGB_DCA
dca_register_notify(&dca_notifier);
#endif
ret = pci_register_driver(&igb_driver);
return ret;
}
module_init(igb_init_module);
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
IGB Driver 初始化流程的核心是 pci_register_driver(),它维护了一个 pci_device_id 映射表,通过读取 Device PCI configuration space(如下图)中的 Vendor ID 和 Device ID 来并识别出 Device 具体的型号以及对应的驱动程序。
例如 Intel I350 的主板型号为 board_82575。
struct pci_device_id {
__u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/
__u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */
__u32 class, class_mask; /* (class,subclass,prog-if) triplet */
kernel_ulong_t driver_data; /* Data private to the driver */
__u32 override_only;
};
static const struct pci_device_id igb_pci_tbl[] = {
...
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I350_COPPER), board_82575 },
...
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_82580_COPPER), board_82575 },
...
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
pci_register_driver() 还会将 IGB Driver 的各种回调函数注册到一个 pci_driver 结构体实例中,例如:IGB Driver 具体实现的 igb_driver_name() 和 igb_probe() 等函数。如此的,IGB Driver 就被注册到了 Kernel 的设备接口层(Device agnostic interface)中。
static struct pci_driver igb_driver = {
.name = igb_driver_name,
.id_table = igb_pci_tbl,
.probe = igb_probe,
.remove = igb_remove,
#ifdef CONFIG_PM
.driver.pm = &igb_pm_ops,
#endif
.shutdown = igb_shutdown,
.sriov_configure = igb_pci_sriov_configure,
.err_handler = &igb_err_handler
};
- 6
- 7
- 8
- 9
- 10
- 11
- 12
后面,当计算机插入一张 Intel I350 PCIe 网卡后,Kernel 就可以通过 PCIe Device 的 Vendor ID 和 Device ID 来匹配到对应的 Probe(探针)函数 igb_probe(),然后进入 IGB Driver 的初始化流程。
4、网卡驱动程序初始化流程
IGB Driver 的初始化流程从 linux/drivers/net/ethernet/intel/igb/igb_main.c igb_probe() 函数开始,如下图所示。
/**
* igb_probe - Device Initialization Routine
* @pdev: PCI device information struct
* @ent: entry in igb_pci_tbl
*
* Returns 0 on success, negative on failure
*
* igb_probe initializes an adapter identified by a pci_dev structure.
* The OS initialization, configuring of the adapter private structure,
* and a hardware reset occur.
**/
static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
- 6
- 7
- 8
- 9
- 10
- 11
- 12
-
dma_set_mask() 申请 DMA 内存空间和 I/O 端口。
-
pci_request_selected_regions() 获取 PCIe 设备的 Resource,包括:Memory BAR、I/O BAR 和 MSI-X BAR 这 3 个 Regions,并通过这些 BARs 完成一系列访问和初始化,例如更新 Linux 文件系统 /sys/bus/pci/devices/{BDF}/。
-
alloc_etherdev_mq() => alloc_netdev_mq():
- 实例化 Kernel 的 net_device(网络设备管理,包含了 Device 的详细信息)结构体和 IGB 私有的 igb_adapter(包含了 IGB Driver 的特性信息)结构体。
- 实例化 Kernel 的 netdev_queue(网络设备队列管理)结构体,并关联到 net_device。
- 初始化 net_device 实例。
-
设置 net_device->netdev_ops 设备操作函数集(包含了 Device 的各种操作回调函数,例如:igb_open 设备启动函数、收/发包函数等)和 net_device->ethtool_ops ethtool 操作函数集。
-
igb_sw_init() => igb_init_interrupt_scheme():
- igb_set_interrupt_capability():设置网卡的发送队列数量、接收队列数量、中断描述符数量,调用 pci_enable_msix() 获得网络的 MSIX 中断号。并将其保存到 pci_dev->msi_list 的每一项 msi_desc.irq 中。
- igb_alloc_q_vectors():根据之前设置的中断描述符个数,初始化中断描述符 igb_q_vector,并加入到 igb_adapter->q_vector[] 列表中。同时初始化 igb_q_vector 中的 napi 结构体(注册 NAPI 收包机制所必须的 poll() 函数),然后将 napi 实例挂载到 net_device->napi_list 链表中。
- igb_alloc_queues():根据之前设置的发送队列个数,实例化 igb_ring 结构体,然后添加到 igb_adapter->tx_ring[] 列表中。同样的,为接收队列实例化 igb_ring 结构体,并添加到 igb_adapter->rx_ring[] 列表。
- igb_map_ring_to_vector():将 Rx Ring、Tx Ring 实例和 igb_q_vector 关联起来,即:igb_q_vector->tx_ring 和 igb_q_vector->rx_ring。
-
igb_init_hw_timer() 设置网卡硬件定时器。
-
igb_probe_vfs() 设置 SR-IOV 特性,如果没有开启则将 igb_adapter->vfs_allocated_count 设置为 0。
-
igb_irq_disable() 关闭网卡设备的中断。
-
设置网卡特性标志 net_device->features 和 net_device->vlan_features。
-
获取 Ethernet Ports 的 MAC 地址并保存到 net_device->dev_addr 中。
-
register_netdev() 将新建的 net_device 实例注册到 Kernel 中。
如此的,Kernel 就掌握了 PCIe Device 的详细信息以及各类操作函数入口,并以此完成对 Device 控制。
net_device 结构体
struct igb_adapter {
/* 网络设备的标准属性和状态 */
struct net_device *netdev; // net_device 结构体
/* PCI 设备相关信息 */
struct pci_dev *pdev; // PCI 设备详细信息。
const struct pci_device_id *id; // PCI 设备 ID。
/* 记录软硬件状态信息 */
struct igb_hw hw;
struct igb_ring rx_ring; // 接收队列。
struct igb_ring tx_ring[IGB_MAX_TX_QUEUES]; // 发送队列。
struct igb_ring test_ring; // 测试队列。
u16 vfs_allocated_count; // 已分配的虚拟网卡数量。
/* 记录网络设备的 MAC 地址 */
u8 perm_addr[ETH_ALEN]; // MAC 地址
u8 mc_addr[IGB_MAX_MULTICAST_ADDRS][ETH_ALEN]; // 多播地址。
u32 num_mc_addrs; // 多播地址数量。
u32 flags;
u32 wol;
unsigned long state; // 设备状态。
/* 记录统计信息 */
struct igb_stats stats; // 数据包统计信息。
u64 tx_timeout_count; // 发送超时次数。
u32 watchdog_timer; // watchdog 计时器。
u32 link_speed; // 链路速度。
u8 link_duplex; // 链路双工模式。
bool link_up; // 链路状态。
/* 记录中断相关信息 */
int num_vectors;
cpumask_var_t active_vlans;
struct igb_q_vector *q_vector[0];
};
struct net_device {
/* 网络设备名称及类型 */
char name[IFNAMSIZ];
const struct net_device_ops *netdev_ops; // net_device_ops 结构体
struct device dev;
/* 网络设备状态和属性 */
struct net_device_stats stats;
unsigned flags;
unsigned short gflags;
/* 网络设备的地址和路由信息 */
struct net_device * master;
struct net_device * real_dev;
struct netdev_hw_addr_list hw_addr_list;
unsigned int ifindex;
struct hlist_node name_hlist;
/* 网络设备队列相关信息 */
struct list_head napi_list;
struct netdev_queue *tx_queue;
struct Qdisc *qdisc;
struct netdev_rx_queue *rx_cpu_rmap;
struct rps_map *rps_cpu_map;
struct xps_map *xps_cpu_map;
/* 网络设备的回调函数 */
struct netdev_features *features;
struct net_device_ops *ethtool_ops;
const struct ethtool_link_ksettings *link_ksettings;
struct phy_device *phydev;
struct netdev_adjacent *adj_list;
};
- 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
net_device_ops 结构体
static const struct net_device_ops igb_netdev_ops = {
.ndo_init = igb_init_netdev, /* 初始化网络设备 */
.ndo_uninit = igb_uninit_netdev, /* 停止网络设备 */
.ndo_start_xmit = igb_xmit_frame, /* 发送网络数据包 */
.ndo_rx_handler = igb_rx_handler, /* 收到网络数据包 */
.ndo_rx_callback = igb_rx_cleanup, /* 收到网络数据包后的处理函数 */
.ndo_tx_timeout = igb_tx_timeout, /* 取消发送数据包 */
.ndo_get_stats64 = igb_get_stats64, /* 获取网络设备统计信息 */
...
};
- 6
- 7
- 8
- 9
- 10
初始化数据结构全景图
5、创建网络接口
在 igb_probe() 的后期调用了 linux/include/linux/netdevice.h register_netdev() 将 net_device 实例注册到 Kernel 中,并会创建对应的 Network Interface。然后我们在 Shell 中就可以看见对应的网卡设备了。
extern int register_netdev(struct net_device *dev);
- 1
register_netdev() 读取 net_device 实例提供的信息,并根据 IGB Driver 设定的 Network interface name prefix(前缀),为其生成一个唯一的 Interface name。例如:ethX(Ethernet)。
ifconfig 指令
当我们执行指令 ifconfig eth0 时,就可以查看到 net_device 实例提供的 Name、MAC、Mask、MTU 等信息。这些信息。这实际上是 ifconfig 指令通过调用 Socket I/O SCI 来实现的。
$ ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.1.3 netmask 255.255.255.0 broadcast 192.168.1.255
inet6 fe80::a6d6:97f7:7a9a:4c71 prefixlen 64 scopeid 0x20<link>
ether 52:54:00:08:ea:e0 txqueuelen 1000 (Ethernet)
RX packets 1072779 bytes 591984204 (564.5 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 308802 bytes 80454446 (76.7 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
- 6
- 7
- 8
- 9
- 10
-
RX errors:NIC 总的收包错误数量,包括 too-long-frames 错误,Rx Ring 溢出错误,CRC 校验错误,Frame 同步错误,FIFO Overruns 错误、Missed pkg 错误等。
-
RX dropped:NIC 总的丢包数量,通常是由于 skb_buffer 内存空间不足导致的,表示 CPU 处理能力低于 NIC 带宽。
-
RX overruns:NIC 总的 FIFO Overruns 错误数量,通常是由于 CPU 无法及时处理 NIC 发出的硬件中断导致的,表示硬件中断可能没有均衡的分布在多个 CPU Cores 上。
-
RX frame:表示 Misaligned 的 Frames。
ethtool 指令
也可以使用 ethtool 命令行工具用于查看并 net_device 结构体的配置信息。ethtool 指令则是通过调用 ioctl I/O SCI 与 Net device 注册的 ethtool 函数进行交互来实现的。
- 查看 NIC 的基础信息,包括 Supported ports(TP 电口、Fiber 光口)、Supported link modes、Speed(速率)、Duplex(双工)、Link detected 等。
$ ethtool enp2s0
Settings for enp2s0:
Supported ports: [ TP MII ]
Supported link modes: 10baseT/Half 10baseT/Full
100baseT/Half 100baseT/Full
1000baseT/Half 1000baseT/Full
Supported pause frame use: No
Supports auto-negotiation: Yes
Supported FEC modes: Not reported
Advertised link modes: 10baseT/Half 10baseT/Full
100baseT/Half 100baseT/Full
1000baseT/Half 1000baseT/Full
Advertised pause frame use: No
Advertised auto-negotiation: Yes
Advertised FEC modes: Not reported
Link partner advertised link modes: 10baseT/Half 10baseT/Full
100baseT/Half 100baseT/Full
Link partner advertised pause frame use: Symmetric Receive-only
Link partner advertised auto-negotiation: Yes
Link partner advertised FEC modes: Not reported
Speed: 100Mb/s
Duplex: Full
Port: MII
PHYAD: 0
Transceiver: internal
Auto-negotiation: on
Supports Wake-on: pumbg
Wake-on: d
Current message level: 0x00000033 (51)
drv probe ifdown ifup
Link detected: yes
- 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
- 查看 NIC 的数据统计。
$ ethtool -S enp2s0
NIC statistics:
tx_packets: 3255
rx_packets: 39837
tx_errors: 0
rx_errors: 0
rx_missed: 0
align_errors: 0
tx_single_collisions: 0
tx_multi_collisions: 0
unicast: 3094
broadcast: 7873
multicast: 36743
tx_aborted: 0
tx_underrun: 0
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 监控 NIC 的 errors、dropped、overruns、frame 等错误信息。
$ for i in `seq 1 100`; do ifconfig enp2s0 | grep RX | grep overruns; sleep 1; done
- 1