TCP接收方存在3种队列:
1 Backlog Queue (sk->backlog)
2 Prequeue Queue (tp->ucopy.prequeue)
3 Receive Queue (sk->receive_queue)
然后来看3个队列的区别。
首先sk_backlog队列是当当前的sock在进程上下文中被使用时,如果这个时候有数据到来,则将数据拷贝到sk_backlog.
prequeue则是数据buffer第一站,一般都是这里,如果prequeue已满,则会拷贝数据到receive_queue队列种。
最后一个receive_queue也就是进程上下文第一个取buffer的队列
这里为什么要有prequeue呢,直接放到receive_queue不就好了.因为receive_queue的处理比较繁琐
(看tcp_rcv_established的实现就知道了,分为slow path和fast path),而软中断每次只能处理一个数据包
(在一个cpu上),因此为了软中断能尽快完成,我们就可以先将数据放到prequeue中(tcp_prequeue),然后软
中断就直接返回. 而处理prequeue就放到进程上下文(tcp_recvmsg调用中)去处理了.
最后在分析tcp_v4_rcv和tcp_recvmsg之前,我们要知道tcp_v4_rcv还是处于软中断上下文,
而tcp_recvmsg是处于进程上下文,因此比如socket_lock_t才会提供一个owned来锁住对应的sock。
而我们也就是需要这3个队列来进行软中断上下文和进程上下文之间的通信。最终当数据拷贝到对应队列,
则软中断调用返回。这里要注意的是相同的函数在软中断上下文和进程上下文种调用是不同的,我们下面就会看到(比如tcp_rcv_established函数) 。
首先数据包进入软中断上下文的tcp_v4_rcv函数
该函数的处理过程是:
首先bh_lock_sock_nested调用加自旋锁
然后判断当前sock是否被用户进程占用(sock_owned_by_user函数判断)。如果没有的话,就调用tcp_prequeue将数据包加入
prequeue队列中;否则调用sk_add_backlog将它加入backlog队列中。
tcp_prequeue调用流程如下
该函数的本意是将数据包加入prequeue队列,以待tcp_recvmsg函数调用(通过函数tcp_prequeue_process),这时就返回0;
如果ucopy.task为NULL的话,表示当前没有pending的进程,函数返回1,数据包在软中断(函数tcp_v4_do_rcv)中处理。
还有一种情况是prequeue已满,则在软中断上下文中处理该队列中的所有数据包(函数 sk_backlog_rcv)。
最后,如果发现该skb使得prequeue从空变为非空,则调用wake_up_interruptible(sk->sk_sleep)唤醒在该sock上的等待进程
(该进程在tcp_recvmsg函数中通过sk_wait_data调用进入该sock的等待队列)。
不管是软中断中的数据包处理还是系统调用中的数据包的处理,都是调用tcp_v4_do_rcv。在连接建立后,该函数的作用是处理数据包,
数据包加入receive queue中。
先分析数据包如何接收到用户进程的——tcp_recvmsg函数。
下一步从receive queue中读取数据包
receive queue的特征是
(1) already acked
(2) guaranteed in order
(3) contain no holes but
(4) apparently may contain overlapping data(数据可能重叠)
当receive queue没有可用数据或已经读取完后,进入下面流程
接下来程序调用函数
该函数的主要作用是发送一个通告窗口更新的ACK,因为用户进程消费了读缓存中的数据。
流程到此的条件是:
● the receive queue is empty,
● no serious errors or state changes were noted and
● we haven't consumed sufficient data to return to the caller.
分析sk_wait_data函数
接下来的是receive queue中skb的数据读取
最后在跳出循环后,prequeue队列又一次被处理(因为其中可能还有数据,可以读取到本进程中)
数据包处理函数将在后面分析