Understanding Linux Networking internal 系列之 Critical Data Structures
背景
在 Linux 的网络栈实现代码中,引用到了一些数据结构。要理解 Linux 内部的网络实现,需要先理清这些数据结构的作用。关键数据结构主要有两个: sk_buff
和 net_device
。
- struct
sk_buff
: 是整个网络数据包存储的地方。这个数据结构会被网络协议栈中的各层用来储存它们的协议头、用户数据和其他它们完成工作需要的数据。 - struct
net_device
: 在 Linux 内核中,这个数据结构将用来代表网络设备。它会包含设备的硬件和软件配置信息。 - 在 Linux 的网络实现中,核心数据结构还有
struct sock
, 它被用来储存 socket 的信息。但是 Socket 其实是内核为用户态程序提供的一组 Api, 用来访问内核的网络栈实现,所以它不属于内核内部的网络实现,也就不再这里介绍了。
本文将着重理解 net_device
数据结构,上一文为对 sk_buff
的理解。
net_device
net_device 数据结构储存着与网络设备有关的所有信息。无论真实设备还是虚拟设备,每个设备都一种这样的结构。系统上所有设备的 net_device
信息会被放到一个全局的列表中,全局指针 dev_base
指向这个列表。net_device
定义在 include/linux/netdevice.h。
net_device
结构体里的字段相当多并且有许多属于不同功能特有的字段和属于不同层的字段。
网络设备可以根据类型(比如:以太网卡和令牌环网卡)进行分类。对于相同类型的所有设备,net_device 的某些字段被设置为相同的值;对于不同型号的设备则必须将某些字段设置为不同值。因此,几乎对于每种类型,Linux 提供了一个通用的函数来初始化参数,这些参数的值在所有型号的设备中都保持不变。每个设备驱动程序除了设置其驱动的设备具有唯一值的那些字段外,也会调用这个函数来初始化通用的参数。当然驱动程序也能够重写已经被内核初始化了的字段。
net_device
结构体中的字段大致可以分为以下几类:
- Configuration 与配置相关的字段
- Statistics 与统计相关
- Device status 与设备状态相关
- List management 维护
net_device
列表相关的函数 - Traffic management 流量管理函数
- Feature specific 特有功能的函数
- Generic 通用的一些字段
- Function pointers 一些函数指针
Identifiers
net_device 结构体包含了3个表示标识符的字段:
int ifindex
:一个唯一 ID,每个设备通过调用dev_new_index
分配一个唯一的 IDint iflink
:这个字段被(虚拟)隧道设备使用,用来标示隧道设备另一端将要到达的真实的设备unsigned short dev_id
:这个字段用于区分可以同时在不同操作系统之间共享同一设备的虚拟实例
Configuration
内核为某些配置字段提供默认值,具体取决于网络设备的类别,某些字段留给驱动程序填充。驱动程序可以改变默认值,并且一些字段能够在运行时通过命令去修改(比如:ifconfig
ip
命令)。实际上,在加载设备模块时,用户通常会设置几个参数(base_addr,if_port,dma 和 irq)。另一方面,虚拟设备一般不使用这些参数。
char name[IFNAMESIZ]
设备的名称,比如 eth0
unsigned long mem_start
unsigned long mem_end
这两个字段描述了设备与内核共享的内存的开始和结束位置。它们仅在设备驱动程序中初始化和访问;高层不需要关心它们。
unsigned long base_addr
I/O 内存映射到设备自身内存的开始地址。
unsigned int irq
(interrupt number)中断编号,当设备想与内核通信时使用。可以在多个设备之间共享。驱动程序使用request_irq
函数分配此变量,并使用 free_irq
释放它。
unsigned char if_port
(interface port) 设备使用接口在计算机上的端口。
比如我们笔记本电脑上的网卡其实支持双绞线(就是用水晶头的那个接口)和同轴电缆两种网络接入方式,双绞线和同轴电缆在我们电脑上会被分配两个端口号,网卡在工作时就需要知道自己要从哪个端口去读写数据。
unsigned char dma
设备使用的DMA通道。从内核获取和是否 DMA 通道,需要使用 request_dma
和 free_dma
函数。启用或关闭已经获取的 DMA 通道,需要使用 enable_dma
和 disable_dma
函数。DMA 并非适用于所有设备,因为某些总线没有使用它。
unsigned short flags
unsigned short gflags
unsigned short priv_flags
flags
标志位字段,其中的某些位表示网络设备的功能(例如 IFF_MULTICAST),其他位表示设备的状态(例如 IFF_UP
或 IFF_ RUNNING
)。设备驱动程序通常在初始化时设置功能,状态标志的管理则通过内核对外部事件的响应进行。
~$ ifconfig lo lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 UP LOOPBACK RUNNING MTU:65536 Metric:1 //...
比如 ifconfig lo
命令的结果中,UP LOOPBACK RUNNING
就对应到 flags 中的 IFF_UP
,IFF_LOOPBACK
,IFF_RUNNING
标志位。
priv_flags
存储用户空间不可见的标志。现在,该字段由VLAN和网桥虚拟设备使用。
gflags
几乎从未使用过,出于兼容性原因而存在。 上面的标志位能够通过 dev_change_flags
函数修改。
int features
标记的另一个位图用于存储其他设备功能。该数据结构包含的多个标志变量不是多余的。features
字段表示网卡与 CPU 进行通信的能力,例如网卡是否可以对高速内存进行 DMA 通信,或对硬件中的所有数据包进行校验和。该参数由设备驱动程序初始化。可以在 include/linux/netdev_features.h 中找到带有明确注释的 NETIF_F_XXX
宏。
unsigned int mtu
MTU 代表最大传输单位,它表示设备(比如:以太网网卡)可以处理的最大帧大小。
以太网中常用设备的 MTU:
设备类型 | MTU(单位:字节) |
---|---|
Ethernet | 1500 |
Token Ring 4 MB/s | 4464 |
Token Bus | 8182 |
Token Ring 6 MB/s | 17914 |
Hyperchannel | 65535 |
以太网 MTU 值得聊下。以太网帧规范将最大有效负载大小定义为 1500 字节。有时,你会发现以太网 MTU 定义为 1518 或 1514:第一个是包含报头的以太网帧的最大大小,第二个是包含报头,但不包括帧校验序列(校验和的4个字节)的最大大小。
1998年,Alteon Networks 提出了一项将以太网帧的最大有效负载增加到 9KB 的倡议。后来,该提案通过IETF Internet 草案正式化,但IEEE从未接受。在 IEEE 规范中,超过 1500 字节的帧通常称为巨型帧,并与千兆以太网一起使用以提高吞吐量(这是因为较大的帧意味着用于大型数据传输的帧减少,中断次数减少,因此CPU使用率降低,标头开销减少等)。要讨论增加以太网 MTU 的好处以及IEEE 为什么不同意此扩展的标准化,可以搜索白皮书 “Use of Extended Frame Sizes in Ethernet Networks”。Extended Ethernet Frame Size Support 下面也有 IEEE 不同意扩展到 9KB 的回复。
unsigned short type
设备属于的类别。
unsigned short hard_header_len
设备头的字节长度。比如以太网设备头的长度是 14 字节。每个设备头的长度在该设备的头文件中定义。ETH_HLEN
定义在 include/uapi/linux/if_ether.h 中。
unsigned char broadcase[MAX_ADDR_LEN]
链路层广播地址。
unsigned char dev_addr[MAX_ADDR_LEN]
unsigned char addr_len
dev_addr
是设备链路层的地址;地址的长度(以字节为单位)由addr_len
给出。addr_ len
的值取决于设备的类型。以太网设备的地址为6个字节。
int promiscuity
表示设备是否开启混杂模式。
接口类型和端口
有些设备带有不止一个连接器(最常见的组合是 BNC(同轴电缆)和 RJ45(双绞线水晶头)),并允许用户根据自己的需要选择其中之一。此参数用于设置设备的端口类型。如果配置命令没有强制设备驱动程序选择特定的端口类型,则只需选择默认端口类型。
在某些情况下,单个设备驱动程序可以处理不同类型的接口。在这种情况下,接口可以通过简单地按特定顺序尝试所有端口类型来发现要使用的端口类型。
这段代码显示了一个设备驱动程序如何根据配置方式来设置接口型号:
|
|
混杂模式
混杂模式的解释可以看 wiki。
可以注意到 net_device
中的 promiscuity
是一个 int 类型,并不是一个常见的用 char 来表示布尔的变量。用 int 的原因是:promiscuity
字段其实是开启混杂模式的计数器。因为可能多个程序会要求设备开启混杂模式。进入混杂模式时,计数器都会递增;离开混杂模式时,计数器都会递减。直到计数器为零,设备才会关闭混杂模式。函数 set_promiscuity
用来管理混杂模式。
只要 promiscuity
不为0,flags
的 IFF_PROMISC
位标志也将置1,并由配置接口的函数检查。
Statistics
net_device 没有提供用于保留统计信息的字段集合,而是包含一个名为 priv
的指针,该指针由驱动程序设置为指向存储有关接口信息的私有数据结构。私有数据包含了统计信息,例如发送和接收的数据包数量以及遇到的错误数量。
priv
指向的数据结构的格式取决于设备类型和特定型号,不同的以太网卡可能使用不同的私有结构。但是,几乎所有结构都包含一个 net_device_stats
类型的字段(在 include/linux/netdevice.h 中定义),该字段包含所有网络设备共有的统计信息,并且可以使用 get_stats
方法进行检索。
Device Status
为了控制与 NIC(Network interface control: 对网络设备的抽象称呼,比如:网卡就是一种 NIC) 的交互,每个设备驱动程序都必须维护诸如时间戳和标志之类的信息,以指示接口需要哪种行为。在多处理器系统中,内核还必须确保正确处理了来自不同 CPU 的对同一设备的并发访问。net_device 的几个字段专用于这类的信息:
unsigned long state
网络排队子系统使用的一组标志。它们由枚举 netdev_state_t
中的常量索引,该常量在 include/linux/netdevice.h 中定义,并为 state
的每个位设置诸如 _ _LINK_STATE_XOFF
之类的常量。单个位是使用通用函数 set_bit
和 clear_bit
设置和清除的,这些函数通常通过包装函数来调用,该包装函数隐藏使用的位的详细信息。例如,要停止设备队列,子系统将调用 netif_stop_queue
(network interface stop queue),如下所示:
|
|
流量控制子系统将在后面文章介绍。
enum {…} reg_state
(registration state)设备的注册状态。
unsigned long trans_start
最后一帧传输开始的时间。设备驱动程序在开始传输之前进行设置。如果在给定的时间后网卡仍未完成传输,则该字段用于检测网卡的问题。传输时间过长意味着有问题。在这种情况下,驱动程序通常会重置网卡。
unsigned long last_rx
接收到最后一个数据包的时间。目前,它还没有用于任何特定目的,但是可以在需要时使用。
*struct net_device master
存在一些协议,这些协议允许将一组设备组合在一起并被视为一个设备。这些协议包括EQL(用于串行网络接口的均衡器负载均衡器),Bonding(也称为 EtherChannel和中继)和流量控制的TEQL(真实均衡器)排队规则。组中的设备之一被选为所谓的主机(master),它扮演着特殊的角色。该字段是指向该组主设备的 net_device 数据结构的指针。如果一个设备不是该组的成员,则指针为NULL。
spinlock_t xmit_lock
int xmit_lock_owner
xmit_lock
锁用于序列化对驱动程序函数 hard_start_ xmit
的访问。这意味着每个CPU一次只能在任何给定设备上执行一次传输。xmit_lock_owner
是持有锁的CPU的ID。在单处理器系统上,它始终为0;在多处理器系统上未被锁定时,则始终为–1。当设备驱动程序支持时,也可能具有无锁传输。
*void atalk_ptr
*void ip_ptr
*void dn_ptr
*void ip6_ptr
*void ec_ptr
*void ax25_ptr
这6个字段是指向特定协议特定数据结构的指针,每个数据结构都包含该协议专用的参数。
例如,ip_ptr
指向 in_device
类型的数据结构,该数据结构包含了在设备上配置的IP 地址列表中的不同 IPv4 相关参数。
List Management
net_device 数据结构被插入到全局列表和两个哈希表中。下面的字段被用来维护全局列表和哈希表:
*struct net_device next
指向在全局列表中的下一个 net_device 的指针。
struct hlist_node name_hlist
struct hlist_node index_hlist
将 net_device 链接到两个哈希表的数据列表中。
Traffic Management
Linux 提供了一些流量控制的机制。相关的字段也定义在 net_device 中:
*struct net_device next_sched
由软件中断使用
*struct Qdisc qdisc
*struct Qdisc qdisc_sleeping
*struct Qdisc qdisc_ingress
struct list_head qdisc_list
这些字段用于管理入口和出口数据包队列,以及从不同的CPU访问设备。
spinlock_t queue_lock
spinlock_t ingress_lock
流量控制模块为每个网络设备定义一个专用出口队列。queue_lock
用于避免同时访问它。ingress_lock
对入口流量执行相同的操作。
unsigned long tx_queue_len
设备的传输队列的长度。当内核打开了流量控制时,可能不使用 tx_queue_len
。可以使用 sysfs
文件系统调整其值(/sys/class/net/device_name/ 目录)。
Generic
除了前面讨论的net_device结构的列表管理字段之外,还有一些其他字段用于管理结构并确保在不需要它们时将其删除:
atomic_t refcnt
net_device 被引用的计数。在此计数器变为零之前,无法注销该设备。
int watchdog_timeo
struct timer_list watchdog_timer
这些字段与前面讨论的tx_timeout变量一起实现了 watchdog 定时器。
*int (poll)(…)
struct list_head poll_list
int quota
int weight
由 NAPI 功能使用
*const struct iw_handler_def wireless_handlers
*struct iw_public_data wireless_data
无线设备使用的其他参数和函数指针
struct list_head todo_list
网络设备的注册和注销分两个步骤进行。 todo_list用于处理第二个。
struct class_device class_dev
由新的通用内核驱动程序基础结构使用。
Function Pointers
net_device 中定义了许多函数指针,它们按用途大致能分为:
- 传输和接收数据帧
- 增加或者解析链路层 header
- 改变设备的配置
- 获取统计信息
- 和特别的功能交互
*int (init)(…)
*void (uninit)(…)
*void (destructor)(…)
*int (open)(…)
*int (stop)(…)
以上函数指针,被用来初始化、清空、销毁、打开、关闭一个设备。
struct net_device_stats (get_stats)(…)
struct iw_statistics (get_wireless_stats)(…)
设备驱动程序收集的一些统计信息可以让用户空间应用程序显示,例如 ifconfig 和ip 命令,而其他统计信息则由内核使用。这两种方法用于收集统计信息。 get_stats 在普通设备上运行,而 get_wireless_stats 在无线设备上运行。
*int (hard_start_xmit)(…)
用于传输帧
*int (hard_header)(…)
*int (rebuild_header)(…)
*int (hard_header_cache)(…)
*void (header_cache_update)(…)
*int (hard_header_parse)(…)
*int (neigh_setup)(…)
由相邻层使用的函数指针。
*int (do_ioctl)(…)
我们知道用户态进程能够使用 ioctl
这个系统调用,向设备发出命令。上面这个函数就是用来处理 ioctl
命令的。
*int (set_mac_address)(…)
更改设备的 MAC 地址。当设备不提供此功能时(如Bridge虚拟设备),则将其设置为NULL。
*int (set_config)(…)
配置驱动程序的参数,例如硬件参数 irq,io_addr 和 if_port。较高层的参数(例如协议地址)由 do_ioctl 处理。
*int (change_mtu)(…)
改变设备的 MTU。更改这个字段对设备驱动程序没有影响,只是会强制内核软件按照新的 MTU 去处理分片。
*void (tx_timeout)(…)
在 watchdog 计时器到期时调用的方法,计时器确定传输是否花费了可疑的长时间完成。除非定义了此方法,否则 watchdog 计时器甚至不会启动。