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