1、关键数据结构
struct sk_buff{
....
}
1.1 嵌套字缓冲区
一个数据封包就存储在这里,所有网络分层都会使用这个结构来存储其报头、有关的用户数据信息(有效载荷),以及用来协调其工作的其他内部信息。
当该结构从一个分层由上至下传到另一个分层时,其不同的字段会随之发生变化,在首部附加上该层自己的报头。
附加报头比起在分层之间拷贝更有效率
附加报头就要在缓冲区开端新增空间,也就是要改变指向该缓冲区的变量,内核提供了skb_reserve()
函数,为该协议的报头预留空间。
当缓冲区由下至上传经各个网络分层时,每个源自旧分层的报头就不再有用处,把指向有效载荷的指针向前移动到上层报头的开端。
1.2 布局字段
内核在一个双向链表中维护所有的sk_buff
结构,但是该表的组织逼传统的双向链表更为复杂
每个sk_buff
结构必须能够迅速找出这个表的头,在表的开端额外增加一个sk_buff_head
结构作为一种哑元元素
struct sk_buff_head{
/* 这两个成员必须在最前 */
struct sk_buff *next;
struct sk_buff *prev;
__u32 qlen; //代表表中元素的数目
spinlock_t lock; //用于防止对表的并发访问
}
1.3 缓冲区的克隆和拷贝
当同一个缓冲区需要由不同消费者个别处理时,那些消费者可能需要修改sk_buff扫描符的内容(指向协议报头的h和nh指针),但内核不需要完全拷贝sk_buff结构和相关联的数据缓冲区。
为了提高效率,内核可以克隆(clone)原始值,也就是只拷贝sk_buff结构,然后使用引用计数,以避免过早释放共享的数据块。缓冲区的克隆由skb_clone函数实现。
2、网络接口
2.1 网络接口的命名
- eth0: ethernet的简写,一般用于以太网接口
- wifi0: wifi是无线局域网,因此wifi0一般指无线网络接口
- ath0: Atheros的简写,一般指Atheros芯片所包含的无线网络接口
- lo: local的简写,一般指本地环回接口
2.2 网络接口如何工作
网络接口是用来发送和接收数据包的基本设备。系统中所有网络接口组成一盒连撞节后,应用层程序使用时按名称调用。每个网络接口在linux系统中对应一个struct net_device
结构体,包含name
,mac
,mask
,mtu
...信息。每个硬件网卡(一个MAC)对应一个网络接口,其工作完全由相应的驱动程序控制。
2.3 虚拟网络接口
虚拟网络接口的应用范围非常广泛,最著名的是“lo”,基本上每个linux系统都有这个接口
虚拟网络接口并不真实地从外界接收和发送数据包,而是在系统内部接受和发送数据包,因此虚拟网络接口不需要驱动程序。虚拟网络接口和真实存在的网络接口在使用上是一致的
硬件网卡的网络接口由驱动程序创建,而虚拟的网络接口由系统创建货通过应用层程序创建
2.4 linux中的lo(回环接口)
用于本地进程间数据包通信的虚拟网络接口
在linux系统中,除了网络接口eth0,还可以有别的接口,比如lo(本地环路接口)
假如包是由一个本地进程为另一个进程产生的,他们将通过外出链的“lo”接口,具体参考包过滤器的相关内容
负责接收帧的代码分成两部分:首先,驱动程序把该帧拷贝到内核可访问的输入队列,然后内核在予以处理,通常是把把那个帧传给一个相关协议(如IP)专用的处理函数。第一部分会在中断环境中执行,而且可以抢占第二部分的执行。接收输入帧并将其拷贝到队列的代码比实际处理帧的代码的优先级要高。
接收-活锁(receive-livelock)
在高流量负载下,中断代码会持续抢占正在处理的代码。输入队列已满,但是由于应该让帧退出队列并予以处理的代码的优先级过低而没有机会执行,结果系统就崩溃了。新的帧无法排入队列,而旧的帧无法被处理
通常的Linux报文处理方式中,报文分组到达内核,触发设备中断,中断处理程序为新报文创建套接字缓冲区,分组内容以DMA的方式从网卡传输到缓冲区中指向的物理内存。然后内核分析新报文的首部,此时报文处理已由网卡驱动代码转到网络层的通用接口,同时该函数将接收的报文送入特定的CPU等待队列中,触发该CPU的NET_RX_SOFTIRQ软中断,并且退出中断上下文。特定CPU的NET_RX_SOFTIRQ软中断处理对应等待队列中的报文的嵌套字缓冲区。
正式由此开始协议栈之旅:由netif_receive_skb分析报文类型,skb_buff结构经过网络层报文分类和分别处理(解析、分片重组等)进入传输层,由传输层进一步处理,最后用户空间应用程序通过socket API调用获取报文内容,亦或是由自定义的内核模块获取报文进行处理转发或统计。自定义内核模块可在协议栈多个点通过不同方式获取报文,如netfilter方式。
DPDK支持Run to Completion和Pipeline两种报文处理模式,用户可以依据需求灵活选择,或者混合使用。Run to Completion是一种水平调度方式,利用网卡的多队列,将报文分发给多个CPU核处理,每个核均独立处理到达该队列的报文,资源分配相对固定,减少了报文在核间的传递开销,可以随着核的数目灵活扩展处理能力; Pipeline模式则通过共享环在核间传递数据报文或消息,将系统处理任务分解到不同的CPU核上处理, 通过任务分发来减少处理等待时延。
3、原始套接字SOCK_RAW
我们常用的网络编程都是在应用层的报文首发操作,大多数接触到的流式套接字SOCK_STREAM和数据包套接字SOCK_DGRAM。而哲学数据包都是由系统提供的协议栈实现,用户只需要填充应用层报文即可,由系统完成底层报文头的填充并发送。
然而在某些情况下需要执行更底层的操作,比如修改报文头,避开系统协议栈等,这个时候就需要使用其他方法来实现。
SOCK_RAW实现于系统核心,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以,其次,SOCK_RAW也可以处理特殊的IPv4报文。总体来说,SOCK_RAW可以处理普通的网络报文之外,还可以处理一些特殊协议报文以及操作IP层及其以上的数据。
原始套接字可以用来自行组装IP数据包,然后将数据包发送到其他终端。必须在管理员权限下才能使用原始套接字,这么做可以防止普通用户往网络写出他们自行构造的IP数据报。
3.1、原始套接字的输入
- 接收到的UDP或者TCP分组绝不传递到任何原始套接字,如果一个进程想要读取含有UDP分组或TCP分组的IP数据报,它就必须在数据链路层读取这些分组
- 如果某个数据报以片段形式到达,那么在它的所有片段均到达且重组出改数据报之前,不传递任何分组到原始套接字。
4、NAPI
为了处理驱动程序使用NAPI接口的设备,有四个新字段添加到net_device
数据结构中以供软中断使用。
poll
这个虚函数可用于把缓冲区从设备的输入队列中退出,此队列时使用NAPI设备的私有队列。而softnet_data->input_pkt_queue供其他设备使用。poll_list
这是设备列表,列表中的设备就是在入口队列中有等待被处理的新帧的设备。这些设备就是所谓的处于轮询状态quota
配额是一个整数,代表的是poll虚函数一次可以从队列推出缓冲区的最大数目。配额越低,表示潜在的延时越低,因此让其他设备饿死的风险就越低。另一方面,低配额会增加设备间的切换量,因此整体的耗费会增加。weight
quota值的增加以weight为单位,用于在不同设备之间施加某种公平性。
5、
5.1 netif_rx
当新的输入帧正等待处理时,设备驱动程序通常是调用定义在net/core/dev.c
中的netif_rx
函数。此函数的工作是为短暂执行的软IRQ(把帧退出队列然后予以处理)调度
netif_rx
通常是在中断环境下被驱动程序调用,netif_rx
在其启动时会关闭本地CPU的中断事件,然后当其完成工作时回再予以重新开启
5.2 softnet_data
每个CPU都有其队列,用来接收进来的帧,因为每个CPU都有其数据结构处理入口和出口流量,因此,不同CPU之间没有必要使用上锁机制。此队列的数据结构softnet_data
就定义在include/linux/netdevice.h
中
struct softnet_data
{
int throttle;
int cng_level;
struct sk_buff_head input_pkt_queue;
struct list_head poll_list;
struct net_device *output_queue;
struct sk_buff *completion_queue;
struct net_device backlog_dev;
}
此结构的字段可用于接收和传输,换而言之,NET_RX_SOFTIRQ
和NET_TX_SOFTIRQ
软IRQ都引用此数据结构。入口帧会排入input_pkt_queue
,而出口队列会放入由流量控制(QoS层)所处理的特殊队列(而不是由软IRQ和softnet_data
结构所处理)
- throttle
布尔变量,超负载为真,否则为假。其值依赖于input_pkt_queue中帧的数目,当其置位时,此CPU接收到的数据帧全部丢弃 - avg_blog
代表input_pkt_queue队列长度加权后的平均值,可用于计算cng_level - cng_level
代表拥塞等级
这三个参数有拥塞管理算法使用,默认每接收一个帧这三个字段都会被更新。与CPU相关,因此可用于非NAPI设备,共享每个CPU所用的队列input_pkt_queue
input_pkt_queue
这个队列(在net_dev_init
中初始化)用来保存进来的帧(被驱动程序处理前)backlog_dev
是一个完整的嵌入式数据结构,不只是一个指针,类型为net_device,代表着一个设备已在相关联的CPU上为net_rx_action
调度以准备执行。这个字段是由非NAPI驱动程序使用,称为积压设备。poll_list
一个双向列表,其中的设备都带有输入帧等着被处理output_queue / completion_queue
output_queue是设备列表,其中的设备有数据要传输。completion_queue是缓冲区列表,其中的缓冲区已经成功传输,因此可以释放掉