TCP receive 接收流程

  1. 网卡驱动注册到内核,方便内核与网卡进行交互。
  2. 内核启动网卡,为网卡工作分配资源(ring buffer)和注册硬中断处理 e1000_intr。
  3. 网卡(NIC)接收数据。
  4. 网卡通过 DMA 方式将接收到的数据写入主存(步骤 2 内核通过网卡驱动将 DMA 内存地址信息写入网卡寄存器,使得网卡获得 DMA 内存信息)。
  5. 网卡触发硬中断,通知 CPU 已接收数据。
  6. CPU 收到网卡的硬中断,调用对应的处理函数 e1000_intr。
  7. 网卡驱动函数先禁止网卡中断,避免频繁硬中断,降低内核的工作效率。
  8. 网卡驱动将 napi_struct.poll_list 挂在 softnet_data.poll_list 上,方便后面软中断调用 napi_struct.poll 获取网卡数据。
  9. 然后启用 NET_RX_SOFTIRQ -> net_rx_action 内核软中断。
  10. 内核软中断线程消费网卡 DMA 方式写入主存的数据。
  11. 内核软中断遍历 softnet_data.poll_list,调用对应的 napi_struct.poll -> e1000_clean 读取网卡 DMA 方式写入主存的数据。
  12. e1000_clean 遍历 ring buffer 通过 dma_sync_single_for_cpu 接口读取 DMA 方式写入主存的数据,并将数据拷贝到 e1000_copybreak 创建的 skb 包。
  13. 网卡驱动读取到 skb 包后,需要将该包传到网络层处理。在这过程中,需要通过 GRO (Generic receive offload) 接口:napi_gro_receive 进行处理,将小包合并成大包,然后通过 __netif_receive_skb 将 skb 包交给 TCP/IP 协议逐层处理,最后将 skb 包追加到 socket.sock.sk_receive_queue 队列,等待应用处理;如果 read / epoll_wait 阻塞等待读取数据,那么唤醒进程/线程。
  14. skb 包需要传到网络层,如果内核开启了 RPS (Receive Package Steering) 功能,为了利用多核资源,(enqueue_to_backlog)需要将数据包负载均衡到各个 CPU,那么这个 skb 包将会通过哈希算法,挂在某个 cpu 的接收队列上(softnet_data.input_pkt_queue),然后等待软中断调用 softnet_data 的 napi 接口 process_backlog(softnet_data.backlog.poll)将接收队列上的数据包通过 __netif_receive_skb 交给网络层处理。
  15. 网卡驱动读取了网卡写入的数据,并将数据包交给协议栈处理后,需要通知网卡已读(ring buffer)数据的位置,将位置信息写入网卡 RDT 寄存器(writel(i, hw->hw_addr + rx_ring->rdt)),方便网卡继续往 ring buffer 填充数据。
  16. 网卡驱动重新设置允许网卡触发硬中断(e1000_irq_enable),重新执行步骤 3。
  17. 用户程序(或被唤醒)调用 read 接口读取 socket.sock.sk_receive_queue 上的数据并拷贝到用户空间

 

网卡 PCI 驱动,NAPI 中断缓解技术,软硬中断,DMA

 

 

 

 

 

 

 

kernel对于TCP包得处理大致可以分为两类:
  如果处理in-sequence的包时,application正阻塞在read操作中,则接收到的数据包的数据会被直接拷贝到user buffer。
  否则,in-sequence包会被放在receive queue中,out-of-order包会放置于out-of-order queue中。

TCP层处理IPV4-TCP数据包的第一个函数是tcp_v4_rcv(),首先从这个函数开始理解整个流程:

tcp_v4_rcv()  // net/ipv4/tcp_ipv4.c  
    => sk = __inet_lookup_skb()  // 找到skb属于的sock结构体  
    => if (!sock_owned_by_user(sk))  // sock未被加锁  
        => if (!tcp_prequeue(sk, skb))  // 如果符合加入prequeue的原则,则加入prequeue,返回true;反之返回false  
            => ret = tcp_v4_do_rcv(sk, skb)  
                => tcp_rcv_established()  // receive function for the ESTABLISHED state  
                    => if (len == tcp_header_len)  // 如果是纯ack包
                        => tcp_ack(sk, skb, 0)  // dealing with incoming acks  
                            => flag |= tcp_clean_rtx_queue()  // see if we can take anything off of the retransmit queue  
                            => if (tcp_ack_is_dubious(sk, flag)  // 判断时候出现可疑情况,具体看下代码吧。  
                                => tcp_fastretrans_alert()  // 进入快速重传  
                                    => tcp_cwnd_down()  // decrease cwnd each second ack, 该函数就是快速重传对cwnd操作的关键函数  
                                    => tcp_xmit_retransmit_queue(sk)  // 在重传阶段,该函数负责找到合适的数据进行重传  
                        => __kfree_skb(skb)  // free an sk_buff  
                        => tcp_data_snd_check(sk)  // 如果有数据需要发送,则发送数据到对端  
                            => tcp_push_pending_frames(sk)   // 发送pending的数据
                                => tcp_write_xmit()  // writes packets to the network, 这部分在上一章已经分析过了   
                            => tcp_check_space(sk)  //  如果有内存释放,则唤醒等待内存的队列  
                                /* when incoming ACK allows to free some skb from write_queue,  
                                 * we remember this event in flag SOCK_QUEUE_SHRUNK and wake up socket  
                                 * on the exit from tcp input hander.
                                 *  
                                 * PROBLEM: sndbuf expansion does not work well with largesend. 
                                 */
                                => tcp_new_space(sk)  
                                    => sk->sk_sndbuf = min(sndmem, sysctl_tcp_wmem[2])  // expand the sndbuf if possible  
                    => else // 如果是带数据的包  
                        /* 此数据包刚好是下一个读取的数据,并且用户空间可存放下该数据包 */
                        => if (tp->copied_seq == tp->rcv_nxt && len - tcp_header_len <= tp->ucopy.len)  
                            /* 如果函数在进程上下文调用并且sock被用户占用的话 */
                            => if (tp->ucopy.task == current && sock_owned_by_user(sk) && !copied_early)  
                                => tcp_copy_to_iovec()  // 直接copy 到用户空间  
                        => if (!eaten)  // 没有直接读到用户空间  
                            /* 当truesize大于sk_forward_alloc时,表示已分配的限额已经用完,不能直接放到receive queue中, 此时往往要重新计算sk_forward_alloc */
                            => if (skb->truesize > sk->sk_forward_alloc) goto step5  
                            => eaten = tcp_queue_rcv()  
                                => tcp_try_coalesce()  // try merge skb to prior one  
                                => if (!eaten) __skb_queue_tail()  // 如果上一步未成功,则将skb放入receive queue中  
                        => tcp_event_data_recv(sk, skb)  // 数据包接收后续处理  
                            /* 每次收到超过128字节的数据报后,需要调用tcp_grow_window增加rcv_ssthresh的值 */
                            => if (skb->len >= 128) tcp_grow_window(sk, skb) 
                        => __tcp_ack_snd_check(sk, 0) // check if sending an ack is needed  

                    => tcp_validate_incoming(sk, skb, th, 1)  // standard slow path, [details ignored]  
                    => tcp_data_queue(sk, skb)  // 对数据包进行处理  
                        => if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt)   // 如果是待接收的报文  
                            => if (tcp_receive_window(tp) == 0) goto out_of_window;  // 如果超出rwnd,则直接丢掉  
                            => //如果正在读,且正是要读的数据,那么直接拷贝到用户空间  
                            => else eaten = tcp_queue_rcv()  // 将数据放入receive queue中  
                            => if (!skb_queue_empty(&tp->out_of_order_queue))  // 如果out of ordre queue不为空  
                                => tcp_ofo_queue(sk)  // This one checks to see if we can put data from the out-of-order queue into the receive-queue  
                            => tcp_fast_path_check(sk)  // 检查是否可以从slowpath回到fastpath  

                        => else tcp_data_queue_ofo(sk, skb)  // 将数据包放到out-of-order queue中  
                    => tcp_data_snd_check(sk)  // 如果有数据需要发送,则发送数据到对端  
                    => tcp_ack_snd_check(sk)  // 判断是否有必要发送一个ack 

 

TCP的接收队列的处理主要是在tcp_recvmsg()函数中,所以先从这个函数入手

tcp_recvmsg()  // this routine copies from a sock struct into the user buffer  
    => lock_sock(sk)  // become a socket user  
    => skb_queue_walk()  // get a skb  

    => //如果有skb可供拷贝  
        => err = skb_copy_datagram_iovec()  // copy data into iovec if found_ok_skb  
        /* This function should be called every time data is copied to user space.  
         * It calculates the appropriate TCP receive buffer space.  
         */  
        => tcp_rcv_space_adjust(sk)  
            => //调整至少每隔一个RTT才进行一次  
            => space = 2 * (tp->copied_seq - tp->rcvq_space.seq)  // 一个RTT内,接收并复制到用户空间的数量的2倍  
            ...  
            => sk->sk_rcvbuf = space  // 调整接收缓冲区的大小  
        => sk_eat_skb(sk, skb, copied_early)  // 如果一个skb内数据被拷贝完了,则释放掉该skb  

    =>// 如果没有skb可供拷贝  
        /* 如果设置了MSG_WAITALL,target == len; 否则target == 1 */
        => if (copied >= target && !sk->sk_backlog.tail) break;  // 如果读够了target,且backlog queue 为空则直接return  

        => tcp_cleanup_rbuf(sk, copied)  
            /* 注意区分这个函数与sk_eat_skb()  后者是清掉某个skb及其内存, 前者的主要功能是发送一个接收窗口更新的ACK--因为用户进程消费了读缓存中的数据 */
            => if (inet_csk_ack_scheduled(sk))  // if the ack is scheduled by calling tcp_ack_scheduled()  
                => if delayed ACK was blocked by socket lock, send an ACK  
                => if we have not ACKed data of length > 1mss, send an ACK  
                => if we have emptied the receive buffer, and there is data flow only in one direction, send an ACK  
            => rcv_window_now = tcp_receive_window(tp)  // 计算当前的应该通知对方的receive window
                => win = tp->rcv_wup + tp->rcv_wnd - tp->rcv_nxt  // 左边界 + 当前receive_window - 已用  
            => new_window = __tcp_select_window(sk)  // 计算新的接收窗口大小, 约为rcvbuf空闲部分的一半  
            => if (new_window && new_window >= 2 * rcv_window_now)  send an ACK  

            => if (time_to_ack) tcp_send_ack(sk)   // 如果上面有需要发送ack的需求,则发送一个ACK  
        /* if prequeue is not empty, we have to process it before releasing socket  
         * queue的处理优先级如下:  
         * receive queue 最高
         * prequeue queue 次之
         * backlog queue 最低
         */  
        => if prequeue is not empty, goto do_prequeue
            => tcp_prequeue_process(sk)  
                => sk_backlog_rcv(sk, skb)  == tcp_v4_do_rcv()  

        => if (copied >= target)   // 下面两个步骤主要就是为了处理backlog queue  
            => release_sock(sk)  
                => if (sk->sk_backlog.tail)   
                    => __release_sock(sk)  
                        => sk_backlog_rcv(sk, skb)  == tcp_v4_do_rcv()  
            => lock_sock(sk) 
        => else  
            => sk_wait_data(sk, &timeo)  // 睡眠等待新数据的到来

 

posted @ 2019-05-11 17:01  codestacklinuxer  阅读(352)  评论(0编辑  收藏  举报