进程虚拟地址空间 ======= | 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | | ------- |:-------:|:-------:|:-------:|:-------:|:-------:| | 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | #1 管理套接字缓冲区数据 ------- 在内核分析(收到的)网络分组时, 底层协议的数据将传递到更高的层. 发送数据时顺序相反, 各种协议产生的数据(首部和净荷)依次向更低的层传递, 直至最终发送. 这些操作的速度对网络子系统的性能有决定性的影响, 因此内核使用了一种特殊的结构,称为套接字缓冲区(socket buffer). #2 套接字缓冲区结构体sk_buff ------- 套接字缓冲区(socket buffer)用struct sk_buff结构来表示, 定义在[`include/linux/skbuff.h?v=4.7, line 626`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L626) ```cpp struct sk_buff { union { struct { /* These two members must be first. */ struct sk_buff *next; struct sk_buff *prev; union { ktime_t tstamp; struct skb_mstamp skb_mstamp; }; }; struct rb_node rbnode; /* used in netem & tcp stack */ }; struct sock *sk; struct net_device *dev; /* * 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] __aligned(8); unsigned long _skb_refdst; void (*destructor)(struct sk_buff *skb); #ifdef CONFIG_XFRM struct sec_path *sp; #endif #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) struct nf_conntrack *nfct; #endif #if IS_ENABLED(CONFIG_BRIDGE_NETFILTER) struct nf_bridge_info *nf_bridge; #endif unsigned int len, data_len; __u16 mac_len, hdr_len; /* Following fields are _not_ copied in __copy_skb_header() * Note that queue_mapping is here mostly to fill a hole. */ kmemcheck_bitfield_begin(flags1); __u16 queue_mapping; __u8 cloned:1, nohdr:1, fclone:2, peeked:1, head_frag:1, xmit_more:1; /* one bit hole */ kmemcheck_bitfield_end(flags1); /* fields enclosed in headers_start/headers_end are copied * using a single memcpy() in __copy_skb_header() */ /* private: */ __u32 headers_start[0]; /* public: */ /* if you move pkt_type around you also must adapt those constants */ #ifdef __BIG_ENDIAN_BITFIELD #define PKT_TYPE_MAX (7 << 5) #else #define PKT_TYPE_MAX 7 #endif #define PKT_TYPE_OFFSET() offsetof(struct sk_buff, __pkt_type_offset) __u8 __pkt_type_offset[0]; __u8 pkt_type:3; __u8 pfmemalloc:1; __u8 ignore_df:1; __u8 nfctinfo:3; __u8 nf_trace:1; __u8 ip_summed:2; __u8 ooo_okay:1; __u8 l4_hash:1; __u8 sw_hash:1; __u8 wifi_acked_valid:1; __u8 wifi_acked:1; __u8 no_fcs:1; /* Indicates the inner headers are valid in the skbuff. */ __u8 encapsulation:1; __u8 encap_hdr_csum:1; __u8 csum_valid:1; __u8 csum_complete_sw:1; __u8 csum_level:2; __u8 csum_bad:1; #ifdef CONFIG_IPV6_NDISC_NODETYPE __u8 ndisc_nodetype:2; #endif __u8 ipvs_property:1; __u8 inner_protocol_type:1; __u8 remcsum_offload:1; /* 3 or 5 bit hole */ #ifdef CONFIG_NET_SCHED __u16 tc_index; /* traffic control index */ #ifdef CONFIG_NET_CLS_ACT __u16 tc_verd; /* traffic control verdict */ #endif #endif union { __wsum csum; struct { __u16 csum_start; __u16 csum_offset; }; }; __u32 priority; int skb_iif; __u32 hash; __be16 vlan_proto; __u16 vlan_tci; #if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS) union { unsigned int napi_id; unsigned int sender_cpu; }; #endif union { #ifdef CONFIG_NETWORK_SECMARK __u32 secmark; #endif #ifdef CONFIG_NET_SWITCHDEV __u32 offload_fwd_mark; #endif }; union { __u32 mark; __u32 reserved_tailroom; }; union { __be16 inner_protocol; __u8 inner_ipproto; }; __u16 inner_transport_header; __u16 inner_network_header; __u16 inner_mac_header; __be16 protocol; __u16 transport_header; __u16 network_header; __u16 mac_header; /* private: */ __u32 headers_end[0]; /* public: */ /* These elements must be at the end, see alloc_skb() for details. */ sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head, *data; unsigned int truesize; atomic_t users; }; ``` 套接字缓冲区用于在网络实现的各个层次之间交换数据, 而无须来回复制分组数据, 对性能的提高很可观. 套接字结构是网络子系统的基石之一, 因为在产生和分析分组时,在各个协议层次上都需要处理该结构. #3 使用套接字缓冲区管理数据 ------- 套接字缓冲区通过其中包含的各种指针与一个内存区域相关联, 网络分组的数据就位于该区域中. 假定我们使用的是32位系统(在64位机器上, 套接字缓冲区的组织稍有不同, 读者稍后就会看到). ##3.1 sk_buff的主要数据字段 ------- ```cpp struct sk_buff { /* ...... */ __u16 inner_transport_header; __u16 inner_network_header; __u16 inner_mac_header; __be16 protocol; __u16 transport_header; __u16 network_header; __u16 mac_header; /* private: */ __u32 headers_end[0]; /* public: */ /* These elements must be at the end, see alloc_skb() for details. * 这些成员必须在末尾,详见alloc_skb() */ sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head, *data; unsigned int truesize; atomic_t users; }; ``` ![套接字缓冲区和网络分组数据之间的关联](../images/layout.png) | 字段 | 描述 | |:-----:|:-----:| |`mac_header` | 指向MAC协议首部的起始. | | `network_header` | 指向网络层协议首部的起始. | | `transport_header` | 指向传输层协议首部的起始 | ##3.2 缓冲区的布局信息 ------- 套接字缓冲区的基本思想是, 通过操作指针来增删协议首部. | 字段 | 描述 | |:-----:|:-----:| | `head`和`end` |指向数据在内存中的起始和结束位置 | | `data`和`tail` | 指向协议数据区域的起始和结束位置 | >这个区域可能大于实际需要的长度,因为在产生分组时,尚不清楚分组的长度. ![套接字缓冲区在各个协议层之间传递时,对缓冲区的操作](../images/trans_data.png) 内核实现了一些宏用来获取缓冲区数据段的结束位置. 这些函数定义在[`include/linux/skbuff.h?v=4.7, line 1165`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L1165) ```cpp #ifdef NET_SKBUFF_DATA_USES_OFFSET static inline unsigned char *skb_end_pointer(const struct sk_buff *skb) { return skb->head + skb->end; } static inline unsigned int skb_end_offset(const struct sk_buff *skb) { return skb->end; } #else static inline unsigned char *skb_end_pointer(const struct sk_buff *skb) { return skb->end; } static inline unsigned int skb_end_offset(const struct sk_buff *skb) { return skb->end - skb->head; } #endif ``` 内核实现了一些宏用来获取协议数据区域的结束的结束位置tail的函数. 这些函数定义在[`include/linux/skbuff.h?v=4.7, line 1849`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L1849) ```cpp #ifdef NET_SKBUFF_DATA_USES_OFFSET /* 64 bit */ static inline unsigned char *skb_tail_pointer(const struct sk_buff *skb) { return skb->head + skb->tail; } static inline void skb_reset_tail_pointer(struct sk_buff *skb) { skb->tail = skb->data - skb->head; } static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset) { skb_reset_tail_pointer(skb); skb->tail += offset; } #else /* NET_SKBUFF_DATA_USES_OFFSET */ static inline unsigned char *skb_tail_pointer(const struct sk_buff *skb) { return skb->tail; } static inline void skb_reset_tail_pointer(struct sk_buff *skb) { skb->tail = skb->data; } static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset) { skb->tail = skb->data + offset; } #endif /* NET_SKBUFF_DATA_USES_OFFSET */ ``` 在字长32位的系统上,数据类型sk_buff_data_t用来表示各种类型为简单指针的数据(`char *`), 定义在[`include/linux/skbuff.h?v=4.7, line 487`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L487), 在64位 CPU上, 可使用一点小技巧来节省一些空间. `sk_buff_data_t`的定义改为整型变量 ```cpp #if BITS_PER_LONG > 32 /* 64位的linux系统中long是8个字节 > 4 */ #define NET_SKBUFF_DATA_USES_OFFSET 1 #endif #ifdef NET_SKBUFF_DATA_USES_OFFSET /* 64位系统 */ typedef unsigned int sk_buff_data_t; /* 4个字节 */ #else /* 32位系统 */ typedef unsigned char *sk_buff_data_t; /* 4个字节 */ #endif ``` 套接字缓冲区需要很多指针来表示缓冲区中内容的不同部分. 由于网络子系统必须保证较低的内存占用和较高的处理速度, 因而对`struct sk_buff`来说,我们需要保持该结构的长度尽可能小. ##3.3 通过偏移计算地址 ------- `data`和`head`是常规的指针, 而所有`sk_buff_data_t`类型的成员现在都解释为相对于前两者的偏移量. 这使得内核可以将套接字缓冲区用于所有协议类型。正确地解释数据需要做简单的类型转换, 为此提供了几个辅助函数. 例如,套接字缓冲区可以包含TCP或UDP分组. 来自传输层协议首部的对应信息分别可以用tcp_hdr和udp_hdr提取. 这两个函数都将原始指针(raw pointer)转换为某种适当的数据类型。其他传输层协议也提供了形如XXX_hdr的辅助函数, 这类函数需要一个指向`struct sk_buff`的指针作为参数, 并返回重新解释的传输首部数据. 例如, 观察如何从套接字缓冲区获取TCP首部, 该功能由tcp_hdr函数来完成. 定义在[include/linux/tcp.h?v=4.7, line 27](http://lxr.free-electrons.com/source/include/linux/tcp.h?v=4.7#L27) >在64位处理器的体系结构上, 整型变量占用的内存只有指针变量的一半(前者是4字节, 后者是8字节), 该结构的长度缩减了20字节, 但套接字缓冲区中包含的信息仍然是同样的 ```cpp static inline struct tcphdr *tcp_hdr(const struct sk_buff *skb) { return (struct tcphdr *)skb_transport_header(skb); } ``` `struct tcphdr`是一个结构, 包含了TCP首部中的所有字段. 定义在[`include/uapi/linux/tcp.h?v=4.7, line 24`](http://lxr.free-electrons.com/source/include/uapi/linux/tcp.h?v=4.7#L24) `data`和`tail`使得在不同协议层之间传递数据时, 无须显式的复制操作 指向传输层首部的指针现在计算如下, 定义在[include/linux/skbuff.h?v=4.7, line 2109](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L2109) ```cpp static inline unsigned char *skb_transport_header(const struct sk_buff *skb) { return skb->head + skb->transport_header; } ``` 这种做法是有效的, 因为4字节偏移量足以描述长达4GB的内存区, 套接字缓冲区不可能超过这个长度. 还有其他类似的转换函数供网络子系统使用. 对我们来说, `ip_hdr`是最重要的,它用于解释一个IP分组的内容. 定义在[`include/linux/ip.h?v=4.7, line 23`](http://lxr.free-electrons.com/source/include/linux/ip.h?v=4.7#L23) 由于假定套接字缓冲区的内部表示对通用网络代码是不可见的, 所以提供了如下几个辅助函数来访问`struct sk_buff`的成员. 这些函数都定义在[`include/linux/skbuff.h?v=4.7`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7)中,编译时会自动选择其中适当的变体使用. | 函数 | 描述 | |:-----:|:-----:| | skb_transport_header(const struct sk_buff *skb) | 从给定的套接字缓冲区获取传输层首部的地址. | | skb_reset_transport_header(struct sk_buff *skb) | 将传输层首部重置为数据部分的起始位置. | | skb_set_transport_header(struct sk_buff *skb, const int offset) | 根据数据部分中给定的偏移量来设置传输层首部的起始地址 | 对MAC层和网络层首部来说,也有同样一组函数可用,只需将transport分别替换为mac或network即可. 这些函数均定义在[`include/linux/skbuff.h?v=4.7`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7) ##3.3. 分组的产生于接收 ------- 在一个**新分组产生**时, TCP层首先在用户空间中分配内存来容纳该分组数据(首部和净荷). 分配的空间大于数据实际需要的长度,因此较低的协议层可以进一步增加首部. 分配一个套接字缓冲区, 使得head和end分别指向上述内存区的起始和结束地址,而TCP数据位于`data`和`tail`之间 在套接字缓冲区传递到互联网络层时, 必须增加一个新层. 只需要向已经分配但尚未占用的那部分内存空间写入数据即可, 除了`data`之外所有的指针都不变,`data`现在指向IP首部的起始处. 下面的各层会重复同样的操作,直至分组完成,即将通过网络发送. 对**接收的分组进行分析的过程**是类似的. 分组数据复制到内核分配的一个内存区中, 并在整个分析期间一直处于该内存区中. 与该分组相关联的套接字缓冲区在各层之间顺序传递, 各层依次将其中的各个指针设置为正确值. 内核提供了一些用于操作套接字缓冲区的标准函数 | 函数 | 语 义 | |:-----:|:------:| | alloc_skb | 分配一个新的sk_buff实例 | | skb_copy | 创建套接字缓冲区和相关数据的一个副本 | | skb_clone | 创建套接字缓冲区的一个副本,但原本和副本将使用同一分组数据 | | skb_tailroom | 返回数据末端空闲空间的长度 | | skb_headroom | 返回数据起始处空闲空间的长度 | | skb_realloc_headroom | 在数据起始处创建更多的空闲空间. 现存数据不变 | #4 管理套接字缓冲区数据 ------- 套接字缓冲区结构不仅包含上述指针,还包括用于处理相关的数据和管理套接字缓冲区自身的其他成员. 其中不常见的成员在本章中遇到时才会讨论。下面列出的是一些最重要的成员 ##4.1 套接字缓冲区的部分字段 ------- ```cpp struct sk_buff { union { struct { /* These two members must be first. */ struct sk_buff *next; struct sk_buff *prev; union { ktime_t tstamp; struct skb_mstamp skb_mstamp; }; }; struct rb_node rbnode; /* used in netem & tcp stack */ struct sock *sk; struct net_device *dev; /* ...... */ int skb_iif; }; ``` | 字段 | 描述 | |:-----:|:-----:| | tstamp | 保存了分组到达的时间 | | dev | 指定了处理分组的网络设备. `dev`在处理分组的过程中可能会改变, 例如, 在未来某个时候, 分组可能通过计算机的另一个设备发出 | | skb_iif | 输入设备的接口索引号总是保存在iif中 | | sk | 是一个指针,指向用于处理该分组的套接字对应的socket实例 | | dst |表示接下来该分组通过内核网络实现的路由。这里使用了一个特殊的格式 | | next和prev | 用于将套接字缓冲区保存到一个双链表中. 这里没有使用内核的标准链表实现, 而是使用了一个手工实现的版本 | | rbnode | 将套接字缓冲区保存在红黑树中, 用于tcp的栈中, 这里使用了内核标准的红黑树结构 | ##4.2 套接字缓冲区的头节点sk_buff_head ------- 我们知道套接字缓冲区sk_buff通过其next和prev指针域, 将所有的缓冲区保存在了一个双向链表中, 内核并没有使用标准的内核list数据结构, 而是对其进行了进一步的封装, 设计了一个带头节点的双向链表, 使用了一个表头`sk_buff_head`来实现套接字缓冲区的等待队列. 其结构定义如下 ```cpp struct sk_buff_head { /* These two members must be first. * 这两个成员必须在最前面 */ struct sk_buff *next; struct sk_buff *prev; __u32 qlen; spinlock_t lock; }; ``` >为什么需要设计一个单独的表头(头节点)呢? > >* 带头节点的双向链表在插入不需要特殊的处理. > >* 我们需要保存缓冲区的数目length, 这样设计一个带length和标识的头节点更灵活. > >* 带头节点的缓冲区更符合面向对象的思想. | 字段 | 描述 | |:-----:|:-----:| | qlen |指定了等待队列的长度,即队列中成员的数目 | | next和prev | 用于创建一个循环双链表,套接字缓冲区的list成员指回到表头 | lock | 双向链表的锁 | 分组通常放置在等待队列中,例如分组等待处理时,或需要重新组合已经分析过的分组时. ![通过双链表管理套接字缓冲区](../images/dou_link.png) ##4.3.3 skb_shared_info结构体 ------- 内核`skb_shared_info`结构体该类型用来管理数据包分片信息, 该结构定义在[`include/linux/skbuff.h?v=4.7, line 408`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L408) ```cpp /* This data is invariant across clones and lives at * the end of the header data, ie. at skb->end. */ struct skb_shared_info { unsigned char nr_frags; __u8 tx_flags; unsigned short gso_size; /* 尺寸 */ /* Warning: this field is not always filled in (UFO)! */ unsigned short gso_segs; /* 顺序 */ unsigned short gso_type; struct sk_buff *frag_list; struct skb_shared_hwtstamps hwtstamps; /* 硬件时间戳 */ u32 tskey; __be32 ip6_frag_id; /* * Warning : all fields before dataref are cleared in __alloc_skb() */ atomic_t dataref; /* 使用计数 */ /* Intermediate layers must ensure that destructor_arg * remains valid until skb destructor */ void * destructor_arg; /* must be last field, see pskb_expand_head() */ skb_frag_t frags[MAX_SKB_FRAGS]; }; ``` 通过宏可以表示与skb的关系 ```cpp /* Internal */ #define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB))) ```