释放内存:kfree_skb和dev_kfree_skb
这两个函数会释放一个缓冲区,使其返回缓冲池,kfree_skb是直接由和dev_kfree_skb包裹函数调用并启动的。随后由设备驱动程序定义并使用,而其名称和dev_alloc_skb类似,但是其组成只有一个简单的宏,这个宏不做任何事情,只是调用kfree_skb。只有当skb->users 计数器为1时(缓冲区已经无任何用户时),这个基本函数才会释放一个缓冲区。否则,就只是递减该计数器。所以,如果一个缓冲区有三位用户,则只有第三次调用dev_kfree_skb或kfree_skb时才会释放内存。
图2-6的流程图显示出释放一个缓冲区时的所有步骤。在第三十三章会看到,一个sk_buff结构可以持有一次对dst_entry数据结构的引用。因此,当sk_buff结构被释放时,也必须调用dst_release 以递减相关的dst_entry 数据结构的引用计数值。
当destructor函数指针已经被初始化时,就会在这里调用。
图2-5 看起来是一个简单的场景,一个sk_buff数据结构与另一个实际存储数据的内存块相关联。然而,如图2-5所示,在该数据区块底端的skb_shared_info数据结构可以持有一些指向其他内存片段的指针,参见第二十一章的实例。kfree_skb 也会释放这些片段所持有的内存。最后,sk_buff数据结构返回skbuff_head_cache缓存。
数据预留及对齐:skb_reserve, skb_put, skb_push以及skb_pull。
skb_reserve会在缓冲区的头部预留一些空间,通常允许插入一个报头,或者强迫数据对齐某个边界。此函数会移动标记有效载荷开端指针data和尾端指针tail。图2-4 显示出调用skb_reserve 之后的结果。缓冲区分配之后,通常马上就会调用此函数,此时data和tail的值仍然相同。
如果你看一下几种Ethernet驱动程序之一的接收函数,你会发现,它们把任何数据存储在刚分配到的缓冲区之前,都会使用下列命令:
递减skb引用计数
skb->users
|
skb引用计数为0吗? 否 返回
|
skb在列表中吗?
打印警告信息(可能是bug)
|
skb->destructor 已经初始化了吗?
是 执行解析函数 否 skb_releaese_data
skb是克隆吗? 否 释放该主要缓冲区以及任何片段。
dataref 的引用计数位0吗?
否
skb返回缓存。
图2-6 kfree_skb 函数
skb_reserve(skb, 2) //把IP对齐在16字节地址边界上
由于知道要把一个带有14个字节头的Ethernet 帧拷贝到缓冲区中,参数2会使缓冲区的头移动2个字节。这样IP报头就可以从缓冲区开始按照16字节边界对齐,并紧接在Ethernet报头之后,如图2-7所示。
(a)
struct sk_buff
len = 0
head
data
tail
end
(b)
struct sk_buff
len = 0;
head
data
tail
end
...
(c)
struct sk_buff
len=L
head
data
tail
end
填充区域 2
Ethernet
报头 14
IP报头
IP 有效载荷
图2-7 在skb_reserve之前b在skb_reserve 之后C拷贝帧到缓冲区之后。
图2-8所示为数据在传输期间在相反方向使用skb_reserve的实力。
(a)
struct sk_buff
len = 0
head
data
tail
end
(b)
struct sk_buff
len = 0;
head
data
tail
end
(c)
len = L1
head
data
tail
end IP有效载荷
图2-8 缓冲区穿过协议栈从TCP曾向下到链路层。
1 当TCP倍请求传输一些数据时,会根据一些准则TCP MSS .支持分散-聚集IO分配一个缓冲区。
2 TCP会在缓冲区头部预留足够的空间,以容纳所有层TCP 的报头,参数MAX_TCP_HEADER是所有层报头的总和,其计算要考虑最坏的情况,因为TCP曾不知道锁传输用的接口类型, 因此会为每个分层预留最大可能的报头。甚至会考虑多个IP报头的可能性,IP-over-IP通道时,就可能有多个报头。
3 TCP有效载荷拷贝到缓冲区,注意,图2-8只是一个例子而已,TCP有效载荷可能以不同的方式组织,如果可以作为多个片段来存储,在第二十一章中,我们会看到片段缓冲区的形式。
4 TCP层添加报头
5 TCP层把缓冲区传给IP,IP层也同样添加其报头
6 IP层把IP风暴传给邻居层,邻居层吧链路层报头添加进来。
注意,当缓冲区往下传播经过网络协议栈时,每个协议都会吧skb-》data传下传,并将其报头拷贝进来,然后更新skb->len ,这一切都是用我们在图2-4中所看到的函数完成的。
注意,skb_reserve函数没有其他任何东西移入数据缓冲区内,只是更新2-4所示的两个指针而已。
static inline void skb_reserve(struct sk_buff *skb, unsigned int len)
{
skb->data += len;
skb->tail+=len;
}
skb_push 会把一个数据块添加到缓冲区的开端,而skb->put会把一个数据块添加到缓冲区的尾端。像skb_reserve一样,这些函数并没有真的把数据添加到缓冲区,只是简单的移动指向头尾的指针,新的数据应该有其他函数明确的拷贝进来,skb_pull是通过head指针向前移动,把一个数据块从缓冲区的头部删除。