理解Linux内部网络实现之关键数据结构 net_device

Understanding Linux Networking internal 系列之 Critical Data Structures

背景

在 Linux 的网络栈实现代码中,引用到了一些数据结构。要理解 Linux 内部的网络实现,需要先理清这些数据结构的作用。关键数据结构主要有两个: sk_buffnet_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 分配一个唯一的 ID
  • int 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_dmafree_dma 函数。启用或关闭已经获取的 DMA 通道,需要使用 enable_dmadisable_dma 函数。DMA 并非适用于所有设备,因为某些总线没有使用它。

unsigned short flags
unsigned short gflags
unsigned short priv_flags

flags 标志位字段,其中的某些位表示网络设备的功能(例如 IFF_MULTICAST),其他位表示设备的状态(例如 IFF_UPIFF_ 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(双绞线水晶头)),并允许用户根据自己的需要选择其中之一。此参数用于设置设备的端口类型。如果配置命令没有强制设备驱动程序选择特定的端口类型,则只需选择默认端口类型。

在某些情况下,单个设备驱动程序可以处理不同类型的接口。在这种情况下,接口可以通过简单地按特定顺序尝试所有端口类型来发现要使用的端口类型。

这段代码显示了一个设备驱动程序如何根据配置方式来设置接口型号:

1
2
3
4
5
6
7
8
switch (dev->if_port) {
case IF_PORT_10BASE2:
writeb((readb(addr) & 0xf8) | 1, addr);
break;
case IF_PORT_10BASET:
writeb((readb(addr) & 0xf8), addr);
break;
}

混杂模式

混杂模式的解释可以看 wiki

可以注意到 net_device 中的 promiscuity 是一个 int 类型,并不是一个常见的用 char 来表示布尔的变量。用 int 的原因是:promiscuity 字段其实是开启混杂模式的计数器。因为可能多个程序会要求设备开启混杂模式。进入混杂模式时,计数器都会递增;离开混杂模式时,计数器都会递减。直到计数器为零,设备才会关闭混杂模式。函数 set_promiscuity 用来管理混杂模式。

只要 promiscuity 不为0,flagsIFF_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_bitclear_bit 设置和清除的,这些函数通常通过包装函数来调用,该包装函数隐藏使用的位的详细信息。例如,要停止设备队列,子系统将调用 netif_stop_queue(network interface stop queue),如下所示:

1
2
3
4
5
6
7
8
9
static inline void netif_stop_queue(struct net_device *dev)
{
netif_tx_stop_queue(netdev_get_tx_queue(dev, 0));
}

static __always_inline void netif_tx_stop_queue(struct netdev_queue *dev_queue)
{
set_bit(__QUEUE_STATE_DRV_XOFF, &dev_queue->state);
}

流量控制子系统将在后面文章介绍。

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 计时器甚至不会启动。