Linux网络代码中结构体

struct sk_buff
   一个封包就存在这里,所有网络分层会使用这个结构来储存其报头、有关用户数据的信息,以及用来协调其工作的其他内部信息。
struct net_device
  每种网络设备都用这个数据结构表示,包括软硬件的配置信息。
struct sock
   用于存储套接字的网络信息。


套接字缓冲区:sk_buff数据结构

sk_buff数据结构定义文件位置

sk_buff结构定义在<include/linux/skbuff.h>头文件中,由变量堆组成。

sk_buff数据结构分布

整个结构体分为:布局;通用;功能专用;管理函数

sk_buff数据结构在传输中的流程

多个不同的网络分层都会使用这个结构体。结构体从一个分层转到另一个分层时,其不同的字段会随之发生改变。
缓冲区往上传经各个网络分层:
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_head结构体

每个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结构
在这里插入图片描述

struct sock *sk

这是一个指针,指向拥有此缓冲区的套接字的sock数据结构。该数据以及套接字相关的信息会由L4(TCP/UDP)以及用户应用程序使用。当缓冲区只是被转发时,该指针就是NULL

unsigned int len

指缓冲区中数据区块的大小。len = 主要缓冲区(由head所指)的数据 + 片段缓冲区(fragment)
注:当缓冲区从一个网络分层移往下一个网络分层时,其值会变化(协议栈往上移动时报头会丢弃,往下移动时报头会添加)

unsigned int data_len

data_len只计算片段中的数据大小

unsigned int mac_len

MAC报头的大小

atomic_t users

引用计数,或者使用这个sk_buff缓冲区的实例的数目。
这个参数的主要用途是避免在某人依然使用此sk_buff结构时,把这个结构给释放掉。因此,此缓冲区的每个用户在必要时都要递增和递减此字段。
users有时直接用atomic_inc和atomic_dec函数递增和递减,大多数使用skb_get和kfree_skb进行处理。

unsigned int truesize

此字段代表此缓冲区总的大小,包括sk_buff结构本身。当此缓冲区得到所分配的len个字节的数据请求空间时。每当skb->len的值增加时,字段会更新
初始化由alloc_skb函数设置成len+sizeof(sk_buff)

unsigned int tail

unsigned int end

unsigned char *head

unsigned char *data

这些字段代表缓冲区的边界以及其中的数据。每一分层为其工作而准备缓冲区时,可能会为一个报头或更多的数据分配更多的空间
head和end指向已分配缓冲区空间的开端和尾端
date和tail指向实际数据的开端和尾端
head和data之间的空隙填上一个协议报头;tail和end之间添加新数据(包含一个附加报头)
在这里插入图片描述

void (*destructor)(struct sk_buff *skb)

此函数指针可以被初始化为一个函数,当缓冲区被删除时,可完成某些工作。
此缓冲区不属于一个套接字时,不会初始化;属于一个套接字时,通常设成sock_rfree或sock_wfree(可用于更新套接字队列中所持有的内存)


通用字段

与特定内核功能无关的字段

struct timeval stamp

通常只对一个已接收的封包才有意义。这是时间戳,用于表示封包何时被接收,或者有时用于封包预定传输时间。

struct net_device *dev

描述一个网络设备。dev所代表的角色依赖于存储在该缓冲区的封包是即将传输的还是刚被接收的而定。
有些网络功能允许一些设备按组集合起来代表专一的虚拟接口(没有和硬设备直接相关联),由一个虚拟设备驱动程序提供接口服务
驱动程序会从其群组中选择一个特定设备,然后dev参数改为指向该设备的net_device数据结构

struct net_device *input_dev

这是已被接收的封包所源自的设备。当封包是由本地产生时,为NULL指针。主要由流量控制使用

struct net_device *real_dev

只对虚拟设备有意义。代表的是与虚拟设备所关联的真实设备。
例:Bonding(多个网卡使用统一个IP)和VLAN(交换机)接口使用改字段,记下真实设备的输入流量是从什么地方接收而来的

union {…} h

union {…} nh

union {…} mac

这些是指向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层报头的开始
在这里插入图片描述

char cb[48]

是一个“控制缓冲区”,或者说是私有信息的存储空间,为每一层内部使用起维护作用。在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;

unsigned int csum

unsigned char ip_summed

代表校验以及相关联的状态标识

unsigned char cloned

当一个boolean标识置位时,表示该结构是另一个sk_buff缓冲区的克隆

unsigned char pkt_type

会根据帧的L2目的地址进行类型划分。可取值include/linux/if_packet.h

__u32 priority

表示正被传输或转发的封包Qos(服务质量)等级。

unsigned short protocol

由于每种协议都有自己的函数处理例程用来处理输入的封包。因此,驱动程序使用这个字段通知上层该使用哪个处理例程。每个驱动程序会调用netif_rx用来启动上面的网络分层的处理例程。在该函数被调用前protocol字段必须初始化。

unsigned short security

封包的安全


功能专用

Linux内核是模块化的,允许你选择要包含什么或省略什么。只有当内核编译为支持特定功能,如防火墙或Qos等。字段才会包含在sk_buff数据结构中


管理函数

分配内存:alloc_skb和dev_alloc_skb

定义在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

两个函数会释放一个缓冲区,使其返回缓冲池(缓存)。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也会释放这些片段所持有的内存。
在这里插入图片描述

数据预留及对齐:skb_reserve、skb_put、skb_push以及skb_pull

在这里插入图片描述

skb_reserve

会在缓冲区的头部预留一些空间(头空间),通常允许插入一个报头,或者强迫数据对齐某个边界
缓冲区分配之后,通常马上就会调用该函数

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结构和skb_shinfo函数

数据缓冲区尾端有个名为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

缓冲区的克隆和拷贝skb_clone函数和pskb_cope/skb_cope函数

为什么要缓冲区的克隆?
当同一个缓冲区需要由不同消费者个别处理时,那些消费者可能需要修改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>

skb_queue_head_init

用一个元素为空的队列对sk_buff_head

skb_queue_head skb_queue_tail

把一个缓冲区分别添加到队列的头或尾

skb_dequeue skb_dequeue_tail

把一个元素分别从队列的头或尾去掉。

skb_queue_purge

把队列变为空队列

skb_queue_walk

依次循环运行队列里的每个元素
这类函数都必须以原子方式执行。因为队列在添加元素或从队列中删除元素等异步事件所中断,如到期的定时器调用的函数


参考文献

《Understanding Linux Network Internals》

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐