TCP实现之:TCP报文接收
本章节讲述了内核TCP
协议层快速收报的流程,包括从IP
层将报文传递给TCP
层,一直到用户调用系统调用收到报文数据的过程。之所以说是快速收报过程,是因为本文暂不分析异常网络情况下的报文,例如紧急报文、失序报文等的处理过程。
一、SOCK锁机制
sock
中的sk_lock
字段是用来对sock加锁的,该字段的类型为socket_lock_t
。在对sock中的报文接收队列进行处理(包括报文的读取和添加)时,需要先获取该套接字上的锁,其中socket_lock_t
的定义如下:
typedef struct {
spinlock_t slock;
int owned;
wait_queue_head_t wq;
/*
* We express the mutex-alike socket_lock semantics
* to the lock validator by explicitly managing
* the slock as a lock variant (in addition to
* the slock itself):
*/
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} socket_lock_t;
slock
用于保护加锁和释放锁的过程(即防止在进行加锁过程中进程被中断等)。owned
代表当前套接字是否被进程所持有,即已经被上锁。wq
为等待获取该锁的进程的队列,锁被释放的时候会唤醒队列中的进程。
注意:对sock加锁只会在进程上线文中进行,准确的说是在用户进程中进行,不会在软中断或者中断中进行。
lock_sock
该函数用来获取套接字上的锁,注意该函数可能会引起睡眠,其定义如下:
static inline void lock_sock(struct sock *sk)
{
lock_sock_nested(sk, 0);
}
void lock_sock_nested(struct sock *sk, int subclass)
{
might_sleep();
spin_lock_bh(&sk->sk_lock.slock);
if (sk->sk_lock.owned)
__lock_sock(sk);
sk->sk_lock.owned = 1;
spin_unlock(&sk->sk_lock.slock);
/*
* The sk_lock has mutex_lock() semantics here:
*/
mutex_acquire(&sk->sk_lock.dep_map, subclass, 0, _RET_IP_); //调试用的
local_bh_enable();
}
在获取锁的过程中,通过自旋锁sk->sk_lock.slock
来保护加锁过程。如果当前锁已经被占用,那么调用__lock_sock
将当前进程加入到锁的等待队列并进入睡眠状态。
static void __lock_sock(struct sock *sk)
{
DEFINE_WAIT(wait);
for (;;) {
prepare_to_wait_exclusive(&sk->sk_lock.wq, &wait,
TASK_UNINTERRUPTIBLE);
spin_unlock_bh(&sk->sk_lock.slock);
schedule();
spin_lock_bh(&sk->sk_lock.slock);
if (!sock_owned_by_user(sk))
break;
}
finish_wait(&sk->sk_lock.wq, &wait);
}
在调度之前,先调用spin_unlock_bh
释放当前持有的自旋锁,随后进入不可打断的睡眠模式。在被唤醒后,检查锁是否空闲,不空闲的话继续进入睡眠模式,直到锁可以被获取。进程的唤醒是在release_sock
中进行的,该函数用来释放当前进程持有的套接字锁。
release_sock
void release_sock(struct sock *sk)
{
/*
* The sk_lock has mutex_unlock() semantics:
*/
mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);
spin_lock_bh(&sk->sk_lock.slock);
if (sk->sk_backlog.tail)
__release_sock(sk);
sk->sk_lock.owned = 0;
if (waitqueue_active(&sk->sk_lock.wq))
wake_up(&sk->sk_lock.wq);
spin_unlock_bh(&sk->sk_lock.slock);
}
在释放套接字锁的时候,会先获取自旋锁,如前面加锁过程一致。随后,会检查当前套接字的backlog
队列中是否有报文待处理。这里的报文是在加锁的时候,系统又接收到报文,但是由于sock
被锁定,因此不能将其放到接受队列,所以就放到了backlog
队列,等待套接字被释放的时候进行处理。值得注意的是,将报文添加到接收队列中的过程是在软中断中进行的,而这里的处理backlog
中的报文是在用户进程中进行的。
随后,会唤醒sk->sk_lock.wq
队列中的进程。
二、收包队列
想要了解TCP报文的收包过程,首先要清楚地了解TCP用于收包的三个队列:prequeue
队列、backlog
队列以及receive
队列。
receive
队列:套接口正常的收包队列,内核在收到正常的顺序到达的报文时,会将其添加到这个队列等到用户态程序使用系统调用(如recv
等)将其读走。加到这里面的报文都是经过处理的,例如已经剥离了报文头部。backlog
队列:从上文的加锁过程中我们知道,在用户程序读取套接字的时候,会将其锁定。此时,内核是不能往receive
加报文的,这期间到达的报文会被内核加到backlog
队列。注意,加到这里的报文并不一定是正常的报文,因为这个队列里的报文都是TCP
协议层还未处理的报文,只是刚从IP
层传递上来等待处理,它可能是个乱序报文,也可能会被丢弃等。prequeue
队列:这个队列是用于提高系统吞吐量的,在进程设置为阻塞模式接收报文时,阻塞期间的报文将会被添加到这个队列,从而降低在软中断中的工作量。这个队列中报文的处理是在用户读取报文时,在用户进程中进行的。
从队列的角度上来分析收包过程,可以简单理解为下面的流程图:
三、收包流程解析
收包流程可以总体分为两部分:内核收包和用户态收包。其中,内核收包指的是从IP
将报文传递给TCP
层,一直到TCP
将报文放入receive
队列的过程;用户态收包指的是用户态主动从receive
队列将报文接收到用户态的过程。两者的区别,一个是在软中断中进行的,一个是在用户进程上下文中进行的。
3.1 内核收包
TCP
层收包的入口函数为tcp_v4_rcv
,这个从TCP的网络层协议描述结构体中就可以看出来:
static const struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
};
值得注意的是,真正对报文进行处理的函数为tcp_v4_do_rcv
,上文中的处理backlog
和prequeue
队列中的报文调用的就是这个函数。
tcp_v4_rcv
该函数会根据TCP报文的情况,对其进行一个大致的分类处理,包括:
- 查找报文的sock,丢弃找不到sock的报文;
- 根据报文内容,初始化skb的私有数据;
- 针对特殊报文(那种不需要接收队列的报文,如SYN报文、FIN报文),直接调用相应的函数来处理;
- 决定是否现在调用
tcp_v4_do_rcv
进行报文的处理,现在不处理的报文判断是将其加到backlog
还是prequeue
队列。
int tcp_v4_rcv(struct sk_buff *skb)
{
const struct iphdr *iph;
const struct tcphdr *th;
struct sock *sk;
int ret;
struct net *net = dev_net(skb->dev);
/* 丢弃不是发往本机的报文 */
if (skb->pkt_type != PACKET_HOST)
goto discard_it;
/* Count it even if it's bad */
TCP_INC_STATS_BH(net, TCP_MIB_INSEGS);
/* 查看skb空间是否足够 */
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;
th = tcp_hdr(skb);
if (th->doff < sizeof(struct tcphdr) / 4)
goto bad_packet;
if (!pskb_may_pull(skb, th->doff * 4))
goto discard_it;
/* An explanation is required here, I think.
* Packet length and doff are validated by header prediction,
* provided case of th->doff==0 is eliminated.
* So, we defer the checks. */
if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))
goto csum_error;
th = tcp_hdr(skb);
iph = ip_hdr(skb);
/* This is tricky : We move IPCB at its correct location into TCP_SKB_CB()
* barrier() makes sure compiler wont play fool^Waliasing games.
*/
memmove(&TCP_SKB_CB(skb)->header.h4, IPCB(skb),
sizeof(struct inet_skb_parm));
barrier();
/* 初始化skb的TCP层私有数据 */
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th);
TCP_SKB_CB(skb)->tcp_tw_isn = 0;
TCP_SKB_CB(skb)->ip_dsfield = ipv4_get_dsfield(iph);
TCP_SKB_CB(skb)->sacked = 0;
lookup:
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk) /* 丢弃找不到sock的报文 */
goto no_tcp_socket;
process:
if (sk->sk_state == TCP_TIME_WAIT) /* 处理正在关闭的sock的报文 */
goto do_time_wait;
if (sk->sk_state == TCP_NEW_SYN_RECV) { /* 处理半连接状态的报文 */
......
}
......
/* 调用当前sock上的bpf程序对报文进行过滤 */
if (tcp_filter(sk, skb))
goto discard_and_relse;
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
skb->dev = NULL;
/* 套接字处于LISTEN状态,不需要涉及skb接收队列,直接进行处理 */
if (sk->sk_state == TCP_LISTEN) {
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
sk_incoming_cpu_update(sk);
bh_lock_sock_nested(sk); /* 获取sock上的自旋锁 */
tcp_sk(sk)->segs_in += max_t(u16, 1, skb_shinfo(skb)->gso_segs);
ret = 0;
if (!sock_owned_by_user(sk)) {
/* 套接字没有被锁定,尝试将报文加到prequeue队列,加不进去的话,进行报文的处理。加到
* prequeue队列有两个条件:(1)没有开启低延迟选项(2)当前套接口有阻塞的进程。
* 注意:在加到prequeue队列时,会唤醒SOCK上的阻塞进程。
*/
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);
} else if (unlikely(sk_add_backlog(sk, skb,
sk->sk_rcvbuf + sk->sk_sndbuf))) {
/* 套接口被锁定的话,将报文加到backlog队列 */
bh_unlock_sock(sk);
NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
goto discard_and_relse;
}
bh_unlock_sock(sk);
put_and_return:
sock_put(sk);
return ret;
......
}
tcp_v4_do_rcv
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
/* 对于已经建立TCP连接的套接字,调用tcp_rcv_established函数进行处理 */
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk->sk_rx_dst;
sock_rps_save_rxhash(sk, skb);
sk_mark_napi_id(sk, skb);
if (dst) {
if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
!dst->ops->check(dst, 0)) {
dst_release(dst);
sk->sk_rx_dst = NULL;
}
}
tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len);
return 0;
}
/* 检查TCP校验码 */
if (tcp_checksum_complete(skb))
goto csum_err;
if (sk->sk_state == TCP_LISTEN) {
/* 检查SYNCOOKIE,即通过报文的时间戳判断其里面是否包含COOKIE信息 */
struct sock *nsk = tcp_v4_cookie_check(sk, skb);
if (!nsk)
goto discard;
if (nsk != sk) { /* 说明这是一个包含SYNCOOKIE的报文,调用tcp_child_process处理剩下的部分 */
sock_rps_save_rxhash(nsk, skb);
sk_mark_napi_id(nsk, skb);
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
} else
sock_rps_save_rxhash(sk, skb);
/* 根据报文状态进行相应的处理 */
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
return 0;
......
}
tcp_rcv_established
这个函数用于处理已经建立连接的套接字的TCP报文。在处理过程中,它分为快速路径和慢速路径。其中,快速路径指的是网络状态理想的情况下,报文没有出现乱序等情况下的报文处理过程;相对的,慢速路径则处理乱序等情况的报文。据统计,网络状态良好的情况下,90%的报文是直接通过快速路径处理的。这里先分析其快速路径。
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
struct tcp_sock *tp = tcp_sk(sk);
if (unlikely(!sk->sk_rx_dst))
inet_csk(sk)->icsk_af_ops->sk_rx_dst_set(sk, skb);
tp->rx_opt.saw_tstamp = 0;
/* 检查该报文是否是快速路径报文,包括检查其序列号是否是待接收的下一个序列号、其确认号是否是发送的最后一个报文等。 */
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 (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
/* No? Slow path! */
if (!tcp_parse_aligned_timestamp(tp, th))
goto slow_path;
/* If PAWS failed, check it more carefully in slow path */
if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
goto slow_path;
}
if (len <= tcp_header_len) {
/* TCP报文里面没有数据,说明这可能是一个单纯的ack报文,对其进行处理 */
if (len == tcp_header_len) {
if (tcp_header_len ==
(sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);
tcp_ack(sk, skb, 0);
__kfree_skb(skb);
tcp_data_snd_check(sk);
return;
} else { /* 大小不符合,丢弃 */
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);
goto discard;
}
} else {
int eaten = 0;
bool fragstolen = false;
/* 检查当前套接字是否被锁定。因为这个函数在用户态收包过程中也会调用,因此当前可能是在用户进程中 */
if (tp->ucopy.task == current &&
tp->copied_seq == tp->rcv_nxt &&
len - tcp_header_len <= tp->ucopy.len &&
sock_owned_by_user(sk)) {
__set_current_state(TASK_RUNNING);
/* 这种情况下,直接将报文从内核态拷贝到用户态,其中tp->ucopy存储着当前操作该套接字的进程的信息,
* 包括存储报文的用户态buff
*/
if (!tcp_copy_to_iovec(sk, skb, tcp_header_len)) {
/* 拷贝成功,更新一些信息,包括sock的rcv_nxt、统计信息等。 */
if (tcp_header_len ==
(sizeof(struct tcphdr) +
TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);
tcp_rcv_rtt_measure_ts(sk, skb);
__skb_pull(skb, tcp_header_len);
tcp_rcv_nxt_update(tp, TCP_SKB_CB(skb)->end_seq);
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITSTOUSER);
eaten = 1;
}
}
/* 没有直接将报文拷贝到用户态 */
if (!eaten) {
if (tcp_checksum_complete_user(sk, skb))
goto csum_error;
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5;
if (tcp_header_len ==
(sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);
tcp_rcv_rtt_measure_ts(sk, skb);
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITS);
/* 将报文添加到receive队列,等待用户进行主动读取 */
eaten = tcp_queue_rcv(sk, skb, tcp_header_len,
&fragstolen);
}
tcp_event_data_recv(sk, skb);
/* 判断报文携带的ack信息是否有效,有效的话,更新当前发送窗口状态。 */
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
/* Well, only one small jumplet in fast path... */
tcp_ack(sk, skb, FLAG_DATA);
tcp_data_snd_check(sk);
if (!inet_csk_ack_scheduled(sk))
goto no_ack;
}
__tcp_ack_snd_check(sk, 0);
no_ack:
if (eaten)
kfree_skb_partial(skb, fragstolen);
sk->sk_data_ready(sk);
return;
}
}
......
}
当收到的报文是乱序报文(不是下一个待接收的序列号),那么会进入慢速路径。慢速路径中会在tcp_validate_incoming
进行有效性检查,检查的内容包括:
- 防止序列号回绕
PAWS
。该过程是通过报文的时间戳来实现的,如果报文的时间戳比记录的时间戳要新或者小于一定的差值,那么检查通过。 - 对报文序列号进行检查,包括报文的序列号要在当前窗口内。
- 如果是
RST
报文,那么对连接进行重置,并结束下面的流程。 - 如果是
SYN
等无效报文,那么发送挑战ACK
,这种ACK
速度会被限制。
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:
//进行ACK的处理,相当核心的一部分内容
if (tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) < 0)
goto discard;
//更新RTT
tcp_rcv_rtt_measure_ts(sk, skb);
/* Process urgent data. */
tcp_urg(sk, skb, th);
/* 数据处理 */
tcp_data_queue(sk, skb);
tcp_data_snd_check(sk);
tcp_ack_snd_check(sk);
return;
csum_error:
TCP_INC_STATS(sock_net(sk), TCP_MIB_CSUMERRORS);
TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
discard:
tcp_drop(sk, skb);
tcp_data_queue
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
bool fragstolen;
int eaten;
if (sk_is_mptcp(sk))
mptcp_incoming_options(sk, skb);
//报文没有内容,说明是单纯的ACK报文,不用处理
if (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq) {
__kfree_skb(skb);
return;
}
//释放该skb上的路由引用
skb_dst_drop(skb);
//剥离tcp头部
__skb_pull(skb, tcp_hdr(skb)->doff * 4);
tp->rx_opt.dsack = 0;
/* 如果报文序列号是下一个待接收的序列号,那么准备将其放到receive队列
*/
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;
}
//将报文加到队列中,并更新rcv_nxt
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
//通知应用有数据到来了
if (skb->len)
tcp_event_data_recv(sk, skb);
//检查是否有fin标志,有的话进入到四次挥手流程。从这里可以看出
//fin报文是可以携带数据的。
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);
/* 按照协议,如果窗口空洞被填充,那么需要立刻回应ack报文
*/
if (RB_EMPTY_ROOT(&tp->out_of_order_queue))
inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW;
}
//对TCP套接口是四个SACK块进行更新。如果没有失序报文,那么清空SACK;
//是否使用rcv_nxt检查每个SACK是否还有效,并移出失效的SACK
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;
}
// 如果报文在rcv_nxt之内,那么说明收到了重复、过期的报文,这种报文被称为“伪重传”。
// 对于这种情况,内核采用DSACK的方式,即将这个报文也放到SACK中,便于发送方调整超时
// 时间。
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);
//将DSACK记录下来,等待SACK
tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq);
out_of_window:
//进入快速ACK模式。快速ACK模式可以通过路由来设置,内核也会在一些情况进入快速ACK
tcp_enter_quickack_mode(sk, TCP_MAX_QUICKACKS);
//将ACK的状态设置为待调度,准备发送DSACK
inet_csk_schedule_ack(sk);
drop:
tcp_drop(sk, skb);
return;
}
/* 超出了接收窗口 */
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)) {
/* 存在部分数据发生了重传 */
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;
}
//进入失序报文的处理流程
tcp_data_queue_ofo(sk, skb);
}
tcp_data_queue_ofo
static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct rb_node **p, *parent;
struct sk_buff *skb1;
u32 seq, end_seq;
bool fragstolen;
//ECN标志检查
tcp_ecn_check_ce(sk, skb);
//内存检查
if (unlikely(tcp_try_rmem_schedule(sk, skb, skb->truesize))) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPOFODROP);
sk->sk_data_ready(sk);
tcp_drop(sk, skb);
return;
}
/* Disable header prediction. */
tp->pred_flags = 0;
inet_csk_schedule_ack(sk);
tp->rcv_ooopack += max_t(u16, 1, skb_shinfo(skb)->gso_segs);
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPOFOQUEUE);
seq = TCP_SKB_CB(skb)->seq;
end_seq = TCP_SKB_CB(skb)->end_seq;
p = &tp->out_of_order_queue.rb_node;
if (RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
/* 失序队列为空,那么初始化它,并更新一个SACK块. */
if (tcp_is_sack(tp)) {
tp->rx_opt.num_sacks = 1;
tp->selective_acks[0].start_seq = seq;
tp->selective_acks[0].end_seq = end_seq;
}
//更新红黑树。因为失序的报文是需要根据序列号来排序的,因此这里使用了红黑树来实现。
rb_link_node(&skb->rbnode, NULL, p);
rb_insert_color(&skb->rbnode, &tp->out_of_order_queue);
tp->ooo_last_skb = skb;
goto end;
}
/* 尝试将报文合并到最后一个失序报文的分散聚合数据区中
*/
if (tcp_ooo_try_coalesce(sk, tp->ooo_last_skb,
skb, &fragstolen)) {
coalesce_done:
/* For non sack flows, do not grow window to force DUPACK
* and trigger fast retransmit.
*/
if (tcp_is_sack(tp))
tcp_grow_window(sk, skb);
kfree_skb_partial(skb, fragstolen);
skb = NULL;
goto add_sack;
}
/* Can avoid an rbtree lookup if we are adding skb after ooo_last_skb */
if (!before(seq, TCP_SKB_CB(tp->ooo_last_skb)->end_seq)) {
parent = &tp->ooo_last_skb->rbnode;
p = &parent->rb_right;
goto insert;
}
/* Find place to insert this segment. Handle overlaps on the way. */
parent = NULL;
while (*p) {
parent = *p;
skb1 = rb_to_skb(parent);
if (before(seq, TCP_SKB_CB(skb1)->seq)) {
p = &parent->rb_left;
continue;
}
if (before(seq, TCP_SKB_CB(skb1)->end_seq)) {
if (!after(end_seq, TCP_SKB_CB(skb1)->end_seq)) {
/* All the bits are present. Drop. */
NET_INC_STATS(sock_net(sk),
LINUX_MIB_TCPOFOMERGE);
tcp_drop(sk, skb);
skb = NULL;
tcp_dsack_set(sk, seq, end_seq);
goto add_sack;
}
if (after(seq, TCP_SKB_CB(skb1)->seq)) {
/* Partial overlap. */
tcp_dsack_set(sk, seq, TCP_SKB_CB(skb1)->end_seq);
} else {
/* skb's seq == skb1's seq and skb covers skb1.
* Replace skb1 with skb.
*/
rb_replace_node(&skb1->rbnode, &skb->rbnode,
&tp->out_of_order_queue);
tcp_dsack_extend(sk,
TCP_SKB_CB(skb1)->seq,
TCP_SKB_CB(skb1)->end_seq);
NET_INC_STATS(sock_net(sk),
LINUX_MIB_TCPOFOMERGE);
tcp_drop(sk, skb1);
goto merge_right;
}
} else if (tcp_ooo_try_coalesce(sk, skb1,
skb, &fragstolen)) {
goto coalesce_done;
}
p = &parent->rb_right;
}
insert:
/* Insert segment into RB tree. */
rb_link_node(&skb->rbnode, parent, p);
rb_insert_color(&skb->rbnode, &tp->out_of_order_queue);
merge_right:
/* Remove other segments covered by skb. */
while ((skb1 = skb_rb_next(skb)) != NULL) {
if (!after(end_seq, TCP_SKB_CB(skb1)->seq))
break;
if (before(end_seq, TCP_SKB_CB(skb1)->end_seq)) {
tcp_dsack_extend(sk, TCP_SKB_CB(skb1)->seq,
end_seq);
break;
}
rb_erase(&skb1->rbnode, &tp->out_of_order_queue);
tcp_dsack_extend(sk, TCP_SKB_CB(skb1)->seq,
TCP_SKB_CB(skb1)->end_seq);
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPOFOMERGE);
tcp_drop(sk, skb1);
}
/* If there is no skb after us, we are the last_skb ! */
if (!skb1)
tp->ooo_last_skb = skb;
add_sack:
if (tcp_is_sack(tp))
tcp_sack_new_ofo_skb(sk, seq, end_seq);
end:
if (skb) {
/* For non sack flows, do not grow window to force DUPACK
* and trigger fast retransmit.
*/
if (tcp_is_sack(tp))
tcp_grow_window(sk, skb);
skb_condense(skb);
skb_set_owner_r(skb, sk);
}
}
3.2 用户态收包
通过TCP
在sock
层的协议定义,我们可以看出来,网络收包系统调用recv
最终会调用tcp_recvmsg
函数来进行收包。
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
.sendpage = tcp_sendpage,
.backlog_rcv = tcp_v4_do_rcv,
.release_cb = tcp_release_cb,
......
}
这个函数相对比较复杂,我们先来看一个流程图,大致了解一下其收包逻辑。
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
struct tcp_sock *tp = tcp_sk(sk);
int copied = 0;
u32 peek_seq;
u32 *seq;
unsigned long used;
int err;
int target; /* Read at least this many bytes */
long timeo;
struct task_struct *user_recv = NULL;
struct sk_buff *skb, *last;
u32 urg_hole = 0;
if (unlikely(flags & MSG_ERRQUEUE))
return inet_recv_error(sk, msg, len, addr_len);
if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) &&
(sk->sk_state == TCP_ESTABLISHED))
sk_busy_loop(sk, nonblock);
lock_sock(sk); /* 对sock进行加锁,此时可能会睡眠。 */
err = -ENOTCONN;
if (sk->sk_state == TCP_LISTEN)
goto out;
/* 计算超时时间,即获取的数据不足buff的时候,等待的时候。如果是
* 非阻塞方式,超时时间为0
*/
timeo = sock_rcvtimeo(sk, nonblock);
/* Urgent data needs to be handled specially. */
if (flags & MSG_OOB)
goto recv_urg;
if (unlikely(tp->repair)) {
err = -EPERM;
if (!(flags & MSG_PEEK))
goto out;
if (tp->repair_queue == TCP_SEND_QUEUE)
goto recv_sndq;
err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out;
/* 'common' recv queue MSG_PEEK-ing */
}
/* 获取上一次拷贝到哪里了。PEEK标志代表只获取数据,而不将其从接收队列移除。 */
seq = &tp->copied_seq;
if (flags & MSG_PEEK) {
peek_seq = tp->copied_seq;
seq = &peek_seq;
}
/* 要拷贝的数据量。这里,如果没有设置MSG_WAITALL,取报文队列和buff长度的较小值;否则,取buff的长度。 */
target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);
do {
u32 offset;
/* Are we at urgent data? Stop if we have read anything or have SIGURG pending. */
if (tp->urg_data && tp->urg_seq == *seq) {
if (copied)
break;
if (signal_pending(current)) {
copied = timeo ? sock_intr_errno(timeo) : -EAGAIN;
break;
}
}
/* 取出receive队列中的一个报文进行处理。虽然这里使用的是skb_queue_walk,但是并不是循环遍历,
* 因此它在里面进行了跳转。这里的last记录了从receive队列中取出的最后一个skb,用于后面阻塞唤
* 条件醒判断的。
*/
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
/* Now that we have two receive queues this
* shouldn't happen.
*/
if (WARN(before(*seq, TCP_SKB_CB(skb)->seq),
"TCP recvmsg seq # bug: copied %X, seq %X, rcvnxt %X, fl %X
",
*seq, TCP_SKB_CB(skb)->seq, tp->rcv_nxt,
flags))
break;
/* 计算报文偏移,因为我们可能不是要从这个报文的开头开始拷贝数据,比如之前拷贝了一半。
* 这个是根据seq(已拷贝的序列号)与当前报文序列号来判断的。
*/
offset = *seq - TCP_SKB_CB(skb)->seq;
/* 如果是SYN报文,减少一个偏移量,因为SYN标志会消耗一个序列号。 */
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_SYN)
offset--;
if (offset < skb->len)
goto found_ok_skb;/* 跳转到报文处理流程。 */
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
goto found_fin_ok;/* 跳转到FIN报文处理流程。 */
WARN(!(flags & MSG_PEEK),
"TCP recvmsg seq # bug 2: copied %X, seq %X, rcvnxt %X, fl %X
",
*seq, TCP_SKB_CB(skb)->seq, tp->rcv_nxt, flags);
}
/* 走到这里,说明receive队列中的报文处理完了。此时,如果用户态的buff已经满了,并且backlog队列没有报文,
* 那么跳出大循环,结束本次报文接收。
*/
if (copied >= target && !sk->sk_backlog.tail)
break;
if (copied) {
/* 拷贝了一部分数据,但是buff还没有满。此时,检查一下套接口的状态,如果套接口状态异常、
* 或者当前是不阻塞模式、或者当前进程有待处理的信号,那么结束本次收包。
* 这意味着,即使当前进程是阻塞模式收包,但是有待处理的信号,那么进程将不会进入阻塞状态。
*/
if (sk->sk_err ||
sk->sk_state == TCP_CLOSE ||
(sk->sk_shutdown & RCV_SHUTDOWN) ||
!timeo ||
signal_pending(current))
break;
} else {
/* 走到这里说明一个报文都没有拷贝。下面也是进行一波SOCK状态的检查,
* 存在状态异常的话,就结束收包。
*/
if (sock_flag(sk, SOCK_DONE))
break;
if (sk->sk_err) {
copied = sock_error(sk);
break;
}
if (sk->sk_shutdown & RCV_SHUTDOWN)
break;
if (sk->sk_state == TCP_CLOSE) {
if (!sock_flag(sk, SOCK_DONE)) {
/* This occurs when user tries to read
* from never connected socket.
*/
copied = -ENOTCONN;
break;
}
break;
}
if (!timeo) {
copied = -EAGAIN;
break;
}
if (signal_pending(current)) {
copied = sock_intr_errno(timeo);
break;
}
}
tcp_cleanup_rbuf(sk, copied);
/* 下面进入到prequeue队列的处理流程。注意,如果设置了sysctl_tcp_low_latency(低延迟模式),
* 那么将不会启用prequeue队列。
*/
if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
/* 此时如果sock上没有设置接收器,则将当前进程设置为其接收器。这里的设置在处理prequeue
* 和backlog队列时比较有用, 处理过程中会直接将报文内容拷贝到接收器的buff里,而不是放到
* receive队列里。
*/
if (!user_recv && !(flags & (MSG_TRUNC | MSG_PEEK))) {
user_recv = current;
tp->ucopy.task = user_recv;
tp->ucopy.msg = msg;
}
tp->ucopy.len = len;
WARN_ON(tp->copied_seq != tp->rcv_nxt &&
!(flags & (MSG_PEEK | MSG_TRUNC)));
if (!skb_queue_empty(&tp->ucopy.prequeue))
goto do_prequeue;
}
/* 走到这里,说明receive队列和prequeue队列都已经处理完了。此时检查是否
* buff满了,没有满的话进入阻塞状态(睡眠),等待数据的到来。
* 注意,这里释放sock锁的时候会进行backlog队列的处理。
*/
if (copied >= target) {
release_sock(sk);
lock_sock(sk);
} else {
/* 当前进程进入睡眠模式,最长睡眠timeo的时间。唤醒有两种可能:超时时间到了,
* 或者新的报文到了。
*/
sk_wait_data(sk, &timeo, last);
}
if (user_recv) {
int chunk;
/* __ Restore normal policy in scheduler __ */
chunk = len - tp->ucopy.len;
if (chunk != 0) {
NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk);
len -= chunk;
copied += chunk;
}
if (tp->rcv_nxt == tp->copied_seq &&
!skb_queue_empty(&tp->ucopy.prequeue)) {
do_prequeue:
/* 处理prequeue队列 */
tcp_prequeue_process(sk);
chunk = len - tp->ucopy.len;
if (chunk != 0) {
NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
len -= chunk;
copied += chunk;
}
}
}
/* PEEK模式,这里不详讲。 */
if ((flags & MSG_PEEK) &&
(peek_seq - copied - urg_hole != tp->copied_seq)) {
net_dbg_ratelimited("TCP(%s:%d): Application bug, race in MSG_PEEK
",
current->comm,
task_pid_nr(current));
peek_seq = tp->copied_seq;
}
continue;
found_ok_skb:
......
}