本系列文章
上一篇我们分析了从网卡到达数据包到
IP
层的处理流程,接下来我们将深入探讨 L4 层 TCP 协议栈的处理流程,也就是来到了图中第 7 步协议栈 L2、L3、L4 三层处理的最后阶段。

图片来自 Linux Networking Stack tutorial: Receiving Data
7.3#
TCP
协议栈处理
TCP
协议
栈的入口函数是 tcp_v4_rcv,这个函数比较复杂,主要完成以下几件事情:
- 检查 TCP 头部是否合法。
- 根据 TCP 头部信息查找对应的 socket。
- 根据 socket 的状态,进行相应的处理
socket
状态的处理分发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| /*
* From tcp_input.c
*/
int tcp_v4_rcv(struct sk_buff *skb)
{
// ... 代码省略
// 获取 TCP 头指针
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
lookup:
// 查找 socket
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
if (!sk)
goto no_tcp_socket;
process:
// 处理 TIME_WAIT 状态的 socket
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
// 处理 TCP_NEW_SYN_RECV 状态的 socket,表示 socket 正在建立连接
if (sk->sk_state == TCP_NEW_SYN_RECV) {
// ... 代码省略
}
...
// LISTEN 状态处理
if (sk->sk_state == TCP_LISTEN) {
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
// 处理 ESTABLISHED 状态的 socket
ret = 0;
if (!sock_owned_by_user(sk)) {
skb_to_free = sk->sk_rx_skb_cache;
sk->sk_rx_skb_cache = NULL;
ret = tcp_v4_do_rcv(sk, skb);
} else {
if (tcp_add_backlog(sk, skb))
goto discard_and_relse;
skb_to_free = NULL;
}
...
// // 释放缓存的skb
if (skb_to_free)
__kfree_skb(skb_to_free);
...
}
|
可以看到针对 LISTEN、ESTABLISHED、TIME_WAIT 以及建立连接的不同状态,内核分别采取了不同的处理策略。这里我们只关注正常收包的 LISTEN 和 ESTABLISHED 状态处理:
- 对于 LISTEN 状态的 socket,内核会将数据包交给
tcp_v4_do_rcv() 函数进行处理。 - 对于 ESTABLISHED 的状态,有一个
!sock_owned_by_user(sk) 的判断,如果为真,也是执行 tcp_v4_do_rcv() 函数进行处理,否则会调用 tcp_add_backlog() 将数据包放入 backlog 队列。
!sock_owned_by_user(sk) 这个判断的意思是当前 socket 是否被用户空间所拥有,如果我们在使用 socket,比如执行 recv()、send()、setsockopt() 等系统调用时,内核会将 socket 标记为“被用户空间拥有”。如果 socket 被用户空间拥有,内核就不能直接对其进行操作,而是需要将数据包放入 backlog 队列,等待用户空间应用程序来处理。从而避免内核和用户态进程同时修改 socket。

backlog#
缓存
队列
当 socket 被占用无法立即处理时,socket 会将这些数据包缓存到 backlog 队列。虽然也叫 backlog,但注意和半连接队列的 backlog 是不同。内核将数据包放入 backlog 队列中后,等待用户空间应用程序通过 recv() 等系统调用来读取。队列的长度计算如下:
1
2
3
4
5
| // 基础限制 = 接收缓冲区大小 + 发送缓冲区大小的一半
limit = (u32)READ_ONCE(sk->sk_rcvbuf) + (u32)(READ_ONCE(sk->sk_sndbuf) >> 1);
// 添加64KB的安全边界
limit += 64 * 1024;
|
因此适当的调大 rcv_buf 和 snd_buf 的大小,可以有效提高 backlog 队列的长度,从而提升网络性能。
tcp_v4_do_rcv() 接收数据#
现在数据包来到 tcp_v4_do_rcv() 函数进行处理,该函数逻辑比较简单:
- 如果是 ESTABLISHED 状态的 socket,调用
tcp_rcv_established() 函数进行处理。 - 如果是其他状态,调用
tcp_rcv_state_process() 函数进行处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
...
tcp_rcv_established(sk, skb);
return 0;
}
...
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
return 0;
}
|
在 tcp_rcv_state_process 函数可以看到对 TCP 协议所有状态的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
| int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
...
switch (sk->sk_state) {
case TCP_CLOSE:
goto discard;
case TCP_LISTEN:
if (th->ack)
return 1;
if (th->rst)
goto discard;
if (th->syn) {
...
return 0;
}
goto discard;
case TCP_SYN_SENT:
...
tcp_data_snd_check(sk);
return 0;
}
switch (sk->sk_state) {
case TCP_SYN_RECV:
tcp_set_state(sk, TCP_ESTABLISHED);
sk->sk_state_change(sk);
...
break;
case TCP_FIN_WAIT1: {
...
tcp_set_state(sk, TCP_FIN_WAIT2);
if (tp->linger2 < 0) {
tcp_done(sk);
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
return 1;
}
tmo = tcp_fin_time(sk);
if (tmo > TCP_TIMEWAIT_LEN) {
inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
} else if (th->fin || sock_owned_by_user(sk)) {
inet_csk_reset_keepalive_timer(sk, tmo);
} else {
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
goto discard;
}
break;
}
case TCP_CLOSING:
if (tp->snd_una == tp->write_seq) {
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
goto discard;
}
break;
case TCP_LAST_ACK:
if (tp->snd_una == tp->write_seq) {
tcp_update_metrics(sk);
tcp_done(sk);
goto discard;
}
break;
}
/* step 7: process the segment text */
switch (sk->sk_state) {
case TCP_CLOSE_WAIT:
case TCP_CLOSING:
case TCP_LAST_ACK:
...
case TCP_FIN_WAIT1:
case TCP_FIN_WAIT2:
...
case TCP_ESTABLISHED:
tcp_data_queue(sk, skb);
queued = 1;
break;
}
/* tcp_data could move socket to TIME-WAIT */
if (sk->sk_state != TCP_CLOSE) {
tcp_data_snd_check(sk);
tcp_ack_snd_check(sk);
}
return 0;
}
|
感兴趣的同学不妨参考着 TCP 状态图来读读这段代码,可以对相关状态的处理有更深入的理解。

tcp_rcv_established() 数据处理#
基于接收包的信息,tcp_rcv_established() 会有两种不同路径:
- Fast path: 快速路径,正常情况下的收包处理,当收到的包正好是期望的包是,会执行该路径进行快速处理。
- Slow path: 慢速路径,当传输过程存在问题时,比如丢包、乱序、Zero Window 等情况,会执行该路径进行更加复杂严格的处理。
对于快速路径,还会分为无数据和有数据两种情况,前者只需要响应 ACK 即可;后者则需要将数据放入接收缓冲区并通知上层应用。
快速路径处理#
当发送接收端和网络状态都正常时,tcp_rcv_established() 函数会进入快速路径处理。这是大多数情况下的处理流程。核心逻辑如是在有数据时会调用 tcp_queue_rcv 函数将数据放入接收缓冲区,写入流程也非常简单,正常情况下数据时有序到达的,因此直接将数据添加到队列末尾即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
| void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
// ... 代码省略
// 快速路径判断
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
int tcp_header_len = tp->tcp_header_len;
...
// 无数据
if (len <= tcp_header_len) {
/* Bulk data transfer: sender */
if (len == tcp_header_len) {
// 响应 ACK
tcp_ack(sk, skb, 0);
// 释放 skb
__kfree_skb(skb);
// 检查是否需要发送数据
tcp_data_snd_check(sk);
...
return;
} else { /* Header too small */
TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
goto discard;
}
} else {
/* Bulk data transfer: receiver */
// 移除TCP头部,只保留数据部分
__skb_pull(skb, tcp_header_len);
// 将数据写入接收缓冲区
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
// 触发数据接收事件,主要为了调整拥塞窗口
tcp_event_data_recv(sk, skb);
...
no_ack:
// 释放 skb
if (eaten)
kfree_skb_partial(skb, fragstolen);
// 通知应用层数据已准备好,快速处理路径完成
tcp_data_ready(sk);
return;
}
}
slow_path:
if (len < (th->doff << 2) || tcp_checksum_complete(skb))
goto csum_error;
if (!th->ack && !th->rst && !th->syn)
goto discard;
/*
* Standard slow path.
*/
if (!tcp_validate_incoming(sk, skb, th, 1))
return;
step5:
if (tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) < 0)
goto discard;
tcp_rcv_rtt_measure_ts(sk, skb);
/* Process urgent data. */
tcp_urg(sk, skb, th);
/* step 7: process the segment text */
tcp_data_queue(sk, skb);
tcp_data_snd_check(sk);
tcp_ack_snd_check(sk);
return;
csum_error:
trace_tcp_bad_csum(skb);
TCP_INC_STATS(sock_net(sk), TCP_MIB_CSUMERRORS);
TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
discard:
tcp_drop(sk, skb);
}
|
慢速路径处理#
如果收到的包不满足快速路径的条件,就会进入慢速路径处理。慢速路径处理的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| slow_path:
if (len < (th->doff << 2) || tcp_checksum_complete(skb))
goto csum_error;
if (!th->ack && !th->rst && !th->syn)
goto discard;
/*
* Standard slow path.
*/
if (!tcp_validate_incoming(sk, skb, th, 1))
return;
step5:
if (tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) < 0)
goto discard;
tcp_rcv_rtt_measure_ts(sk, skb);
/* Process urgent data. */
tcp_urg(sk, skb, th);
/* step 7: process the segment text */
tcp_data_queue(sk, skb);
tcp_data_snd_check(sk);
tcp_ack_snd_check(sk);
return;
|
这里的核心逻辑是在 tcp_data_queue 函数中处理的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
| static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
bool fragstolen;
int eaten;
...
skb_dst_drop(skb);
__skb_pull(skb, tcp_hdr(skb)->doff * 4);
tp->rx_opt.dsack = 0;
/* Queue data for delivery to the user.
* Packets in sequence go to the receive queue.
* Out of sequence packets to the out_of_order_queue.
*/
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
if (tcp_receive_window(tp) == 0) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPZEROWINDOWDROP);
goto out_of_window;
}
/* Ok. In sequence. In window. */
queue_and_out:
if (skb_queue_len(&sk->sk_receive_queue) == 0)
sk_forced_mem_schedule(sk, skb->truesize);
else if (tcp_try_rmem_schedule(sk, skb, skb->truesize)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPRCVQDROP);
sk->sk_data_ready(sk);
goto drop;
}
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
if (skb->len)
tcp_event_data_recv(sk, skb);
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
tcp_fin(sk);
if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
tcp_ofo_queue(sk);
/* RFC5681. 4.2. SHOULD send immediate ACK, when
* gap in queue is filled.
*/
if (RB_EMPTY_ROOT(&tp->out_of_order_queue))
inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW;
}
if (tp->rx_opt.num_sacks)
tcp_sack_remove(tp);
tcp_fast_path_check(sk);
if (eaten > 0)
kfree_skb_partial(skb, fragstolen);
if (!sock_flag(sk, SOCK_DEAD))
tcp_data_ready(sk);
return;
}
if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
tcp_rcv_spurious_retrans(sk, skb);
/* A retransmit, 2nd most common case. Force an immediate ack. */
NET_INC_STATS(sock_net(sk), LINUX_MIB_DELAYEDACKLOST);
tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq);
out_of_window:
tcp_enter_quickack_mode(sk, TCP_MAX_QUICKACKS);
inet_csk_schedule_ack(sk);
drop:
tcp_drop(sk, skb);
return;
}
/* Out of window. F.e. zero window probe. */
if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt + tcp_receive_window(tp)))
goto out_of_window;
if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
...
goto queue_and_out;
}
tcp_data_queue_ofo(sk, skb);
}
|
这里对数据包分了五种场景进行处理,我们来分别看下。
场景一:正常数据流#
这里的判断条件是 TCP_SKB_CB(skb)->seq == tp->rcv_nxt,也就是到达的数据包刚好是我们期望的数据包,没有丢包、乱序和重传。这是如果窗口还有空间,则会调用 tcp_queue_rcv() 将数据写入缓冲区。
除此之外,这里还会检查 out_of_order_queue 乱序队列中是否有提前到达的数据,可以在当前数据包达到后连起来,有的话也会一起写入到缓冲区。比如我们有 1、3、4、5 号数据包,1 和 2 收到后接着收到了 4 和 5,这两个包就会放入乱序队列,3 号包达到后数据就连起来了,此时可以从乱序队列将 4、5 号包取出放入缓冲区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // https://elixir.bootlin.com/linux/v5.15.139/source/net/ipv4/tcp_input.c#L5044
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
// 接收窗口已满
if (tcp_receive_window(tp) == 0) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPZEROWINDOWDROP);
goto out_of_window;
}
...
// 将数据写入缓冲区
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
if (skb->len)
tcp_event_data_recv(sk, skb);
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
tcp_fin(sk);
// 处理乱序队列
if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
tcp_ofo_queue(sk);
/* RFC5681. 4.2. SHOULD send immediate ACK, when
* gap in queue is filled.
*/
if (RB_EMPTY_ROOT(&tp->out_of_order_queue))
inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW;
}
..
// 通知应用层 数据处理完毕
if (!sock_flag(sk, SOCK_DEAD))
tcp_data_ready(sk);
return;
}
|
场景二:数据重传#
这里的判断条件是 !after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt):
end_seq 表示当前报文的最大序列号。rcv_nxt 表示接收窗口。
这里判断的意思就是收到包的报文不晚于 RCV.NXT,如图所示也就是位于已经接收并确认的区域内。很明显是重传包,因此这里会再次进行 ACK,但会通过 tcp_dsack_set() 标记为 Duplicate ACK。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // https://elixir.bootlin.com/linux/v5.15.139/source/net/ipv4/tcp_input.c#L5088
if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
tcp_rcv_spurious_retrans(sk, skb);
/* A retransmit, 2nd most common case. Force an immediate ack. */
NET_INC_STATS(sock_net(sk), LINUX_MIB_DELAYEDACKLOST);
tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq);
out_of_window:
tcp_enter_quickack_mode(sk, TCP_MAX_QUICKACKS);
inet_csk_schedule_ack(sk);
drop:
tcp_drop(sk, skb);
return;
}
|
场景三:零窗口处理#
这里的判断条件是 !before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt + tcp_receive_window(tp)),也就是收到的包的序列号位于图中的黄色区域,已经超出了接收窗口的可接受范围。

1
2
3
4
5
6
7
8
| // https://elixir.bootlin.com/linux/v5.15.139/source/net/ipv4/tcp_input.c#L5103
out_of_window:
tcp_enter_quickack_mode(sk, TCP_MAX_QUICKACKS);
inet_csk_schedule_ack(sk);
/* Out of window. F.e. zero window probe. */
if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt + tcp_receive_window(tp)))
goto out_of_window;
|
这种情况通常意味着发送速度超过了接收速度,此时需要通知发送方进行调整。这里会执行 Quick ACK,立即通知发送方,告知自己的窗口大小,减缓发送速度。
场景四:部分重传#
这里的判断条件是 before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt),也就是收到的包的序列号小于接收窗口的下一个期望序列号,简单来说就是收到的包一部分是旧数据,一部分是新数据。这种情况下会对数据进行裁剪,将新数据写入到缓冲区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // https://elixir.bootlin.com/linux/v5.15.139/source/net/ipv4/tcp_input.c#L5106
if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
/* Partial packet, seq < rcv_next < end_seq */
tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, tp->rcv_nxt);
/* If window is closed, drop tail of packet. But after
* remembering D-SACK for its head made in previous line.
*/
if (!tcp_receive_window(tp)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPZEROWINDOWDROP);
goto out_of_window;
}
goto queue_and_out;
}
|
场景五:乱序#
上述四种情况都处理结束后,如果还有包,那就是乱序包。因为其:
- seq 大于 rcv.nxt 且小于 rcv.nxt + window,说明其落在滑动窗口内
- seq 不等于 rcv.nxt 说明并不是期望的包
因此这个包就只会是乱序包。

这种情况下会调用 tcp_data_queue_ofo(sk, skb); 将数据包写入 out_of_order_queue 乱序队列,等后续包继续到达后再场景一下被取出并写入缓冲区。
tcp_data_ready 处理#
无论是快速路径还是慢速路径,都是调用 tcp_queue_rcv 将数据写入缓冲区,最后调用 tcp_data_ready 来通知应用层的。
1
2
3
4
5
6
| void tcp_data_ready(struct sock *sk)
{
if (tcp_epollin_ready(sk, sk->sk_rcvlowat) // 缓冲区是否有数据
|| sock_flag(sk, SOCK_DONE)) // socket 接收完毕(收发 FIN)
sk->sk_data_ready(sk); // 调用回调函数
}
|
如果缓冲区有数据就会调用 sk_data_ready 回调函数来处理,这里一般默认是 sock_def_readable 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // https://elixir.bootlin.com/linux/v5.15.139/source/net/core/sock.c#L3173
void sock_init_data_uid(struct socket *sock, struct sock *sk, kuid_t uid)
{
...
sk->sk_data_ready = sock_def_readable;
}
// https://elixir.bootlin.com/linux/v5.15.139/source/net/core/sock.c#L3066
void sock_def_readable(struct sock *sk)
{
struct socket_wq *wq;
rcu_read_lock();
wq = rcu_dereference(sk->sk_wq);
if (skwq_has_sleeper(wq))
wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN | EPOLLPRI |
EPOLLRDNORM | EPOLLRDBAND);
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
rcu_read_unlock();
}
|
这个函数做的事情非常简单:
- 如果 socket 的 wq(wait queue)等待队列有等待的任务((epoll_wait/poll/select 或阻塞 recv 等)。),则执行
wake_up_interruptible_sync_poll 函数唤醒这些任务。 sk_wake_async:向开启了异步通知的进程发送 SIGIO(异步 IO 信号),通知有数据到达。这主要是为了兼容一些没有使用 poll/epoll 的旧的应用程序。
到这里就完成了图中的第7、第 8 步,内核协议栈将数据写入缓冲区并通知应用程序,内核的收包处理就到此为止了,后续由应用程序在用户态空间继续进行处理。