Linux内核网络协议栈1

Linux内核网络协议栈1(基于Linux6.6)---数据包的接收过程介绍

一、概述

在Linux系统中,网络数据包从网卡传到进程手中的过程是一个复杂而精细的任务,涉及多个层次和组件的协同工作。以下是该过程的概述:

1.1、接收准备

在准备好接收网络数据包之前,Linux系统需要进行一系列初始化工作,包括网络子系统的初始化、协议栈的注册、网卡驱动的初始化以及启动网卡等。这些准备工作确保系统能够正确识别和处理网络数据包。

1.2、数据包到达网卡

  1. 当一个网络数据包到达网卡时,网卡会利用DMA(Direct Memory Access,直接存储器访问)技术,将这个数据包放到接收队列中。DMA传输方式无需CPU直接控制传输,而是通过硬件为RAM和IO设备开辟一条直接传输数据的通道,从而大大提高了CPU的效率。
  2. 网卡通过硬中断通知CPU,表示已经收到了网络数据包。

1.3、中断处理

  1. CPU响应网卡的中断请求,并根据中断注册表找到相应的中断处理函数。
  2. 网卡中断处理程序会为网络数据包分配一个内核数据结构(sk_buff),这是Linux内核网络子系统中的一个结构体,用于表示网络数据包的缓冲区。它包含了数据包的各种信息,如数据内容、协议头部等。
  3. 中断处理程序将网络数据包从网卡的接收队列中拷贝到sk_buff缓冲区中,并更新Ring Buffer中的描述符,以便后续处理。
  4. 为了避免CPU被频繁中断,中断处理程序会屏蔽网卡的中断,并发起一个软中断来继续处理数据。

1.4、软中断处理

  1. 内核中的ksoftirqd线程收到软中断后,会调用相应的软中断处理函数来轮询处理数据。
  2. 软中断处理函数从Ring Buffer中获取一个数据帧(用sk_buff表示),并将其作为一个网络数据包交给网络协议栈从下到上进行逐层处理。

1.5、协议栈处理

  1. 网络接口层:首先检查报文的合法性和正确性,如果不合法或校验不正确则丢弃;否则,找出上层协议的类型(如IPv4或IPv6),去掉帧头和帧尾,然后交给上层即网络层处理。
  2. 网络层:取出IP头,判断网络数据包的下一步走向(是转发还是交给上层)。当确认数据包是要发送给本机后,就取出上层协议的类型(如TCP或UDP),去掉IP头,然后交给传输层处理。
  3. 传输层:取出TCP头或UDP头后,根据四元组(源IP、源端口、目的IP、目的端口)找出对应的Socket,并把数据拷贝到Socket的接收缓冲区中。

1.6、应用层处理

  1. 应用程序通过Socket接口调用recv或read等函数,将内核Socket接收缓冲区中的数据拷贝到应用层的缓冲区中。
  2. 应用程序根据业务逻辑处理接收到的数据。

二、网卡到内存

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

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

                   +-----+
                   |     |                            Memroy
+--------+   1     |     |  2  DMA     +--------+--------+--------+--------+
| Packet |-------->| NIC |------------>| Packet | Packet | Packet | ...... |
+--------+         |     |             +--------+--------+--------+--------+
                   |     |<--------+
                   +-----+         |
                      |            +---------------+
                      |                            |
                    3 | Raise IRQ                  | Disable IRQ
                      |                          5 |
                      |                            |
                      ↓                            |
                   +-----+                   +------------+
                   |     |  Run IRQ handler  |            |
                   | CPU |------------------>| NIC Driver |
                   |     |       4           |            |
                   +-----+                   +------------+
                                                   |
                                                6  | Raise soft IRQ
                                                   |
                                                   ↓
  1. 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,且该网卡没有开启混杂模式,该包会被网卡丢弃。
  2. 网卡将数据包通过DMA的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。注: 老的网卡可能不支持DMA,不过新的网卡一般都支持。
  3. 网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了
  4. CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数
  5. 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。
  6. 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。

三、内核的网络模块

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

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

enqueue_to_backlog函数也会被netif_rx函数调用,而netif_rx正是lo设备发送数据包时调用的函数

四、协议栈(UDP为例)

4.1、IP层

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

          |
          |
          ↓         promiscuous mode &&
      +--------+    PACKET_OTHERHOST (set by driver)   +-----------------+
      | ip_rcv |-------------------------------------->| drop this packet|
      +--------+                                       +-----------------+
          |
          |
          ↓
+---------------------+
| NF_INET_PRE_ROUTING |
+---------------------+
          |
          |
          ↓
      +---------+
      |         | enabled ip forword  +------------+        +----------------+
      | routing |-------------------->| ip_forward |------->| NF_INET_FORWARD |
      |         |                     +------------+        +----------------+
      +---------+                                                   |
          |                                                         |
          | destination IP is local                                 ↓
          ↓                                                 +---------------+
 +------------------+                                       | dst_output_sk |
 | ip_local_deliver |                                       +---------------+
 +------------------+
          |
          |
          ↓
 +------------------+
 | NF_INET_LOCAL_IN |
 +------------------+
          |
          |
          ↓
    +-----------+
    | UDP layer |
    +-----------+
  • 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,那么将会调用该函数,在该函数中,会先调用NF_INET_LOCAL_IN相关的钩子程序,如果通过,数据包将会向下发送到UDP层

4.2、UDP层

          |
          |
          ↓
      +---------+            +-----------------------+
      | udp_rcv |----------->| __udp4_lib_lookup_skb |
      +---------+            +-----------------------+
          |
          |
          ↓
 +--------------------+      +-----------+
 | sock_queue_rcv_skb |----->| sk_filter |
 +--------------------+      +-----------+
          |
          |
          ↓
 +------------------+
 | __skb_queue_tail |
 +------------------+
          |
          |
          ↓
  +---------------+
  | sk_data_ready |
  +---------------+
  • udp_rcv:udp_rcv函数是UDP模块的入口函数,它里面会调用其它的函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,该函数会根据目的IP和端口找对应的socket,如果没有找到相应的socket,那么该数据包将会被丢弃,否则继续
  • sock_queue_rcv_skb: 主要干了两件事,一是检查这个socket的receive buffer是不是满了,如果满了的话,丢弃该数据包,然后就是调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃(在Linux里面,每个socket上都可以像tcpdump里面一样定义filter,不满足条件的数据包将会被丢弃)
  • __skb_queue_tail: 将数据包放入socket接收队列的末尾
  • sk_data_ready: 通知socket数据包已经准备好

调用完sk_data_ready之后,一个数据包处理完成,等待应用层程序来读取,上面所有函数的执行过程都在软中断的上下文中。

4.3、socket

应用层一般有两种方式接收数据,一种是recvfrom函数阻塞在那里等着数据来,这种情况下当socket收到通知后,recvfrom就会被唤醒,然后读取接收队列的数据;另一种是通过epoll或者select监听相应的socket,当收到通知后,再调用recvfrom函数去读取接收队列的数据。两种情况都能正常的接收到相应的数据包。

1、阻塞式recvfrom接收数据流程图

  1. 开始
    • 应用程序启动,并创建一个socket。
  2. 绑定socket
    • 将socket绑定到特定的IP地址和端口(如果需要)。
  3. 监听/连接
    • 对于服务器,调用listen()开始监听连接;对于客户端,调用connect()连接到服务器。
  4. recvfrom阻塞
    • 应用程序调用recvfrom()函数,并等待数据到达。此时,recvfrom()函数处于阻塞状态,直到数据到达或发生错误。
  5. 数据到达
    • 网络数据包到达网卡,经过协议栈处理后,最终到达应用程序的socket接收队列。
  6. recvfrom被唤醒
    • 当数据到达socket接收队列时,recvfrom()函数被唤醒。
  7. 读取数据
    • recvfrom()函数从socket接收队列中读取数据,并返回给应用程序。
  8. 处理数据
    • 应用程序处理接收到的数据。
  9. 循环或结束
    • 应用程序根据需要重复步骤4-8,或处理完数据后结束。

2、基于epoll/select的非阻塞式接收数据流程图

  1. 开始
    • 应用程序启动,并创建一个socket。
  2. 绑定socket
    • 将socket绑定到特定的IP地址和端口(如果需要)。
  3. 监听/连接
    • 对于服务器,调用listen()开始监听连接;对于客户端,调用connect()连接到服务器。
  4. 设置epoll/select
    • 应用程序调用epoll_create()(或select())创建一个epoll实例(或选择集),并将socket添加到该实例(或选择集)中。
  5. 等待事件
    • 应用程序调用epoll_wait()(或select())等待socket上有事件发生(如可读、可写、异常等)。
  6. 检查事件
    • 当epoll_wait()(或select())返回时,应用程序检查哪个socket上有事件发生。
  7. 数据到达
    • 如果事件是socket可读,表示网络数据包已经到达socket接收队列。
  8. 读取数据
    • 应用程序调用recvfrom()函数从socket接收队列中读取数据。
  9. 处理数据
    • 应用程序处理接收到的数据。
  10. 循环或结束
    • 应用程序根据需要重复步骤5-9,或处理完数据后结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值