关于skb
skb是网络协议栈中对包的底层操作结构,它需要满足以下特性:
- 能方便的处理可变长缓存,因为发送和接收的数据长时不固定的
2)能容易实现头尾部增加和移除数据,因为这些缓存区需要在不同网络层次间进行传递
3)添加和移除数据能尽量避免数据的复制
sk_buffer定义:
include/linux/skbuff.h
net/core/skbuff.c
在下载的linux源码中能找到(下来看的或者是ubuntu中下来编译内核的)
ref:<linux内核源码剖析–TCP/IP实现>
sk_buff的结构
大体结构:
分类:
- 与skb组织相关的成员变量
- 通用成员变量
- 标志性变量
- 与特性相关的成员变量
这些与特性相关的成员,往往用#ifdef来限制,若要使用他们,需要系统定义这些宏,而这个需要在编译内核时开启,比如:
编译时选中 Networking->Networking options->QoS and/or fair queueing->action,选中包分类器的功能,对应宏:
CONFIG_NET_CLS_ACT
注意的是:若某个内核模块包含了使用未定义的宏限制的变量,则无法被内核使用;
skb被哪些网络层次处理?
二层的mac或者其他链路层协议,三层的ip协议,四层的tcp和udp协议,某些成员会在层次间传递的时候发生改变,如四层向三层传递时会加一个ip头,
三层向二层传递时会加一个mac头等,反之则删除,而传递时只增加头部可以提高效率;这个需要复杂的指针操作,内核提供了一个函数: skb_reserve();
起到在向下传递skb前,在数据缓存区头部预留空间的作用;
skb相关的两个链表
在正式看skb的结构之前,先看看组织skb的两个链表,来看skb的全局:
在数据被传递进来时,触发中断,中断处理程序会将数据传递进内核,接着一步步拷贝到skb;skb由链表组织:
在sk_buff中有两个成员结构:
struct sk_buff *next;
struct sk_buff *prev;
以此构成skb_buff双向链表;
为了能使每个skb都能被整个链表的头部快速找到,在第一个skb结点前加了一个辅助的sk_buff_head结构的头结点,就像链表头一样,作为
链表的头结点,可以由此遍历skb链表;
1 | struct sk_buff_head { |
结构组织:
<-> sk_buff_head <-> skb1 <-> skb2 …<-> 链接到sk_buff_head形成环形双向链表
skb结构:
skb结构可以被大致分为 描述符(skb本身) 和数据缓冲区 (head等成员指针指向的数据)
以下是内核4.4.0中的include/linux/定义的sk_buff结构:
1 |
|
一张图片来表示skb的线性缓存区:发送和接收时会动态变化;
struct sk_buff
…
head —>headroom起点
data —>data起点
tail —>tailroom起点
end —>skb_shared_info起点
|headroom|data|tailroom|skb_shared_info|
skb_shared_info结构
基本结构
1 | /* This data is invariant across clones and lives at |
通过网络接口在两个本地文件间传输数据下,如何避免四次拷贝
在数据发送到接收时,往往send/write ->recv/read 这期间会涉及四次拷贝:
发送app用户进程空间缓存–>内核态缓存-(DMA复制)->硬件驱动 —-发送—-接收方硬件网络模块-(DMA复制)->内核态缓存–>用户空间缓存;
如果是本地文件–网络接口—本地文件,这种也是四次拷贝,就有点耗费时间了;(或者是收到数据原封不动传输出去,这个貌似还没实现)
调用方式改变:从read/write====>open/sendfile
实际路径改变:接收硬件网络模块(DMA复制)–>内核态缓存—> 发送硬件驱动
什么是聚合分散io数据以及skb_shared_info中对它支持的结构;
这里的聚合分散IO相关数据成员:
struct sk_buff *frag_list; //FRAGLIST类型的IP分片相关结构
unsigned short nr_frags; //聚合分散IO分片数量
skb_frag_t frags[MAX_SKB_FRAGS];//聚合分散IO page相关结构指针;
先理解下网络发送接收和组合分片相关的流程:
发送:用户数据–>四层tcp/udp–>IP:此时包需要一层层传递组合各层头,需要进行比较多的拷贝,此时是组合;在内核传递发送报文给hard_start_xmit()之前,需要判断网络是否
支持NETIF_F_SG,否则只能线性化处理,是则检查nr_frags的值,确定片段数,之后进行分片聚合;
接受:IP分片–IP分片重组–>udp/tcp–>用户;
1 | /* To allow 64K frame to be packed as single skb without frag_list we |
- frag_list的使用:
- 在接受分片组后链接多个分片,组成一个完整的IP数据报;
- UDP数据输出时,将待分片的SKB链接到一个SKB中,然后在输出时快速分片;
- 用于存放FRAGLIST类型的聚合分散IO数据包;
- nr_frag SG类型的聚合分散IO的使用:
- 在输出时需要判断nr_frag=0? 且frag_list==NULL,则没有分片,若nr_frag不为0且frag_list为NULL,则是聚合分散IO;
nr_frag表示数量,内容由frags数组指出:eg:
nr_frag =2; frags[0].page=p1, frags[0].page_offset=0,size=S1; frags[1].page=p1,frags[1].page_offset=S1,size=S2; - 不同的skb实例中的page指向相同的内存,即共享分片结构(共享内存),这个需要两方都不去修改它;
- FRAGLIST类型的聚合分散IO
frag_lsit直接指向了一个skb结构的实例;
关于GSO/GRO/TSO/TRO的基本概念;
如何访问skb_shared_info结构:
可以借助skb_info宏来访问此结构: 它其实就是简单的返回sk_buff结构的end指针的类型转换结果;
#define skb_shinfo(SKB) ((struct skb_shared_info*) ((SKB)->end))
eg: skb_shinfo(skb)->dataref++;
skb的相关管理函数:
内核提供了很多小函数来操作skb变量和链表等,这些函数都有两个版本:do_something和__do_something(),前者是在调用后者的情况下加上
锁和检查等;一般使用用前者的;这些函数定义在skbuff.h和skbuff.c中,我们称为skb的管理操作函数;
以下介绍每个类型的函数,并给出一些简单的例子,可以是demo或者是内核中的相关调用;
SKB的缓存池介绍
在网络模块中,用告诉缓存来为skb分配空间,在初始化skb_init()中,创建了两个用来分配skb的高速缓存:
1 | void __init skb_init(void) |
一般情况下用第一个,当在分配skb的时候知道可能被克隆,则用第二个,因为第二个中会同时分配一个后备的skb,在克隆的时候直接用后备的skb就可以,不用重新分配;
可以看到第二个的单位内存区域是2*size+atomic_t,后者是用来做引用计数的;引用计数取0,1,2分别代表两个都没有被引用,1表示引用了其中一个,2表示两个都被引用;
如何分配SKB
- alloc_skb()
skb的数据缓存区和skb本身描述符是两个不同的实体,所以在分配一个skb时,实际上需要分配两块内存:
一个是skb描述符,一个是数据缓存区;在4.8的内核版本可以看到:1
2
3
4
5static inline struct sk_buff *alloc_skb(unsigned int size,
gfp_t priority)
{
return __alloc_skb(size, priority, 0, NUMA_NO_NODE);
}
函数传入一个size和priority,返回一个sk_buff指针;
而__alloc_skb()则有四个参数:
1 | /** |
注意:__alloc_skb通常不被直接调用,而是封装调用,被封装在__netdev_alloc_skb(),alloc_skb(),alloc_skb_fclone()等;
dev_alloc_skb()也是一个缓存区分配函数,也是返回sk_buff*,通常在设备驱动中断上下文中,是一个alloc_skb()的封装函数,因为在中断处理函数中被调用,所以要求
原子操作(GFP_ATOMIC),4.8版本已经变了,封装的是别的函数,但是类似;
如何释放SKB
dev_kfree_skb()和kfree_skb()都可以用来释放skb,把它返回给高速缓存;dev_kfree_skb()只是简单的封装kfree_skb();
调用时只是简单的递减skb->users的值,直到减完为0才真的释放内存;
具体见skbuff.h/skbuff.c
数据预留和对齐
主要是通过skb_reserve(),skb_put,skb_push,skb_pull,等函数,对数据缓存区相关指针进行更新来做预留空间的操作;
具体怎么移动,要看是接收方向还是发送方向:
- skb_reserve()
skb_reserve()通常用来在数据缓存区头部预留一定的空间,比如插入协议首部或者在某个边界上对齐,而预留操作主要是移动data和tail指针;
注意只能用于空的skb,所以通常在分配后就会调用该函数;此时tail和data一同指向数据区的起始位置;
例如:某个以太网设备驱动接收函数在分配skb后,在向skb缓存区填充数据前,会通过skb_reserve(skb,2)来预留两个字节,因为以太网首部是14B,加了后正好16B对齐;此时是data指针往下移动两个字节;
当数据从上往下传递时,则每层将skb->data指针往上移动,然后复制本层首部,更新len; - skb_push()
在数据缓存区前头增加一块数据。也是只移动data和tail指针,和reserve类似 - skb_put()
修改指向末尾的tail指针,使之向下移动len字节,然后更新len - skb_pull()
data指针向下移动;从而在数据区首部忽略len字节长度,用于收到包后往上传递忽略首部;克隆和复制SKB
- skb_clone() 用来克隆skb,克隆时只复制skb描述符,而对数据缓存区则,引用计数+1;
- pskb_copy() 当一个函数不仅要修改skb描述符,还要修改数据缓存区的时候,就需要同时复制数据缓存区;如果要修改的数据在head-end之间,就可以用这个函数,
不然若还要修改聚合分散IO中的数据,则用skb_copy()做完全的拷贝;
SKB链表的管理函数
在skb链表操作中,为了防止被中断,则必须先获得自旋锁;然后才能访问队列中的元素;
skb_queue_head_init() :初始化skb链表头结点,创建一个空的skb链表;
skb_queue_head()/skb_queue_tail(),加入队列头/尾部,
skb_dequeue和skb_dequeue_tail,从首部和尾部取下一个skb;
skb_queue_purge() ,清空skb链表;
skb_queue_walk() 循环遍历skb链表中每个元素的宏;
添加和删除尾部数据
注意这里指的尾部数据,是data指向数据的结尾,而tail指向的是结尾的空间部分,一般是空的;
skb_add_data() tail指针下移,data尾部增加用户空间传递的数据,len加加;
skb_trim()和上面相反;
pskb_trim() 也处理到聚合分散iO;
拆分数据
就是把一个skb拆成两个skb:
skb_split();分两种情况,一种是拆分的len小于线性缓存区长(即不包含聚合分散IO的),另一种是大于,即分拆点在聚合分散IO中的某个位置;
重新分配SKB的线性数据区;
pskb_expand_head(),可以理解为realloc,就是扩展空间,将原数据复制过去;
其他函数;
pskb_may_pull: 检测skb中的数据是否有指定长度
skb_queue_empty:检测skb队列是否为空
skb_realloc_headroom: 根据指定的skb得到一个新的skb,并确保新的skb存在指定的headroom空间;
skb_get() :引用并返回一个skb;
skb_shared() :检测指定的skb是否被多次引用;
skb_share_check():检测并返回skb,当被检测的skb被引用多次时,则克隆此skb,并返回克隆得到的skb;
skb_unshare():检测并返回skb,当被检查的skb被克隆时,则复制此skb,并返回复制得到的skb;
skb_orphan(): 使得此skb不属于任何传输控制块;
skb_cow(): 确保skb存在指定的head空间,若不足,则会重新分配
skb_pagelen(): 获得skb中线性数据区和SG类型聚合分散io分片中的数据的长度;
关于虚拟设备和物理设备:
linux支持多种形式的虚拟网络设备,并通过一个虚拟网络设备驱动管理。当这个虚拟设备被使用时,dev指针指向该虚拟设备的net_device结构。
在输出时,虚拟设备驱动会在一组设备中选择其中的某个合适的设备,并将dev指针修改为指向这个设备的net_device结构;
在输入时,当原始网络设备收到报文后,根据某种算法选择某个合适的虚拟网络设备,并将dev指针修改为指向这个虚拟设备的net_device结构。
因此,在某些情况下,指向传输设备的指针会在包处理过程中改变;