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机制有何用武之地。