TCP实现之:TCP报文接收

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,上文中的处理backlogprequeue队列中的报文调用的就是这个函数。

tcp_v4_rcv

该函数会根据TCP报文的情况,对其进行一个大致的分类处理,包括:

  1. 查找报文的sock,丢弃找不到sock的报文;
  2. 根据报文内容,初始化skb的私有数据;
  3. 针对特殊报文(那种不需要接收队列的报文,如SYN报文、FIN报文),直接调用相应的函数来处理;
  4. 决定是否现在调用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 用户态收包

通过TCPsock层的协议定义,我们可以看出来,网络收包系统调用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:
    ......
}
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值