struct sk_buff
一个封包就存在这里,所有网络分层会使用这个结构来储存其报头、有关用户数据的信息,以及用来协调其工作的其他内部信息。
struct net_device
每种网络设备都用这个数据结构表示,包括软硬件的配置信息。
struct sock
用于存储套接字的网络信息。
sk_buff结构定义在<include/linux/skbuff.h>头文件中,由变量堆组成。
整个结构体分为:布局;通用;功能专用;管理函数
多个不同的网络分层都会使用这个结构体。结构体从一个分层转到另一个分层时,其不同的字段会随之发生改变。
缓冲区往上传经各个网络分层:
L4(传输层)传给L3(网络层)之前会附加报头;L3(传输层)传给L2(链路层)之前又会加上自己的报头
skb_reserve函数:为了要在一个缓冲区开端新增空间(改变该缓冲区的变量),该协议的报头预留空间
缓冲区往下传经各个网络分层:
每个源自于旧分层的报头不再有用(L2(链路层)报头只由处理L2协议的设备驱动使用,对L3而言没有用)
L3(网络层)不会把L2(链路层)的报头从缓冲区删除,而是把指向有效载荷开端的指针向前移到L3报头的开端
sk_buff是被C预处理程序的#ifdef指示附加字段,在编译期间配置了该字段就会被选中使用
struct sk_buff { ...... #ifdef CONFIG_NET_SWITCHDEV __u8 offload_fwd_mark:1; __u8 offload_mr_fwd_mark:1; #endif #ifdef CONFIG_NET_CLS_ACT __u8 tc_skip_classify:1; __u8 tc_at_ingress:1; __u8 tc_redirected:1; __u8 tc_from_ingress:1; #endif #ifdef CONFIG_TLS_DEVICE __u8 decrypted:1; #endif ...... }
每个sk_buff结构中的next和prev字段实现联系。next字段指向前,而prev指向后。
这个表还有一个必要需求:每个sk_buff结构必须能够迅速找出整个表的头。为了实现这个功能,添加一个sk_buff_head结构体作为哑元元素
struct sk_buff_head { /* 这两个必须是最前面的 */ struct sk_buff *next; struct sk_buff *prev; __u32 qlen; // 元素的数目 spinlock_t lock; // 防止对表的并发访问 };
sk_buff和sk_buff_head的前两元素是相同的:next和prev指针。这两个结构体存在同一个表中。同样的函数可以用于操作这两个结构体。
sk_buff结构体中包含一个指针list,指向专一的sk_buff_head结构
这是一个指针,指向拥有此缓冲区的套接字的sock数据结构。该数据以及套接字相关的信息会由L4(TCP/UDP)以及用户应用程序使用。当缓冲区只是被转发时,该指针就是NULL
指缓冲区中数据区块的大小。len = 主要缓冲区(由head所指)的数据 + 片段缓冲区(fragment)
注:当缓冲区从一个网络分层移往下一个网络分层时,其值会变化(协议栈往上移动时报头会丢弃,往下移动时报头会添加)
data_len只计算片段中的数据大小
MAC报头的大小
引用计数,或者使用这个sk_buff缓冲区的实例的数目。
这个参数的主要用途是避免在某人依然使用此sk_buff结构时,把这个结构给释放掉。因此,此缓冲区的每个用户在必要时都要递增和递减此字段。
users有时直接用atomic_inc和atomic_dec函数递增和递减,大多数使用skb_get和kfree_skb进行处理。
此字段代表此缓冲区总的大小,包括sk_buff结构本身。当此缓冲区得到所分配的len个字节的数据请求空间时。每当skb->len的值增加时,字段会更新
初始化由alloc_skb函数设置成len+sizeof(sk_buff)
这些字段代表缓冲区的边界以及其中的数据。每一分层为其工作而准备缓冲区时,可能会为一个报头或更多的数据分配更多的空间
head和end指向已分配缓冲区空间的开端和尾端
date和tail指向实际数据的开端和尾端
head和data之间的空隙填上一个协议报头;tail和end之间添加新数据(包含一个附加报头)
此函数指针可以被初始化为一个函数,当缓冲区被删除时,可完成某些工作。
此缓冲区不属于一个套接字时,不会初始化;属于一个套接字时,通常设成sock_rfree或sock_wfree(可用于更新套接字队列中所持有的内存)
与特定内核功能无关的字段
通常只对一个已接收的封包才有意义。这是时间戳,用于表示封包何时被接收,或者有时用于封包预定传输时间。
描述一个网络设备。dev所代表的角色依赖于存储在该缓冲区的封包是即将传输的还是刚被接收的而定。
有些网络功能允许一些设备按组集合起来代表专一的虚拟接口(没有和硬设备直接相关联),由一个虚拟设备驱动程序提供接口服务
驱动程序会从其群组中选择一个特定设备,然后dev参数改为指向该设备的net_device数据结构
这是已被接收的封包所源自的设备。当封包是由本地产生时,为NULL指针。主要由流量控制使用
只对虚拟设备有意义。代表的是与虚拟设备所关联的真实设备。
例:Bonding(多个网卡使用统一个IP)和VLAN(交换机)接口使用改字段,记下真实设备的输入流量是从什么地方接收而来的
这些是指向TCO/IP协议栈的协议报头指针:h针对L4;nh针对L3;而mac针对L2
每个协议的结构都由内核中该层来解释。
1、当接收一个数据封包时,负责处理第n层报头的函数,会从第n-l层接收一个缓冲区,该缓冲区的skb->data指向第n层报头的开端。
2、处理第n层的函数会为该层初始化适当的指针(例,L3的处理函数的skb->nh),用以保存skb->data字段。
3、函数完成第n层的处理,把封包传给第n+1层的处理函数之前,更新skb->data,使其指向第n层报头的尾端,也就是n+1层报头的开始
是一个“控制缓冲区”,或者说是私有信息的存储空间,为每一层内部使用起维护作用。在sk_buff结构内静态分配,目前大小48字节容量足以容纳每层所需的私有数据。在每层代码中通过宏进行访问。
// tcp定义结构体 include/net/tcp.h struct tcp_skb_cb { __u32 seq; /* Starting sequence number */ __u32 end_seq; /* SEQ + FIN + SYN + datalen */ __u8 tcp_flags; /* TCP header flags. (tcp[13]) */ ..... } // 定义宏访问 #define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0])) // 应用 shinfo->tskey = TCP_SKB_CB(skb)->seq + skb->len - 1;
代表校验以及相关联的状态标识
当一个boolean标识置位时,表示该结构是另一个sk_buff缓冲区的克隆
会根据帧的L2目的地址进行类型划分。可取值include/linux/if_packet.h
表示正被传输或转发的封包Qos(服务质量)等级。
由于每种协议都有自己的函数处理例程用来处理输入的封包。因此,驱动程序使用这个字段通知上层该使用哪个处理例程。每个驱动程序会调用netif_rx用来启动上面的网络分层的处理例程。在该函数被调用前protocol字段必须初始化。
封包的安全级
Linux内核是模块化的,允许你选择要包含什么或省略什么。只有当内核编译为支持特定功能,如防火墙或Qos等。字段才会包含在sk_buff数据结构中
定义在net/core/skbuff.c中的alloc_skb是分配缓冲区的主要函数。数据缓冲区和报头(sk_buff数据结构)是两种不同的实例
建立一个缓冲区有两次的内存分配(一个是分配缓冲区,另一个是分配sk_buff结构)
__alloc_skb -> skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node); // 从一个缓存中取得一个sk_buff数据结构 -> size = SKB_DATA_ALIGN(size); // 强制对齐 -> size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info)); -> data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc); // 后续对结构体中的一些参数初始化
添加填充区域以强制对齐。skb_shared_info主要用于处理一些IP片段
dev_alloc_skb是由设备驱动程序使用的缓冲区分配函数,这个函数是在中断中执行。这个函数是alloc_skb函数的包装,为了做优化,在申请的大小之上再加需要的字节。
由于中断中处理函数调用,会要求原子操作
两个函数会释放一个缓冲区,使其返回缓冲池(缓存)。kfree_skb是直接由dev_kfree_skb调用并启动的。
只有当skb->users计数器为1时(该缓冲区已无任何用户时),这个基本函数才会释放一个缓冲区。否则,只是递减该计数器。
如果一个缓冲区有3个用户,需要调用3次dev_free_skb或kfree_skb时才会释放内存。
当destructor函数指针已经被初始化时,就会在这里调用。
一个sk_buff结构体可以持有一次对dest_entry数据结构的引用。当sk_buff结构被释放时,也必须调用dst_release以递减相关的dst_entry数据结构的引用计数值。
一个sk_buff数据结构与另一个实际存储数据的内存块相关联。在数据区块底端的skb_shared_info数据结构可以持有一些指向其他内存片段的指针。kfree_skb也会释放这些片段所持有的内存。
会在缓冲区的头部预留一些空间(头空间),通常允许插入一个报头,或者强迫数据对齐某个边界
缓冲区分配之后,通常马上就会调用该函数
static inline void skb_reserve(struct sk_buff *skb, int len) { skb->data += len; skb->tail += len; }
网卡驱动程序中接收函数,把任何数据存储在刚分配到的缓冲区之前都会执行
skb_reserve(skb, 2); // 把IP对齐在16字节地址边界上
把一个带有14个字节的Ethernet帧拷贝到缓冲区中,参数2会时缓冲区的头移动2个字节。这样ip报头就可以从缓冲区开始按照16字节边界对齐,并紧接在ethernet报头之后。
数据在传输期间在相反方向使用skb_reserve实例
1、当TCP被请求传输一些数据时,他会根据一些准则分配一个缓冲区
2、TCP会在缓冲区头部预留足够的空间(用skb_reserve),容纳所有层(TCP、IP、链路层)的报头。参MAX_TCP_HEADER是所有层报头的总和
3、TCP有效载荷拷贝到缓冲区(有效载荷可能以不同的方式组织)
4、TCP层添加报头
5、TCP层把缓冲区传给IP层,IP层也同样添加其报头
6、IP层把IP封包传给邻居层,把链路层报头添加进来。
当缓冲区往下传播经过网络协议栈时
1、每个协议都会把skb->data往下传
2、将其报头拷贝进来
3、更新skb->len
skb_reserve函数没有真正把任何东西移入数据缓冲区内,只是更新两个指针
skb_push会把一个数据块添加到缓冲区的开端,而skb_put会把一个数据块添加到缓冲区的尾部,这两函数和skb_reserve都是移动指针的指向。新的数据应该由其他的函数明确地拷贝进来。
数据缓冲区尾端有个名为skb_shared_info的数据结构,用以保持此数据区块的附加信息。此数据结构紧接在标记数据尾端的end指针之后。
struct skb_shared_info { __u8 __unused; __u8 meta_len; __u8 nr_frags; // 用于处理IP片段 __u8 tx_flags; // 用于处理IP片段 unsigned short gso_size; unsigned short gso_segs; struct sk_buff *frag_list; // 用于处理IP片段 struct skb_shared_hwtstamps hwtstamps; unsigned int gso_type; u32 tskey; atomic_t dataref; // 数据块的“用户”数目 void * destructor_arg; skb_frag_t frags[MAX_SKB_FRAGS]; };
sk_buff结构中没有指向skb_shared_info数据结构的字段。为了访问该结构体,函数必须使用返回end指针的skb_shinfo宏:
#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))
通过这个指针去访问里面的成员
u32 nr_frags = skb_shinfo(skb)->nr_frags + 1
为什么要缓冲区的克隆?
当同一个缓冲区需要由不同消费者个别处理时,那些消费者可能需要修改sk_buff描述符的内容。但是内核不需要完全拷贝sk_buff结构和相关联的数据缓冲区。
为了提高效率,内核可以克隆原始值,也就是只拷贝sk_buff结构体,然后使用引用计数,以免过早释放共享的数据块。
缓冲区的克隆由skb_clone函数实现
skb_clone函数可用于检查一个skb缓冲区的克隆状态
图中为片段缓冲区的一个实例,也就是说,这个缓冲区内有一些数据是存储在一些以flags数值链接起来的数据片段。
skb_share_check函数可用于检查引用计数skb->users,并且当users字段说明该缓冲区是共享时可以克隆该缓冲区。
当一个缓冲区被克隆时,数据区块的内容不能修改。这在访问数据的代码不需要上锁机制。
当函数不仅需要修改sk_buff结构的内容,而且也需要修改数据时,就必须连数据区块一起克隆。这样有两种选择:
第一种:知道需要修改介于skb_start和skb_end的区域的数据内容,可以使用pskb_cope只拷贝该区域
第二种:当知道可能连片段数据区块的内容也要修改时,必须使用skb_cope
skb_shared_info数据结构可以包含一个sk_buff结构列表(链接到一个frag_list字段)。访问方式和数值一样。
这些函数会操作sk_buff元素列表,列表也称为队列。文件:<net/core/skbuff.c> <include/linux/skbuff.h>
用一个元素为空的队列对sk_buff_head
把一个缓冲区分别添加到队列的头或尾
把一个元素分别从队列的头或尾去掉。
把队列变为空队列
依次循环运行队列里的每个元素
这类函数都必须以原子方式执行。因为队列在添加元素或从队列中删除元素等异步事件所中断,如到期的定时器调用的函数
《Understanding Linux Network Internals》