主要参考了《深入linux内核架构》和《精通Linux内核网络》相关章节
与网络相关的头文件的数目巨大,使得内核开发者将这些头文件存储到一个专门的目录include/net中
对某些问题来说,划分为7层过于详细了。因此,实际上通常使用另一种参考模型,其中将ISO/OSI模型的一些层合并为新层。该模型只有4层,因此其结构更为简单。这种模型称为TCP/IP参考模型,IP表示Internet Protocol(网际协议),而TCP表示Transmission Control Protocol(传输控制协议)。当今因特网上的大部分通信都是基于该模型的。两个模型的各个层的比较见图12-1。
内核网络子系统的实现与刚介绍的TCP/IP参考模型非常相似。相关的C语言代码划分为不同层次,各层次都有明确定义的任务,各个层次只能通过明确定义的接口与上下紧邻的层次通信。这种做法的好处在于,可以组合使用各种设备、传输机制和协议。
appletalk(AT)是由Apple公司创建的一组网络协议的名称,它用于Apple系列的个人计算机。IPX是指互联分组交换协议:提供分组寻址和选择路径功能,保证可靠到达,相当于数据报的功能;SPX是顺序报文分组交换协议,这可保证信息流按序、可靠地传送。IPX/SPX为Novell在网络层和传输采用的协议;SDLC是SNA中数据链路层协议,后修改为HDLC(高级数据链路控制)。
数据帧(Frame):指起始点和目的点都是数据链路层的信息单元。
数据帧:帧数据由两部分组成:帧头部和帧数据,帧头部包括接收方主机物理地址的定位及其它网络信息,帧数据区含有一个数据体。
数据包(Packet):指起始点和目的地是网络层的信息单元。
数据报(Datagram):指起始点和目的地都使用无连接网络服务的网络层的信息单元。
段(Segment):指起始点和目的地都是传输层的信息单元。
消息(Message):指起始点和目的地都在网络层以上的信息单元(经常在应用层)。
元素(Cell):指的是一种固定长度的信息,它的起始点和目的地都是数据链路层。元素通常用于异步传输模式(ATM)和交换多兆位数据服务(SMDS)网络等交换环境。
数据单元(Data unit):常用的数据单元有服务数据单元(SDU)、协议数据单元(PDU)。
各协议层的数据划分为首部和数据
数据帧(数据链路层):帧数据由两部分组成:帧头部和帧数据,帧头部包括接收方主机物理地址的定位及其它网络信息,帧数据区含有一个数据体。
IP数据体(网络层)由两个部分组成:数据体头部和数据体的数据区,数据体头部包括IP源地址和IP目标地址及其它信息,数据体的数据区包括用户数据协议、传输控制协议,还有数据包及其它信息。

BSD网络软件中包含两个重要的函数:inet_addr,inet_ntoa。**用来在二进制地址格式和点分十进制字符串格式之间转换,仅支持IPv4。**也有两个函数同时支持IPv4和IPv6:inet_ntop,inet_pton。
网络顺序(大端)和主机顺序(一般为小端)
htonl()–“Host to Network Long”
ntohl()–“Network to Host Long”
htons()–“Host to Network Short”
ntohs()–“Network to Host Short”
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx AF_INET - TCP/IP协议族 */
char sa_data[14]; /* 14 bytes of protocol address */
};
include\uapi\linux\in.h
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. 无用的部分*/
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
使用例子
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFLEN 1024
#define SERVER_PORT 8888
#define BACKLOG 5
#define PIDNUMB 3
static void handle_connect(int s_s)
{
int s_c;
struct sockaddr_in from;
socklen_t len = sizeof(from);
while(1)
{
s_c = accept(s_s, (struct sockaddr*)&from, &len);
time_t now;
char buff[BUFFLEN];
int n = 0;
memset(buff, 0, BUFFLEN);
n = recv(s_c, buff, BUFFLEN,0);
if(n > 0 && !strncmp(buff, "TIME", 4))
{
memset(buff, 0, BUFFLEN);
now = time(NULL);
sprintf(buff, "%24s\r\n",ctime(&now));
send(s_c, buff, strlen(buff),0);
}
close(s_c);
}
}
void sig_int(int num)
{
exit(1);
}
int main(int argc, char *argv[])
{
int s_s;
struct sockaddr_in local;
signal(SIGINT, sig_int);
s_s = socket(AF_INET, SOCK_STREAM, 0);
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(SERVER_PORT);
bind(s_s, (struct sockaddr*)&local, sizeof(local));
listen(s_s, BACKLOG);
pid_t pid[PIDNUMB] = {0};
int i =0;
for(i=0;i<PIDNUMB;i++)
{
pid[i] = fork();
if(pid[i] == 0)
{
handle_connect(s_s);
}
}
while(1);
close(s_s);
return 0;
}
内核网络子系统的实现与本章开头介绍的TCP/IP参考模型非常相似。
相关的C语言代码划分为不同层次,各层次都有明确定义的任务,各个层次只能通过明确定义的接口与上下紧邻的层次通信。这种做法的好处在于,可以组合使用各种设备、传输机制和协议。例如,通常的以太网卡不仅可用于建立因特网(IP)连接,还可以在其上传输其他类型的协议,如Appletalk或IPX,而无须对网卡的设备驱动程序做任何类型的修改。

网络子系统是内核中涉及面最广、要求最高的部分之一。为什么是这样呢?答案是,该子系统处理了大量特定于协议的细节和微妙之处,穿越各层的代码路径中有大量的函数指针,而没有直接的函数调用。这是不可避免的,因为各个层次有多种组合方式,这显然不会使代码路径变得更清楚或更易于跟踪。此外,其中涉及的数据结构通常彼此紧密关联。
Linux网络核心架构分为三层:用户空间的应用层,内核空间的网络协议栈层,物理硬件层。其中最重要的核心是内核空间的协议栈层。
在整个栈按照严格分层设计思想可分为五层:系统调用接口层–>协议无关的接口层–>网络协议实现层–>驱动接口层–>驱动程序层。
1. 系统调用接口层,实质是一个面向用户空间应用程序的接口调用库,向用户空间应用程序提供使用网络服务的接口。
2. 协议无关的接口层,就是SOCKET层,这一层的目的是屏蔽底层的不同协议(更准确的来说主要是TCP与UDP,当然还包括RAW IP, SCTP等),以便与系统调用层之间的接口可以简单,统一。简单的说,不管我们应用层使用什么协议,都要通过系统调用接口来建立一个SOCKET,这个SOCKET其实是一个巨大的sock结构,它和下面一层的网络协议层联系起来,屏蔽了不同的网络协议的不同,只吧数据部分呈献给应用层(通过系统调用接口来呈献)。
3. 网络协议实现层,毫无疑问,这是整个协议栈的核心。这一层主要实现各种网络协议,最主要的当然是IP,ICMP,ARP,RARP,TCP,UDP等。这一层包含了很多设计的技巧与算法,相当的不错。
4. 与具体设备无关的驱动接口层,这一层的目的主要是为了统一不同的接口卡的驱动程序与网络协议层的接口,它将各种不同的驱动程序的功能统一抽象为几个特殊的动作,如open,close,init等,这一层可以屏蔽底层不同的驱动程序。
5. 驱动程序层,这一层的目的就很简单了,就是建立与硬件的接口层。
L2 链路层〔例如,Ethernet )
L3 网络层(例如,IP)
L4 传输层(例如,UDP/TCP/ICMP)
BH 下半部( Bottom Half)
IRQ 中断(事件)
RX 接收
TX 传输

图1-2显示了Linux内核网络栈所涉及的3层。其中,L2、L3和L4这三层分别对应于OSI 7层模型中的数据链路层、网络层和传输层。
从本质上说,Linux内核栈的任务就是将接收到的数据包从L2(网络设备驱动程序)传递给L3(网络层,通常为IPv4或IPv6)。接下来,如果数据包目的地为当前设备,Linux内核网络栈就将其传递给L4(传输层,应用TCP或UDP协议侦听套接字);如果数据包需要转发,就将其交还给L2进行传输。对于本地生成的出站数据包,将从L4依次传递给L3和L2,再由网络设备驱动程序进行传输。这个过程分很多阶段,期间可能会发生如下行为。
Linux内核中的数据包 —— sk_buf
在内核分析(收到)网络分组时,底层协议的数据将传递到更高的层。发送数据时顺序相反,各种协议产生的数据(首部和净荷)依次向更低的层传递,直至最终发送。这些操作的速度对网络子系统的性能有决定性的影响,因此内核使用一种特殊的结构,称为套接字缓冲区(socket buffer),具体源码分析如下:
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; // 本网络报文所属的sock结构,此值仅在本机发出的报文中有效,从网络收到的报文此值为空
union {
// 每个SKB都有一个dev成员—-一个net_device结构实例。对于到来的数据包,
// 这个成员表示接收它的网络设备;而对于外出的数据包,这个成员表示发送它的网络设备。
struct net_device *dev;
/* Some protocols might use this space to store information,
* while device pointer would be NULL.
* UDP receive path is one user.
*/
unsigned long dev_scratch;
};
...
unsigned int len, // 有效数据长度
data_len; // 数据长度
__u16 mac_len, // 连接层头部长度,对于以太网,指MAc地址的用的长度,为6(MAc报文的长度)
hdr_len; // skb的可写头部长度(用于clone时,表示clone的skb的头长度)
...
__be16 protocol;
__u16 transport_header; // 传输层头部(指向四层帧头部的指针)
__u16 network_header; // 网络层头部(指向三层IP结构体的指针)
__u16 mac_header; // 链路层头部(指向二层MAC头部的指针)
/* 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; // skb被克隆引用的次数,在内存申请和克隆时会用到
};
套接字缓冲区管理数据的基本思想是,通过操作指针来增删协议首部。
在字长32/64位的系统上,数据类型sk_buff_data_t用来表示各种类型为简单指针的数据,具体结构sk_buff_data_t如下所示:
typedef unsigned char *sk_buff_data_t;
head和end指向数据在内存中的起始和结束位置。
data和tail指向协议数据区域的起始和结束位置。
data不同层指向的不同
当数据包位于网络设备驱动程序接收路径的L2时,skb->data指向的是L2(以太网)报头;调用方法eth_type_trans()后,数据包即将进入第3层,因此skb->data应指向网络层(L3)报头,而这个报头紧跟在以太网报头后面。
mac_header指向MAC协议首部的起始,而network_header和transport_header分别指向网络层和传输层协议首部的起始。

在套接字缓冲区传递到互联网层时,必须增加一个新层。只需要向已经分配但尚未占用的那部分内存空间定稿的数据即可,除了data之外所有的指针都不变,data现在指向IP首部的起始处。对接收分组进行分析过程类似:分组数据复制到内核分配的一个内存区中,并在整个分析期间一直处于该内存区中。与该分组关联的套接字缓冲区在各层之间顺序传递。
对套接字缓冲区的操作
套接字缓冲区需要很多指针来表示缓冲区中内容的不同部分。由于网络子系统必须保证较低的内存占用和较高的处理速度,因而对struct sk_buff来说,我们需要保持该结构的长度尽可能小。在64位CPU上,可使用一点小技巧来节省一些空间。sk_buff_data_t的定义改为整型变量:(data和head仍然是常规的指针,而所有sk_buff_data_t类型的成员现在都解释为相对于前两者的偏移量。这种做法是有效的,因为4字节偏移量足以描述长达4 GiB的内存区,套接字缓冲区不可能超过这个长度。)
#ifdef NET_SKBUFF_DATA_USES_OFFSET
typedef unsigned int sk_buff_data_t;
#else
typedef unsigned char *sk_buff_data_t;
#endif
套接字缓冲区结构不仅包含上述指针,还包括用于处理相关的数据和管理套接字缓冲区自身的其他成员。下面列出的是一些最重要的成员。
使用一个表头来实现套接字缓冲区的等待队列。其结构类型定义如下:
struct sk_buff_head {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
__u32 qlen;
spinlock_t lock;
};
qlen指定了等待队列的长度,即队列中成员的数目。sk_buff_head和sk_buff的next和prev用于创建一个循环双链表,套接字缓冲区的list成员指回到表头。