一. 前言
内核提供了大量实用的操作sk_buff的函数,在开发网络设备驱动程序和修改网络协议栈代码时需要用到。这些函数从功能上可以分为三类:创建,释放和复制socket buffer;操作sk_buff结构中的参数和指针;管理socket buffer的队列。本文主要介绍其中一些典型的函数。本文使用的内核版本是3.18.45。
二. 创建和释放socket buffer
1. alloc_skb
alloc_skb是__alloc_skb函数的封装,是为socket buffer分配内存的主要函数。其中具体的实现不再分析了,主要介绍调用alloc_skb函数后,sk_buff各个参数和指针的情况。其中,skb->head,skb->data和skb->tail都指向分配内存的开始地址,skb->end指向分配内存的结束地址,分配的数据大小是4字节对齐的。如下图所示。
注意:调用alloc_skb分配的skb,由于实际数据长度还不确定,其中skb->len = 0。
2. kree_skb
kree_skb函数是__kfree_skb函数的封装,由于同一个socket buffer可能被多个地方使用,所以kfree_skb只有在skb->users = 1时,才会完全释放socket buffer,其他情况只会将skb->users减1。
三. 操作sk_buff参数和指针的函数
1. skb_reserve
skb_reserve的函数原型:
static inline void skb_reserve(struct sk_buff *skb, int len)
{
skb->data += len;
skb->tail += len;
}
skb_reserve函数的使用是在alloc_skb函数之后,作用是为网络协议栈的二层,三层以及数据负载留出空间。如下图所示:
上图中,留出的reserve len就是为协议栈的其他数据压入预留空间,从中也可以看出,skb->data到skb->tail之间保存的是真实负载数据的内容。
2. skb_put
skb_put的函数原型:
unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
{
unsigned char *tmp = skb_tail_pointer(skb);
SKB_LINEAR_ASSERT(skb);
skb->tail += len;
skb->len += len;
if (unlikely(skb->tail > skb->end))
skb_over_panic(skb, len, __builtin_return_address(0));
return tmp;
}
从函数实现可知,skb_put函数将skb->tail加了len,表示将skb的实际负载数据的长度加len,要实现这一点,alloc_skb分配的len减去skb_reserve的len应该要大于等于skb_put的len,这样才能保证skb_put函数运行没问题。示意图如下:
3. skb_push
skb_push的函数原型:
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
{
skb->data -= len;
skb->len += len;
if (unlikely(skb->data<skb->head))
skb_under_panic(skb, len, __builtin_return_address(0));
return skb->data;
}
从函数实现可以看出,skb_push将data指针往低地址移动len长度,同时skb->len长度加len,然后程序将向该地址压入长度为len长度的数据。示意图如下:
4. skb_pull
skb_pull的函数原型:
static inline unsigned char *__skb_pull(struct sk_buff *skb, unsigned int len)
{
skb->len -= len;
BUG_ON(skb->len < skb->data_len);
return skb->data += len;
}
static inline unsigned char *skb_pull_inline(struct sk_buff *skb, unsigned int len)
{
return unlikely(len > skb->len) ? NULL : __skb_pull(skb, len);
}
unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)
{
return skb_pull_inline(skb, len);
}
从函数实现可以看出,skb_pull是__skb_pull函数的封装,skb_pull_inline判断len要小于skb->len。skb_pull函数就是skb_push的反向操作函数,作用是将skb->data指针向高地址移动len,同时,skb->len减len,相当于从skb中删除了长度为len的数据。
5. skb_trim
skb_trim函数的原型:
static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset)
{
skb->tail = skb->data + offset;
}
static inline void __skb_trim(struct sk_buff *skb, unsigned int len)
{
if (unlikely(skb_is_nonlinear(skb))) {
WARN_ON(1);
return;
}
skb->len = len;
skb_set_tail_pointer(skb, len);
}
void skb_trim(struct sk_buff *skb, unsigned int len)
{
if (skb->len > len)
__skb_trim(skb, len);
}
从函数实现可以看出,skb_trim是__skb_trim的封装,作用是将skb的负载数据长度修剪为len,并修剪长度是通过修改skb->tail的指针来实现的。示意图如下:
四. 管理socket buffer的队列
1. skb_queue_head_init
skb_queue_head_init函数的原型:
static inline void __skb_queue_head_init(struct sk_buff_head *list)
{
list->prev = list->next = (struct sk_buff *)list;
list->qlen = 0;
}
static inline void skb_queue_head_init(struct sk_buff_head *list)
{
spin_lock_init(&list->lock);
__skb_queue_head_init(list);
}
从函数实现可以看出,skb_queue_head_init是__skb_queue_head_init函数的封装,作用是初始化链表的指针以及链表的队列长度。
2. skb_dequeue
skb_dequeue的函数原型:
static inline struct sk_buff *__skb_dequeue(struct sk_buff_head *list)
{
struct sk_buff *skb = skb_peek(list);
if (skb)
__skb_unlink(skb, list);
return skb;
}
struct sk_buff *skb_dequeue(struct sk_buff_head *list)
{
unsigned long flags;
struct sk_buff *result;
spin_lock_irqsave(&list->lock, flags);
result = __skb_dequeue(list);
spin_unlock_irqrestore(&list->lock, flags);
return result;
}
函数的作用是从链表的头部取出一个skb,并且将skb从链表中删除。
3. skb_dequeue_tail
skb_dequeue_tail的函数原型:
static inline struct sk_buff *__skb_dequeue_tail(struct sk_buff_head *list)
{
struct sk_buff *skb = skb_peek_tail(list);
if (skb)
__skb_unlink(skb, list);
return skb;
}
struct sk_buff *skb_dequeue_tail(struct sk_buff_head *list)
{
unsigned long flags;
struct sk_buff *result;
spin_lock_irqsave(&list->lock, flags);
result = __skb_dequeue_tail(list);
spin_unlock_irqrestore(&list->lock, flags);
return result;
}
函数的作用是从链表的尾部取出一个skb,并且将skb从链表中删除。
4. skb_append
skb_append的函数原型:
static inline void __skb_insert(struct sk_buff *newsk,
struct sk_buff *prev, struct sk_buff *next,
struct sk_buff_head *list)
{
newsk->next = next;
newsk->prev = prev;
next->prev = prev->next = newsk;
list->qlen++;
}
static inline void __skb_queue_after(struct sk_buff_head *list,
struct sk_buff *prev,
struct sk_buff *newsk)
{
__skb_insert(newsk, prev, prev->next, list);
}
void skb_append(struct sk_buff *old, struct sk_buff *newsk, struct sk_buff_head *list)
{
unsigned long flags;
spin_lock_irqsave(&list->lock, flags);
__skb_queue_after(list, old, newsk);
spin_unlock_irqrestore(&list->lock, flags);
}
函数的作用是在链表中的old节点后面插入newsk的skb,链表的队列长度也加1。
五. 实例
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/skbuff.h>
#include <linux/udp.h>
#include <linux/ip.h>
#define UDP_SRC_PORT 3015
#define UDP_DST_PORT 3016
char data[32] = "chaos is a ladder!";
char src_ip[] = "192.168.0.56";
char dst_ip[] = "192.168.0.105";
char src_mac[] = {0x00, 0x0C, 0x43, 0x28, 0x80, 0xF5};
char dst_mac[] = {0x3C, 0x7C, 0x3F, 0xD7, 0x94, 0xAF};
struct sk_buff *skb;
struct udphdr *uhdr;
struct iphdr *ihdr;
struct ethhdr *ehdr;
struct net_device * dev = NULL;
void assemble_udp_header(struct udphdr *udp_hdr)
{
udp_hdr->source = htons(UDP_SRC_PORT);
udp_hdr->dest = htons(UDP_DST_PORT);
udp_hdr->len = htons(sizeof(data) + sizeof(struct udphdr));
udp_hdr->check = ~csum_tcpudp_magic(ihdr->saddr, ihdr->daddr, skb->len, IPPROTO_UDP, 0);
}
void assemble_ip_header(struct iphdr *ip_hdr)
{
ip_hdr->version = IPVERSION;
ip_hdr->ihl = sizeof(struct iphdr)/4;
ip_hdr->tos = 0;
ip_hdr->tot_len = htons(sizeof(data) + sizeof(struct udphdr) + sizeof(struct iphdr));
ip_hdr->id = htons(42351);
ip_hdr->frag_off = 0;
ip_hdr->ttl = 64;
ip_hdr->protocol = IPPROTO_UDP;
ip_hdr->check = 0;
ip_hdr->saddr = in_aton(src_ip);
ip_hdr->daddr = in_aton(dst_ip);
}
void assemble_eth_header(struct ethhdr *eth_hdr)
{
memcpy(eth_hdr->h_dest, dst_mac, 6);
memcpy(eth_hdr->h_source, src_mac, 6) ;
eth_hdr->h_proto = htons(ETH_P_IP);
}
int __init skb_package_init(void)
{
printk("skb init\n");
unsigned char *p;
skb = alloc_skb(MAX_SKB_SIZE, GFP_ATOMIC);
dev = dev_get_by_name(&init_net, "br0");
skb->dev = dev;
skb_reserve(skb, 2 + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) + sizeof(data));
p = skb_push(skb, sizeof(data));
memcpy(p, (void*)data, sizeof(data));
p = skb_push(skb, sizeof(struct udphdr));
uhdr = (struct udphdr *)p;
skb_reset_transport_header(skb);
p = skb_push(skb, sizeof(struct iphdr));
ihdr = (struct iphdr *)p;
skb_reset_network_header(skb);
p = skb_push(skb, sizeof(struct ethhdr));
ehdr = (struct ethhdr *)p;
skb_reset_mac_header(skb);
assemble_ip_header(ihdr);
assemble_udp_header(uhdr);
assemble_eth_header(ehdr);
dev_queue_xmit(skb);
return 0;
}
void __exit skb_package_exit(void)
{
kfree_skb(skb);
printk("skb exit\n");
}
module_init(skb_package_init);
module_exit(skb_package_exit);
MODULE_LICENSE("GPL");
代码的功能是组一个UDP包并且通过网口br0发送出去。其中涉及到了skb的分配,预留数据包的空间,将协议头和实际负载数据放入到skb->data中,最后调用dev_queue_xmit将包发送出去的过程。
六. 总结
本文总结了socket buffer的一些常用的操作函数,并介绍了它们的作用,其中包括skb分配,以及skb的指针的操作和skb的队列的操作,这些函数是编写网络设备驱动程序的基础,应该熟练掌握。