0. 概述
前边我们学习了Linux的进程与内存部分,这里我们来看看linux网络协议栈。
从以往的学习中,有如下3点的认识:
- socket的使用流程:建立、通信、关闭
- TCP/IP的分层协议栈:链路层、网络层、传输层等
- Linux操作系统是硬件与应用程序之间的代理特性
这样也就组成了对Linux网络协议栈的基本认识,整体如图:
整个结构也是从物理层、到链路层、到网络层、到传输层、最后到应用层,我们依次来看看,重点在于tcp/ip的网络层与传输层。
与内存管理类似,这块学习主要参考《嵌入式Linux网络系统结构设计与TCP/IP协议栈》,次要参考《深入理解Linux网络技术内幕》。
1. socket buffer
sk_buff与数据包
内核中TCP/IP协议栈是分层实现的,数据包在内核中要经过协议栈的传输层、网络层数据链路层,socket buffer会贯穿在整个协议栈的各层。
一个完整的socket buffer由2个实体组成:
- 数据包,存放实际要在网络中传输的数据缓冲区。
- 管理数据结构sk_buff,在内核中处理数据包时,还需要一些其他的数据来管理、操作数据包。
-
sk_buff的具体属性
下边来看看sk_buff的具体属性:
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next; // sk_buff双向链表,指向后一个元素的指针
struct sk_buff *prev; // 指向前一个元素的指针
ktime_t tstamp; // 描述接收数据包达到内核的时间
struct sock *sk; // 指向套接字的指针
struct net_device *dev; // 指向代表网络设备数据结构的指针,收到或发出
char cb[48] __aligned(8); // Control Buffer,各层协议在处理数据包时存放私有信息或变量的地方。
unsigned long _skb_refdst;
#ifdef CONFIG_XFRM
struct sec_path *sp; // Security Path,用于 IPsec 协议跟踪网络数据包的传送路径
#endif
unsigned int len, // 数据包的总长度,包括各分片
data_len; // 只精确计算被分了片的数据长度
__u16 mac_len, // 数据链路层协议头长度
hdr_len; // 针对克隆数据包时使用,表名克隆数据包的头长度
union {
__wsum csum; // 存放数据包的校验和
struct {
__u16 csum_start; // 以skb->head为起始地址的偏移量
__u16 csum_offset; // 以start以起始地址的偏移量
};
};
__u32 priority; // 用来实现质量服务QoS功能特性
....
__be16 protocol; // 接收数据包的网络层协议,如IP协议
void (*destructor)(struct sk_buff *skb); // 指向Sokect Buffer的析构函数
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct nf_conntrack *nfct; // 连接跟踪,跟踪计数
#endif
#ifdef NET_SKBUFF_NF_DEFRAG_NEEDED
struct sk_buff *nfct_reasm; // 连接跟踪,数据包的重组装
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
struct nf_bridge_info *nf_bridge; // 网桥,存放数据包的数据结构
#endif
int skb_iif;
#ifdef CONFIG_NET_SCHED
__u16 tc_index; // 流量控制,存放数据包发送选择算法的索引号
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd; // 流量控制,存放选择流量控制后对数据包所做处理的索引号
#endif
#endif
__u32 rxhash;
__u16 queue_mapping; // 发送网络数据包所在队列与设备硬件发送队列的映射关系
...
union {
__u32 mark; // 数据包为常规数据包的标志
__u32 dropcount;
};
__u16 vlan_tci; // 虚拟局域网的标志
sk_buff_data_t transport_header; // 传输层协议头在网络数据包中的地址
sk_buff_data_t network_header; // 网络层协议头在网络数据包中的地址
sk_buff_data_t mac_header; // 数据链路层协议头在网络数据包中的地址
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail; // 指向实际数据的结束位置
sk_buff_data_t end; // 指向整个数据包的结束位置
unsigned char *head, // 指向整个数据包的起始位置
*data; // 指向实际数据的起始位置
unsigned int truesize; // 整个Sokcet Buffer大小,sk_buff结构体+数据包
atomic_t users; // 引用计数,正在使用该sk_buff缓冲区的进程计数
};
-
协议栈中的socket buffer
接收和发送数据过程中,各层的协议头不断的加入或解析掉,不断调整sk_buff中有效数据data的指向。
-
socket buffer的操作
为此内核在系统初始化时已创建了两个 sk_buff 的内存对象池:skbuff_head_cache与skbuff_fclone_cache。每当需要为 sk_buff 数据结构分配内存时,根据所需的 sk_buff 是克隆的还是非克隆的,分别从以上两个 cache 中获取内存对象,释放 sk_buff 时也就将对象放回以上两个 cache。下边简要的梳理一下对sk_buff的操作函数。
函数 | 描述 | 备注 |
__alloc_skb | 为 Socket Buffer 分配内存的主要函数 | 创建 |
alloc_skb | 对__alloc_skb的封装 | |
__dev_alloc_skb | 给网络设备驱动程序使用的函数,也是对__alloc_skb的封装 | |
kfree_skb | 释放 Socket Buffer 的内存 | 释放 |
kfree_release_data | 释放数据包的主缓冲区和数据片缓冲区 | |
dst_release | 释放对路由表的引用 | |
skb_reserve | 在 skb 的头和尾预留指定长度的空间 | 对齐 |
skb_put | 在 skb 的尾部预留指定长度的空间 | |
skb_push | 在 skb 的头部预留指定长度的空间 | |
skb_pull | 从 skb 的头部移走指定长度的数据 | |
skb_trim | 从 skb 的尾部移走指定长度的数据 | |
skb_clone | 产生一个对 skb 的克隆数据结构,指向相同的数据包 | 复制 |
skb_copy | 复制sk_buff、数据包、分片数据包 | |
pskb_copy | 复制sk_buff、数据包,不复制分片数据包 | |
skb_transport_header | 返回传输层协议头数据的地址 | 协议头指针操作 |
skb_reset_transport_header | 复位传输层协议头数据地址指针 | |
skb_set_transport_header | 将传输层协议头信息地址指针设置到相应位置 | |
skb_network_head | 返回网络层协议头数据的地址 | |
skb_reset_network_heade | 复位网络层协议头数据地址指针 | |
skb_set_network_header | 将网络协议头信息地址指针设置到相应位置 | |
skb_mac_heade | 返回数据链路层协议头数据的地址 | |
skb_reset_mac_header | 复位数据链路层协议头数据地址指针 | |
skb_set_mac_header | 将数据链路层协议头信息地址指针设置到相应位置 | |
skb_shared_info
skb_shared_info 是用于支持 IP 数据分片(fragmentation)和 TCP 数据分段(segmentation)的数据结构.
struct skb_shared_info {
unsigned short nr_frags; // 描述了一个数据包最终被分成了多少个数据片,用于支持 IP 分片
unsigned short gso_size;
unsigned short gso_segs; // 给出 TCP 数据包被分段的数量
unsigned short gso_type; // 与gso_size一起,说明网络设备是否具有在硬件上实现对 TCP分段的能力
__be32 ip6_frag_id;
__u8 tx_flags;
struct sk_buff *frag_list; // 如果数据包被分成片段,该域是指向存放分片数据链表起始地址的指针
struct skb_shared_hwtstamps hwtstamps;
atomic_t dataref; // 描述了对主数据包缓冲区的引用计数,如skb_clone时+1
void * destructor_arg;
skb_frag_t frags[MAX_SKB_FRAGS];// 这是一个页表入口数组,每一个入口就是一个 TCP 的段
};
有的网卡在硬件上可以完成对 TCP 层数据的分段(segment),可以减轻 CPU 的负载,从而提高网络的速度,这种技术被成为GSO(Generic Segmentation Offload)或(TCP Segmentation Offload)
skb_shared_info 数据结构紧接在数据包缓冲区之后,由 sk_buff 的 end 指针来寻址。
当用于 IP 数据分片时,frag_list指针指向包含 IP 数据片的Socket Buffer 的链表;
当用于 TCP 数据分段时,frags包含了一个相关的页面数据,其中存放了分段的数据。
2. 物理层
网路设备属于物理层的内容,这里我们简单来看看网络设备在在内中的抽象、网络设备驱动程序等相关内容.
2.1 数据结构
-
net_device
Linux 内核在实现对各种网络设备硬件的广泛支持的同时,又要屏蔽网络设备硬件与驱动程序的实现细节,不至于因设备的更换而修改内核上层协议代码的实现以及数据传送方法。为此在 Linux 网络体系结构中将网络设备抽象为 net_device 数据结构,net_device 数据结构的数据域可以根据不同的网络设备初始化为不同的属性。
协议栈软件与网络设备之间的关系如下图所示:
分成了3个部分:
-
网络设备驱动程序
所 有 网 络 设 备 驱 动 程 序 必 须 首 先 完 成 的 任 务 : 初 始 化 一 个 net_device 数据结构的实例作为网络设备在内核中的实体,并将 net_device 数据结构实例的各数据域初始化为可工作的状态,然后将设备实例注册到内核中,为协议栈提供传送服务。
-
net_device数据结构
struct net_device 数据结构代表了上层的网络协议和硬件之间的一个通用接口,使我们可以将网络协议层的实现从具体的网络硬件部件中抽象出来,独立于硬件设备。
-
dev.c文件中的接口函数
这里的dev.c 文件中实现了对上层协议的统一调用接口。
net_device 数据结构的数据域根据其功能,可以划分为以下各类:
- 配置(configuration)。
- 统计(statistics)。
- 设备状态(device status)。
- 链表管理(list management)。
- 流量管理(traffic management)。
- 常规域(generic)。
- 函数指针(function pointer)。
我们简单看几个属性:
struct net_device {
char name[IFNAMSIZ]; // 网络设备名
struct pm_qos_request pm_qos_req;
struct hlist_node name_hlist; // 以网络设备名为关键字建立的哈希链表,支持对网络设备的快速搜索。
char *ifalias; // 网络设备的别名,主要用于简单邮件传输协议(SMTP)访问网络设备的别名。
unsigned long mem_end;
unsigned long mem_start; // 设备和内核之间通信的共享存储区,只在网络设备驱动程序中初始化和访问这段区域,上层协议不需关心。
unsigned long base_addr; // 设备上的存储区映射到内存地址范围的 I/O 起始地址。
unsigned int irq; // 表示网络设备使用的中断号,中断号也可以由多个设备共享。
unsigned long state; // 表示网络设备状态,如:网络设备是否打开,是否连接到传输介质,是否暂停
struct list_head dev_list; // 网络设备实例组成的链表
struct list_head napi_list; // 持 NAPI(New Access Protocol Interface)功能的网络设备组成的链表
struct list_head unreg_list;
...
int ifindex; // 唯一标识网络设备的索引号。当向内核注册网络设备时调用 dev_new_index函数为每个设备分配设备索引号
int iflink; // iflink 标识符由虚拟设备使用,标识在虚拟设备中实际完成数据传送的网络设备
struct net_device_stats stats;
atomic_long_t rx_dropped; /* dropped packets by core network
* Do not use this in drivers.
*/
#ifdef CONFIG_WIRELESS_EXT
const struct iw_handler_def * wireless_handlers; // 函数指针,指向一组函数,这组函数是无线网络设备用以处理设备扩展功能的
struct iw_public_data * wireless_data; // 无线网络处理函数使用数据。
#endif
...
};
-
net_device_ops
这是接口,前边的net_device是数据,这里net_device_ops就是对net_device操作的接口,我们简单看几个接口:
struct net_device_ops {
int (*ndo_init)(struct net_device *dev); // 设备初始化
void (*ndo_uninit)(struct net_device *dev); // 设备注销,卸载设备
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); // 这是初始化数据包发送过程的方法
u16 (*ndo_select_queue)(struct net_device *dev, struct sk_buff *skb); // 在支持多个发送队列的网络设备中选择网络设备的发送队列,将包含发送数据包的 Socket Buffer 放入网络设备的某个发送队列中
void (*ndo_change_rx_flags)(struct net_device *dev, int flags); // 修改网络设备的接收工作模式标志
void (*ndo_set_rx_mode)(struct net_device *dev); // 根据 struct net_device 数据结构中标志域 flags来设置网络设备接收模式
int (*ndo_set_mac_address)(struct net_device *dev, void *addr); // 如果网络设备支持对硬件地址的修改功能,可以实现这个方法。大多数网络设备都不具有这个功能。
int (*ndo_validate_addr)(struct net_device *dev); // 检查 dev 中的地址域 dev_addr 中的值是否为有效地址。
int (*ndo_do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd); // 执行网络接口特定的 ioctl 命令
int (*ndo_set_config)(struct net_device *dev,struct ifmap *map); // 配置网络设备的参数,例如硬件参数、irq、base_addr 等
int (*ndo_change_mtu)(struct net_device *dev, int new_mtu); // 修改网络设备的最大传输单元(MTU)值
int (*ndo_neigh_setup)(struct net_device *dev, struct neigh_parms *); // 用于建立与相邻协议(ARP)的连接。
void (*ndo_tx_timeout) (struct net_device *dev); // 数据包发送超时错误处理函数。
void (*ndo_poll_controller)(struct net_device *dev); // 该函数用于让驱动程序在中断被禁止的情况下查看网络接口上发生的事件,如远程控制终端,通过网络跟踪调试内核代码等
...
}
2.2 网络设备初始化
-
内核初始化
这里先来看看内核初始化指的是网络设备初始化的流程,从start_kernel开始,到rest_init,一直到do_initcall如下图所示:
static void __init do_initcalls(void)
{
initcall_t *fn;
for (fn = __early_initcall_end; fn < __initcall_end; fn++)
do_one_initcall(*fn);
}
do_one_initcalls,就逐一地执行____early_initcall_end 与__initcall_end 之间的子系统初始化函数。
subsys_initcall、device_initcall等用于注册系统启动时由 do_initcall 调用的初始化函数,宏在表中所列的顺序规定了初始化函数的优先级顺序。
-
网络子系统初始化
网络子系统的初始化例程是 net_dev_init。
通过skb_queue_head_init(&queue->input_pkt_queue),为每个 CPU 建立传送队列;
通过register_pernet_device()遍历整个网络设备链表(dev_list),将初始化失败的设备数据结构实例从设备链表中移走(通常都是没安装的硬件),将有效设备数据结构实例留在列表中.
通过open_softirq()注册网络子系统的接收/发送网络数据包软件中断处理函数;
通过dst_init()初始化路由表。
-
网络设备初始化
网络设备设备的初始化有2种方式:可以直接编译成内核的一个组件,也可以编译成模块,在系统运行期间插入内核。
-
当设备驱动程序静态编译到内核时(即不为模块),宏 module_init 定义为__initcall的别名,module_init 描述的函数就分类为内核启动时的初始化例程;
-
当网络设备驱动程序编译成模块时,module_init 宏声明的函数会在系统运行期间用户发出 insmod 或 modprobe 命令时被调用执行,以初始化网络设备。
网络设备驱动程序初始化函数:
- 首要任务就是分配网络设备数据结构实例net_device的内存空间;
alloc_netdev_mq()
- 其次是初始化数据结构net_device,初始化设备的收发队列,建立函数指针;
xxx_setup()
如以太网的ether_setup()
- 将网络设备实例注册到内核中。
register_netdevice()
从这以后,设备就挂接到了 TCP/IP 协议栈上。
2.3 网络设备管理
网 络 设 备 的 net_device 数 据 结 构 实 例 在 创 建 后 会 插 入 到 一 个 全 局 链 表dev_base_head
和两个哈希链表中,这些数据结构使内核查找设备更容易.
内核实现了以下两个函数,将网络设备的 net_device 数据结构实例插入或移出全局链表和两个哈希链表:
static int list_netdevice(struct net_device *dev);
static void unlist_netdevice(struct net_device *dev);
两个Hash表分别是:以设备名(dev->name)为关键字的哈希链表与以设备索引号为关键字的哈希链表(dev->ifindex)。用以下两个函数来创建:
static inline struct hlist_head *dev_name_hash(struct net *net, const char *name)
{
unsigned hash = full_name_hash(name, strnlen(name, IFNAMSIZ));
return &net->dev_name_head[hash & ((1 << NETDEV_HASHBITS) - 1)];
}
static inline struct hlist_head *dev_index_hash(struct net *net, int ifindex)
{
return &net->dev_index_head[ifindex & ((1 << NETDEV_HASHBITS) - 1)];
}
有了hash表与链表之后,剩下的是对设备的查询,这里提供了如下几个函数:
struct net_device *dev_get_by_name(struct net *net, const char *name); // 以网络设备名为关键字建立的哈希链表中查找网络设备
struct net_device *dev_get_by_index(struct net *net, int ifindex); // 以网络设备索引号为关键字的哈希链表中查找网络设备
struct net_device *dev_getfirstbyhwtype(struct net *net, unsigned short type); // 以网络设备硬件类型(以太网卡,令牌环网络设备等)为关键字在全局链表中查找第一个与指定类型相符的网络设备
struct net_device * dev_get_by_flags (struct net *net , unsigned short if_flags, unsigned short mask ) //在全局链表中查找第一个与给定 flags 值相符的网络设备
2.4 事件通知链
内核中的许多子系统都相互依赖,其中某个子系统的状态发生了改变或发生了某个特定事件,别的子系统都需要做出相应的处理。为了满足让协议栈实例获取网络设备状态信息的需求, Linux 内核中实现了事件通知链(notification chain)机制。
该机制与生产者模式的sub-pub很类似。
事件通知链中是一个一个的 struct notifier_block 数据结构类型的成员列表。
struct notifier_block {
int (*notifier_call)(struct notifier_block *, unsigned long, void *); // 指向事件处理函数
struct notifier_block __rcu *next; // 下一个的指针
int priority; // 表示事件处理函数的优先级
};
-
网络子系统中创建的事件通知链
网 络 子 系 统 共 创 建 了 3 个 事 件 的 通 知 链 : inetaddr_chain
、 inet6addr_chain
和netdev_chain
。其中 netdev_chain
为网络设备状态发生变化时的事件通知链。
register_netdevice_notifier(&bond_netdev_notifier);
register_inetaddr_notifier(&bond_inetaddr_notifier);
-
向通知链注册事件处理函数
int notifier_chain_register(struct notifier_block **list, struct notifier_block *n);
-
通知子系统有事件发生
static int __kprobes notifier_call_chain(struct notifier_block **nl,
unsigned long val, void *v,
int nr_to_call, int *nr_calls);
2.5 网络设备驱动程序
网络设备驱动程序是网络设备硬件与 Linux 内核间的接口,这里我们只作简单介绍。
设备与硬件的交互方式:
-
轮询
-
中断
中断控制的硬件逻辑如下:
网络设备的驱动程序功能包括:
要实现一个网络设备的驱动程序,就需要实现以上功能的接口
初始化功能函数:netcard_probe()等
设备活动功能函数对应的就是net_device_ops中的接口
3. 数据链路层
在 TCP/IP 协议栈中,数据链路层的关键任务是:将由网络设备驱动程序从设备硬件缓冲区复制到内核地址空间的网络数据帧挂到 CPU 的输入队列,并通知上层协议有网络数据帧到达,随后上层协议就可以从 CPU 的输入队列中获取网络数据帧并处理。
上层协议实例要向外发送的数据帧会由数据链路层放到设备输出队列,再由设备驱动程序的硬件发送函数 hard_start_xmit将设备输出队列中的数据帧复制到设备硬件缓冲区,实现对外发送。
3.1 数据结构
-
napi_data
Linux 内核 2.5 以后的版本在数据链路层实现了一组新的处理网络输入数据帧的 API,即 NAPI。它由 poll 函数将设备硬件缓冲区中数据复制到内核,poll 函数在接收软件中断中执行,一次可读入多个数据帧,提高了效率。对于NAPI除了net_device之外,内核还定义了一个新的数据结构napi_struct
来管理这类设备的新特性与操作。
struct napi_struct {
struct list_head poll_list; // 连接到CPU的poll_list列表,该列表都是已产生中断的网络设备
unsigned long state;
int weight; // 设备的 poll 函数在 NET_RX_SOFTIRQ 期间执行时,可以从设备缓冲区中读入的最大数据帧数,以太网卡默认是64
int (*poll)(struct napi_struct *, int); // 指向 NAPI 网络设备驱动程序的 poll 函数的指针
#ifdef CONFIG_NETPOLL
spinlock_t poll_lock;
int poll_owner;
#endif
unsigned int gro_count;
struct net_device *dev; // 指向接收数据帧的包网络设备的 struct net_device 数据结构实例
struct list_head dev_list; // 内核的全局网络设备列表
struct sk_buff *gro_list; // 数据帧被分割成片段后,所有分片数据 Socket Buffer 的列表
struct sk_buff *skb; // 存放接收数据帧的 Socket Buffer
};
-
softnet_data
每个 CPU 都有自己的队列来管理输入数据帧,管理CPU 队列的数据结构为 struct softnet_data。
struct softnet_data {
struct Qdisc *output_queue; // 用于专门管理网络设备输出队列的数据结构
struct Qdisc **output_queue_tailp;
struct list_head poll_list; // 网络设备链表,连接`napi_struct`的poll_list
struct sk_buff *completion_queue; // 这些套接字缓冲区中的数据已被成功发送或接收,缓冲区可以释放
struct sk_buff_head process_queue;
/* stats */
unsigned int processed;
unsigned int time_squeeze;
unsigned int cpu_collision;
unsigned int received_rps;
#ifdef CONFIG_RPS
struct softnet_data *rps_ipi_list;
/* Elements below can be accessed between CPUs for RPS */
struct call_single_data csd ____cacheline_aligned_in_smp;
struct softnet_data *rps_ipi_next;
unsigned int cpu;
unsigned int input_queue_head;
unsigned int input_queue_tail;
#endif
unsigned dropped;
struct sk_buff_head input_pkt_queue; // 每个 CPU 的输入队列,该队列是存放网络输入数据帧 SocketBuffer 的链表
struct napi_struct backlog;
};
-
关系
3.2 初始化
这些数据的初始化是在前边讲到的网络设备初始化net_dev_init
中:
static int __init net_dev_init(void){
...
for_each_possible_cpu(i) {
struct softnet_data *sd = &per_cpu(softnet_data, i);
memset(sd, 0, sizeof(*sd));
skb_queue_head_init(&sd->input_pkt_queue); // 输入队列
skb_queue_head_init(&sd->process_queue); // 处理队列
sd->completion_queue = NULL;
INIT_LIST_HEAD(&sd->poll_list);
sd->output_queue = NULL;
sd->output_queue_tailp = &sd->output_queue;
...
sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
sd->backlog.gro_list = NULL;
sd->backlog.gro_count = 0;
}
...
}
3.3 接收数据
NAPI 的核心概念是使用中断与轮询相结合的方式来代替纯中断模式:
- 当网络设备从网络上收到数据帧后,向 CPU 发出中断请求,内核执行设备驱动程序的中断服务程序接收数据帧;
- 在内核处理完前面收到的数据帧前,如果设备又收到新的数据帧,这时设备不需产生新的中断(设备中断为关闭状态),内核继续读入设备输入缓冲区中的数据帧(通过调用驱动程序的 poll 函数来完成),直到设备输入缓冲区为空,再重新打开设备中断。
这样结合了中断与轮询的特点,比起传统的每个数据帧都产生中断,效率会更高。
接收数据的机制如下图所示:
先来看看硬件中断部分:
再来看看软件中断:
在__netif_receive_skb()
中通过调用deliver_skb()
向将输入数据帧发送给网络层所有注册了接收处理程序的协议实例。
3.4 链路层与网络层的接口
网络层接收到数据帧后,从网络层的协议头中解析该数据帧的传输层使用的协议,确定应将数据帧传给传输层的哪个协议处理程序接收。
Linux 内核支持的协议(L3层)标识符定义在 include/linux/if_ether.h 文件中,标识符的命名格式为 ETH_P_XXX。
协议标识符 | 值 | 处理函数 |
ETH_P_IP | 0x0800 | ip_rcv |
ETH_P_ARP | 0x0806 | arp_rcv |
ETH_P_IPV6 | 0x86DD | ipv6_rcv |
ETH_P_802_2 | 0x0004 | llc_rcv |
ETH_P_ALL | 0x0003 | 不是真正的协议,用于处理如网络嗅探器接收所有的网络数据帧 |
来看看内核对协议的封装结构体:
struct packet_type {
__be16 type; // 协议标识符ETH_P_XXX 之一
struct net_device *dev; // 指向接收数据帧的网络设备的 struct net_device 数据结构实例的指针
int (*func) (struct sk_buff *,
struct net_device *,
struct packet_type *,
struct net_device *); // 指向协议实例实现的接收处理程序的函数指针
struct sk_buff *(*gso_segment)(struct sk_buff *skb, u32 features);
int (*gso_send_check)(struct sk_buff *skb);
struct sk_buff **(*gro_receive)(struct sk_buff **head, struct sk_buff *skb);
int (*gro_complete)(struct sk_buff *skb);
void *af_packet_priv;
struct list_head list; // 用于链接几个 struct packet_type 数据结构的链表。
};
在 Linux 内核中定义了两个结构中来存放packet_type,其一为:ptype_base(type为键值的hash表),其二为ptype_all(链表)。
另外实现了dev_add_pack 和 dev_remove_pack,分别将 packet_type 实例变量加入/移出 ptype_base/ptype_all 哈希链表。
例如在内核启动时间初始化 IPv4 协议时,会执行 IPv4 的初始化函数 inet_init,它会调用 dev_add_pack 函数将 IPv4 的 packet_type 实例 ip_packet_type插入 ptype_base 哈希链表中。
3.5 发送数据
在 网络设备驱动程序的open 方法的最后就调用了 netif_start_queue 函数来启动网络设备发送队列;
当设备驱动程序意识到设备硬件没有足够的空间缓存新的数据帧时,它就用netif_stop_queue 函数来停止发送队列,避免浪费资源,因为内核已知发送会失败,就不会尝试发送新数据帧给网络设备;
中断处理程序调用了 netif_wake_queue 函数来重启网络设备的发送队列,它重新允许网络发送,同时也要求内核查看在网络设备的发送队列中是否有数据帧在等待发送。
主要由dev_queue_xmit函数将上层协议发送来的数据帧放到网络设备的发送队列(针对有发送队列的网络设备),随后流量控制系统按照内核配置的队列管理策略,将网络设备发送队列中的数据帧依次发送出去。其整个机制如下图所示:
几乎所有的网络设备都使用队列来调度管理数据帧的输出流量,内核可以使用队列策略算法来安排先发送哪个数据帧后发送哪个数据帧,使发送过程更高效。
流量控制系统中的每种队列策略都提供了自己的函数,供数据链路层调用来完成队列的操作。
- Enqueue:向队列加入一个成员。
- Dequeue:从队列中提取一个成员。
- Requeue:将前面从队列中提取的数据成员重新放回队列。
4. 网络层
IP层涉及的内容比链路层复杂,比如路由选择、IP分片等内容,但从整体上看它既然属于整个协议栈,也就包含在init、接收数据、发送数据这个模式中。
以前文章中,对IP协议本身介绍的已经比较清楚了,这里我们跳过IP协议本身,来看看Linux内核的实现。
4.1 数据结构
IP报文头如下:
与其对应的数据结构是iphdr,如下:
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4,
ihl:4;
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos;
__be16 tot_len;
__be16 id;
__be16 frag_off;
__u8 ttl;
__u8 protocol;
__sum16 check;
__be32 saddr;
__be32 daddr;
/*The options start here. */
};
4.2 初始化
PF_INET 协议族的初始化是ip协议正确运行的前提,它建立起TCP/IP协议栈运行的环境,其对应的函数是:inet_init()
inet_init()
在initcall5中被内核调用,它做了很多工作,初始化了很多协议,并通过dev_add_pack注册IP数据包的接收处理函数ip_rcv。
dev_add_pack(&ip_packet_type);
...
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
.gso_send_check = inet_gso_send_check,
.gso_segment = inet_gso_segment,
.gro_receive = inet_gro_receive,
.gro_complete = inet_gro_complete,
};
ip的初始化由ip_init
来完成,它主要任务是:
- 通过 ip_rt_init初始化路由子系统,建立独立于协议的路由缓冲表rt_hash_table
- 通过igmp_mc_proc_init初始化裸IP的基本功能
- 如果配置了ip组发功能,通过igmp_mc_proc_init();初始化在/proc文件系统的入口。
4.3 接收收据
ip层对数据包的处理包括:接收、前送、发送数据包,相应的处理函数会分成2个阶段:do_something(如ip_rcv)、do_something_finish(如ip_rcv_finish),前者负责检查或初始化,后者负责实际功能。
下边沿着ip_rcv()来看看接收数据的处理。
在ip_rcv()中,
-
先对数据包的 IP 协议头做各种合法性检查:协议头的长度,协议版本,校验和是否正确,如有任何一项出错,扔掉数据包;
pskb_may_pull 的功能是保证 skb->data 指向的区域至少包含一个数据块,其长度与 IP协议头信息的长度一致
-
然后进行清理工作,数据包已到达 IP 层,不再需要数据链路层给数据包尾所加的补丁,通过pskb_trim_rcsum,去掉链路层填充的数据。
-
然后进入到正式的功能函数ip_rcv_finish()中
-
在这里通过先是通过ip_route_input_noref()
来获取路由信息
这里会调用到ip_route_input_slow()中,在这里会判断是否是local,如果是local就将相应的函数指针初始化为ip_local_deliver(),如果前送会初始化为ip_forward(),但在这里不会调用
-
接着会处理ip选项,通过 ip_rcv_options()
来完成,这里暂不做详细解读
-
接着进入dst_input()中,在这里调用了前边初始化的函数.
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}
下边来分别来看看ip_local_deliver
与ip_forward
先从从 IP 协议头中获取传输层协议的编码:iphdr->protocol,如果该协议是裸套接字处理函数,交给raw_local_deliver()进行处理;然后在inet_protos 向量表中查询对应阐述层协议,交给传输来处理。
再来看看ip_forward()
ip路由的很多概念都在ip_forward中实现的,
它会判断ttl,如果小于1,则会通过icmp_send发送ICMP_TIME_EXCEEDED
;
它会根据协议头的分段标志(IP_DF)来判断数据包的长度是否大于传输路由上的最大传输单元(MTU),如果大于则通过icmp_send发送ICMP_DEST_UNREACH
;
它会减小ttl,最后进入到ip_forwrd_finish中,在这里就不再向上层协议转发数据,而是调用发送数据的相关函数了。
下边来看看发送数据。
4.4 发送数据
发送数据是一个主动的过程,主要由上层传输层来调用,传输层的协议主要是UDP;CP,也有基于IP协议的网络层协议如ICMP、IGMP。

这里涉及到2个数据结构sock与inet_sock,sock是是描述套接字层信息的数据结构;inet_sock 数据结构是Linux 内核实现 TCP/IP 协议栈 PF_INET 协议族的套接字数据结构,它嵌入了sock数据结构。
TCP通过ip_queue_xmit来向外发送数据,如下图:
从函数名可以看出,它是ip层的队列发送,它会进行路由寻址(ip_route_output_ports),构造相应的ip头,然后调用ip_local_out()进行发送,在ip_finish_output_finish()中,会判断数据包的大小与MTU的大小,如果大于MTU会进行IP的分片(ip_fragment),最后调用邻居子系统neigh_output()进行下一层(数据链路层)的发送。
UDP是通过ip_append_data() + ip_push_pending_frames()来进行的发送,这里暂不进行解析。
5. 传输层TCP协议
传输层主要有UDP协议与TCP协议两个,从复杂度上看,TCP的复杂度会高一些,它要有3次握手、流量的控制、拥塞的控制等。在这里传输层协议,我只看一下TCP协议的相关实现。
TCP协议本身见TCP协议简介.
5.1 数据结构
-
报文头
TCP的报文头结构如下图所示:
其对应的结构为tcphdr
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__sum16 check;
__be16 urg_ptr;
};
-
tcp控制缓冲区
struct tcp_skb_cb {
union {
struct inet_skb_parm h4; // 存放输入数据段的 IP 选项,h4是ipv4,h6是ipv6
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct inet6_skb_parm h6;
#endif
} header; /* For incoming frames */
__u32 seq; // 输出数据段的起始序列号。
__u32 end_seq; // 最后一个输出数据段结束序列号
__u32 when; // 用于计算 RTT(Round-trip time)的值
__u8 tcp_flags; /* TCP header flags. (tcp[13]) */
__u8 sacked; // 保存了选择回答(SACK :Selective Acknowledge)和前送回答(FACK:Forward Acknowledge)的状态标志
__u8 ip_dsfield; /* IPv4 tos or IPv6 dsfield */
__u32 ack_seq; /* Sequence number ACK'd */
};
-
其他
TCP 套接字的数据结构中包含了管理 TCP 协议各方面的信息,如发送和接收序列号、TCP 窗口尺寸、避免网络阻塞等。这些管理信息由 TCP 套接字的数据结构 tcp_sock
描述。
msghdr
数据结构中包含了来自应用层数据的信息,它将作为参数从套接字传送给 TCP 协议实例的传送处理函数。
5.2 初始化
这里的初始化包括不仅包含tcp本身的初始化,也包括tcp与ip的接口相关内容。
在网络层(ip)的初始化是在inet_init()函数中完成,TCP的初始化也是在这里。
static int __init inet_init(void){
...
rc = proto_register(&tcp_prot, 1);
..
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n");
..
tcp_init();
..
}
先来看看tcp_prot,管理 TCP 传输层与套接字层之间接口的关系,是由 struct proto 数据结构定义的。
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
.sendpage = tcp_sendpage,
...
};
proto相当于对传输层的封装。
再来看看TCP使用ip的接口:inet_connection_sock_af_ops
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit, // 发送
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.get_peer = tcp_v4_get_peer,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.bind_conflict = inet_csk_bind_conflict,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_ip_setsockopt,
.compat_getsockopt = compat_ip_getsockopt,
#endif
};
5.3 接收数据
接着前边的网络层,它调用的是handler,而这个handler指向的就是tcp_v4_rcv。
static const struct net_protocol tcp_protocol = {
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.gso_send_check = tcp_v4_gso_send_check,
.gso_segment = tcp_tso_segment,
.gro_receive = tcp4_gro_receive,
.gro_complete = tcp4_gro_complete,
.no_policy = 1,
.netns_ok = 1,
};
下边来看看tcp_v4_rcv。
当 TCP 协议实例收到一个数据包后,它首先通过协议头来预定向数据包的去处:“Fast Path”或“Slow Path”。
-
Fast Path,数据段会放入 prequeue 队列中,这时用户进程被唤醒,在 prequeue 队列中的数据段就由用户层的进程来处理,这个过程省略很多“Slow Path”处理中的步骤,从而加大了数据吞吐量。
-
Fast Path会直接走tcp_prequeue
,Slow Path才会进入到tcp_v4_do_rcv()
中,在这里会根据连接的状态进行不同的处理。
- TCP协议中流量控制的滑动窗口就是在tcp_rcv_established()中实现的。
- 处于LISTEN状态,如果收到的是一个有效的 SYN 数据段,则将套接字状态转变为接收状态,这个过程由函数 tcp_v4_hnd_req 完成。
- 其他状态的处理在tcp_rcv_state_process中处理
其流程如下:
5.4 发送数据
tcp向socket层提供了tcp_sendmsg()函数用于发送数据,其简单的调用栈如下所示:
拥塞避免的所发也在这里实现
6. 尾声
对linux网路协议栈的学习暂时告以段落,在这里简单介绍了各个协议栈的大体脉络,具体的实现算法尚未在代码层面讨论到,这也为以后的学习留下空间。