博客:blog.focus-linux.net linuxfocus.blog.chinaunix.net
======================================================================================================
在Linux内核的TCP实现中,TCP有三个接收队列——除去错误队列。这三个队列分别是struck sock中的sk_receive_queue和sk_backlog,以及struct tcp_sock中的prequeue。这三个队列作用,网上已经有很多文章论述了。这里只简单介绍一下,sk_receive_queue是真正的接收队列,收到的TCP数据包经过检查和处理后,就会保存到这个队列中。sk_backlog是当socket处于用户进程的上下文时(即用户正在对socket进行系统调用,如recv),Linux收到数据包时,在软中断处理过程中,会将数据包保存到sk_backlog中,然后直接返回。而prequeue则是在,该socket没有正在被用户进程使用时,由软中断直接将数据包保存在prequeue中,然后返回。
我们可以从tcp的接收处理函数中,验证上面的结果。下面的代码来自tcp_v4_rcv函数
bh_lock_sock_nested(sk); ret = 0; if (!sock_owned_by_user(sk)) {
/* 当sock没有被用户进程占有的时候,可以将数据包保存到prequeue中。失败的话,才进入tcp的真正的数据包处理过程 */ if (!tcp_prequeue(sk, skb)) ret = tcp_v4_do_rcv(sk, skb); } else if (unlikely(sk_add_backlog(sk, skb))) {
/* 当sock被用户进程占有时,将数据包保存到backlog中,然后返回 */ bh_unlock_sock(sk); NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP); goto discard_and_relse; } bh_unlock_sock(sk);
在sock被用户进程占有时,kernel将数据包保存到backlog中,这个是可以理解的。因为即使是软中断处理流程,也需要尽快完成,好让kernel尽快处理下一个数据包。那么当sock不被用户进程占有时,kernel将数据包保存到prequeue中,自然也是这个道理。这些数据包会在用户进程调用receive的时候,再进行TCP数据包完整的处理流程。
网上的大部分资料,都只是介绍了这三个队列的用途。但是看到这里,其实我们应该有一个疑问。backlog和prequeue都是保存的未经处理的数据,为什么需要两个不同的队列呢?为了解答这个疑问,我们需要研究一下prequeue和backlog是如何应用的?前面是两个队列的写入操作,下面看看两个队列何时被读取。prequeue的处理函数tcp_prequeue_process,如前文所说,在TCP的读取数据函数tcp_recvmsg中调用。在tcp_recvmsg的入口,会调用lock_sock来设置sk->sk_lock.owned,表示该sock由用户进程占有,然后会对receive_queue和prequeue中的数据包进行处理。正因为sock被用户进程处理时,会访问prequeu,所以软中断只能将数据保存到backlog中,以避免竞争。那么为什么在sock不由用户进程占有时,只能保存到prequeu中,而不能重入backlog呢?
让我们继续跟进,看看何时处理backlog的数据包。Oh,my god,居然是在__release_sock中,这真的有点出乎我的意料。这也就解释了,为什么需要两个队列来保存未处理数据包。对于sock来说,一共有两种状态:1. 用户进程占用该sock;2. 用户进程未占用该sock;而kernel需要在任何情况下,都要能够保证tcp数据包处理的软中断快速返回。而保存未处理数据包的队列,无论如何也要在上述的一个情况下,访问未处理的数据包。那么这不可避免的会有资源竞争。所以为了避免这种情况,当sock被用户进程占用时,让它处理prequeue中的数据包,软中断则往backlog中保存。当sock不被用户进程占用时,会去访问backlog中的数据包,软中断则往prequeue中保存。