sk_buff详解

更新时间:2024-06-08 09:29:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

sk_buff详解

杨毅(yangyi@http://www.njliaohua.com/) 2008-12-31

概述

本文对Linux内核网络子系统中的核心数据结构sk_buff进行分析,以2.6.21-7的内核来本文档将回答以下几个与sk_buff有关的问题:

含skb_headlen(),skb_pagelen()等,分别在何种环境下使用?

2. 几个引用计数的区别:skb->users, skb->cloned, skb_shared_info->dataref; 3. 几个指针的关系和移动:head/data/tail/end, h.raw, nh.raw, mac.raw; 4. 与skb共享复制有关的几个操作有什么区别?

5. skb分配,释放的实现细节;网络子系统中会在哪些地方分配skb,有哪些区别? 6. skb的数据区分为哪几部分?为什么需要这么多种类,分别都应用在何种场景?互相之间的

转化关系如何?

分析,其余版本可能存在差别。

1. 几个长度有关的成员变量:skb->len, skb->data_len, skb->truesize之间的关系,还包

struct sk_buff成员变量

如下变量的区别需要注意: struct net_device *dev; int iif;

dev和iif

这几个变量都用于跟踪与 packet 相关的 device。由于 packet 在接收的过程中,可能会经过多个 virtual driver 处理,因此需要几个变量。

接收数据包的时候, dev 和 iif 都指向最初的 interface,此后,如果需要被 virtual driver 处理,那么 dev 会发生变化,而 iif 始终不变。

len,data_len, mac_len, truesize

unsigned int

len, data_len, mac_len; truesize;

unsigned int

‘len’ 表示此 SKB 管理的 Data Buffer 中数据的总长度;

如上图,一个skb结构描述的内存区域包含几个部分: 1)sk_buff结构体本身;

2)线性buffer区域,由skb->head,data,tail,end等几个指针来描述,并且包含skb_shared_info结构;

3)“paged data”:通过skb_shared_info结构管理的一组保存在 page 中的数据; 4)skb_shared_info(skb)->frag_list队列包含分片的skb队列,队列成员是skb链表指针。

skb有效数据区的长度使用skb->len记录,一共包括三个部分:

skb->len = skb_headlen()(?== linear_buffer_len) + skb->data_len 1. 从skb->data开始到skb->tail结束的线性buffer区,这一部分的长度可以使用skb_headlen()获得; 线性区有效数据长度:

linear_buffer_len = skb->tail - skb->data;(alloc_skb时为0) ?== skb_headlen() 线性区总长度:skb_data_len = skb->end - skb->head;(不包括skb_shared_info)

2. skb_shared_info(skb)->frags[]数组里包含的“paged data”,长度为所有frags->size的和;这一部分加上1中的数据长度可以使用skb_pagelen()获得;“paged data”占用长度: page_data_len = sum(skb_shared_info->frags[i]->size); skb_pagelen() = skb_headlen() + page_data_len

3. skb_shared_info(skb)->frag_list链表里所有skb的数据区长度(len),这一部分的长度加上2中的长度记录在skb->data_len里。

skb->data_len = page_data_len + sum(skb_shared_info->frag_list->len)

truesize = sizeof(struct sk_buff) + SKB_DATA_ALIGN(skb_data_len)+ skb->data_len (alloc_skb时为sizeof(struct sk_buff) + SKB_DATA_ALIGN(size)) len, data_len, truesize是否包含frag_list里的长度?? ? 是!

skb->truesize这个成员变量衡量的是整个skb结构所占的内存大小(为啥不包含struct sk_shared_info??),在“ip分片处理”和“socket的读写缓存分配”中使用,前者将在以后的ip层处理相关文档中详细说明,后者挑选几个典型应用如下: ?

skb_set_owner_w,skb_set_owner_r:datagram的skb和socket的写,读缓存联系起来: sock_hold(sk); skb->sk = sk;

skb->destructor = sock_wfree/sock_rfree;

atomic_add(skb->truesize, &sk->sk_wmem_alloc/ sk_rmem_alloc); ?

sock_wfree和sock_rfree:在kfree_skb时或skb_orphan时调用skb的destructor函数: struct sock *sk = skb->sk;

atomic_sub(skb->truesize, &sk-> sk_wmem_alloc/sk_rmem_alloc); ?

sock_queue_rcv_skb,sock_queue_err_skb在把收到的或者错误的skb放入队列中时会对socket接收缓存,skb->truesize及当前已经占用的读内存进行判断: if (atomic_read(&sk->sk_rmem_alloc) + skb->truesize >=

(unsigned)sk->sk_rcvbuf)

return -ENOMEM;

而发送时的判断是调用比较复杂:sk_stream_alloc_pskb? sk_stream_wmem_schedule static inline struct sk_buff *sk_stream_alloc_pskb(struct sock *sk, { ??

skb = alloc_skb_fclone(size + hdr_len, gfp); }

static inline int sk_stream_wmem_schedule(struct sock *sk, int size) { }

跟这几个长度有关的几个inline函数如下: ?

skb_is_nonlinear():return skb->data_len; 即是否包含“paged data”或frag_list

return size <= sk->sk_forward_alloc || sk_stream_mem_schedule(sk, size, 0); if (skb) { }

skb->truesize += mem;

if (sk_stream_wmem_schedule(sk, skb->truesize)) { }

__kfree_skb(skb);

skb_reserve(skb, hdr_len); return skb;

int size, int mem, gfp_t gfp)

??

? ?

skb_headlen():return skb->len - skb->data_len; 即只包含线性区域实际使用长度,或“有效长度”;(??与skb->tail - skb->data的差别??)

skb_pagelen():所有“paged data”长度之和,再加上线性区域实际使用长度; static inline int skb_pagelen(const struct sk_buff *skb) { }

int i, len = 0;

for (i = (int)skb_shinfo(skb)->nr_frags - 1; i >= 0; i--)

len += skb_shinfo(skb)->frags[i].size; return len + skb_headlen(skb);

? ? ? ?

skb_push, skb_pull, skb_reserve, skb_put这几个函数移动data或tail指针,都会更改skb->len

skb_trim:修改skb->len的值,并且skb->tail = skb->data+len skb_headroom:skb->data - skb->head;

skb_tailroom:如果没有paged data或fragment list,为skb->end - skb->tail,否则为0

‘mac_len’ 指 MAC 头的长度。目前,它只在 IPSec 解封装的时候被使用。将来可能从 SKB 结构中去掉。

cloned, ip_summed, nohdr, users, dataref等

__u8

local_df:1,

cloned:1, ip_summed:2, nohdr:1, nfctinfo:3;

atomic_t }

users;

struct skb_shared_info {

atomic_t dataref; ??

“local_df”在IPv4中使用,设为1后代表允许对已经分片的数据包进行再次分片,在IPSec等情况下使用;

“ip_summed”代表网卡是否支持计算接收包checksum,在老版本kernel里可取值如下: ? ?

CHECKSUM_NONE:代表网卡不算checksum;

CHECKSUM_HW:代表网卡支持硬件计算checksum(对L4 head + payload的校验),并且已经将计算结果复制给skb->csum,软件需要计算“伪头(pseudo header)”的校验和,与skb->csum相加得到L4的结果; ?

CHECKSUM_UNNECESSARY:网卡已经计算并校验过整个包的校验,包括伪头,软件无需再次计算,一般用于loopback device。 本版本的内核里是如下定义,并有详细的注释:

#define CHECKSUM_NONE 0

#define CHECKSUM_PARTIAL 1

#define CHECKSUM_UNNECESSARY 2 #define CHECKSUM_COMPLETE 3

/* A. Checksumming of received packets by device. *

* NONE: device failed to checksum this packet. * *

* UNNECESSARY: device parsed packet and wouldbe verified checksum. *

skb->csum is undefined.

* It is bad option, but, unfortunately, many of vendors do this. * Apparently with secret goal to sell you new device, when you * will add new protocol to your host. F.e. IPv6. 8) *

* COMPLETE: the most generic way. Device supplied checksum of _all_ * the packet as seen by netif_rx in skb->csum.

* NOTE: Even if device supports only some protocols, but * is able to produce some skb->csum, it MUST use COMPLETE, * not UNNECESSARY. *

* B. Checksumming on output. *

* NONE: skb is checksummed by protocol or csum is not required. *

* PARTIAL: device is required to csum packet as seen by hard_start_xmit * from skb->h.raw to the end and to record the checksum * at skb->h.raw+skb->csum. *

* Device must show its capabilities in dev->features, set * at device setup time.

* NETIF_F_HW_CSUM - it is clever device, it is able to checksum *

everything.

* NETIF_F_NO_CSUM - loopback or reliable single hop media. * NETIF_F_IP_CSUM - device is dumb. It is able to csum only * * * *

* Any questions? No questions, good. */

“nohdr”:The 'nohdr' field is used in the support of TCP Segmentation Offload ('TSO' for short). Most devices supporting this feature need to make some minor modifications to the TCP and IP headers of an outgoing packet to get it in the right form for the hardware

--ANK

TCP/UDP over IPv4. Sigh. Vendors like this

way by an unknown reason. Though, see comment above about CHECKSUM_UNNECESSARY. 8) skb->csum is undefined.

to process. We do not want these modifications to be seen by packet sniffers and the like. So we use this 'nohdr' field and a special bit in the data area reference count to keep track of whether the device needs to replace the data area before making the packet header modifications.

简单来说,nohdr代表了skb_shared_info里的dataref有没有被分成两部分: ? ?

nohdr = 0:dataref代表整个skb数据区的引用计数;

nohdr = 1:dataref的高16bits代表skb数据区“payload部分”的引用计数,低16bits代表整个skb数据区的引用计数。

与之相关的函数有:skb_header_cloned(), skb_header_release()等。

“cloned”代表skb是否被clone,为了能迅速的引用一个 SKB 的数据,当 clone 一个已存在的 SKB 时,会产生一个新的 SKB,但是这个 SKB 会共享已有 SKB 的数据区。当一个 SKB 被 clone 后,原来的 SKB 和新的 SKB 结构中,‘cloned’都要被设置为1。

“users”:是sk_buff结构本身的引用计数,在kfree_skb时会先先使用原子操作对users计数-1,如果=0,才调用__kfree_skb真正去释放skb。 ? ?

skb_shared()检查skb->users是否为1;

skb_shared_check():如果skb->users > 1,则clone一个新的。

有关dataref,clone,users这些引用计数的区别和应用场景请见后续的“skb共享复制相关操作”一节。

“nfctinfo”:用于netfilter子系统中的conntrack模块记录连接状态,可取值为enum变量: enum ip_conntrack_info {

/* Part of an established connection (either direction). */ */ */ };

IP_CT_NEW, IP_CT_IS_REPLY,

IP_CT_NUMBER = IP_CT_IS_REPLY * 2 - 1 /* >= this indicates reply direction */

/* Number of distinct IP_CT types (no NEW in reply dirn). */

IP_CT_RELATED,

/* Started a new connection to track (only IP_CT_DIR_ORIGINAL); may be a retransmission.

IP_CT_ESTABLISHED,

/* Like NEW, but related to an existing connection, or ICMP error (in either direction).

pkt_type, fclone, ipvs_property

__u8

pkt_type:3,

fclone:2, ipvs_property:1;

“pkt_type”表示根据L2的目的地址得到包类型,可能取值在include/linux/if_packet.h,以太网设备中使用eth_type_trans函数来初始化这个值:

? PACKET_HOST:

? ? ? ? ? ?

PACKET_MULTICAST: PACKET_BROADCAST: PACKET_OTHERHOST: PACKET_OUTGOING: PACKET_LOOPBACK: PACKET_FASTROUTE:

“fclone”:是较新版kernel中添加的一个特性,以前版本的kernel中skb在分配的时候都是从后备高速缓存(lookaside cache)skbuff_head_cache中获取sk_buff的结构;而现在可以在调用alloc_skb(),通过fclone参数选择从skbuff_head_cache或者skbuff_fclone_cache中分配。两者的区别在于skbuff_head_cache在创建时指定的单位内存区域的大小是

sizeof(structsk_buff),可以容纳任意数目的struct sk_buff,而skbuff_fclone_cache在创建时指定的单位内存区域大小是2*sizeof(struct sk_buff)+sizeof(atomic_t),它的最小区域单位是一对strcut sk_buff和一个引用计数,这一对sk_buff是克隆的,即它们指向同一个数据缓冲区,引用计数值是0(SKB_FCLONE_UNAVAILABLE),1(SKB_FCLONE_ORIG)或2(SKB_FCLONE_CLONE),表示这一对中有几个sk_buff已被使用: ? ?

分配skb时,skb->fclone = SKB_FCLONE_ORIG; atomic_set(fclone_ref, 1);

child->fclone = SKB_FCLONE_UNAVAILABLE; skb_clone时,如下处理 struct sk_buff *n; n = skb + 1;

if (skb->fclone == SKB_FCLONE_ORIG && n->fclone == SKB_FCLONE_UNAVAILABLE) { }

atomic_t *fclone_ref = (atomic_t *) (n + 1); n->fclone = SKB_FCLONE_CLONE; atomic_inc(fclone_ref);

n = kmem_cache_alloc(skbuff_head_cache, gfp_mask); if (!n)

return NULL;

n->fclone = SKB_FCLONE_UNAVAILABLE;

} else {

即如果设置了fclone,第一次clone采用快速clone,直接使用child_skb,并且增加fclone_ref,child_skb->fclone = SKB_FCLONE_CLONE;其他情况都从skbuff_head_cache里分配新的skb; ?

在释放skb结构时如下处理

switch (skb->fclone) { case SKB_FCLONE_UNAVAILABLE:

kmem_cache_free(skbuff_head_cache, skb); break;

case SKB_FCLONE_ORIG:

fclone_ref = (atomic_t *) (skb + 2); if (atomic_dec_and_test(fclone_ref)) kmem_cache_free(skbuff_fclone_cache, skb);

break;

case SKB_FCLONE_CLONE:

fclone_ref = (atomic_t *) (skb + 1); other = skb - 1;

/* The clone portion is available for * fast-cloning again. */

skb->fclone = SKB_FCLONE_UNAVAILABLE; if (atomic_dec_and_test(fclone_ref))

kmem_cache_free(skbuff_fclone_cache, other); break;

};

? ?

SKB_FCLONE_UNAVAILABLE:直接归还给skbuff_head_cache;(虽然没有经过clone的child_skb->fclone也是这个标志,但没有被clone也不会被释放)

SKB_FCLONE_ORIG:代表释放的是从skbuff_fclone_cache里分配的skb,如果fclone_ref = 1(未被clone过)则归还skb给skbuff_fclone_cache;否则(已经被clone过)只是将fclone_ref--; ?

SKB_FCLONE_CLONE:代表释放的代表释放的是从skbuff_fclone_cache里分配的skb经过clone操作后得到的新skb,则把fclone标志置为SKB_FCLONE_UNAVAILABLE,留作下次快速clone使用。如果fclone_ref--=0,代表这个skb的兄弟skb已经被释放了,则归还skb给skbuff_fclone_cache。

根据skb->fclone标志:

“ipvs_property”是为ip_vs模块添加的变量,置为1代表已经被ip_vs某个部分处理过,后续不需要再处理。可以参考ip_vs_out()函数。

head/data/tail/end指针,h/nh/mac指针

unsigned char

*head, *data, *tail, *end;

*th; *uh;

union {

struct tcphdr struct udphdr

struct icmphdr *icmph; struct igmphdr *igmph; struct iphdr *ipiph; struct ipv6hdr *ipv6h; unsigned char

*raw;

} h; union {

struct iphdr *iph; struct ipv6hdr *ipv6h; struct arphdr unsigned char

*arph; *raw;

} nh; union {

head, data, tail, end四个指针分别指向data buffer的不同位置,如下图:

unsigned char *raw; } mac;

这四个指针的移动,最常用的四个函数为:(a)skb_put, (b)skb_push, (c)skb_pull, (d)skb_reserve ,如下图所示。

h,nh,mac分别指向各层网络协议的头部,下面讨论各个指针在接收时都在哪些地方被初始化: ? { ?? }

static inline struct ethhdr *eth_hdr(const struct sk_buff *skb)

skb->mac.raw = skb->data; skb_pull(skb, ETH_HLEN); eth = eth_hdr(skb);

mac为L2的头部指针,在eth_type_trans里初始化

__be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev)

??

{ } ?

通过了eth_type_trans后,skb->data指针指向网络层的头部,在netif_receive_skb里初始化nh.raw和h.raw:

skb->h.raw = skb->nh.raw = skb->data; skb->mac_len = skb->nh.raw - skb->mac.raw; netif_receive_skb ? packet_type->func() ? ip_rcv() ?

如果只是转发,不用处理到L4,只有发给本机的包才需要,在进入L4处理之前,ip_local_deliver_finish会把h.raw初始化为L4的头部: int ihl = skb->nh.iph->ihl*4;

__skb_pull(skb, ihl);

/* Point into the IP datagram, just past the header. */ skb->h.raw = skb->data;

return (struct ethhdr *)skb->mac.raw;

其他相关结构

skb_shared_info

当调用 alloc_skb() 构造 SKB 和 data buffer时,需要的 buffer 大小是这样计算的: data = kmalloc(size + sizeof(struct skb_shared_info), gfp_mask);

除了指定的 size 以外,还包括一个 struct skb_shared_info 结构的空间大小。也就是说,当调用 alloc_skb(size) 要求分配 size 大小的 buffer 的时候,同时还创建了一个 skb_shared_info 。这个结构定义如下: struct skb_shared_info { };

“dataref”:skb的data区域(线性buffer)引用计数,即有多少个skb结构指向这块data区域(skb_clone时++),被分为两部分:高16bits用于描述skb->data中payload部分的引用计数(skb->nohdr = 1时才有效),低16bits用来描述整个skb->data的引用计数,如下: /* We divide dataref into two halves. The higher 16 bits hold references * to the payload part of skb->data. The lower 16 bits hold references to

atomic_t dataref;

unsigned short nr_frags; unsigned short gso_size;

/* Warning: this field is not always filled in (UFO)! */ unsigned short gso_segs; unsigned short gso_type; __be32 ip6_frag_id; struct sk_buff *frag_list; skb_frag_t frags[MAX_SKB_FRAGS];

* the entire skb->data. It is up to the users of the skb to agree on * where the payload starts. *

* All users must obey the rule that the skb->data reference count must be * greater than or equal to the payload reference count. *

* Holding a reference to the payload part means that the user does not * care about modifications to the header part of skb->data. */

#define SKB_DATAREF_SHIFT 16

#define SKB_DATAREF_MASK ((1 << SKB_DATAREF_SHIFT) - 1)

“nr_frags”: “paged_data”计数,最多为MAX_SKB_FRAGS = 65536/PAGE_SIZE + 2(4k的PAGE_SIZE,为18)

“skb_frag_t frags[MAX_SKB_FRAGS]”:用来记录paged_data实际位置,包含page指针,在page中的offset,以及占用的size 。(可以有不同的skb指向同一个page中的不同offset和size,也可以指向相同的,由page里头的_count来代表引用计数) struct skb_frag_struct { };

“gso_size”,“gso_segs”,“gso_type”由“GSO”(Generic Segmentation Offload,或叫“TSO”,即TCP Segmentation Offload)功能使用 enum { };

“frag_list”:分片处理中,记录分片使用。

我们只要把 end 从 char* 转换成skb_shared_info* ,就能访问到这个结构 Linux 提供一个宏来做这种转换:

#define skb_shinfo(SKB) ((struct skb_shared_info *)((SKB)->end)) 那么,这个隐藏的结构用意何在?它至少有两个目的: 1、 用于管理 paged data 2、 用于管理分片

SKB_GSO_TCPV4 = 1 << 0, SKB_GSO_UDP = 1 << 1,

/* This indicates the skb is from an untrusted source. */ SKB_GSO_DODGY = 1 << 2,

/* This indicates the tcp segment has CWR set. */ SKB_GSO_TCP_ECN = 1 << 3, SKB_GSO_TCPV6 = 1 << 4, struct page *page; __u16 page_offset; __u16 size;

sk_buff_head和skb队列

协议栈处理中经常用到sk_buff的队列,队列头使用struct sk_buff_head来描述,如下: struct sk_buff_head { };

其中包含一个指示队列长度的qlen,和一个自旋锁lock用于不同的进程同时操作队列时的保护。 一个sk_buff队列如下图:

/* These two members must be first. */ struct sk_buff *next; struct sk_buff *prev; __u32

qlen;

spinlock_t lock;

sk_buff操作

下面对net/core/skbuff.c里调用EXPORT_SYMBOL导出的函数进行较为详细的分析,但仅限于每个函数本身完成的功能,具体调用的位置还得参考协议栈处理其他部分的代码。

EXPORT_SYMBOL(___pskb_trim); EXPORT_SYMBOL(__kfree_skb); EXPORT_SYMBOL(kfree_skb); EXPORT_SYMBOL(__pskb_pull_tail); EXPORT_SYMBOL(__alloc_skb); EXPORT_SYMBOL(__netdev_alloc_skb); EXPORT_SYMBOL(pskb_copy); EXPORT_SYMBOL(pskb_expand_head);

EXPORT_SYMBOL(skb_checksum); EXPORT_SYMBOL(skb_clone);

EXPORT_SYMBOL(skb_clone_fraglist); EXPORT_SYMBOL(skb_copy);

EXPORT_SYMBOL(skb_copy_and_csum_bits); EXPORT_SYMBOL(skb_copy_and_csum_dev); EXPORT_SYMBOL(skb_copy_bits); EXPORT_SYMBOL(skb_copy_expand); EXPORT_SYMBOL(skb_over_panic); EXPORT_SYMBOL(skb_pad);

EXPORT_SYMBOL(skb_realloc_headroom); EXPORT_SYMBOL(skb_under_panic); EXPORT_SYMBOL(skb_dequeue); EXPORT_SYMBOL(skb_dequeue_tail); EXPORT_SYMBOL(skb_insert); EXPORT_SYMBOL(skb_queue_purge); EXPORT_SYMBOL(skb_queue_head); EXPORT_SYMBOL(skb_queue_tail); EXPORT_SYMBOL(skb_unlink); EXPORT_SYMBOL(skb_append); EXPORT_SYMBOL(skb_split);

EXPORT_SYMBOL(skb_prepare_seq_read); EXPORT_SYMBOL(skb_seq_read); EXPORT_SYMBOL(skb_abort_seq_read); EXPORT_SYMBOL(skb_find_text);

EXPORT_SYMBOL(skb_append_datato_frags);

skb分配相关操作

__alloc_skb, alloc_skb,alloc_skb_fclone, __dev_alloc_skb,

__netdev_alloc_skb,sock_alloc_send_pskb, sock_alloc_send_skb, sk_stream_alloc_skb, sk_stream_alloc_pskb, sock_wmalloc等

基本上内核里所有的skb分配都是通过直接调用__alloc_skb或相应的包装函数来完成,下面先对__alloc_skb进行分析,然后再稍微讨论几个包装函数。 1. 输入参数:跟内存分配/相关的暂时不讨论;

a) size:指的是线性buffer的长度,即skb->end - skb->head; b) gfp_mask:allocation_mask,跟内存分配的优先级等相关;

c) fclone:从skbuff_fclone_cache还是skbuff_head_cache分配skb结构,用于快速clone;

d) node:用于分配内存的numa node

2. 具体操作:

a) 分配skb结构本身:根据fclone选择cache,然后把gfp_mask&~__GFP_DMA,得到

skb,分配失败则退出;

b) 分配skb->data线性buffer区:

i. ii. iii.

size = SKB_DATA_ALIGN(size); 根据L1_CACHE_BYTES(X86上为128 bytes)调整大小,补齐为128的整数倍;

kmalloc的内存大小为size + sizeof(struct skb_shared_info) 初始化skb成员变量

1. truesize之前的都置为0;

2. truesize = SKB_DATA_ALIGN(size) + sizeof(struct sk_buff),并不包

含skb_shared_info; 3. users置为1,head/data/tail指向线性buffer的开始;end指向结束; 4. 根据fclone设置skb->fclone,以及对应的兄弟skb及fclone_ref;

iv. skb_shared_info初始化 1. dataref置为1; 2. 其他置为0或NULL;

3. 返回值:skb或data内存分配失败,返回NULL;否则返回创建的sk_buff指针。

常见的__alloc_skb包装函数包括:

? alloc_skb和alloc_fclone_skb:原型如下,一个fclone,另一个不理;numa node

都是-1;

static inline struct sk_buff *alloc_skb(unsigned int size,gfp_t priority) {

return __alloc_skb(size, priority, 0, -1);

}

static inline struct sk_buff *alloc_skb_fclone(unsigned int size,gfp_t priority) { } ?

dev_alloc_skb和__dev_alloc_skb:一般是网卡驱动分配用于接收缓存的skb,length会加上NET_SKB_PAD,调用完alloc_skb后,会通过skb_reserve在线性buffer的头部保留一块长度为NET_SKB_PAD的区域,有关NET_SKB_PAD和NET_IP_ALIGN的说明,可以参见内核代码中的注释; /*

* CPUs often take a performance hit when accessing unaligned memory * locations. The actual performance hit varies, it can be small if the * hardware handles it or large if we have to take an exception and fix it * in software. *

* Since an ethernet header is 14 bytes network drivers often end up with * the IP header at an unaligned offset. The IP header can be aligned by * shifting the start of the packet by 2 bytes. Drivers should do this * with:

*

* skb_reserve(NET_IP_ALIGN); *

return __alloc_skb(size, priority, 1, -1);

* The downside to this alignment of the IP header is that the DMA is now * unaligned. On some architectures the cost of an unaligned DMA is high * and this cost outweighs the gains made by aligning the IP header. *

* Since this trade off varies between architectures, we allow NET_IP_ALIGN * to be overridden. */

#ifndef NET_IP_ALIGN #define NET_IP_ALIGN #endif /*

* The networking layer reserves some headroom in skb data (via

* dev_alloc_skb). This is used to avoid having to reallocate skb data when * the header has to grow. In the default case, if the header has to grow * 16 bytes or less we avoid the reallocation. *

* Unfortunately this headroom changes the DMA alignment of the resulting * network packet. As for NET_IP_ALIGN, this unaligned DMA is expensive * on some architectures. An architecture can override this value, * perhaps setting it to a cacheline in size (since that will maintain * cacheline alignment of the DMA). It must be a power of 2. *

* Various parts of the networking layer expect at least 16 bytes of * headroom, you should not reduce this. */

#ifndef NET_SKB_PAD #define NET_SKB_PAD 16 #endif ?

netdev_alloc_skb和__netdev_alloc_skb:也是用于网卡驱动分配skb,与前面提到的dev_alloc_skb的区别在于:调用alloc_skb时多了一个numa node的判断,如下: static inline struct sk_buff *netdev_alloc_skb(struct net_device *dev, unsigned int length) { }

struct sk_buff *__netdev_alloc_skb(struct net_device *dev, {

unsigned int length, gfp_t gfp_mask)

return __netdev_alloc_skb(dev, length, GFP_ATOMIC);

2

int node = dev->dev.parent ? dev_to_node(dev->dev.parent) : -1; struct sk_buff *skb;

} ?

skb = __alloc_skb(length + NET_SKB_PAD, gfp_mask, 0, node); if (likely(skb)) { skb_reserve(skb, NET_SKB_PAD); }

skb->dev = dev;

return skb;

sock_alloc_send_skb和sock_alloc_send_pskb:sock_alloc_send_skb是

sock_alloc_send_pskb的包装,一般在ip_output,af_unix, af_packet, raw.c,udp等处调用,我们主要关心skb的分配和初始化情况,socket相关的代码不作讨论: 1. 输入参数:

a) size:线性buffer的长度;(header_len)

b) data_len:paged data的长度;kernel里目前都是直接使用

sock_alloc_send_skb来调用sock_alloc_send_pskb,所以data_len = 0

2. skb分配过程:

a) 先调用alloc_skb,传入的size参数为header_len; b) 如果没有data_len,直接返回;否则 i. ii. iii.

根据data_len,获得page数目,赋给skb_shinfo(skb)->nr_frags; skb->truesize += data_len; 通过调用alloc_page分配paged data,除了最后一个frags之外,其他的frags->offset = 0; frags->size = PAGE_SIZE;最后一个的frags->size = data_len % PAGE_SIZE。

?

sk_stream_alloc_skb是sk_stream_alloc_pskb的包装,从名字上看,就是用于stream发送的时候分配skb,一般是在tcp层调用,下面三处调用sk_stream_alloc_pskb ? tso_fragment ?

tcp_sendmsg

? do_tcp_sendpages

下面两个调用sk_stream_alloc_skb ? tcp_fragment ? tcp_mtu_probe 下面分析代码: 1. 输入参数:

a) size:待发送的字节流长度;

b) mem:不知道干啥用的,看起来只是修改了skb->truesize,所有调用的地方

传递的值也都是0;

2. skb分配过程:

a) size + 根据sk->sk_proto算出来的最大头部长度(对于TCP,#define

MAX_TCP_HEADER (128 + MAX_HEADER))作为分配的skb里线性Buffer的长度;

b) 调用alloc_skb_fclone分配skb(因为很快就会被clone?) c) 调用skb_reserve,把头部空出来以备后用。

? sock_wmalloc:如果在发送一个很长的数据包,L4在处理的时候会为IP层的分片提前

做一些工作,比如将数据放置在一系列的skb中,一般调用sock_alloc_send_skb来创建这一系列skb中的第一个(可以参考ip_append_data),调用sock_wmalloc来分配剩下的分片skb。函数体本身很简单,如下:

struct sk_buff *sock_wmalloc(struct sock *sk, unsigned long size, int force, gfp_t priority) { }

if (force || atomic_read(&sk->sk_wmem_alloc) < sk->sk_sndbuf) {

struct sk_buff * skb = alloc_skb(size, priority); if (skb) { }

skb_set_owner_w(skb, sk); return skb;

}

return NULL;

skb释放

kfree_skb和__kfree_skb

kfree_skb()是__kfree_skb()的一个包装,首先检查skb的引用计数skb->users,为1才继续调用__kfree_skb(),否则只是skb->users - 1(原子操作)。

在kernel的协议栈代码中,很多地方都是先显式的调用atomic_inc(&skb->users);然后处理一堆内容后,调用kfree_skb,而不是调用atomic_dec(&skb->users)。这样做的好处是,可以在合适的地方尽快释放skb,并且可以让处理skb结构的代码逻辑更独立。(调用处理skb的函数之前可能增加过skb->users,也可能没有)

__kfree_skb()是真正释放skb的函数:先将skb结构里指向其他网络子系统的指针release或put,并且调用析构函数skb->destructor(),最后通过kfree_skbmem()来释放内存: ? 首先调用skb_release_data来处理线性buffer区域和paged_data(包括frag_list);

判断skb的Data Buffer区域可以被释放的条件是以下二者之一: ?

!skb->cloned:skb没有被clone(注意被clone的和clone生成的skb->cloned

都被置1,两个skb里的skb_shinfo(skb)->dataref都atomic_inc,见skb_clone的讨论),skb没被clone,说明数据区肯定只有唯一一个skb指向;

!atomic_sub_return(skb->nohdr ? (1 << SKB_DATAREF_SHIFT) + 1 : 1, &skb_shinfo(skb)->dataref) 对skb_shinfo(skb)->dataref进行判断:

? 如果nohdr被设置,代表dataref被分成两部分,前面SKB_DATAREF_SHIFT(16)

bit代表skb->data里的payload部分的引用计数;后面16bits代表整个

skb->data的引用计数。所以需要把dataref-(1 << SKB_DATAREF_SHIFT) + 1),来判断是否可以释放skb->data;

? nohdr没被设置,不需要把skb->data的payload单独考虑引用计数,只需要?

dataref-1来决定是否释放skb->data 释放Data Buffer分为三个部分:

?

? put_page释放“paged data”;

? skb_drop_fraglist,进而调用kfree_skb,来释放frag_list里的所有skb; ? kfree(skb->head),释放“线性Buffer”。

然后根据skb->fclone标志将skb结构本身归还slab cache。见fclone讨论部分

?

skb共享复制相关操作

skb_clone, skb_cloned, skb_header_cloned, skb_shared, skb_share_check, skb_unshare, skb_clone_fraglist, skb_copy, pskb_copy, skb_header_release

内核的网络子系统核心数据结构是sk_buff,同一个skb可能会被很多个子系统共享,同时使用或修改,出于性能的考虑,内核中提供了一系列对skb的共享和拷贝函数,网络子系统根据各自的需求来调用:

skb_share: 只读的共享需求

? ?

需要共享的地方调用:直接调用atomic_inc(skb->users),或者skb_get(); 判断是否被共享:skb_shared()

static inline int skb_shared(const struct sk_buff *skb) { return atomic_read(&skb->users) != 1; }

? ?

取消共享的地方调用:kfree_skb()(见kfree_skb一节的讨论) 对分片队列的共享:skb_clone_fraglist(),(pskb_expand_head()里用了),其实就是把skb队列里每一个skb的引用计数+1;然后在取消共享时,对队列里每一个skb调用kfree_skb()见减小引用计数。

static inline void skb_drop_fraglist(struct sk_buff *skb) { }

skb_drop_list(&skb_shinfo(skb)->frag_list);

static void skb_drop_list(struct sk_buff **listp) { }

struct sk_buff *list = *listp; *listp = NULL;

do { struct sk_buff *this = list;

list = list->next; kfree_skb(this);

} while (list);

static void skb_clone_fraglist(struct sk_buff *skb) { struct sk_buff *list; }

for (list = skb_shinfo(skb)->frag_list; list; list = list->next) skb_get(list);

skb_clone: 只修改sk_buff结构本身

?

需要共享的地方调用:skb_clone() i. 首先根据skb->fclone决定采用快速clone机制还是普通机制;

新分配的skb暂时不用和skb链表,socket关联,都设为NULL;

其他成员的值都copy,需要增加引用计数的也都使用XXX_get()处理;

新skb的users为1(与alloc_skb()一样),n->nohdr = 0, destructor = NULL; 新老skb的skb->cloned = 1;

新老skb指向的数据区是一样的(包括线性buffer和paged data,分片等),则需要把dataref+1:atomic_inc(&(skb_shinfo(skb)->dataref))(整个数据区的引用计数)

ii. iii. iv. v. vi.

?

判断是否被共享: ?

skb_cloned():返回1的条件为skb->cloned = 1并且dataref的低16bit部分(整个数据区的引用计数)不为1;

static inline int skb_cloned(const struct sk_buff *skb) {

return skb->cloned &&

(atomic_read(&skb_shinfo(skb)->dataref) & SKB_DATAREF_MASK) != 1;

本文来源:https://www.bwwdw.com/article/x056.html

Top