IP层组合出一包数据后,如果数据包首部的协议字段表明上层协议为TCP,则调用TCP的tcp_v4_rcv()函数将数据传递给传输层继续处理,传输层的整体处理过程是非常复杂的,这篇笔记就先来看看传输层的入口处是如何处理的。
1. 三个队列
TCP对输入数据包的整体处理流程可以简单的用下图表达:
从上图中可以看到TCP的接收过程会涉及到三个队列:prequeue队列、receive队列以及backlog队列,这里首先介绍下这三个队列的作用,然后再跟踪源代码实现。
从数据接收的角度考虑,可以将TCP的传输控制块的状态分为如下三种:
- 用户进程正在读写数据,此时TCB是被锁定的
- 用户进程正在读写数据,但是因为没有数据可用而进入了休眠态,等待数据可用,这时TCB是不会被用户进程锁定的
- 用户进程根本就没有在读写数据,当然这时TCB也不会被用户进程锁定
再考虑一点,由于协议栈对输入数据包的处理实际上都是软中断中进行的,出于性能的考虑,我们总是期望软中断能够快速的结束。
这样,再来理解上图:
- 如果被用户进程锁定,那么处于情形一,此时由于互斥,没得选,为了能快速结束软中断处理,将数据包放入到backlog队列中,这类数据包的真正处理是在用户进程释放TCB时进行的;
- 如果没有被进程锁定,那么首先尝试将数据包放入prequeue队列,原因还是为了尽快让软中断结束,这种数据包的处理是在用户进程读数据过程中处理的;
- 如果没有被进程锁定,prequeue队列也没有接受该数据包(出于性能考虑,比如prequeue队列不能无限制增大),那么没有更好的办法了,必须在软中断中对数据包进行处理,处理完毕后将数据包加入到receive队列中。
综上,可以总结如下:
- 放入receive队列的数据包都是已经被TCP处理过的数据包,比如校验、回ACK等动作都已经完成了,这些数据包等待用户空间程序读即可;相反,放入backlog队列和prequeue队列的数据包都还需要TCP处理,实际上,这些数据包也都是在合适的时机通过tcp_v4_do_rcv()处理的;
- 设计三个队列是有其特殊目的的,理解背后的设计意图非常重要。
2. 接收入口:tcp_v4_rcv()
tcp_v4_rcv()是TCP协议的接收入口函数。
int tcp_v4_rcv(struct sk_buff *skb)
{
const struct iphdr *iph;
struct tcphdr *th;
struct sock *sk;
int ret;
//非本机数据包扔掉
if (skb->pkt_type != PACKET_HOST)
goto discard_it;
/* Count it even if it's bad */
TCP_INC_STATS_BH(TCP_MIB_INSEGS);
//下面主要是对TCP段的长度进行校验。注意pskb_may_pull()除了校验,还有一个额外的功能,
//如果一个TCP段在传输过程中被网络层分片,那么在目的端的网络层会重新组包,这会导致传给
//TCP的skb的分片结构中包含多个skb,这种情况下,该函数会将分片结构重组到线性数据区
//保证skb的线性区域至少有20个字节数据
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;
//保证skb的线性区域至少包括实际的TCP首部
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_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
goto bad_packet;
//初始化skb中的控制块
th = tcp_hdr(skb);
iph = ip_hdr(skb);
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin