NOTE:

内核版本: 2.6.19, maybe

简介

struct sk_buff

  • 存储一个封包 (packet)
  • 所有网络分层都会使用这个结构存储
    • 报头
    • 有关用户数据的信息(有效载荷)
    • 用来协调其工作的其它内部信息

struct net_device

  • 在 Linux 内核中每种网络设备都有这个数据结构表示,包括软硬件的配置信息

struct sock

  • 存储套接字的网络信息

套接字缓冲区:sk_buff 结构

网络选项以及内核结构

  • 只有在编译期间定义了 CONFIG_NET_SCHED 符号,字段 tc_index 才会是此数据结构的一部分
1
2
3
4
5
6
7
8
9
struct sk_buff {
...

#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#endif

...
};

布局字段

  • 内核在一个双向链表中维护所有的 sk_buff 结构,但该表的组织比传统的双向链表更复杂
    • 每个 sk_buff 结构必须能够迅速找出整个表的头。为了实现这项需求,在表的开端额外增加一个 sk_buff_head 结构作为 dummy element
1
2
3
4
5
6
7
8
struct sk_buff_head {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;

__u32 qlen; // 表中元素的数目
spinlock_t lock; // 防止对表的并发访问
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;

// 指向拥有此缓冲区的套接字的 sock 数据结构
// 1. 当数据在本地产生或正由本地进程接收时,
// 该数据以及套接字相关的信息会由 L4(TCP或UDP) 以及用户应用程序使用
// 2. 当缓冲区只是被转发时,该指针为 NULL
struct sock *sk;
// 时间戳,表示封包何时接收
// 由 netif_rx 函数用 net_timestap 设置
struct skb_timeval tstamp;
// 网络设备
struct net_device *dev;
// 已被接收的封包所源自的设备
struct net_device *input_dev;

// 指向 TCP/IP 协议栈的协议报头的指针
// L4
union {
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct ipv6hdr *ipv6h;
unsigned char *raw; // 用于初始化
} h;

// L3
union {
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
unsigned char *raw;
} nh;

// L2
union {
unsigned char *raw;
} mac;

// 由路由子系统使用
struct dst_entry *dst;
struct sec_path *sp;

/*
* This is the control buffer. It is free to use for every
* layer. Please put your private variables there. If you
* want to keep them across layers you have to do a skb_clone()
* first. This is owned by whoever has the skb queued ATM.
*/
char cb[48];

// 缓冲区中数据区块的大小 (包括协议报头)
// 包括:主要缓冲区(head 所指)的数据和一些片段的数据(fragmented buffer)
// 当缓冲区从一个网络分层移往下一个网络分层时,其值会变化
unsigned int len,
// 只计算片段中的数据大小
data_len,
// MAC 报头的大小
mac_len,
// 校验和
csum;
// 正被传输或转发的封包 QoS(Quality of Service, 服务质量) 等级
__u32 priority;
__u8 local_df:1,
// 该结构是另一个 sk_buff 缓冲区的克隆
cloned:1,
ip_summed:2,
nohdr:1,
nfctinfo:3;
// 根据帧的 L2 目的地址进行类型划分
// PACKET_HOST: To us
// PACKET_BROADCAST: To all, 接收接口的广播地址
// PACKET_MULTICAST:To group, 该接口已注册的多播地址之一
// PACKET_OTHERHOST:To someone else
// PACKET_OUTGOING:Outgoing of any type, 封包正被发送
// PACKET_LOOPBACK:封包正传送至回环设备(loopback device)
// PACKET_FASTROUTE: 用 Fastroute 功能路由封包。2.6版本内核不再支持 Fastroute
__u8 pkt_type:3,
fclone:2,
ipvs_property:1;
// IP, IPv6, ARP 等,驱动程序使用这个字段通知其上层使用哪个处理例程
__be16 protocol;

// 当此缓冲区属于一个套接字时,通常设为 sock_rfree 或 sock_wfree
void (*destructor)(struct sk_buff *skb);
#ifdef CONFIG_NETFILTER
struct nf_conntrack *nfct;
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct sk_buff *nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
struct nf_bridge_info *nf_bridge;
#endif
__u32 nfmark;
#endif /* CONFIG_NETFILTER */
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd; /* traffic control verdict */
#endif
#endif
#ifdef CONFIG_NET_DMA
dma_cookie_t dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif


/* These elements must be at the end, see alloc_skb() for details. */
// 缓冲区总的大小,包括 sk_buff 结构本身。
// len + sizeof(sk_buff)
unsigned int truesize;
// 引用计数,使用这个 sk_buff 缓冲区的示例的数目
// 有时直接用 atomic_inc 和 atomic_dec 函数递增和递减
// 大多数情况,使用 skb_get 和 kfree_skb
atomic_t users;
unsigned char *head, // 已分配缓冲区空间的开端
*data, // 实际数据的开端
*tail, // 实际数据的尾端
*end; // 已分配缓冲区空间的尾端
};

char cb[48]

  • 私有信息的存储空间,为每一层内部使用起维护的作用

  • 在 sk_buff 结构内静态分配

TCP 使用这个空间存储 tcp_skb_cb 数据结构

1
2
3
4
5
6
7
8
struct tcp_skb_cb {
...
__u32 seq; /* Starting sequence number */
__u32 end_seq; /* SEQ + FIN + SYN + datalen */
__u32 when; /* used to compute rtt's */
__u8 flags; /* TCP header flags. */
...
};
  • 在每一层的代码中通过宏进行访问
1
#define TCP_SKB_CB(__skb)	((struct tcp_skb_cb *)&((__skb)->cb[0]))

TCP 使用 tcp_transmit_skb 函数把一个数据段压入 IP 层以便于传输

管理函数

include/linux/skbuff.hnet/core/skbuff.c 文件中几乎所有函数都有两个版本

  • do_something:wrapper 函数,增加了额外的合理性检查,或在调用第二个函数前后加入上锁机制

  • __do_something

分配内存:alloc_skb

建立一个缓冲区会涉及到两次内存分配

  1. 分配缓冲区
  2. 分配 sk_buff 结构
  • alloc_skb
1
2
3
4
5
6
7
8
9
/* Get the HEAD */
skb = kmem_cache_alloc(cache, gfp_mask & ~__GFP_DMA);
if (!skb)
goto out;

/* Get the DATA. Size must match skb_add_mtu(). */
size = SKB_DATA_ALIGN(size);
data = kmalloc_track_caller(size + sizeof(struct skb_shared_info),
gfp_mask);

分配内存:dev_alloc_skb

  • 由设备驱动程序使用的缓冲区分配函数,应该在中断模式中执行
  • 只是一个包裹 alloc_skb 的函数
  • 要求原子操作
1
2
3
4
5
6
7
8
9
10
11
12
13
static inline struct sk_buff *dev_alloc_skb(unsigned int length)
{
return __dev_alloc_skb(length, GFP_ATOMIC);
}

static inline struct sk_buff *__dev_alloc_skb(unsigned int length,
gfp_t gfp_mask)
{
struct sk_buff *skb = alloc_skb(length + NET_SKB_PAD, gfp_mask);
if (likely(skb))
skb_reserve(skb, NET_SKB_PAD);
return skb;
}

释放内存:kfree_skb

  • 释放一个缓冲区,使其返回缓冲池(缓存)
  • skb->users 计数器为 1 时,才会释放一个缓冲区;否则,递减计数器

释放内存:dev_kfree_skb

1
#define dev_kfree_skb(a)	kfree_skb(a)

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

几种 Ethernet 驱动程序的接收函数,把任何数据存储到刚分配的缓冲区之前,都会使用下列命令:

1
skb_reserve(skb, 2);

当缓冲区往下传播经过网络协议栈时,每个协议都会把 skb->data 往下传,并将其报头拷贝进来,然后更新 skb->len

skb_reserve 函数没有真正把任何东西移入数据缓冲区,只是更新两个指针。

1
2
3
4
5
static inline void skb_reserve(struct sk_buff *skb, int len)
{
skb->data += len;
skb->tail += len;
}

skb_push 把一个数据块添加到缓冲区的开端

skb_put 把一个数据块添加到缓冲区的尾端

skb_pull 通过 head 指针向前移动,把一个数据块从缓冲区的头部删除。

skb_shared_info 结构和 skb_shinfo 函数

如图 2-5 所示,数据缓冲区尾端有个名为 skb_shared_info 的数据结构,用以保持此数据区块的附加信息。

1
2
3
4
5
6
7
8
9
10
11
12
struct skb_shared_info {
// 数据块的用户数目
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;
unsigned int ip6_frag_id;
struct sk_buff *frag_list;
skb_frag_t frags[MAX_SKB_FRAGS];
};
  • skb_is_nonlinear 函数:检查缓冲区是否为片段

  • skb_linearize 函数:把几个片段合压(collapse)成一个均一的(single flat)缓冲区

sk_buff 结构中没有指向 skb_shared_info 数据结构的字段,必须使用返回 end 指针的 skb_shinfo 宏:

1
#define skb_shinfo(SKB)		((struct skb_shared_info *)((SKB)->end))

skb_clone

skb_clone 函数:只拷贝 sk_buff 结构,然后使用引用计数,以免过早释放共享的数据块。

  • skb_buff 的克隆没有链接到任何表(list),也没有引用套接字的拥有者
  • skb->cloned 在克隆和原有的缓冲区都置为 1
  • 克隆的 skb->users 置为 1
  • 对包含数据的缓冲区的引用数目 (dataref) 则会递增

也可用于检查一个 skb 缓冲区的克隆状态。

skb_share_check

检查引用计数 skb_users,当该缓冲区是共享时,可以克隆该缓冲区。

skb_copy && skb_copy

当一个缓冲区被克隆时,数据区块的内容不能修改,访问该数据的代码不需要加锁。

  • 只需修改介于 skb->startskb->end 的区域的数据内容时,可以使用 pskb_copy 只克隆该区域

  • 必须连片段数据区块的内容也修改时,必须使用 skb_copy

列表管理函数

操作 sk_buff 元素列表,列表也称为队列(queue)

skb_queue_head_init:

skb_queue_head, skb_queue_tail

skb_dequeue, skb_dequeue_tail

skb_queue_purge: 把队列变为空队列

skb_queue_walk: 依次循环运行队列中的每个元素

这类函数都必须以原子方式执行,每个函数按照以下方式实现:

1
2
3
4
5
6
7
8
void skb_queue_head(struct sk_buff_head *list, struct sk_buff *newsk)
{
unsigned long flags;

spin_lock_irqsave(&list->lock, flags);
__skb_queue_head(list, newsk);
spin_unlock_irqrestore(&list->lock, flags);
}

net_device 结构

  • 存储特定网络设备的所有信息
  • 每种设备都有一个这种结构,无论真实设备(Ethernet NIC)或虚拟设备(Bonding, VLAN)
  • 所有设备的 net_device 结构放在一个全局变量 dev_base 所指的全局列表中。

标识符

int ifindex

独一无二的 ID,当设备以 dev_new_index 注册时分派给每个设备

由(虚拟)隧道设别使用,可用于标识抵达隧道另一端的真实设备

unsigned short dev_id

IPv6

配置

统计数据

设备状态

列表管理

流量管理

功能专用

通用

函数指针

本章涉及的文件