4.4 TCP Small Queue(TSQ)

  TCP发送的数据经过IP层添加IP包头后,会被发送到IP数据栈和网卡之间的队列中,当网卡能够发送数据时会到这个队列中去取skb。当TCP发送数据过快时,或一个带宽很大或者包速率很大的非TCP数据流会把队列里的所有空间占满,造成数据包丢失和延时的问题。更糟的是,这样很可能会使另一个缓存产生,进而产生一个静止队列(standing queue),造成更严重的延时并使TCP的RTT和拥塞窗口的计算出现问题。Linux3.6.0出现后,Linux内核增加了TCP小队列(TCP Small Queue)的机制,用于解决该问题。TCP小队列对每个TCP数据流中,能够同时参与排队的字节数做出了限制,这个限制是通过net.ipv4.tcp_limit_output_bytes内核选项实现的。当TCP发送的数据超过这个限制时,多余的数据会被放入另外一个队列中,再通过tastlet机制择机发送。下面分析一下TSQ的实现。

  TSQ tasklet是一个每CPU变量,这样可以保证在多核条件下的并发:

 678 struct tsq_tasklet {     
 679     struct tasklet_struct   tasklet;
 680     struct list_head    head; /* queue of tcp sockets */
 681 };
 682 static DEFINE_PER_CPU(struct tsq_tasklet, tsq_tasklet);

  TSQ tasklet的初始化是在内核加载时完成:

3375 void __init tcp_init(void)
3376 {
...
3456     tcp_tasklet_init();
3457 }
 772 void __init tcp_tasklet_init(void)
 773 {   
 774     int i;
 775     
 776     for_each_possible_cpu(i) {
 777         struct tsq_tasklet *tsq = &per_cpu(tsq_tasklet, i);
 778 
 779         INIT_LIST_HEAD(&tsq->head);
 780         tasklet_init(&tsq->tasklet,
 781                  tcp_tasklet_func,
 782                  (unsigned long)tsq); //初始化tasklet任务
 783     }
 784 }       
  tasklet_init函数指定了tcp_tasklet_func函数为处理函数:
 697 static void tcp_tasklet_func(unsigned long data)
 698 {
 699     struct tsq_tasklet *tsq = (struct tsq_tasklet *)data;
 700     LIST_HEAD(list);
 701     unsigned long flags;
 702     struct list_head *q, *n;
 703     struct tcp_sock *tp;
 704     struct sock *sk;
 705 
 706     local_irq_save(flags);
 707     list_splice_init(&tsq->head, &list); //将tsq->head队列中的成员转移到list中
 708     local_irq_restore(flags);
 709 
 710     list_for_each_safe(q, n, &list) { //每一个成员就是一个socket
 711         tp = list_entry(q, struct tcp_sock, tsq_node);
 712         list_del(&tp->tsq_node);
 713 
 714         sk = (struct sock *)tp;
 715         bh_lock_sock(sk);
 716 
 717         if (!sock_owned_by_user(sk)) { //socket没有被进程访问
 718             tcp_tsq_handler(sk); //发送数据
 719         } else { //scoket正在被进程访问
 720             /* defer the work to tcp_release_cb() */
 721             set_bit(TCP_TSQ_DEFERRED, &tp->tsq_flags);
 722         }
 723         bh_unlock_sock(sk);
 724 
 725         clear_bit(TSQ_QUEUED, &tp->tsq_flags);
 726         sk_free(sk);
 727     }
 728 }
  如果scoket没有被进程访问,则直接调用tcp_tsq_handler发送数据;否则设置TCP_TSQ_DEFERRED标记,这样当用户进程调用release_sock函数解锁socket时会调用tcp_release_cb函数:

 741 void tcp_release_cb(struct sock *sk)
 742 {
 743     struct tcp_sock *tp = tcp_sk(sk);
 744     unsigned long flags, nflags;
 745 
 746     /* perform an atomic operation only if at least one flag is set */
 747     do {
 748         flags = tp->tsq_flags;
 749         if (!(flags & TCP_DEFERRED_ALL))
 750             return; 
 751         nflags = flags & ~TCP_DEFERRED_ALL;
 752     } while (cmpxchg(&tp->tsq_flags, flags, nflags) != flags);
 753     
 754     if (flags & (1UL << TCP_TSQ_DEFERRED)) //如果设置了TCP_TSQ_DEFERRED标记
 755         tcp_tsq_handler(sk);
 756     
 757     if (flags & (1UL << TCP_WRITE_TIMER_DEFERRED)) {
 758         tcp_write_timer_handler(sk);
 759         __sock_put(sk);
 760     }   
 761     if (flags & (1UL << TCP_DELACK_TIMER_DEFERRED)) {
 762         tcp_delack_timer_handler(sk);
 763         __sock_put(sk);
 764     }   
 765     if (flags & (1UL << TCP_MTU_REDUCED_DEFERRED)) {
 766         sk->sk_prot->mtu_reduced(sk);
 767         __sock_put(sk);
 768     }   
 769 }   
  可见在进程锁定socket的情况下tcp_tasklet_func函数只是延迟调用了tcp_tsq_handler函数:
 684 static void tcp_tsq_handler(struct sock *sk)
 685 {
 686     if ((1 << sk->sk_state) &
 687         (TCPF_ESTABLISHED | TCPF_FIN_WAIT1 | TCPF_CLOSING |
 688          TCPF_CLOSE_WAIT  | TCPF_LAST_ACK))
 689         tcp_write_xmit(sk, tcp_current_mss(sk), 0, 0, GFP_ATOMIC);
 690 }
  TSQ tasklet的主要功能是在软中断上下文中遍历挂入TSQ队列中的socket,并调用tcp_write_xmit函数发送socket发送队列中的数据。那么socket在什么情况下会被加入到TSQ队列中呢?先来看负责发送TCP包的tcp_transmit_skb函数:
 828 static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
 829                 gfp_t gfp_mask)
 830 {
...
 890     skb->destructor = (sysctl_tcp_limit_output_bytes > 0) ?
 891               tcp_wfree : sock_wfree;
 892     atomic_add(skb->truesize, &sk->sk_wmem_alloc);
 893 
...
  如果net.ipv4.tcp_limit_output_bytes大于0,则skb->destructor会被设置为tcp_wfree函数,sk->sk_wmen_alloc也会被加上skb的真实大小。当skb被释放时tcp_wfree函数就会被调用:

 791 void tcp_wfree(struct sk_buff *skb)
 792 {
 793     struct sock *sk = skb->sk;
 794     struct tcp_sock *tp = tcp_sk(sk);
 795 
 796     if (test_and_clear_bit(TSQ_THROTTLED, &tp->tsq_flags) &&
 797         !test_and_set_bit(TSQ_QUEUED, &tp->tsq_flags)) { //设置TSQ_QUEUED标记,防止重复处理
 798         unsigned long flags;
 799         struct tsq_tasklet *tsq;
 800     
 801         /* Keep a ref on socket.
 802          * This last ref will be released in tcp_tasklet_func()
 803          */
 804         atomic_sub(skb->truesize - 1, &sk->sk_wmem_alloc);
 805 
 806         /* queue this socket to tasklet queue */
 807         local_irq_save(flags);
 808         tsq = &__get_cpu_var(tsq_tasklet); //获取当前CPU对应的tsq_tasklet
 809         list_add(&tp->tsq_node, &tsq->head); //将socket加入到TSQ队列中
 810         tasklet_schedule(&tsq->tasklet); //将需要调度的tasklet挂到tasklet_hi_vec链表,待软中断被调度到时运行
 811         local_irq_restore(flags);
 812     } else {
 813         sock_wfree(skb);
 814     }
 815 }
  如果设置了TSQ_THROTTLED标记tcp_wfree函数才会将socket放入TSQ队列中等待Tasklet发送。TSQ_THROTTLED标记是什么时候设置的呢?来看tcp_write_xmit函数:

1811 static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
1812                int push_one, gfp_t gfp)
1813 {
...
1832     while ((skb = tcp_send_head(sk))) {
1833         unsigned int limit;
...
1867         if (atomic_read(&sk->sk_wmem_alloc) >= sysctl_tcp_limit_output_bytes) {
1868             set_bit(TSQ_THROTTLED, &tp->tsq_flags);
1869             break;
1870         }
...
1884         if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
1885             break;
...
  sk->sk_wmem_alloc在tcp_transmit_skb中用于累加skb的truesize,在skb释放时再减去skb的truesize;由tcp_transmit_skb函数发送出去的skb有两种情况会被释放:1)底层队列已满;2)skb中的数据被网卡发送出去。故sk->sk_wmem_alloc的意义为TCP放入底层队列中尚未被网卡发送的skb所占用的内存大小。sk->sk_wmem_alloc大于sysctl_tcp_limit_output_bytes意味着当前TCP连接放入底层队列中的数据大小达到限制,这时必须使用TSQ机制。

  现在我们来理顺一下TSQ的运行原理:首先设置net.ipv4.tcp_limit_output_bytes内核选项来指定sysctl_tcp_limit_output_bytes的值,在TCP发包系统调用使用tcp_write_xmit来调用tcp_transmit_skb函数发送skb时会进行设置使得skb在释放时会调用tcp_wfree函数,并增加sk->sk_wmem_alloc的值。当sk->sk_wmem_alloc达到限制时,即底层队列允许当前TCP连接放入的数据的总大小达到限制时(此时底层队列未必会满),tcp_write_xmit函数就不允许继续发送skb,而是设置标记使得在由tcp_transmit_skb函数发送的任意skb在释放时会调用tcp_wfree函数,将发送skb的任务放入TSQ tasklet中,然后系统调用返回。此后由TSQ tasklet在软中断上下文中驱动tcp_write_xmit函数发送skb,但放入底层队列中的skb的总大小仍然不能超过限制。一旦底层队列中的skb被陆续释放使得sk->sk_wmem_alloc的值减小到低于限制时,TSQ tasklet就可以即时地发送skb到队列中。这样既可以通过限制一个TCP连接向底层队列放入skb的速度来缓解队列拥堵导致的丢包问题,也能够在队列空间宽松时及时地发送数据,从而减小了TCP延时。

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