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延时。