TCP 全连接队列
理解 listen 的第二个参数
int listen(int sockfd, int backlog);
backlog
参数表示 全连接队列(accept 队列)的最大长度。
那什么是全连接队列呢?
三次握手 &
accept()
处理流程
- 客户端发送 SYN,服务器收到并放入半连接队列。
- 服务器回 SYN+ACK,客户端收到后发送 ACK。
- 服务器收到 ACK,握手完成,连接进入全连接队列等待
accept()
处理。- 服务器调用
accept()
取出连接,建立 TCP 连接。
也就是说我们访问服务器会由内核自动进行3次握手,完成后会连入全连接队列中,服务端再调用accept(),从队列中获取连接进行处理。而listen的第二个参数就是全连接队列的最大连接长度,如果服务器来不及调用accept()处理连接,连接会堆积在全连接队列。超过最大连接长度,再进行连接就会三次握手失败。
所以全连接队列本质就是一种生产消费者模型,
backlog
不能太长也不能太短。1.太短,可能丢失大量连接(客户端需要重试,增加网络负担)。
2.太长,一方面会让用户等待过长的时间,另一方面会占用大量内存。
Linux 内核 TCP socket
先看一下从进程的文件描述符到 TCP socket的整体结构
1. 任务(task_struct)到文件描述符表
- 进程的
task_struct
结构体包含files_struct
,用于管理进程的文件表。files_struct
中有一个fd_array[]
(文件描述符数组),存储着所有打开的文件(struct file
指针)。
2. 文件结构(struct file)
struct file
代表进程打开的一个文件,可能是普通文件、设备文件或者套接字(socket)。- 其中
private_data
用于存储 socket 相关信息。- 当文件描述符指向 socket 时,它的
private_data
指向struct socket
结构体。
3. socket 结构(struct socket)
struct socket
代表一个高层的 socket 抽象,内部包含:
struct file *file
:指向与 socket 关联的文件结构。struct sock *sk
:指向具体的sock
结构体,代表底层协议相关的数据结构。struct socket {socket_state state; // socket 当前状态,如 SS_CONNECTEDshort type; // socket 类型,如 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP)unsigned long flags; // socket 标志struct file *file; // 关联的 struct file 结构体struct sock *sk; // 关联的 struct sock 结构体(底层协议数据)const struct proto_ops *ops; // 指向 socket 操作函数 };
4. sock 结构(struct sock)
struct sock
是底层 socket 内部的核心结构,存储协议无关的信息。struct sock
可以指向不同的协议(如 TCP、UDP、RAW)扩展成更具体的协议结构。
5. TCP Socket结构
从高层到底层,TCP socket 主要涉及以下结构:
- struct sock(通用 socket 结构)
- struct inet_sock(IP 层 socket)
- struct inet_connection_sock(面向连接的 socket)
- struct tcp_sock(TCP 连接的具体实现)
1
struct sock
(核心 socket 结构)
struct sock
是 Linux 内核中最基础的 socket 结构,几乎所有的协议(TCP、UDP、RAW)都会用到它。它存储了 socket 相关的通用数据,例如:struct sock {struct socket *sk_socket; // 关联的 socket 结构struct sock_common __sk_common; // 存放通用的网络信息,如 IP、端口等atomic_t sk_refcnt; // 引用计数struct sk_buff_head sk_receive_queue; // 接收缓冲队列struct sk_buff_head sk_write_queue; // 发送缓冲队列struct proto *sk_prot; // 指向协议处理函数(TCP、UDP等) };
关键字段解析
sk_socket
:指向struct socket
,用于在高层 socket 结构和底层 sock 结构之间建立关联。sk_receive_queue
:存储接收到但尚未被应用层读取的数据包。sk_write_queue
:存储尚未发送的数据包。sk_prot
:指向协议处理函数,例如 TCP 或 UDP 的操作集合。struct socket中type是TCP就调用TCP的函数作用
struct sock
作为所有协议(TCP/UDP)的基类,包含通用 socket 操作。- 由
struct socket
通过sk
字段关联。sk_prot
用于决定 socket 的具体协议操作,例如tcp_prot
(TCP 协议处理)。
2
struct inet_sock
(IP 层 socket)
struct inet_sock
继承自struct sock
,用于 IPv4 相关的 socket 操作。这个结构体增加了 IP 层的字段,例如源 IP、目的 IP、端口号等。struct inet_sock {struct sock sk; // 继承 struct sock__be32 inet_saddr; // 本地 IP 地址__be32 inet_rcv_saddr; // 绑定的本地 IP 地址(可能是 0.0.0.0)__be16 inet_sport; // 本地端口(主机字节序)__be16 inet_dport; // 远程端口(主机字节序)__be32 inet_daddr; // 远程 IP 地址struct ip_options *inet_opt; // 额外的 IP 选项(如 IP 选路) };
关键字段解析
inet_saddr
:本地 IP 地址。inet_daddr
:目标 IP 地址。inet_sport
:本地端口号。inet_dport
:远程端口号。inet_opt
:可选的 IP 头部选项。作用
- 增加 IP 层信息,使得
struct sock
能够支持 IPv4 协议。- 存储 socket 关联的 IP 地址和端口号。
- 供 TCP 和 UDP 继承,并扩展额外功能。
3
struct inet_connection_sock
(面向连接的 socket)
struct inet_connection_sock
继承struct inet_sock
,用于 面向连接的协议(如 TCP)。它增加了 TCP 连接管理所需的数据,例如超时控制、连接状态等。struct inet_connection_sock {struct inet_sock icsk_inet; // 继承 struct inet_sockstruct request_sock_queue icsk_accept_queue; // 半连接队列struct list_head icsk_ack_list; // TCP Fast Open 相关struct timer_list icsk_retransmit_timer; // 重传定时器struct request_sock *icsk_accept_head; // 指向全连接队列的头部 };
关键字段解析
字段 作用 icsk_inet
继承 struct inet_sock
,包含 IP 地址、端口信息icsk_accept_queue
半连接队列,存放 SYN-RECEIVED
连接icsk_ack_list
TCP Fast Open 机制,加快 SYN
建立连接icsk_retransmit_timer
TCP 重传定时器,用于 SYN+ACK
超时重传icsk_accept_head
全连接队列,存放 ESTABLISHED
连接,等待accept()
作用
- 维护 TCP 连接状态(如 SYN、ESTABLISHED)。
- 控制 TCP 连接的超时机制。
- 监听 socket 维护
accept()
所需的连接队列。
4
struct tcp_sock
(TCP 连接的实现)
struct tcp_sock
继承struct inet_connection_sock
,专门用于 TCP 连接,存储 TCP 专有的数据,例如 TCP 窗口大小、拥塞控制状态等。struct tcp_sock {struct inet_connection_sock inet_conn; // 继承 struct inet_connection_socku32 snd_nxt; // 发送窗口的下一个序列号u32 rcv_nxt; // 接收窗口的下一个序列号u32 snd_una; // 发送窗口的最早未确认序列号u32 snd_wnd; // 发送窗口大小u32 rcv_wnd; // 接收窗口大小struct sk_buff *retransmit_skb_hint; // 指向要重传的数据struct tcp_congestion_ops *cong_control; // 拥塞控制算法 };
关键字段解析
snd_nxt
:下一个要发送的 TCP 序列号。rcv_nxt
:期望接收的下一个 TCP 序列号。snd_una
:最早未被确认的 TCP 序列号(累计确认)。snd_wnd
/rcv_wnd
:TCP 窗口大小,决定流量控制。retransmit_skb_hint
:指向当前需要重传的 TCP 报文。cong_control
:TCP 拥塞控制算法(如 Reno、CUBIC)。作用
- 维护 TCP 发送、接收窗口,实现流量控制。
- 管理 TCP 拥塞控制机制,影响 TCP 传输速率。
- 控制 TCP 数据包重传,保证可靠传输。
结构体关系总结
struct sock
(核心 socket 结构,通用于所有协议)。struct inet_sock
(增加 IP 地址、端口等)。struct inet_connection_sock
(增加 TCP 连接管理功能)。struct tcp_sock
(TCP 特有字段,如序列号、窗口大小等)
UDP Socket 结构层次
UDP(User Datagram Protocol)是无连接的传输协议,与 TCP 相比,它没有连接管理、流量控制和可靠性机制,因此在 Linux 内核中的实现相对简单。UDP 的 socket 仍然依赖于
struct sock
,但不需要像 TCP 那样扩展struct inet_connection_sock
或struct tcp_sock
。UDP 在内核中的数据结构层级如下:
struct sock
(核心 socket 结构)struct inet_sock
(IP 层 socket,增加源/目标 IP、端口等信息)struct udp_sock
(UDP 特有的数据结构)TCP 需要
struct inet_connection_sock
和struct tcp_sock
来处理连接管理,而 UDP 直接使用struct inet_sock
,并在struct udp_sock
中扩展少量的 UDP 相关字段。
listen()监听TCP连接创建tcp socket的内核过程
当服务器使用
listen()
监听 TCP 连接,并在accept()
中接受新的连接时,Linux 内核会经历以下几个关键步骤:
listen()
监听 TCP 端口- 三次握手期间,创建
struct sock
并存入半连接队列,三次握手完成后连入全连接队列中accept()
取出struct sock
,创建struct socket
- 创建
struct file
并存入struct files_struct->fd_array[]
1.
listen()
:初始化监听 socket 并设置全连接队列Linux 内核执行
tcp_listen_start()
,配置struct sock
进入监听状态。
listen()
作用
- 设置
struct sock->sk_state = TCP_LISTEN
- 初始化
icsk_accept_queue
(半连接队列)和icsk_accept_head
(全连接队列)- 设置
backlog
,决定全连接队列最大容量
2. 客户端三次握手,创建
struct sock
并进入全连接队列当客户端发起 TCP 连接时:
客户端发送
SYN
- 服务器
TCP_LISTEN
状态,调用tcp_v4_rcv()
处理SYN
。- 服务器分配
struct request_sock
,存入 半连接队列 (icsk_accept_queue
)。服务器回复
SYN+ACK
- 服务器仍然在
TCP_LISTEN
状态。SYN+ACK
发送后,客户端需要回复ACK
。客户端发送
ACK
,服务器进入ESTABLISHED
tcp_v4_rcv()
处理ACK
。- 服务器 创建
struct sock
,从半连接队列转移到 全连接队列 (icsk_accept_head
),等待accept()
处理。
3.
accept()
处理全连接队列,创建struct socket
当服务器调用:accept()
accept()
取出全连接队列icsk_accept_head
里的struct sock
。- 创建 新的
struct socket
并关联struct sock
。- 当
accept()
创建struct socket
后,还需要struct file
结构,以便进程可以通过文件描述符访问它。创建struct file
,并存入fd_array[]
(文件描述符表)。
1.struct sock在三次握手过程中被创建并进入半连接队列,
2.三次握手完成后,struct sock连接到全连接队列中,accpet()调用后创建对应的struct socket并用struct sock*sk关联,
3.再创建struct file用private_data连接struct socket,struct socket中struct file*file再反指向file。
4.把file的指针填入struct files_struct 中fd_array[]文件描述符表数组分配文件描述符fd。
5.再由accept()返回创建的fd