5.6 TCP prequeue

来源:互联网 发布:ubuntu安装交叉编译器 编辑:程序博客网 时间:2024/05/16 17:36

  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))  没有被放入prequeue2037                 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) //内核要求低延迟或不是处于进程上下文,则不能使用prequeue1924         return false;1925  //现在是处于进程上下文1926     if (skb->len <= tcp_hdrlen(skb) && //skb中无数据1927         skb_queue_len(&tp->ucopy.prequeue) == 0) prequeue中没有skb1928         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             }17101711             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;17721773             /* __ Restore normal policy in scheduler __ */17741775             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             }17801781             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中进行处理17851786                 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);18981899     if (user_recv) {1900         if (!skb_queue_empty(&tp->ucopy.prequeue)) {1901             int chunk;19021903             tp->ucopy.len = copied > 0 ? len : 0;19041905             tcp_prequeue_process(sk);19061907             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         }19131914         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) { //进程已经锁定socket5176                     __set_current_state(TASK_RUNNING);51775178                     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);13851386     NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPPREQUEUED);13871388     /* RX process wants to run with disabled BHs, though it is not1389      * 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();   13941395     /* 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机制有何用武之地。

1 0
原创粉丝点击