linux内核中协议栈--tcp实现的一点细节

tcp_recvmsg中的release_sock,如有接收到的包,都必须往backlog链表中添加,在__release_sock之后sk->sk_lock.owner = NULL,表明此时的sock拥有进程上下文,并且还没有人使用,那么此时进入的包可以有机会进入prequeue队列,此时的backlog队列和receive_queue已经清空,此时进入prequeue的设为数据1,然后又锁住sock,至此之后再进来的包将进入backlog,设为数据2,显然数据2比数据1更靠后,因此再函数的最后,首先将更靠前的数据1放入receive_queue,然后在最后release_sock的时候处理backlog的skb。有一个很重要的点,那就是:
if (copied >= target) {
/* Do not sleep, just process backlog. */
release_sock(sk);
lock_sock(sk);
}
如果数据已经拷贝完毕,那么就要将所有未决的skb全部处理完。在tcp_recvmsg,首先遍历receive_queue,一个一个的处理,receive_queue的入队规则保证它里面的skb是正序的,然后如果没有了数据就会睡眠一会,睡之前处理backlog,醒来后会去处理prequeue,因为prequeue中的skb是进程处理完backlog之后,睡眠之前或者之后插入的,如果是之后,那么进程就会被唤醒。总之,正如注释中所说:1. packets in flight;2. backlog;3. prequeue;4. receive_queue这四个队列,只有下一个空了才能处理上一个。下面有个完整的描述:
首先只要有进程lock住了一个socket,那么新接收的skb必须放到backlog队列中,如果在进程recvmsg的中途因为某种原因用户进程释放了socket,但是还是保持了ucopy结构体的话,那么此时新接收的skb将有机会进入prequeue,在理解起来更简单的情况下,我们可以忽略prequeue,因此这个队列仅仅是一个可选的优化。为了从最简单起步,我们暂且不考虑prequeue,也就是说从底层接收的skb要么进入receive_queue,要么进入backlog_queue,如上所述,如果一个socket没有在进程上下文中被使用,那么该skb就链接进receive_queue,相反地情况链接进backlog_queue,为了保证用户进程按顺序接收,在tcp_recvmsg中的最后对backlog中的skb进行了处理,处理方式就是将backlog队列中skb一个个取出来然后再一个个放入receive_queue,这样就保证了用户进程能够按照顺序接收skb,这里的前提是receive_queue中的skb是按照其序列号的顺序放入的,在放入receive_queue队列的时候要判断skb的序列号看正确与否,如果不正确的话将把此skb放入一个out_of_order的队列,等到以后裁决,实际上也不是什么以后裁决,在每次插入skb到receive_queue的时候,都会检查这个out_of_order的队列,看一下能否从中抽出一个skb放入到receive_queue,换句话说只要接收到下一个要接收的skb,那么就检查out_of_order队列,尽可能的将out_of_order队列的skb拼接进receive_queue,举一个简单的例子,主机下一个要接收skb的序列号是20,然后主机接收到一个序列号为20的skb,那么很显然将它排入了receive_queue,然后更新下一个要接收的序列号为21,此时主机接收到一个序列号为23的skb,没有按序,因此不更新下一个要接收的序列号,继续等待序列号为21的skb,同时将23号放入out_of_order的队列,接下来接收到一个序列号为22的skb,不更新,继续等待21,同时将22号放入out_of_order的队列,然后21号的skb姗姗来迟,首先将下一个要接收的skb的序列号更新为22,然后察看out_of_order的队列的所有的skb,最终22号和23号都出来了,更新下一个要接收的skb的序列号为24...要指出的是,out_of_order队列中的skb是按照序列号的顺序排列的,并不是乱排的,然而backlog中的skb却不考虑顺序,因为最终它还要经过receive_queue裁决的。
接下来看看接收过程的fast-path和slow-path,所谓fast指的是尽可能的直接往用户空间复制,这样会减少队列操作,但是这种快速操作的要求是很高的,比如必须是按序的skb才能直接往用户空间拷贝,另外还有很多别的要求,否则就要往receive_queue中链接,可能还要涉及到out_of_order队列,这就是所谓的慢速操作。因此一个skb只有三种结局,一个是加入receive_queue,然后拷至用户空间,一个是加入out_of_order队列,然后再加入receive_queue,最终拷贝到用户空间,还有一种是直接拷贝到用户空间
现在看一下这个prequeue优化,事实上不要prequeue而直接将skb放入receive_queue也是可以的,但是首先我们可以看一下往receive_queue中添加skb的繁琐度,主要在函数tcp_rcv_established中体现,另外我们看一下往prequeue中添加skb的繁琐度,直接入队就返回了,不用进行任何判断,从代码执行效率的角度来看,tcp_v4_rcv是在软中断上下文中执行的,一般不太希望在这种非进程上下文中执行繁琐的操作(软中断也可能执行于进程上下文ksoftirqd),因此使用prequeue比直接往receive_queue放skb要更好一些,在一个就是看看tcp_prequeue中往prequeue中添加skb的条件,那就是socket虽然还有进程上下文,但是该进程已经不再lock该socket了,这种情况发生在进程在tcp_recvmsg中,暂时没有数据可读的时候调用sk_wait_data睡眠了,那么此时如果在软中断上下文中接收了一个skb,那么此skb就会进入到prequeue中,一旦积累了足够多的skb,就会调用:
while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
sk->sk_backlog_rcv(sk, skb1);
NET_INC_STATS_BH(LINUX_MIB_TCPPREQUEUEDROPPED);
}
在tcp_rcv_established的处理逻辑中,由于tp->ucopy.task == current这个判断不通过,因此就将此skb尽可能的放入receive_queue中,然后唤醒睡眠的进程,当然也可能放入out_of_order队列中,不过以上的这个所谓的积累足够多是比较不容易发生的,如果总发生,那还不如直接往receive_queue中放呢,事实上在tcp_recvmsg中的lock_sock一直到睡眠,所有进来的skb都没有机会进入prequeue,而是进入了backlock队列中:
if (!sock_owned_by_user(sk)) {
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);
} else
sk_add_backlog(sk, skb);
这些backlog队列中的skb将在进程睡眠前的release_sock中得到处理,如果处理完了之后还是没有完成读取len的任务,那么继续,不过要知道,在处理完全放入receive_queue的backlog队列的skb的时候,还是会有skb进来然后进入backlog的,如果没有任何skb进来,偏偏在sk_wait_data中的release和lock之间有skb进来的话,那么它就会调用tcp_prequeue进入prequeue,然后看看tcp_prequeue中的判断:
else if (skb_queue_len(&tp->ucopy.prequeue) == 1)
wake_up_interruptible(sk->sk_sleep);
这就是说,只要第一个skb到达prequeue上,那么就会唤醒socket所属于的进程,这样的话,在tcp_recvmsg接下来的逻辑中就会处理prequeue:
do_prequeue:
tcp_prequeue_process(sk);
顺便看一下sk_wait_data->sk_wait_event:
#define sk_wait_event(__sk, __timeo, __condition) /
({ int rc; /
release_sock(__sk); /
rc = __condition; /
if (!rc) { /
*(__timeo) = schedule_timeout(*(__timeo)); /
rc = __condition; /
} /
lock_sock(__sk); /
rc; /
})
如果tcp_prequeue中的wake_up_interruptible在sk_wait_event中的release_sock和schedule_timeout之间发生的话,那么进程就唤不醒了,但是这种概率非常小,也就是说必须等到__timeo时间之后才会退出睡眠状态从而继续往下走,在这段冤枉的时间里面,可能网络传来的skb非常之多,这些skb瞬间填满了整个ucopy.prequeue的空间,然后就导致了上面那个while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL)的发生,我觉得这里的实现不是很好,可能会带来一些性能上的问题,也不知道到底这个prequeue是优化选项还是退优化选项,如果说是退优化的话,那么前面已经说了原因了,现在说一下说它是优化的原因,既然receive_queue中没有skb了,那么接下来收到的skb很有可能就是需要接收的,注意由于可能有乱序的skb进来,这里仅仅说是可能,于是进一步在处理prequeue的时候,也就是调用tcp_prequeue_process的时候,进而调用tcp_rcv_established的时候,由于此时是在socket所有者进程的上下文,既然可能是正序的skb,那么也就很有可能可以直接将skb复制到用户空间并且成功,这样的话当进程从tcp_recvmsg中的tcp_prequeue_process返回的时候,数据已经进入用户空间了,而不用再重新来一次循环从receive_queue中拉取skb了:
...
if (copied >= target) { //处理未决的skb,所谓未决就是skb不在receive_queue中而是因为receive_queue暂时被使用而不得不加入到了别的队列中,比如backlog队列,这里处理它主要是因为用户进程必须按序接收数据,而一次tcp_recvmsg的调用前后都必须保证所有的还没有读取的skb是顺序存在于receive_queue的,注意并不在这里处理prequeue,也并不认为prequeue中的skb是未决的,它只是receive_queue的暂存地罢了,顺便看看能否偷个懒将skb直接送给用户进程。
release_sock(sk);
lock_sock(sk);
} else
sk_wait_data(sk, &timeo);
if (user_recv) {
int chunk;
if ((chunk = len - tp->ucopy.len) != 0) { //在sk_wait_data中的release中可能已经将backlog中的数据直接拷贝到用户空间了,并且更新了ucopy的各个相关字段,tp->ucopy.len在进程上下文调用的sk_backlog_rcv也就是tcp_rcv_established->tcp_copy_to_iovec中被更新,因此在这里能发现这件事
NET_ADD_STATS_USER(LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk);
len -= chunk;
copied += chunk;
}
if (tp->rcv_nxt == tp->copied_seq &&
skb_queue_len(&tp->ucopy.prequeue)) {
do_prequeue:
tcp_prequeue_process(sk);
if ((chunk = len - tp->ucopy.len) != 0) { //和backlog相同的方式发现数据在prequeue中已经拷贝到用户空间了
NET_ADD_STATS_USER(LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
len -= chunk;
copied += chunk;
}
}
...
receive_queue是最重要的队列,backlog是一个临时队列,也很重要,这是因为软中断不能睡眠,也尽量不要长时间争抢锁,如果用户在从内核复制一个很大的数据,那么软中断的tcp_v4_recv就要浪费大量时间等待进程释放sk_lock.owner了,而prequeue是一个可有可无的队列,并且它的存在喜忧参半,无所谓有也无所谓无

你可能感兴趣的:(linux)