TCP中用于接收skb的缓存除了sk->sk_receive_queue之外,还有prequeue。TCP prequeue中的包会在进程上下文中处理,而非软中断上下文。TCP prequeue特性会给带来较大的延迟,其优点在于这个特性在理论上给与了别的进程以及别的socket连接更多的公平性,但实际情况如何不得而知。开启这个功能的条件是net.ipv4.tcp_low_latency内核选项为0,即允许较大的延迟,这也从另一个角度说明prequeue机制的延迟比较大。
下面我们研究一下prequeue机制延迟大的原因。TCP收到skb后调用tcp_v4_do_rcv函数进行处理之前会先调用tcp_prequeue函数:
1961 int tcp_v4_rcv(struct sk_buff *skb)
1962 {
...
2026 if (!sock_owned_by_user(sk)) {
...
2035 {
2036 if (!tcp_prequeue(sk, skb)) 没有被放入prequeue
2037 ret = tcp_v4_do_rcv(sk, skb);
2038 }
...
tcp_prequeue函数在成功将数据放入prequeue时会返回“true”:
1919 bool tcp_prequeue(struct sock *sk, struct sk_buff *skb)
1920 {
1921 struct tcp_sock *tp = tcp_sk(sk);
1922
1923 if (sysctl_tcp_low_latency || !tp->ucopy.task) //内核要求低延迟或不是处于进程上下文,则不能使用prequeue
1924 return false;
1925 //现在是处于进程上下文
1926 if (skb->len <= tcp_hdrlen(skb) && //skb中无数据
1927 skb_queue_len(&tp->ucopy.prequeue) == 0) prequeue中没有skb
1928 return false;
1929
1930 skb_dst_force(skb);
1931 __skb_queue_tail(&tp->ucopy.prequeue, skb); //skb先放入preuque中,暂时跳过TCP协议处理
1932 tp->ucopy.memory += skb->truesize;
1933 if (tp->ucopy.memory > sk->sk_rcvbuf) { //缓存被占满
1934 struct sk_buff *skb1;
1935
1936 BUG_ON(sock_owned_by_user(sk));
1937
1938 while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
1939 sk_backlog_rcv(sk, skb1); //调用tcp_v4_do_rcv函数进行处理
1940 NET_INC_STATS_BH(sock_net(sk),
1941 LINUX_MIB_TCPPREQUEUEDROPPED);
1942 }
1943
1944 tp->ucopy.memory = 0;
1945 } else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
1946 wake_up_interruptible_sync_poll(sk_sleep(sk),
1947 POLLIN | POLLRDNORM | POLLRDBAND);
1948 if (!inet_csk_ack_scheduled(sk))
1949 inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,
1950 (3 * tcp_rto_min(sk)) / 4,
1951 TCP_RTO_MAX);
1952 }
1953 return true;
1954 }
1926-1928:无数据的包应用进程不感兴趣,但只有prequeue中没有skb时无数据的skb才不需要放入prequeue,否则会造成乱序
1933-1944:如果prequeue队列中积累过多的数据,则需要将队列中所有的skb全部送入TCP协议处理函数
1945-1951:如果prequeue中从无到有增加了一个skb,则需要唤醒等待数据的进程进行处理,并设置延迟ACK定时器
tp->ucopy.task是在tcp_recvmsg函数中设置的:
1545 int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
1546 size_t len, int nonblock, int flags, int *addr_len)
1547 {
1548 struct tcp_sock *tp = tcp_sk(sk);
1549 int copied = 0;
1550 u32 peek_seq;
1551 u32 *seq;
1552 unsigned long used;
1553 int err;
1554 int target; /* Read at least this many bytes */
1555 long timeo;
1556 struct task_struct *user_recv = NULL;
1557 bool copied_early = false;
1558 struct sk_buff *skb;
1559 u32 urg_hole = 0;
...
1703 if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {//第一次调用tcp_sendmsg时tp->ucopy.task和user_recv都为NULL,判断成立
1704 /* Install new reader */
1705 if (!user_recv && !(flags & (MSG_TRUNC | MSG_PEEK))) {
1706 user_recv = current;
1707 tp->ucopy.task = user_recv;
1708 tp->ucopy.iov = msg->msg_iov;
1709 }
1710
1711 tp->ucopy.len = len; //允许读len个字节的数据
...
1742 if (!skb_queue_empty(&tp->ucopy.prequeue)) //prequeue中已经有skb了,赶快去处理
1743 goto do_prequeue;
...
1758 if (copied >= target) {//完成数据copy任务
1759 /* Do not sleep, just process backlog. */
1760 release_sock(sk);//处理backlog队列中的包;这些包会进入tcp_v4_do_rcv函数
1761 lock_sock(sk);
1762 } else//未完成数据copy任务
1763 sk_wait_data(sk, &timeo);//睡眠,等待数据到来
...
1770 if (user_recv) {//只有设置了tp->ucopy.task的进程才会进入这个分支
1771 int chunk;
1772
1773 /* __ Restore normal policy in scheduler __ */
1774
1775 if ((chunk = len - tp->ucopy.len) != 0) {//有数据在进程上下文的快速路径中被copy到了用户缓存
1776 NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk);
1777 len -= chunk;
1778 copied += chunk;
1779 }
1780
1781 if (tp->rcv_nxt == tp->copied_seq && //接收缓存中没有数据
1782 !skb_queue_empty(&tp->ucopy.prequeue)) {//但prequeue中有数据
1783 do_prequeue:
1784 tcp_prequeue_process(sk);//将prequeue中的skb放入tcp_v4_do_rcv中进行处理
1785
1786 if ((chunk = len - tp->ucopy.len) != 0) {//有数据在进程上下文的快速路径中被copy到了用户缓存
1787 NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
1788 len -= chunk;
1789 copied += chunk;
1790 }
1791 }
1792 }
...
1897 } while (len > 0);
1898
1899 if (user_recv) {
1900 if (!skb_queue_empty(&tp->ucopy.prequeue)) {
1901 int chunk;
1902
1903 tp->ucopy.len = copied > 0 ? len : 0;
1904
1905 tcp_prequeue_process(sk);
1906
1907 if (copied > 0 && (chunk = len - tp->ucopy.len) != 0) {
1908 NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
1909 len -= chunk;
1910 copied += chunk;
1911 }
1912 }
1913
1914 tp->ucopy.task = NULL;//禁用prequeue队列
1915 tp->ucopy.len = 0;
1916 }
...
1705-1708:设置tp->ucopy.task不为空,从而安装了一个接收器;这时由于进程没有调用release_sock,故软中断收包时不能进入tcp_prequeue函数,只能将包放入backlog队列
1760-1761:调用release_sock完毕到调用lock_sock完毕的这段时间里,因为这时tp->ucopy.task不为空,故可能有一些包在软中断上下文中进入prequeue
1763:用sk_wait_data函数会一直等到有包进入prequeue:
841 #define sk_wait_event(__sk, __timeo, __condition) \
842 ({ int __rc; \
843 release_sock(__sk); \
844 __rc = __condition; \
845 if (!__rc) { \
846 *(__timeo) = schedule_timeout(*(__timeo)); \
847 } \
848 lock_sock(__sk); \
849 __rc = __condition; \
850 __rc; \
851 })
tcp_prequeue函数1946-1947行代码会将sk_wait_data函数从846行唤醒。sk_wait_event在睡眠以前,会调用release_sock将socket释放,这样在其醒来并调用lock_sock之前软中断就可以将收到的包放入prequeue队列中然后唤醒睡眠的进程。但这里有个问题:如果进程在sk_wait_event函数中刚刚调用了release_sock释放socket,然后立即被软中断打断(这时进程还没有睡眠),有包被放入空的prequeue队列中。在tcp_prequeue函数会执行唤醒动作,但此时没有进程睡眠。然后软中断返回,进程恢复运行,并睡眠。这时虽然软中断中还可能会有包放入prequeue中,但不会唤醒进程,进程会一直睡眠掉超时。这种情况会造成更大的收包延迟,只不过这种概率很低。进程在调用release_sock时会调用tcp_v4_do_rcv函数处理backlog中的数据,这时如果数据进入了快速路径,则会直接被copy到用户缓存中: 5076 int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
5077 const struct tcphdr *th, unsigned int len)
5078 {
5079 struct tcp_sock *tp = tcp_sk(sk);
...
5174 if (tp->ucopy.task == current &&//当前是在进程上下文中运行
5175 sock_owned_by_user(sk) && !copied_early) { //进程已经锁定socket
5176 __set_current_state(TASK_RUNNING);
5177
5178 if (!tcp_copy_to_iovec(sk, skb, tcp_header_len)) //copy数据到用户缓存中,不必交付接收缓存
5179 eaten = 1;
5180 }
...
在慢速路径中,数据会交付tcp_data_queue函数:
4300 static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
4301 {
...
4326 if (tp->ucopy.task == current &&
4327 tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&
4328 sock_owned_by_user(sk) && !tp->urg_data) {
4329 int chunk = min_t(unsigned int, skb->len,
4330 tp->ucopy.len);
4331
4332 __set_current_state(TASK_RUNNING);
4333
4334 local_bh_enable();
4335 if (!skb_copy_datagram_iovec(skb, 0, tp->ucopy.iov, chunk)) { //copy数据到用户缓存中,不必交付接收缓存
4336 tp->ucopy.len -= chunk;
4337 tp->copied_seq += chunk;
4338 eaten = (chunk == skb->len);
4339 tcp_rcv_space_adjust(sk);
4340 }
4341 local_bh_disable();
4342 }
...
tcp_prequeue_process函数会将prequeue中的skb放入tcp_v4_do_rcv函数中:
1381 static void tcp_prequeue_process(struct sock *sk)
1382 {
1383 struct sk_buff *skb;
1384 struct tcp_sock *tp = tcp_sk(sk);
1385
1386 NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPPREQUEUED);
1387
1388 /* RX process wants to run with disabled BHs, though it is not
1389 * necessary */
1390 local_bh_disable();
1391 while ((skb = __skb_dequeue(&tp->ucopy.prequeue)) != NULL)
1392 sk_backlog_rcv(sk, skb);//调用tcp_v4_do_rcv,如果在快速路径中数据也会被直接copy到用户缓存
1393 local_bh_enable();
1394
1395 /* Clear memory counter. */
1396 tp->ucopy.memory = 0;
1397 }
tcp_prequeue_process函数调用tcp_v4_do_rcv处理skb时会关闭本地软中断,这样进程就不会被软中断打断,也不会被其它进程抢占,从而获得较高的运行优先级。
下面总结一下prequeue机制下应用进程收包的流程。
(1)应用进程在通过系统调用使用tcp_recvmsg函数接收数据时安装一个prequeue队列的接收器,释放sock,然后守株待兔
(2)内核收包软中断在进入tcp_v4_rcv函数时如果sock没有被进程锁定,则会将skb放入prequeue中,并唤醒进程
(3)进程被唤醒后锁定sock,调用tcp_prequeue_process函数将prequeue中所有的skb送入tcp_v4_do_rcv函数进行处理
(4)在进程锁定sock的时候,内核软中断会将skb放入backlog队列中而不是prequeue队列,在进程是否sock的时候backlog队列中的skb也会被送入tcp_v4_do_rcv函数
(5)送入tcp_v4_do_rcv函数的数据会被送入到快速路径或慢速路径进行处理。而无论是进入快速路径还是慢速路径,skb中的数据最终都会被copy到应用进程的缓存中
可见,prequeue机制与普通机制的主要区别在于,在进程没有收取到足够的数据而睡眠等待时,prequeue机制会将skb放入prequeue队列中再唤醒进程,再由进程对skb进行TCP协议处理,再copy数据;而普通模式下skb会在软中断上下文处理,在放入sk->sk_receive_queue队列中后再唤醒进程,进程被唤醒后只是copy数据。对比普通模式,prequeue机制下使得skb的TCP协议处理延迟,延迟的时间为从skb被放入prequeue队列并唤醒进程开始,到进程被调度到时调用tcp_prequeue_process函数处理skb时截止。对于收数据的进程而言在一次数据接收过程中其实并没有延迟,因为普通模式下进程也会经历睡眠-唤醒的过程。但由于TCP协议处理被延迟,导致ACK的发送延迟,从而使数据发送端的数据发送延迟,最终会使得整个通信过程延迟增大。
现在我们知道prequeue机制延迟大的原因了:skb的TCP协议处理不是在软中断中进行,而是推迟到应用进程调用收包系统调用时。在极力追求速度与效率的互联网世界,对于以高吞吐量、低延迟而称雄的TCP而言,不知高延迟的prequeue机制有何用武之地。