5.6 TCP prequeue

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

你可能感兴趣的:(tcp,linux内核)