Linux网络 - 数据包的接收过程

Linux网络包收发总体过程

  就TCP/IP而言,IP和TCP的报文结构并不是最重要的,但是很多文章都在讨论他们,就体系而言,最重要的应该是各栈的流转流程。如果应用的话,重点应该在4次挥手(tcp的三次握手与四次挥手及为什么面试官喜欢问这个问题)及粘包和拆包及滑动窗口等。下面简单看下整体的收发过程。

注:Socket是提供给用户访问的TCP层接口,应用层的数据收发都在socket缓冲区中。 对应的数据流和控制流如下:

 

 

网卡到内存

  网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核的网络模块,驱动在加载的时候将自己注册进网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。

  下图展示了数据包(packet)如何进入内存,并被内核的网络模块开始处理:

   

  • 1: 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,且该网卡没有开启混杂模式,该包会被网卡丢弃。
  • 2: 网卡将数据包通过DMA(或DMA)的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。注: 老的网卡可能不支持DMA,不过新的网卡一般都支持,具体多少划给DMA使用,不同的计算机体系有所不同,很多体系全部内存都可用。注:DMA的性能通常远低于cpu和内存之间的速度,可参见https://www.zhihu.com/question/266921250,虽然它不消耗额外的CPU
  • 3: 网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了
  • 4: CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数
  • 5: 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。
  • 6: 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。

内核的网络模块(驱动到IP栈处理前)

  软中断会触发内核网络模块中的软中断处理函数,后续流程如下:

   

  • 7: 内核中的ksoftirqd进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第6步中是网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数
  • 8: net_rx_action调用网卡驱动里的poll函数来一个一个的处理数据包
  • 9: 在pool函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道
  • 10: 驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数
  • 11: napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS,如果开启了,将会调用enqueue_to_backlog
  • 12: 在enqueue_to_backlog函数中,会将数据包放入CPU的softnet_data结构体的input_pkt_queue中,然后返回,如果input_pkt_queue满了的话,该数据包将会被丢弃,queue的大小可以通过net.core.netdev_max_backlog来配置
  • 13: CPU会接着在自己的软中断上下文中处理自己input_pkt_queue里的网络数据(调用__netif_receive_skb_core)
  • 14: 如果没开启RPS,napi_gro_receive会直接调用__netif_receive_skb_core
  • 15: 看是不是有AF_PACKET类型的socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump抓包就是抓的这里的包。
  • 16: 调用协议栈相应的函数,将数据包交给协议栈处理,通常即IP协议栈。
  • 17: 待内存中的所有数据包被处理完成后(即poll函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU
enqueue_to_backlog函数也会被netif_rx函数调用,而netif_rx正是lo设备发送数据包时调用的函数

协议栈-IP层

无论是TCP还是UDP包,所以第一步会进入IP层,然后一级一级的函数往下调:

  • ip_rcv: ip_rcv函数是IP模块的入口函数,在该函数里面,第一件事就是将垃圾数据包(目的mac地址不是当前网卡,但由于网卡设置了混杂模式而被接收进来)直接丢掉,然后调用注册在NF_INET_PRE_ROUTING上的函数
  • NF_INET_PRE_ROUTING: netfilter放在协议栈中的钩子,可以通过iptables来注入一些数据包处理函数,用来修改或者丢弃数据包,如果数据包没被丢弃,将继续往下走
  • routing: 进行路由,如果是目的IP不是本地IP,且没有开启ip forward功能,那么数据包将被丢弃,如果开启了ip forward功能,那将进入ip_forward函数
  • ip_forward: ip_forward会先调用netfilter注册的NF_INET_FORWARD相关函数,如果数据包没有被丢弃,那么将继续往后调用dst_output_sk函数
  • dst_output_sk: 该函数会调用IP层的相应函数将该数据包发送出去,同下一篇要介绍的数据包发送流程的后半部分一样。
  • ip_local_deliver:如果上面routing的时候发现目的IP是本地IP,那么将会调用该函数,该函数会先进行必要的组包(因为IP层负责包拆分为MTU大小),然后调用NF_INET_LOCAL_IN相关的钩子程序,如果通过,数据包将会向下发送到TCP或UDP层。其过程如下:
/*
 *  Deliver IP Packets to the higher protocol layers.
 */
 int ip_local_deliver(struct sk_buff *skb)
 {
      /*
      *  Reassemble IP fragments.
      */
     struct net *net = dev_net(skb->dev);

     //check if it is a fragment
     if (ip_is_fragment(ip_hdr(skb))) {
         //fragment recombination
         if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
             return 0;
     }
     return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
                net, NULL, skb, skb->dev, NULL,
                ip_local_deliver_finish);
}
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    __skb_pull(skb, skb_network_header_len(skb));
    rcu_read_lock();
    {
        //get the protocol of this packet
        int protocol = ip_hdr(skb)->protocol;
        const struct net_protocol *ipprot;
        .....
        //from inet_protos list to get the correct protocol struct depending on protocol as index
        ipprot = rcu_dereference(inet_protos[protocol]);
        if(ipprot) {
            ...
            ret = ipprot->handler(skb);//call the Lay4 handler function.
            ...
        }
    }
    ....
}

从上可知,在ip层处理最后,会从skb(socket buffer)中得到协议,然后调用对应的协议(TCP或UDP)处理器。如下:

 

实际中大多数使用recvfrom,而非recv或recvmsg,那它们的区别是什么呢,参见http://man7.org/linux/man-pages/man2/recvfrom.2.html。

tcp缓冲与pagecache的协作关系

 

unix domain socket的流程

 直接就是将两个 socket 结构体中的指针互相指向对方就行了。然后就可以通信了。

 申请一块内存(skb),把数据拷贝进去。根据 socket 对象找到另一端,直接把 skb 给放到对端的接收队列里了。因为tcp通信报文头负载太重,所以小包1KB以内性能(吞吐量和时延)提升可以翻倍。

 

 

参见:https://segmentfault.com/a/1190000008836467

通过tcpdump对Unix Domain Socket 进行抓包解析 

tcpdump工作机制

 从上可知,tcpdump 抓包使用的是 libpcap 这种机制。它的大致原理是:在收发包时,如果该包符合 tcpdump 设置的规则(BPF filter),那么该网络包就会被拷贝一份到 tcpdump 的内核缓冲区,然后以 PACKET_MMAP 的方式将这部分内存映射到 tcpdump 用户空间,解析后就会把这些内容给输出了。

通过上图你也可以看到,在收包的时候,如果网络包已经被网卡丢弃了,那么 tcpdump 是抓不到它的;在发包的时候,如果网络包在协议栈里被丢弃了,比如因为发送缓冲区满而被丢弃,tcpdump 同样抓不到它。我们可以将 tcpdump 的能力范围简单地总结为:网卡以内的问题可以交给 tcpdump 来处理;对于网卡以外(包括网卡上)的问题,tcpdump 可能就捉襟见肘了。这个时候,你需要在对端也使用 tcpdump 来抓包。

https://cloud.tencent.com/developer/article/1963176

https://blog.csdn.net/Charce1989/article/details/70766955

http://www.ece.virginia.edu/mv/research/DOE09/publications/TCPlinux.pdf

UNIX网络编程 卷1 套接字联网API 第3版

posted @ 2020-01-22 09:19  zhjh256  阅读(4955)  评论(0编辑  收藏  举报