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机制有何用武之地。
- 5.6 TCP prequeue
- TCP的prequeue分析
- TCP receive_queue prequeue backlog
- 关于tcp的prequeue的一些说明
- 评价linux协议栈tcp实现中的prequeue
- TCP offload engine(TCP减负引擎网卡), TCP backlog/prequeue buffer (TCP后备预备队列)
- prequeue和backlog和receive
- tcp
- TCP
- TCP
- tcp
- tcp
- tcp
- tcp
- tcp
- TCP
- TCP
- TCP
- BeautifulSoup 官方文档
- 彻底学通string.Format以及IFormattable,IFormatProvider,ICustomFormatter(1)
- 如何打开Mysql服务
- 一步一步学习ASP.NET 5 (七)- 快速把ASP.NET 5应用以Docker方式部署到Mac上
- [面试时]如何讲清楚objective-c内存管理
- 5.6 TCP prequeue
- frame、center、bounds、transframe属性
- 偶数求和 HDU 2012
- bzoj 3560
- 用Gradle 构建你的android程序
- 文章标题
- Flash基础知识
- HTML合并单元格
- 使用HttpClient时遇到的 java.net.SocketException: Socket closed异常