listen的第二个参数backlog作用是什么?
backlog+1 = TCP全连接队列的容量
全连接队列里面存的都是已经三次握手成功建立的链接 。全连接队列的容量,指的就是这个队列中最多能存多少这样的连接。
值得注意的是,三次握手建立连接的过程和用户是否accept无关 ,在服务器来不及进行accept的时候,底层的TCP listen sock 依然允许用户通过三次握手,成功建立连接,建立好之后,该链接就会被插入到全连接队列中。但是不运行建立太多链接 ---- 当全连接队列中存的链接数量达到上限backlog+1之后,TCP listen sock 就会拒绝来自用户的新TCP链接请求了。
通过上面的描述我们知道,即使我服务器端不调用accept去接受连接,客户端也能与服务器通过三次握手成功建立TCP链接,那假如说服务器中的TCP全连接队列满了,也就是它的容量达到了backlog+1,那后面服务器就不能继续建立新链接了,这时候如果有客户端向服务器发起了新的连接请求,服务器会怎么处理呢?
这就牵扯到我们的另一个队列——未完成队列(也叫半连接队列)
(1)服务器会先去查看全连接队列有没有满
(2)如果满了,则会进一步查看未完成队列有没有满
如果未完成队列有空间,服务器会正常回应 SYN+ACK,将连接放入未完成队列(这一步不受全连接队列的影响!)。
但如果未完成队列也满了(极端情况,比如服务器被 SYN flood 攻击),服务器才会忽略或拒绝新请求。
只有当未完成队列也满时,新的 SYN 请求才会被 “丢弃或拒绝”:
此时服务器无法再处理新的半连接,才会出现 “忽略 SYN 或发 RST” 的行为。
理解全连接队列(模型角度)
说白了,这玩意儿其实就是一个生产消费者模型。全连接队列就是模型中的缓冲区,应用层(消费者)调用accept不断地从全连接队列中获取连接,客户端(生产者)不断的通过三次握手向队列中插入新建立的连接。
全连接队列的容量上限,是不是服务器处理并发请求能力的上限呢?
其实不是,全连接队列的容量我们经常设置地比较小,可能是10,可能是5,但这并不意味着服务器一次最多只能并发处理5个请求,实际上服务器的并发请求处理能力比这多得多
既然如此,为什么全连接队列的容量上限不设置大一些呢?
答案是设置大一些,那么这个队列就很长,对于那些还在长队列的末端的请求来说,其所属的用户要等待的时间就非常长,这会给用户造成非常不好的体验感。与其如此还不如在用户请求连接时,直接告诉他我这边儿满了,你先去干会儿别的事情,过一段时间再来找我吧。这样还干脆一点。用户的体验也就没那么糟糕。
理解全连接队列(内核角度)
我们都知道。在操作系统看来一切皆文件。而操作系统管理文件的方式。主要是通过进程PCB(task_struct)中的进程打开文件表(struct files_struct),进程打开文件表中有一个fd_array数组,里面具体记录了当前进程打开了哪些文件。如果fd_array数组中某个元素不为空指针,那它里面存的指针指向的就是一个struct file类型的结构体,用来描述一个打开的文件。这个struct file中记录的有文件的静态数据(包括文件元数据,文件inode以及文件操作方法),也有文件的动态数据(比如读写指针、IO缓冲区这种会随着进程运行状态不断变化的数据)。
对于服务器端一个建立好的TCP链接来说,他其实最终也会被封装到一个struct file类型的结构体中,然后这个结构体的指针会被存放在当前进程文件打开表的fd_array中,操作系统通过对这个套接字文件的读操作,最终实现向对端发数据,通过对这个套接字文件的写操作,最终实现接受对端发来的数据。
但实际上我们说,我们在程序中通过read系统调用,对这个套接字文件进行读操作,实际上干的事情仅仅是将数据从套接字文件的接受缓冲区中读取到程序中;我们在程序中通过write系统调用,对这个套接字文件进行写操作,实际上干的事情仅仅是将数据从用户区写入套接字文件的发送缓冲区(位于内核区)中。那实际上更进一步的工作,比如数据是如何从对端的发送缓冲区发送到我指定套接字文件的接收缓冲区中的,这个过程由操作系统和网络协同自动实现。
到此为止,其实我们还是有一些疑问没有解决。比如说tcp全连接队列在哪个结构体中?发送缓冲区和接收缓冲区在哪个结构体中?一个tcp连接应该会被描述成一个结构体,这个结构体是什么?
为此我们就有必要需进一步认识内核中网络相关的结构体设计,CSDN中有一个大佬画了这么一张图,我觉得画的还是蛮清晰的。
struct socket
简而言之就file结构体中有一个指针叫做private_data,类型是void *,如果这个文件是一个套接字文件,那么这个file结构体中的private_data就会指向一个struct socket结构体。
struct socket 主要就是用来描述一个套接字,其中记录了这个套接字的类型、状态、操作方法等关键信息,其中有一个指针叫做 struct file * file;
,其指向的就是这个套接字文件的file结构体,没错,它又指回去了,此时我们就已经建立了struct socket与struct file的双向链接,将套接字信息与其对应的套接字文件struct file牢牢地绑定在一起了。
struct socket {
socket_state state; // 套接字状态(如 SS_UNCONNECTED, SS_CONNECTED 等)
ktime_t tstamp; // 时间戳(用于 SO_TIMESTAMP 等选项)
struct socket_wq *wq; // 等待队列,用于处理阻塞操作
struct file *file; // 关联的文件对象(若套接字通过文件描述符访问)
struct sock *sk; // 指向底层传输层的 sock 结构体
const struct proto_ops *ops; // 套接字操作函数集(协议相关)
struct socket_lock *lock; // 用于同步的锁
short type; // 套接字类型(如 SOCK_STREAM, SOCK_DGRAM 等)
unsigned int flags; // 套接字标志(如 SOCK_ASYNC_NOSPACE 等)
struct usock_timers *timers; // 超时定时器(如 SO_RCVTIMEO, SO_SNDTIMEO)
struct fasync_struct *fasync_list; // 异步通知相关链表
struct module *owner; // 模块引用(用于自动卸载)
void *security; // 安全相关数据
struct hlist_node *link; // 用于哈希表的链表节点
struct hlist_node *glue; // 用于协议特定的关联
struct address_space *mm_space; // 内存地址空间(某些协议使用)
};
struct sock
在struct socket 中还有一个非常重要的指针,它的名字是 struct sock *sk;
,指向底层传输层的 sock 结构体。这个sock结构体也是用来描述这个套接字的相关信息的,这时候有人就要问了,既然如此,那这个socket和sock的区别在什么地方呢?主要区别就在于,socket这个结构体描述的套接字相关信息,是应用层关心的信息,也就是说它是面向用户的!而sock这个结构体描述的,则是传输层关心的信息!我们前面问题中提到的发送和接收缓冲区,就存在sock结构体中。
struct sock {
/* 基本状态与标识 */
socket_state sk_state; // 传输层状态(如 TCP_ESTABLISHED)
unsigned short sk_type; // 套接字类型(同 struct socket 的 type)
int sk_family; // 地址族(如 AF_INET, AF_INET6)
__u32 sk_flags; // 套接字标志(如 SOCK_FLAG_REUSEADDR)
/* 协议相关核心信息 */
struct proto *sk_prot; // 指向协议操作结构体(如 &tcp_prot, &udp_prot)
struct net *sk_net; // 所属的网络命名空间
struct sock_common __sk_common; // 公共地址信息(源/目的端口、IP 等)
#define sk_local_addr __sk_common.skc_rcv_saddr // 本地 IP 地址
#define sk_remote_addr __sk_common.skc_daddr // 远程 IP 地址
#define sk_local_port __sk_common.skc_num // 本地端口
#define sk_remote_port __sk_common.skc_dport // 远程端口
/* 连接与状态管理 */
struct sock *sk_next; // 哈希表中的下一个套接字
struct hlist_node sk_node; // 用于哈希表的节点
rcu_t sk_rcu; // RCU 机制相关
atomic_t sk_refcnt; // 引用计数
struct socket *sk_socket; // 反向关联的 struct socket
/* 数据缓冲区 */
struct sk_buff_head sk_receive_queue; // 接收缓冲区队列
struct sk_buff_head sk_write_queue; // 发送缓冲区队列
struct sk_buff_head sk_error_queue; // 错误信息队列
unsigned int sk_rcvbuf; // 接收缓冲区大小
unsigned int sk_sndbuf; // 发送缓冲区大小
/* TCP 相关状态(UDP 可能不使用) */
__u32 sk_seq; // 下一个要发送的序列号
__u32 sk_ack; // 已确认的序列号
__u32 sk_window; // 接收窗口大小
__u32 sk_snd_wnd; // 发送窗口大小
__u32 sk_rcv_wnd; // 接收窗口大小
struct tcp_sock *tcp; // TCP 私有数据(仅 TCP 套接字有效)
/* 计时器 */
struct timer_list sk_timer; // 通用计时器
struct timer_list sk_retrans_timer; // 重传计时器(TCP 用)
struct timer_list sk_keepalive_timer;// 保活计时器(TCP 用)
/* 阻塞与同步 */
wait_queue_head_t sk_sleep; // 等待队列(用于阻塞操作)
struct mutex sk_lock; // 互斥锁
spinlock_t sk_callback_lock; // 回调函数锁
/* 其他信息 */
void *sk_user_data; // 用户私有数据
__u32 sk_priority; // 优先级
__s32 sk_err; // 错误码
__u32 sk_mark; // 标记(用于路由等)
};
struct tcp_sock
看到这里大家可能会有些疑问,如果传输层采用的是tcp协议,其实它要干的事情还挺多的,比如:
- 端到端连接的建立和维护
- 对应用层下发的大块数据进行分段,将数据提交到应用层之前对其进行重组
- 实现数据的可靠传输(TCP报文段解析机制、序号机制、校验机制、确认应答机制、流量控制与拥塞控制机制)
这些机制要实现,都需要有相应的辅助数据结构,比如流量控制机制要有滑动窗口,超时重传机制要有计时器,最后一次重传时间等,我感觉就单靠sock结构体里面那点儿成员变量,干不了那么多事儿吧?
没错,sock结构体中仅包含套接字在传输层方面的一小点儿信息,实现tcp协议所需的完整信息和机制,并不在sock结构体中,而是在tcp_sock中!在 Linux 内核中,struct tcp_sock 是 TCP 协议实现的核心数据结构,用于维护一个 TCP 连接的所有状态和相关信息。它包含了连接状态、序号、窗口信息、重传机制、拥塞控制等关键参数,是 TCP 协议各种机制(如可靠传输、流量控制、拥塞控制)的实现基础。strucr tcp_sock的定义如下
struct tcp_sock {
/* 1. 继承自 inet_connection_sock(面向连接的 INET 协议基类)
* 注:这是嵌套继承的核心——tcp_sock 是一种 inet_connection_sock,
* 后者又继承 inet_sock,最终继承 sock(所有套接字通用基类)
*/
struct inet_connection_sock icsk;
/* 2. TCP 序号相关字段(核心:保障数据按序与可靠传输) */
u32 seq; /* 下一个要发送的字节序号(Send Sequence Number) */
u32 end_seq; /* 已发送但未确认的最后一个字节序号(= snd_nxt + 已发送未确认长度) */
u32 snd_nxt; /* 下一个待发送的序号(与 seq 语义一致,部分场景细分使用) */
u32 snd_una; /* 已发送但未被确认的第一个字节序号(Unacknowledged Sequence) */
u32 snd_wnd; /* 对端告知的发送窗口大小(接收方的接收窗口,用于流量控制) */
u32 rcv_nxt; /* 下一个期望接收的字节序号(Receive Next Sequence) */
u32 rcv_wup; /* 接收窗口更新序号(Window Update Sequence,标记窗口需更新的位置) */
u32 rcv_end; /* 接收窗口的末尾序号(= rcv_nxt + rcv_wnd,标记接收窗口边界) */
u32 snd_sml; /* 已发送的最小序号(用于快速重传时的序号判断) */
u32 rcv_tstamp; /* 最后一次接收数据的时间戳(用于计算 RTT、超时等) */
u32 lsndtime; /* 最后一次发送数据的时间戳(用于保活、超时判断) */
u32 last_data_sent; /* 最后一次发送数据的序号(非控制报文) */
u32 last_data_recv; /* 最后一次接收数据的序号(非控制报文) */
/* 3. RTT(往返时间)与重传相关(核心:保障丢包后可靠恢复) */
u32 srtt_us; /* 平滑往返时间(Smoothed RTT),单位:微秒(用于计算 RTO) */
u32 rttvar_us; /* RTT 偏差(RTT Variance),单位:微秒(用于动态调整 RTO) */
u32 rto; /* 重传超时时间(Retransmission Timeout),单位:毫秒(基于 srtt_us 和 rttvar_us 计算) */
u32 rtt_seq; /* 用于测量 RTT 的基准序号(标记当前正在测量 RTT 的数据段序号) */
u32 retrans_stamp; /* 最后一次重传的时间戳(用于重传频率控制) */
u32 retrans_out; /* 当前未被确认的重传段数量(用于拥塞控制判断) */
u32 snd_lost; /* 检测到的丢失段数量(基于重复 ACK 或超时判断) */
u32 high_seq; /* 已发送的最高序号(包括重传段,用于序号范围判断) */
u32 lost_out; /* 已判定丢失但未重传的段数量 */
u32 retransmit_cnt; /* 重传计数器(累计重传次数,用于超时退避) */
u32 undo_marker; /* 序号回滚标记(用于 SACK 机制中恢复丢失的序号范围) */
u32 undo_retrans; /* 序号回滚对应的重传次数 */
/* 4. 拥塞控制相关(核心:避免网络拥塞,控制发送速率) */
u32 ssthresh; /* 慢启动阈值(Slow Start Threshold):超过该值后进入拥塞避免阶段 */
u32 cwnd; /* 拥塞窗口(Congestion Window):发送方基于网络拥塞状态的发送上限 */
u32 cwnd_cnt; /* 拥塞窗口计数器(每发送 cwnd_cnt 个段,cwnd 增长 1,控制增长速率) */
u32 cwnd_clamp; /* 拥塞窗口上限(限制 cwnd 最大取值,避免过度占用带宽) */
u32 snd_cwnd_clamp; /* 发送窗口上限(= min(snd_wnd, cwnd_clamp),综合流量控制与拥塞控制) */
u32 tcp_cwnd_ssthresh; /* 拥塞窗口的慢启动阈值备份(用于 SACK 恢复等场景) */
u32 prior_cwnd; /* 上一次的拥塞窗口大小(用于连接恢复时的cwnd初始化) */
u32 mss_cache; /* 最大分段大小(MSS)缓存(避免重复计算,提升效率) */
u16 advmss; /* 通告的 MSS(Advertised MSS,向对端声明的最大分段大小) */
u8 cwnd_used; /* 拥塞窗口使用标记(用于判断是否充分利用 cwnd) */
u8 cwnd_event; /* 拥塞窗口事件标记(如 CWND_EVENT_LOSE 表示拥塞丢失) */
struct tcp_congestion_ops *cong; /* 当前使用的拥塞控制算法实例(如 Cubic、Reno 等) */
void *cong_priv; /* 拥塞控制算法私有数据(不同算法的自定义参数) */
/* 5. 乱序与数据重组相关(核心:处理乱序到达的数据包) */
struct sk_buff_head out_of_order_queue; /* 乱序接收的数据包队列(缓存序号不连续的段,待补全后重组) */
u32 tcp_gso_segs; /* GSO(Generic Segmentation Offload)分段数量(硬件分段时的段数) */
u32 tcp_gso_size; /* GSO 分段大小(每段的最大字节数) */
u32 tso_segs_goal; /* TSO(TCP Segmentation Offload)目标分段数(优化硬件分段效率) */
u32 tso_size_goal; /* TSO 目标分段大小 */
u32 copied_seq; /* 已复制到应用层的序号(标记用户空间已读取的数据边界) */
u32 rcv_ssthresh; /* 接收窗口的慢启动阈值(用于接收端流量控制) */
u32 rcv_wnd_cnt; /* 接收窗口计数器(控制接收窗口更新频率) */
/* 6. TCP 选项与标志位(核心:协议特性开关与配置) */
unsigned int flags; /* TCP 核心标志位(如 TCP_FLAG_FIN、TCP_FLAG_SYN、TCP_FLAG_SACK_PERM 等) */
u8 num_retrans; /* 累计重传次数(用于判断是否触发连接断开) */
u8 syn_retries; /* SYN 报文重传次数(三次握手阶段的重试次数) */
u8 synack_retries; /* SYN-ACK 报文重传次数(三次握手阶段的重试次数) */
u8 fin_retries; /* FIN 报文重传次数(断开连接阶段的重试次数) */
u8 keepalive_probes; /* 保活探测报文次数(连接保活时的重试次数) */
u32 keepalive_time; /* 保活时间(连接空闲多久后发起保活探测) */
u32 keepalive_intvl; /* 保活探测间隔(两次保活探测的时间差) */
u32 linger2; /* 半关闭状态下的 linger 时间(FIN_WAIT2 阶段的超时时间) */
u16 window_clamp; /* 窗口大小上限(限制对端通告的 snd_wnd 最大取值,避免窗口过大) */
u16 max_window; /* 历史最大接收窗口大小(用于窗口缩放选项) */
u8 window_bits; /* 窗口缩放位数(TCP 窗口缩放选项的偏移量) */
u8 rx_opt; /* 接收端选项标记(如 RX_OPT_SACK、RX_OPT_WSCALE 表示支持 SACK、窗口缩放) */
u8 tx_opt; /* 发送端选项标记(如 TX_OPT_SACK、TX_OPT_WSCALE 表示启用 SACK、窗口缩放) */
/* 7. 定时器相关(核心:控制重传、保活等超时逻辑) */
struct timer_list retrans_timer; /* 重传定时器(超时未确认则重传数据段) */
struct timer_list keepalive_timer; /* 保活定时器(空闲连接时发起保活探测) */
struct timer_list persist_timer; /* 持续定时器(接收窗口为 0 时,定期探测窗口是否恢复) */
struct timer_list delayed_ack_timer; /* 延迟 ACK 定时器(延迟发送 ACK,减少网络开销) */
/* 8. SACK(选择性确认)相关(优化重传效率,仅确认丢失的段) */
struct tcp_sack_block sack_block[TCP_SACK_BLOCKS_MAX]; /* SACK 块数组(记录已接收的乱序段范围) */
u8 sack_blocks; /* 当前有效的 SACK 块数量 */
u8 sack_block_size; /* SACK 块数组大小(默认 TCP_SACK_BLOCKS_MAX=4) */
u8 sack_recv; /* 接收端 SACK 标记(是否支持 SACK 选项) */
u8 sack_enable; /* 发送端 SACK 使能标记(是否启用 SACK 机制) */
u32 sack_left; /* SACK 机制中未确认的最小序号 */
u32 sack_rtt_seq; /* SACK 机制中用于测量 RTT 的序号 */
/* 9. 其他辅助字段 */
u32 tcp_header_len; /* TCP 头部长度(包含选项,单位:字节) */
u32 tso_segs; /* TSO 分段数量(已发送的 TSO 分段数) */
u32 tso_size; /* TSO 分段大小(已发送的 TSO 分段的单段大小) */
u32 mtu_probe_seq; /* MTU 探测序号(用于路径 MTU 发现) */
u32 mtu_probe_size; /* MTU 探测包大小(路径 MTU 发现时的探测包长度) */
u8 mtu_probe_type; /* MTU 探测类型(如 MTU_PROBE_SYN、MTU_PROBE_DATA) */
u8 repair; /* 修复模式标记(用于 TCP 连接修复、调试) */
u32 repair_queue; /* 修复模式下的队列标记(如 REPAIR_QUEUE_SEND、REPAIR_QUEUE_RECV) */
u32 repair_seq; /* 修复模式下的基准序号 */
};
那既然如此,为啥socket结构体中不直接给一个strcut tcp_sock的指针呢?strcut tcp_sock与struct sock之间又有什么关系呢?
首先回答第一个问题,之所以不直接给一个strcut tcp_sock的指针,是因为你传输层也可能采用UDP协议进行通信,此时记录套接字文件传输层完整信息的结构体是struct udp_sock 结构体,那你给strcut tcp_sock的指针不就没用了吗。有人又会说,既然如此,你就这俩结构体指针都给一份,真正用的时候,哪个不为空用哪个不就行了吗?这种想法当然也行,但是linux有更加巧妙的设计,linux将struct sock设计成struct udp_sock 与strcut tcp_sock的基类,用来记录一些套接字在传输层中的通用信息,然后进一步拓展时,如果传输层采用UDP协议进行通信,就让udp_sock继承sock,如果采用TCP,就让tcp_sock继承sock
然后回答第二个问题,struct sock与strcut tcp_sock 是层层嵌套的继承关系,完整的继承关系如下图所示(这图不是我画的,原文链接在这)
sock与udp_sock的关系图也是类似的,但由于UDP是无连接的,所以没有inet_connection_sock层,inet_sock结构体比sock多出来的,是一些网络层的信息
那现在问题又来了,Linux内核不是用c语言写的吗,C语言哪来的继承啊?
- 答案非常简单,在 struct sock 结构体中,有专门的字段记录传输层协议类型,我们只需要查一下,就可以知道传输层协议的类型了。知道这个sock结构体对应的传输层协议类型之后(假如说传输层采用TCP协议),我们经可以将这个sock结构体指针强转成tcp_sock类型的指针,这样我们就找到记录这个套接字文件完整传输层信息的tcp_sock结构体了。
struct sock {
// ... 其他成员
unsigned short sk_protocol; // 传输层协议类型
// ... 其他成员
};
- 想想为什么?上面图中可以很明显地看到,如果传输层采用TCP协议通信,sock结构体的起始地址和tcp_sock结构体的起始地址是一样一样的!因此在这种结构中我们只需要强转下类型,就可以成功地访问到tcp_sock结构体中的成员变量,上面的过程其实就是linux通过 C 语言的 “结构体嵌套” 模拟面向对象的继承
struct sk_buff
struct sk_buff是 “网络数据包的载体”,存储实际的报文数据(如 SYN 报文、ACK 报文的字节流)及元信息(协议类型、设备指针等)。它是 “数据的容器”,与具体连接的关联是动态的。
一个 TCP 连接(或其他面向连接的协议)在通信过程中会产生多个数据包(如分段的 TCP 报文段、IP数据报)。如果说tcp_sock描述的是一个TCP连接,那struct sk_buff描述的就是一个TCP报文段。相应地,在udp_sock中,struct sk_buff描述的就是一个UDP数据报。
在sock结构体中,收发缓冲区(sock结构体中的sk_write_queue与sk_receive_queue)其本质就是一个sk_buff的队列。
// 套接字核心结构(简化版)
struct sock {
// ... 其他大量字段 ...
/* 发送队列:存储待发送的 sk_buff 数据包 */
struct sk_buff_head write_queue;
/* 接收队列:存储已接收、待应用层读取的 sk_buff 数据包 */
struct sk_buff_head receive_queue;
// ... 其他字段(如错误队列、状态标志等)...
};
// 用于管理 sk_buff 链表的队列头结构
struct sk_buff_head {
struct sk_buff *next; // 队列中第一个 sk_buff
struct sk_buff *prev; // 队列中最后一个 sk_buff
unsigned int qlen; // 队列中 sk_buff 的数量(长度)
spinlock_t lock; // 保护队列操作的自旋锁(并发安全)
};
struct sk_buff {
// 数据缓冲区边界
unsigned char *head; // 缓冲区起始地址
unsigned char *data; // 当前协议层数据起始地址
unsigned char *tail; // 数据末尾地址
unsigned char *end; // 缓冲区结束地址
// 长度信息
unsigned int len; // 当前数据长度(tail - data)
unsigned int data_len;// 数据部分长度(不含协议头)
__u32 truesize;// 整个sk_buff及数据的总大小
// 协议头指针(不同协议层的头部)
struct iphdr *ip_hdr; // IP头部指针(网络层)
struct tcphdr *tcp_hdr; // TCP头部指针(传输层)
struct udphdr *udp_hdr; // UDP头部指针(传输层)
// ... 其他协议头指针
// 网络设备与协议信息
struct net_device *dev; // 接收/发送的网络设备
__be16 protocol;// 上层协议类型(如ETH_P_IP)
__u16 mac_len; // 链路层头部长度
// 内存管理
atomic_t users; // 引用计数
struct sk_buff *next; // 链表指针(用于批量处理)
struct sk_buff *prev; // 双向链表前驱指针
// 控制标记与状态
__u32 mark; // 数据包标记(用于QoS等)
__u8 local_df;// 分片标志
__u8 cloned:1;// 是否为克隆副本
// ... 其他标志位
// 时间与校验和信息
ktime_t tstamp; // 时间戳
__wsum csum; // 校验和
// ... 其他字段
};
sk_buff中缓冲区边界指针的作用
主要就是通过data和tail指针的移动来确定当前层的PDU
假设分配了一块 1024 字节的缓冲区(head 指向 0x1000,end 指向 0x1400,总大小 1024 字节):
- 链路层收到数据后,会将以太网帧(包含以太网头 + IP 头 + TCP 头 + payload)存入缓冲区,此时 data 指向以太网头起始位置(0x1000),tail 指向 payload 末尾(比如 0x1200)。
- 当数据传递到网络层(IP 层),data 会向后移动(跳过以太网头),指向 IP 头起始位置(比如 0x100E),tail 不变。
- 再传递到传输层(TCP 层),data 继续后移(跳过 IP 头),指向 TCP 头起始位置(比如 0x1022)。
通过这四个指针的设计,协议栈各层无需复制数据,只需调整 data 指针即可 “聚焦” 到自己需要处理的部分,极大提升了效率。这块缓冲区的生命周期与 sk_buff 绑定,当 sk_buff 被释放时,缓冲区也会被内核回收。
全连接队列的数据结构描述
上面介绍了那么多结构体,请问全连接队列在哪个结构体中呢?全连接队列被描述成怎样的数据结构了呢?
当我们在程序中调用listen成功创建出一个监听套接字时,内核为为其创建对应的struct file结构体,用来描述这个打开的文件。struct file结构体中有一个private_data。他就指向一个struct sock结构体,其中包含一个 struct request_sock_queue 类型的成员 accept_queue,专门用于维护全连接队列。
// 内核中监听套接字的核心结构
struct sock {
// ... 其他字段
struct request_sock_queue accept_queue; // 全连接队列
// ... 其他字段
};
// 全连接队列的管理结构
struct request_sock_queue {
struct request_sock *rskq_accept_head; // 队列头部(第一个待accept的连接)
struct request_sock *rskq_accept_tail; // 队列尾部(最后一个待accept的连接)
rwlock_t rskq_lock; // 队列操作锁
int qlen; // 当前队列长度(已完成握手的连接数)
int max_qlen; // 队列最大长度(由listen()的backlog参数设置)
// ... 其他控制字段
};
全连接队列中存储的元素是 struct request_sock 结构体(简称 req),它代表一个 “已完成三次握手、等待被应用层接收” 的连接。当应用层调用 accept() 时,内核会从队列头部取出一个 request_sock,并将其转换为一个新的 struct sock(用于数据传输的套接字)。
基于上述结构体,再理解三次握手
(1)半连接队列阶段(SYN 队列)
首先客户端向服务器发送链接请求,当服务器收到客户端的 SYN 报文时,内核会将该报文封装为 sk_buff,并解析其中的 TCP 头部信息(如源端口、序号等)。内核会为这个连接请求创建一个 request_sock 结构体,记录当前握手状态(如已发送 SYN+ACK),并将其加入 半连接队列(struct request_sock_queue 的 syn_wait_queue)。此时,request_sock 会通过指针关联到对应的 sk_buff(或其解析出的关键信息),用于后续处理该连接的后续报文(如客户端的 ACK)。
(2)全连接队列阶段(Accept 队列)
当服务器收到客户端的最终 ACK 报文(完成三次握手),内核会再次将该 ACK 报文封装为 sk_buff,并根据报文信息(四元组)找到对应的 request_sock。内核会将 request_sock 从半连接队列迁移到 全连接队列(accept_queue),此时 request_sock 已代表一个 “已建立的连接”。
此时,sk_buff(承载 ACK 报文)的使命已完成(驱动连接状态变更),会被内核释放或复用,而 request_sock 则在全连接队列中等待应用层 accept() 调用。
(3)连接建立后
当应用层调用 accept() 后,request_sock 会被转换为一个完整的 struct sock(TCP 连接对象,tcp_sock)。此后:
该连接传输的业务数据会被封装为 sk_buff,并通过 struct sock 的发送 / 接收队列(如 sk_write_queue、sk_receive_queue)与连接对象关联。
此时的 sk_buff 直接与 struct sock 绑定,而非 request_sock(request_sock 已完成使命并被转换)。
基于上述结构体,再次理解网络通信
(1)发送流程(sk_write_queue)
- 应用层调用 send() 向套接字写入数据。
- 内核为数据创建 sk_buff,添加 TCP 头(序号、确认号等)、IP 头、链路层头等。
- 将 sk_buff 加入 sk_write_queue 队列。
- TCP 协议栈从队列中取出 sk_buff,根据拥塞控制、滑动窗口等策略发送数据
- 收到对端 ACK 后,从队列中移除 sk_buff 并释放(若未收到 ACK,会触发重传)。
(2)接收流程(sk_receive_queue)
- 网络设备收到数据包,封装为 sk_buff 并上传至内核协议栈。
- TCP 层对 sk_buff 进行校验(序号、校验和等)、重排(解决乱序)、组装(拼接分段数据)。
- 处理完成后,将 sk_buff 加入 sk_receive_queue 队列。
- 应用层调用 recv() 时,内核从队列中取出 sk_buff,将数据复制到用户态缓冲区。
- 数据被读取后,sk_buff 从队列中移除并释放。