一. 前言
在前面我们逐一分析了进程间通信的各种方法:信号,管道,共享内存和信号量,本文开始将分析更为复杂也是更为常用的另一套进程间通信:网络通信。网络通信和其他进程间通信最大的区别在于不局限于单机,因此成为了互联网时代的主流选择,无论是分布式、云计算、微服务、容器及自动化运营都离不开网络通信,其重要性可想而知。
经过30多年的发展,网络协议栈已经变得极为复杂,远远不是一两篇文章能够说清楚的东西,所以这里着重剖析我们更为关注的东西:网络编程涉及到的相关协议栈。从本文开始,将分别介绍套接字及其创建、网络连接的建立、网络包的发送、网络包的接收、Netfilter
剖析、select, poll 及 epoll
剖析。除此之外,介于之前有新同学请教TCP的一些基础问题,打算写一篇扩展篇从设计理念的角度出发好好分析TCP协议的方法面面。
二. 套接字结构体
网络协议封装为多层,因此套接字结构体定义也有着多层结构,但是这里有一点要注意的:在网络通信中,我们通过网卡获取到的数据包至少包括了物理层,链路层和网络层的内容,因此套接字结构体仅仅从网络层开始,即通常我们只定义了传输层的套接字socket
和网络层的套接字sock
。socket
是用于负责对上给用户提供接口,并且和文件系统关联。而 sock
负责向下对接内核网络协议栈。
首先看传输层的socket
结构体,这个结构体表征BSD套接字的通用特性。首先是状态state
,用以表示连接情况。type
是套接字类型,如SOCK_STREAM
。wq
是等待队列,在后续文章中会说明。file
是套接字对应的文件指针,毕竟一切皆文件,所以需要统一的文件系统。sock
结构体的sk
变量则为网络层的套接字,ops
是协议相关的一系列套接字操作。
1 | struct socket { |
接着看看网络层,这一层即IP层,该结构体sock
中包含了一个基本结构体sock_common
,整体较为复杂,所以对于其重要变量进行了说明,以注释的形式在每个变量后进行分析。
1 | struct sock { |
sock_common
是套接口在网络层的最小表示,即最基本的网络层套接字信息,具体内容分析见注释。
1 | struct sock_common { |
三. 套接字缓冲区结构体
套接字结构体用于表征一个网络连接对应的本地接口的网络信息,而sk_buff
则是该网络连接对应的数据包的存储。sk_buff
的详细介绍宜参考《Linux网络技术内幕》,专门有一章来描述该结构体。对于我们学习源码来说,最重要的是了解其重点成员变量以及其整体结构。
其源码大致可以分为四部分:
- 布局:方便搜索以及组织结构,主要是一个双向链表用于管理全部的
sk_buff
。每个sk_buff
对应一个数据包,多个sk_buff
以双向链表的形式组合而成。
除此之外还有指向sock
的指针,缓冲区数据块大小,缓冲区及数据边界tail,end,head,data,truesize
- 通用字段:与特定内核无关的字段,主要包括时间戳
tstamp
,网络设备dev
,源设备input_device
,L2-L4层包头对应的mac_header, network_header, transport_header
等。其头部组织结构如下所示
- 功能专用:当编译防火墙(
Netfilter
) 以及QOS
等时才会用到的特殊字段,在此暂时不做详细介绍 - 管理函数:由内核提供的简单的管理工具函数,用于对
sk_buff
元素和元素列表进行操作,如数据预留及对齐函数skb_put(), skb_push(),skb_pull(),skb_reserve()
再比如分配回收函数alloc_skb()
和dev_alloc_skb()
释放内存函数kfree_skb()
和dev_kfree_skb()
除此之外还有克隆,复制等函数,不做过多展开介绍。
sk_buff
的整体填充过程如下图所示:
通过以上学习,对sk_buff
应该有了较为全面系统的了解,其详细源码如下所示,对于重点部分已写明中文注释,其他参见英文注释。
1 | struct sk_buff { |
四. 创建套接字
众所周知我们通过socket()
生成套接字,其系统调用如下,主要调用sock_create()
创建结构体socket
,并通过sock_map_fd()
将其和文件描述符进行绑定。
1 | SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) |
应用层调用socket()
函数会传入三个参数:
family
:表示使用什么IP
层协议。AF_INET
表示IPv4
,AF_INET6
表示IPv6
。这里需要注意的是,我们会常见到AF_INET, AF_PACKET,AF_UNIX
等,AF_UNIX
用于主机内进程间通信,AF_INET
和AF_PACKET
的区别在于前者只能看到IP层以上,而后者可以看到链路层信息,即作用域不同。type
:表示socket
类型。SOCK_STREAM
是面向数据流的,协议IPPROTO_TCP
属于这种类型。SOCK_DGRAM
是面向数据报的,协议IPPROTO_UDP
属于这种类型。如果在内核里面看的话,IPPROTO_ICMP
也属于这种类型。SOCK_RAW
是原始的IP
包,IPPROTO_IP
属于这种类型。protocol
: 表示的协议,包括IPPROTO_TCP
、IPPTOTO_UDP
。
sock_create()
实际调用__sock_create()
。这里首先调用sock_alloc()
分配套接字结构体sock
并赋值类型为type
,接着调用对应的create()
函数按照protocol
对sock
进行填充。
1 | int sock_create(int family, int type, int protocol, struct socket **res) |
sock_alloc()
中我们看到了熟悉的东西:new_inode_pseudo()
,即依照着虚拟文件系统的方式为套接字生成inode
,接着通过SOCKET_I()
获取其对应的socket
,再进行填充。
1 | struct socket *sock_alloc(void) |
inet_create()
主要逻辑如下
- 通过循环
list_for_each_entry_rcu
查看inetsw[sock->type]
,该数组会根据type
找对应的协议号,如果找到了则得到了符合用户指定的family->type->protocol
的struct inet_protosw *answer
对象。 struct socket *sock
的ops
成员变量被赋值为answer
的ops
。对于 TCP 来讲,就是inet_stream_ops
。后面任何用户对于这个socket
的操作都是通过inet_stream_ops
进行的。- 调用
sk_alloc()
创建一个 网络层struct sock *sk
对象并赋值 - 调用
inet_sk()
创建一个struct inet_sock
结构并赋值。上文已说明INET
作用域,而inet_sock
即是对sock
的INET
形式封装,在sock
的基础上增加了很多新的特性。
1 | static int inet_create(struct net *net, struct socket *sock, int protocol, |
inetsw
数组里面的内容是 struct inet_protosw
,对于每个类型的协议均有一项,这一项里面是属于这个类型的协议。inetsw
数组是在系统初始化的时候初始化的,一个循环会将 inetsw
数组的每一项都初始化为一个链表。接下来一个循环将 inetsw_array
注册到 inetsw
数组里面去。
1 | static struct list_head inetsw[SOCK_MAX]; |
至此,套接字的创建就算完成了。对应于虚拟文件系统,描述符fd
保存于进程file
变量的fdtable
中,fd
和该套接字的file
对应,套接字socket sock
和file
通过指针可以互相访问,sock_mnt
为虚拟文件系统sockfs
的挂载点可以通过file
访问。
总结
本文重点分析了套接字这一网络编程中的重要结构体以及其创建函数背后的逻辑,为后文网络编程的源码解析打下基础。
源码资料
[1] socket
[2] sk_buff
[3] socket()
[4] inet_create()
参考资料
[1] wiki
[3] woboq
[4] Linux-insides
[5] 深入理解Linux内核
[6] Linux内核设计的艺术
[7] 极客时间 趣谈Linux操作系统
[8] 深入理解Linux网络技术内幕