Linux内核中网络重要结构sk_buff
如下图所示是skb数据区域的结构布局、以及skb中各个指针的所表示的含义和位置。
下文将详细描述在申请skb和修改skb过程中各指针的变化情况,如添加头部、添加用户数据、弹出头部等操作。
此外,我们将探讨如何实现页面非线性数据区。
allocate_skb
skb = alloc_skb(len, GFP_KERNEL);
这是使用alloc_skb()分配新的skb后各指针的初始位置,head、data和tail指针都指向数据缓冲区的开头,end指针指向它的结尾。需要特别注意的是:tail指针指向的是用户数据区的末尾。
这个skb的长度为零,因为它根本不包含任何数据。接下来,让我们使用skb_reserve()为协议头保留一些空间。
sbk_reverse
skb_reserve(skb, header_len);
这是调用skb_reserve()后skb的各指针的位置,通常,在构建输出数据包时,我们会为需要的最大头部空间保留足够的字节。大多数IPv4协议可以通过使用套接字值sk->sk_prot->max_header来做到这一点。
当从以太网设备收到数据包时,我们通常调用skb_reserve(skb, NET_IP_ALIGN)。默认情况下,NET_IP_ALIGN定义为“2”。这使得在以太网头之后,协议头将在至少4字节边界上对齐。几乎所有的IPv4和IPv6协议处理都假定头部正确对齐。
接下来,让我们将一些用户数据添加到数据包中。
skb_put
unsigned char *data = skb_put(skb, user_data_len);
int err = 0;
skb->csum = csum_and_copy_from_user(user_pointer, data,
user_data_len, 0,
if (err)
goto user_fault;
这是添加用户数据后skb的各指针位置,skb_put()使'skb->tail'前进指定的字节数,它也将'skb->len'增加该字节数。
需要注意的是,不能在具有任何分页数据的skb上调用此例程。另外还必须确skb中有足够的尾部空间来容纳你尝试放入的字节数。这两个条件都由skb_put()检查,如果违反任何一个规则,就会触发失败断言。
计算的校验和被记住在'skb->csum'中。现在,是时候构建协议头了。我们将构建一个UDP头,然后构建IPv4图。
skb_push
struct inet_sock *inet = inet_sk(sk);
struct flowi *fl = &inet->cork.fl;
struct udphdr *uh;
skb->h.raw = skb_push(skb, sizeof(struct udphdr));
uh = skb->h.uh
uh->source = fl->fl_ip_sport;
uh->dest = fl->fl_ip_dport;
uh->len = htons(user_data_len);
uh->check = 0;
skb->csum = csum_partial((char *)uh,
sizeof(struct udphdr), skb->csum);
uh->check = csum_tcpudp_magic(fl->fl4_src, fl->fl4_dst,
user_data_len, IPPROTO_UDP, skb->csum);
if (uh->check == 0)
uh->check = -1;
这是我们将UDP头添加到skb后的各指针位置。skb_push()将'skb->data'指针递减指定的字节数,还将增加'skb->len'的相应的字节数。调用者必须确保有足够的头部空间来执行push操作。此条件由skb_push()检查,如果违反此规则,将触发失败断言。
现在,是时候添加IPv4头了。
struct rtable *rt = inet->cork.rt;
struct iphdr *iph;
skb->nh.raw = skb_push(skb, sizeof(struct iphdr));
iph = skb->nh.iph;
iph->version = 4;
iph->ihl = 5;
iph->tos = inet->tos;
iph->tot_len = htons(skb->len);
iph->frag_off = 0;
iph->id = htons(inet->id++);
iph->ttl = ip_select_ttl(inet,
iph->protocol = sk->sk_protocol; /* IPPROTO_UDP in this case */
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
ip_send_check(iph);
skb->priority = sk->sk_priority;
skb->dst = dst_clone(
这是我们将IPv4头添加到skb后的各指针位置。就像上面的UDP头一样,skb_push()递减'skb->data'并递增'skb->len'。我们将 'skb->nh.raw'指针更新到新空间的开头,并构建IPv4头。
一旦我们有必要的信息来构建以太网报头(来自邻居层和ARP),这个数据包基本上就可以被发送到接口上了。
使用分页数据
一旦开始使用分页数据,事情就会变得有点复杂。在大多数情况下,为skb使用[page, offset, len]元组的能力出现了,因此文件系统文件内容可以直接通过套接字发送。
一旦开始在skb上使用分页数据,这将对所有未来的skb数据区操作产生特定限制。特别是,不再可能进行skb_put()操作。
实际上有两个与skb相关的长度变量,len和data_len。后者仅在skb中有分页数据时才起作用。skb->data_len告诉skb中有多少字节的分页数据。由此我们可以得出更多的信息:
skb中分页数据的存在由skb->data_len非零表示。可以使用辅助函数skb_is_nonlinear()来进行测试。
skb->data的非分页数据长度可以通过skb->len - skb->data_len来计算。同样,内核定义了一个名为skb_headlen() 的辅助函数。
当有分页数据时,数据包从skb->data开始,经过skb_headlen(skb)字节后,进入分页数据区域,总共分页数据为skb->data_len字节。这就是为什么在有分页数据时尝试执行 skb_put(skb)是不合逻辑的。因为必须将数据添加到分页数据区域的末尾才是正确的。
skb中的每个分页数据块由以下结构描述:
struct skb_frag_struct {
struct page *page;
__u16 page_offset;
__u16 size;
};
有一个指向页面的指针(你必须持有一个正确的引用),这个分页数据块开始的页面内的偏移量,以及总共有多少字节。
分页的碎片在共享skb区域中组织成一个数组,由以下结构定义:
#define MAX_SKB_FRAGS (65536/PAGE_SIZE + 2)
struct skb_shared_info {
atomic_t dataref;
unsigned int nr_frags;
unsigned short tso_size;
unsigned short tso_segs;
struct sk_buff *frag_list;
skb_frag_t frags[MAX_SKB_FRAGS];
};
nr_frags成员说明在frags[]数组中有多少是激活的。tso_size和tso_segs用于将信息传递给设备驱动程序以进行TCP段卸载。frag_list用于维护为分段目的而组织的skb链表,它不用于维护分页数据。最后,frags[]自己保存了frag描述符。
同样,可以使用辅助函数来帮助我们填写页面描述符。
void skb_fill_page_desc(struct sk_buff *skb, int i,
struct page *page,
int off, int size)
这将填充第i个页面偏离size大小的偏移量的页面。它还将nr_frags成员更新为i之后的成员。
如果您希望简单地将现有片段条目扩展一些字节,可以为size成员增加该数量。
由于非线性skb带来的所有复杂性,似乎很难以直接的方式检查数据包的区域,或者将数据从数据包复制到另一个缓冲区。这里有两个可用的辅助函数可以使得这种操作变的非常容易。
首先,请看:
void *skb_header_pointer(const struct sk_buff *skb, int offset, int len, void *buffer)
传入skb、偏移量(以字节为单位)、字节数和一个缓冲区,如果你感兴趣的数据驻留在非线性数据区。
您将返回一个指向数据项的指针,如果您输入无效的偏移量和len参数,则返回 NULL。这里有两种情况:第一,如果您要求的内容直接在skb->data线性数据区域中,您将获得一个指向那里的直接指针。否则,您将获得传入的缓冲区指针。
特别是,检查输出路径上的数据包头的代码应使用此例程来读取和解释协议头。例如netfilter层就大量使用了此功能。
对于协议头以外的较大数据块,使用以下辅助函数可能更合适。
int skb_copy_bits(const struct sk_buff *skb, int offset,
void *to, int len);
这会将给定skb的指定字节数和指定偏移量复制到“to”缓冲区中。这个辅助函数用于将skb数据复制到内核缓冲区,因此它不能用于将skb数据复制到用户空间。还有另一个辅助函数:
int skb_copy_datagram_iovec(const struct sk_buff *from,
int offset, struct iovec *to,
int size);
在这个函数中,用户的数据区域由给定的iovec描述。其他参数与上面传递给skb_copy_bits()的参数几乎相同。
- 参考:http://vger.kernel.org/~davem/skb_data.html