一、Linux内核的组织形式
1.1 描述“连接”的结构
TCP协议的特点是面向连接,一个服务端可能会被多个客户端连接,那这些连接也一定会被操作系统组织起来,接下来我们谈一谈在Linux内核中是如何管理这些连接的。
既然要管理这些连接,首先就需要 “ 先描述在组织 ” ,即这些连接的本质都是结构体字段。在Linux中对于TCP协议就是 struct tcp_sock 结构体,该结构体是在TCP连接建立时创建的。在接收到一个有效的SYN包并准备发送SYN-ACK包时,内核会创建一个新的 struct tcp_sock 实例来存储与该连接相关的所有信息。该结构体还包含了实现TCP协议各种功能所需的字段和算法,包括连接的建立、数据的传输、连接的关闭等。同时,struct tcp_sock 还提供了与TCP流量控制和拥塞控制相关的字段和算法。
struct tcp_sock 结构体的第一个成员是 struct inet_connection_sock 结构体,这个结构体包含了管理面向连接协议(如 TCP)所需的各种字段。用于处理连接的建立、维护、关闭等过程,以及实现协议的各种特性。
而 struct inet_connection_sock 结构体的第一个成员是 struct inet_sock ,它提供了管理套接字所需的所有基本结构和功能,包括端口的绑定、IP地址的分配、套接字选项的处理以及路由信息的缓存等。如 inet_dport 和 inet_sport分别表示目的端口和源端口,inet_daddr 和 inet_saddr分别表示目的 IP 地址和源 IP 地址
struct inet_sock 结构体的第一个字段是 struct sock ,它作为套接字在内核中的表示,存储了套接字的各种关键信息和状态。通过操作这个结构体,内核能够管理套接字的生命周期,处理数据的发送和接收,以及实现协议的各种特性。
上述所有的结构体就是OS所描述的连接,但是我们在写网络通信代码时,首先会通过socket函数返回一个套接字,后续的通讯都是向这个套接字中输入输出信息,那套接字和这些所谓“连接”的结构体怎么联系起来呢?
1.2 网络套接字与文件描述符的关系
网络通信的本质就是主机的进程之间的通信,而通信的本质就是IO,所以套接字的本质也是一个文件描述符描述的文件struct file
当我们创建一个套接字时,系统会创建一个 struct socket 结构体对象,而这个结构体内部有一个struct file* 类型的指针,有了这个指针我们就可以通过套接字来找到对应的文件了。
但是我们更重要的是通过文件描述符来找到套接字,而struct file中也存在存在一个指针,这个指针就是指向struct socket结构体的,所以我们可以通过fd来找到对应的struct file,在通过这个指针找到struct socket对象。
在struct socket结构体中其实还存在一个struct sock*的指针,这个指针是指向tcp_sock结构体的第一个对象,这样文件描述符与套接字结构体及描述连接的结构体都联系在一起了。
此时有人就疑惑了,struct sock*的指针为什么就可以访问整个tcp_sock结构体呢?这种形式是Linux内核中常用的,属于C风格的多态,可以通过将这个指针强转为不同的类型,就可以访问到整个结构体所有的属性。整体结构如下图所示:
上述是TCP在内核中的组织形式,那对于UDP怎么组织呢?
在内核中,UDP的组织形式与TCP是共用一套的,由于UDP协议是不面向连接的,所以他相比于TCP少了一层,没有 struct inet_connection_sock 结构体。可以通过struct socket结构体中的struct sock*指针指向udp_sock对象来获取对应的udp信息,所以struct socket结构体也被称为
“BSD socket ”—> 通用socket接口。
在内核中,这个指针既可以指向tcp_sock也可以指向udp_sock,那系统是怎么区别是哪种协议呢?在socket结构体中存在一个描述属性的字段
int socket(int domain, int type, int protocol);
所以创建一个listen套接字的流程就是申请文件描述符获得文件结构体,创建套接字socket和连接tcp_sock,然后将他们关联起来。
那么调用accept()函数是从listen套接字监听的套接字中获取普通连接并返回,这个过程又是怎样的呢?
实际上在struct inet_connection_sock结构体中维护一个全连接队列,当经历过三次握手后,系统会自动创建一个连接tcp_sock,然后将该连接加入到全连接队列中,当调用accept()函数时,操作系统会申请新的文件描述符和套接字socket,然后从全连接队列中取出一个连接tcp_sock,之后普通套接字socket中的 sk指针 指向该连接tcp_sock,就完成了获取连接的操作。
二、全连接队列
2.1全连接与半连接
-
全连接:
- 指的是TCP连接已经成功建立的状态,即已经完成了三次握手过程,客户端和服务器之间可以开始传输数据。
- 在Linux系统中,全连接队列用于存储已经成功建立连接但尚未被应用程序接受(accept)的TCP连接。
-
半连接:
- 指的是TCP连接建立过程中的一个中间状态,即服务器已经收到了客户端的SYN报文,并发送了SYN-ACK报文,但尚未收到客户端的ACK报文确认。
- 在这个状态下,连接还没有完全建立,服务器需要等待客户端的ACK报文来完成三次握手过程。
- 在Linux系统中,半连接队列用于存储处于半连接状态的TCP连接。
全连接队列的长度实际会受到listen第二个参数的影响,一般TCP全连接队列的长度就等于listen第二个参数backlog
的值加1。
int listen(int sockfd, int backlog);
2.2 全连接的意义
- 防止连接丢失:当服务器的并发连接请求超过其处理能力时,全连接队列可以暂存已经建立但尚未被应用程序接受的连接。这避免了连接请求被丢失,从而提高了服务器的稳定性和可靠性。
- 提高资源利用率:通过有效地管理全连接队列,服务器可以更加高效地利用系统资源。例如,当队列中的连接数量较少时,服务器可以释放部分资源以供其他任务使用;而当队列中的连接数量增加时,服务器可以动态地增加资源分配以满足需求。
- 优化网络性能:全连接队列的存在使得服务器能够更加灵活地处理网络流量。当网络流量较大时,服务器可以通过增加全连接队列的长度来容纳更多的并发连接请求;而当网络流量较小时,则可以减小队列长度以节省资源。
2.3 全连接的长度
在实际应用中,需要根据服务器的性能和需求来设置全连接队列的长度。
- 如果队列长度设置得太短,可能会导致连接请求被拒绝或超时
- 而如果设置得太长,则可能会浪费系统资源并降低性能。
- 所以全连接队列要取一个合适的长度,系统一般设置为5。
全连接队列的长度由两个参数共同决定:
- backlog参数:这是应用程序在调用listen函数时指定的参数,它表示全连接队列的最大长度。然而,这个值并不是最终的全连接队列长度,因为它还需要与另一个参数进行比较。
- somaxconn参数:这是系统级别的参数,通常可以在Linux系统的/proc/sys/net/core/somaxconn文件中找到。它表示系统允许的最大socket连接数,也可以理解为全连接队列的一个上限值。
最终的全连接队列长度是这两个参数中的较小值。也就是说,如果backlog参数的值大于somaxconn参数的值,那么全连接队列的长度将被限制为somaxconn的值;反之,如果backlog参数的值小于或等于somaxconn参数的值,那么全连接队列的长度将被限制为backlog的值。
在Linux系统中,somaxconn参数的默认值通常为128,但可以通过修改/etc/sysctl.conf文件或使用sysctl命令来更改这个值。例如,要将其更改为4096,可以在/etc/sysctl.conf文件中添加或修改以下行:
net.core.somaxconn = 4096