文章目录
- 1.前言
- 2.Linux内核中连接的组织形式
- 2.1套接字和文件描述符
- 2.2创建连接 & 获取连接
- 3.全连接队列
- 3.1为什么有全连接队列?
- 3.2全连接队列的长度
1.前言
TCP是面向连接的,TCP的各种可靠性机制实际都不是从主机到主机的,而是基于连接的。
比如一台服务器启动后可能有多个客户端前来访问,如果TCP不是基于连接的,也就意味着服务器端只有一个接收缓冲区,此时各个客户端发来的数据都会拷贝到这个接收缓冲区当中,此时这些数据就可能会互相干扰。
而我们在进行TCP通信之前需要先建立连接,就是因为TCP的各种可靠性保证都是基于连接的,要保证传输数据的可靠性的前提就是先建立好连接。
而一台机器上可能会存在大量的连接,此时操作系统就不得不对这些连接进行管理。
- 操作系统在管理这些连接时需要“先描述,再组织”,在操作系统中一定有一个描述连接的结构体,该结构体当中包含了连接的各种属性字段,所有定义出来的连接结构体最终都会以某种数据结构组织起来,此时操作系统对连接的管理就变成了对该数据结构的增删查改。
- 建立连接,实际就是在操作系统中用该结构体定义一个结构体变量,然后填充连接的各种属性字段,最后将其插入到管理连接的数据结构当中即可。
- 断开连接,实际就是将某个连接从管理连接的数据结构当中删除,释放该连接曾经占用的各种资源。
以上都是理论层次的理解,那我们今天就具体地探究Linux源码中连接是如何组织起来的:
2.Linux内核中连接的组织形式
2.1套接字和文件描述符
网络通信本质上也是IO的过程,而且之前我们也说过调用send、recv等函数本质上是向Tcp维护的发送缓冲区、接收缓冲区写入和读出数据,既然是IO操作,所以一个套接字的本质其实就是一个文件描述符对应的文件。Linux下一切皆文件。
一个服务器本质上就是一个进程,而进程到文件的关系我们早已经在系统部分学习过:
当我们创建套接字时,Linux系统还会为我们创建一个新的结构体对象struct socket:
我们观察到这个结构体内部包含了一个struct file类型的指针,该指针指向的就是该套接字对应的文件对象,所以我们此时可以通过该套接字找到对应的文件了,但是更重要的是我们需要通过文件描述符找到对应的套接字呢呀,现在只有一个单向的指针,即我现在需要从文件找到对应的套接字对象。
所以在struct file结构体中还包含一个指针:
这个指针指向的就是套接字socket结构,所以我们现在就可以通过该文件描述符完成对套接字的操作了(读取数据、获取连接等)。
2.2创建连接 & 获取连接
我们上面提到过连接本质上是内核中的一种数据结构,在Linux中实际就是struct tcp_sock
结构体,该结构体专门用于TCP协议。
它包含了TCP协议特有的字段和方法,如TCP头部长度(tcp_header_len)、滑动窗口(rcv_wnd、snd_wnd)、拥塞控制算法相关字段(如srtt_us、mdev_us等)以及发送和接收队列等。
这个结构体是TCP连接在内核中的完整表示,包含了TCP协议运行所需的所有状态信息和控制逻辑。
struct tcp_sock
结构体的第一个字段是struct inet_connection_sock
结构体,该结构体增加了与连接管理相关的字段,如连接状态(icsk_state)、重传机制等。这个结构体为TCP连接提供了必要的状态管理和控制机制。
struct inet_connection_sock
结构体的第一个字段是struct inet_sock
结构体,该结构体增加了与IP层相关的字段和方法,如IP地址(sin_addr或sin6_addr)、端口号(sin_port或sin6_port)等。这个结构体为TCP和UDP等基于IP的协议提供了更具体的支持。
struct inet_sock
结构体的第一个字段是struct sock
结构体,该结构体包含了如套接字状态(state)、接收和发送缓冲区(sk_buff)队列、定时器(timer)等通用字段。
更重要的是你会发现与文件描述符相关的struct socket结构体中有一个字段就是struct sock
类型的指针 sk:
所以socket套接字可以通过这个 sk指针 获取tcp_sock
结构体中的所有字段内容(通过类型转换)。
比如想要获取
tcp_sock
结构体中inet_connection_sock
结构体中的字段内容,就可以将sk指针转换成inet_connection_sock
类型获取。这种通过一个指针获取不同结构体中属性的方式被称为“C风格的多态”。
以上是Tcp连接,如果是Udp连接呢?我们说Udp是无连接的通信协议,所以对于Udp来说没有struct inet_connection_sock
结构体,因为该结构体内部维护的是与连接管理的相关字段,但是同样的根据socket
结构体中的 sk指针 指向udp_sock
结构体来获取udp连接的各种属性内容。所以 该socket结构体 被称为 “BSD socket ”— 通用socket接口。
既然socket结构体既可以指向Tcp套接字又可以指向Udp套接字,那是如何区分不同套接字类型的呢?
int socket(int domain, int type, int protocol);
参数type对应着socket结构体中的type字段:
所以创建一个listen套接字的流程就是申请文件描述符获得文件结构体,创建套接字socket和连接tcp_sock,然后将他们关联起来。
那么调用accept()函数是从listen套接字监听的套接字中获取普通连接并返回,这个过程又是怎样的呢?
实际上在struct inet_connection_sock
结构体中维护一个全连接队列,当经历过三次握手后,系统会自动创建一个连接tcp_sock,然后将该连接加入到全连接队列中,当调用accept()函数时,操作系统会申请新的文件描述符和套接字socket,然后从全连接队列中取出一个连接tcp_sock,之后普通套接字socket中的 sk指针 指向该连接tcp_sock,就完成了获取连接的操作。
3.全连接队列
实际TCP在进行连接管理时会用到两个连接队列:
- 全连接队列(accept队列)。全连接队列用于保存处于ESTABLISHED状态,但没有被上层调用accept取走的连接。
- 半连接队列。半连接队列用于保存处于SYN_SENT和SYN_RCVD状态的连接,也就是还未完成三次握手的连接,维护时间比较短。
而全连接队列的长度实际会受到listen第二个参数的影响,一般TCP全连接队列的长度就等于listen第二个参数backlog
的值加一。
int listen(int sockfd, int backlog);
如果将listen的第二个参数值设置为3,此时服务器端最多就允许存在4个处于ESTABLISHED状态的连接。
在服务器端已经有4个ESTABLISHED状态的连接的情况下,再有客户端发来建立连接请求,此时服务器端就会新增状态为SYN_RCVD的连接,该连接实际就是放在半连接队列当中的。
3.1为什么有全连接队列?
一般当服务器压力较大时连接队列的作用才会体现出来,如果服务器压力本身就不大,那么一旦底层有连接建立成功,上层就会立马将该连接读走并进行处理。
服务器端启动时一般会预先创建多个服务线程为客户端提供服务,主线程从底层accept上来连接后就可以将其交给这些服务线程进行处理。
如果向服务器发起连接请求的客户端很少,那么连接一旦在底层建立好就被主线程立马accept上来并交给服务线程处理了。
但如果向服务器发起连接请求的客户端非常多并且业务处理非常繁忙,即当每个服务线程都在为某个连接提供服务时,底层再建立好连接主线程就不能获取上来了,此时底层这些已经建立好的连接就会被放到连接队列当中,只有等某个服务线程空闲时,主线程就会从这个连接队列当中获取建立好的连接。
如果没有这个连接队列,那么当服务器端的服务线程都在提供服务时,其他客户端发来的连接请求就会直接被拒绝。
但有可能正当这个连接请求被拒绝时,某个服务线程提供服务完毕,此时这个服务线程就无法立马得到一个连接为之提供服务,所以一定有一段时间内这个服务线程是处于闲置状态的,直到再有客户端发来连接请求。
而如果设置了连接队列,当某个服务线程提供完服务后,如果连接队列当中有建立好的连接,那么主线程就可以立马从连接队列当中获取一个连接交给该服务线程进行处理,此时就可以保证服务器几乎是满载工作的,降低了服务器的闲置率。
3.2全连接队列的长度
虽然维护连接队列能让服务器处于几乎满载工作的状态,但连接队列也不能设置得太长。
- 如果队列太长,也就意味着在队列较尾部的连接需要等待较长时间才能得到服务,此时客户端的请求也就迟迟得不到响应。
- 此外,服务器维护连接也是需要成本的,连接队列设置的越长,系统就要花费越多的成本去维护这个队列。
- 但与其与其维护一个长连接,造成客户端等待过久,并且占用大量暂时用不到的资源,还不如将部分资源节省出来给服务器使用,让服务器更快的为客户端提供服务。
所以全连接队列要取一个合适的长度,系统一般设置为5。
全连接队列的长度=min(backlog,net.core.somaxconn)+1:
- 用户层调用listen时传入的第二个参数backlog。
- 系统变量net.core.somaxconn,在 Linux 系统中,这个值默认可能因不同的发行版和内核版本而异,但常见的默认值可能是 128。然而,对于高负载的服务器,特别是在处理大量并发连接时,这个默认值可能太低,导致新的连接被拒绝(因为监听队列已满)。
通过以下命令可以查看系统变量net.core.somaxconn的值。
sudo sysctl -a | grep net.core.somaxconn
Stay hungry, Stay foolish. —史蒂夫-乔布斯