Linux内核是如何将用户态数据包通过中断来配合网卡将数据包发送到目标机器上

Linux 内核是如何将用户进程的数据包通过各个中断来配合网卡发送到目标机器上.

本文内容参考张彦飞老师的 <深入理解Linux网络:修炼底层内功,掌握高性能原理>

收货内容:

  • Socket 发送队列基本工作原理

  • RingBuffer 基本工作原理

  • 零拷贝基本工作原理

一、RingBuffer

了解RingBuffer相关,基础数据结构、基础工作原理.

1.1 初始化.

在初始化网络子系统时回对网卡进行初始化,而在对网卡驱动初始化完毕之后会启动网卡,在启动网卡过程中,最主要的就是初始化RingBuffer内存,而对于多队列网卡(一个RingBuffer绑定一个中断号)则会有多个中断号,同时也会有多个RingBuffer,所以多队列网卡会初始化多个RingBuffer。在理解RingBuffer数据结构之前,我们可以先理解一下RingBuffer的主要功能.

RingBuffer主要功能:

RingBuffer主要承担网卡驱动接受数据包后通过DMA技术将数据包skbRingBuffer中DMA区域数组,同时RingBuffer提供了一个供内核来访问的区域数组来访问数据包skb,而在网卡和内核访问的数组的同一个位置指向的是同一个数据包skb。所以RingBuffer承担了网卡驱动和内核之间的数据交互,我认为理解这个是理解RingBuffer的必要条件。

RingBuffer数据结构.

Linux内核是如何将用户态数据包通过中断来配合网卡将数据包发送到目标机器上
image-20230611151657752

RingBuffer内有两个指针数组,一个供内核使用,一个供网卡通过DMA技术直接使用,两者同一个位置指向同一个要接受或者要发送的数据包。

二、数据从用户进程到网卡过程分析.

在这个过程中会涉及几次内存拷贝,也和我们经常遇见的面试题零拷贝相关.

当我们在应用层将要发送的数据包封装好后通过调用应用层已经封装好的系统调用函数来直接发送本次要发送的数据包,此时当前用户进程会陷入进内核态,执行相关系统调用。我们看下Golang中的例子.

func main() {
 // 服务端.
 ln, err := net.Listen("tcp"":8848")
 if err != nil {
  panic(err)
 }
 for {
  conn, err := ln.Accept()
  if err != nil {
   // 其他错误处理.
   continue
  }
  writeFunc := func(conn net.Conn) {
      // 这里会发生系统调用,最终会调用到协议栈传输层tcp_sendmsg函数.
      // 发生的系统调用为:syscall(abi.FuncPCABI0(libc_write_trampoline), uintptr(fd), uintptr(_p0), uintptr(len(p)))
   conn.Write(packWriteData("Hello Word"))
  }
  go writeFunc(conn)
 }
}

func packWriteData(msg interface{}) []byte {
 d, _ := json.Marshal(msg)
 return d
}

对于一般的业务开发,可能只需要能编写出上面的代码就可以了,但是如果想要更加深入理解的话,仅仅只是能写出上面的代码肯定是不行的,而是要知道在调用了conn.Write后发生的一系操作才可以.

我们具体看看发生的系统调用的:其中fd为当前socket的句柄,_po为当前数组指针起始位置,len(p)为当前要发送的数据的长度。

syscall(abi.FuncPCABI0(libc_write_trampoline), uintptr(fd), uintptr(_p0), uintptr(len(p)))

下面我们来看看内核态具体做了什么事情:

  • 根据传入的文件句柄fd找到内核sock对象.
  • 构造struct msghdr对象.
  • 调用sock对象封装好的协议栈发送数据方法:sock.sendmsg,进入到传输层进行控制.
  • 传输层tcp_sendmsg:主要做以下内容
    • 拷贝要发送的数据到内核态skb中.
    • 将本次要发送的数据包挂载到socket数据发送队列上.
    • 传输层发送判断. 有可能系统调用完毕,数据包并不会立马发送出去,立马发送出去有两个必备条件:一个是当前窗口容量大于当前窗口总容量的一半另一个是skb==tcp_send_head(sk)
    • tcp_write_xmit发送函数:执行拥塞控制、滑动窗口等相关逻辑,当上面两个条件都命中后则会进入协议栈的下一层。
  • 网络层:根据本地令居子系统看是否有当前要发送目标方的MAC缓存,如果没有则需要发送ARP请求来缓存本次要发送目标的MAC地址。当MAC数据封装好在数据包后,进入网卡驱动层,我认为可以理解为数据链路层。
  • 数据链路层:网卡驱动将数据包拷贝到RingBuffer中,然后调用网卡发送函数,网卡通过DMA技术将RingBuffer上的数据发送出去,发送之后发送一个硬终端来清理内存. 这里有一个注意的要点就是,如果网卡驱动程序在执行发送相关函数时发现要让出CPU使用权限时,则会发送一个NET_TX_IRQD类型的软中断,从而后续的发送流程将会在net_tx_action软中断函数中继续执行,而软中断函数内部是可以获取到本次要发送的数据包。
  • 清理RingBuffer内存.

**我们知道上面的大概流程之后,我们在来具体了解一下每层具体做了什么操作:**这次我们直接从传输层看起.

传输层:

Linux内核是如何将用户态数据包通过中断来配合网卡将数据包发送到目标机器上
image-20230611151729599

进入到传输层后,首先会根据传入进来的文件描述符fd找到当前内核关联的sock对象,同时会对要发送的数据包开始进行封包:构造struct msghdr结构体,并且申请一个新的skb,之后会将本次要发送的数据内容拷贝到当前新申请的skb中,同时将当前skb挂载到socket数据发送队列中,之后传输层会判断当前socket是否可以执行发送函数,而判断条件有以下两个条件:

  1. 当前窗口容量已经到达当前总窗口的一半。
  2. skb == tcp_send_head(sk)

如果满足条件则会进入传输层发送函数内部:tcp_write_xmit函数内部,该函数内部主要实现了传输层的拥塞控制、滑动窗口等相关控制算法,如果上面控制算法都能命中的话,则会拷贝一个新的skb出来,这里拷贝一个新的skb出来主要是因为数据包丢失造成的重传问题,我们知道当本机网络将当前包发送出去之后会通过硬中断来清理内存的,而如果我们这里不拷贝直接使用原内存数据包进行发送,当发送完之后这个数据包就没有了,就不存在了,假设在一个MSL中未收到ACK信息,则要进行数据重传,而此时我们我们已经丢失了原始数据包的内容,所以重传就会有问题。所以Linux网络在设计的时候就是讲传输层构建好的skb拷贝到网络层,进而假设传输层未收到该包的ACK信息,则依然可以通过socket数据发送队列中的原始数据包进行重传.  下面我们来看看网络层:

网络层:

在网络层最主要首先做的就是查看当前数据包是否大于MTU,如果大于的话,则需要分片发送,这里也设计到内存拷贝,所以业务逻辑尽可能保证一次数据包内容大小控制在MTU之内性能会比较好。

其次就是设置IP头等相关信息,其会查看本地路由表,然后确定当前消息由那个网卡来发送出去,确定之后将网卡信息添加到skb路由信息中,方便将该包发送出去。

同时,在网络层还会执行netfilter过滤,如果设置过多的netfilter则导致CPU使用率过高,因为每次发送的包都要经过netfilter过滤。之后就是进入领居子系统了(该系统是网络层和数据链路层中间的一个系统,其作用是为网络层提供数据链路层的一个封装)。

领居子系统:

在领居子系统中最主要的目标就是查找本地邻居表(该哈希表的含义是:保存了目标IP地址和MAC地址的映射关系,用于准确无误的将包发送到目标MAC地址上),如果ARP缓存中找不得对应的缓存项,则该层则会发出一个ARP请求,用来探测实际IP对应的MAC地址,并将其缓存到本地ARP缓存中。之后封装MAC头部信息,封装完之后就进入网卡驱动层面了(网络设备子系统)。

网络设备子系统:

Linux内核是如何将用户态数据包通过中断来配合网卡将数据包发送到目标机器上
image-20230611151746006

从上图我们可以知道,邻居子系统通过调用函数dev_queue_xmit函数进入网络设备子系统中,而在该系统中最主要的就是选择要讲当前要发送的包发送到那个队列上,选择好之后则会入队。入队之后相关函数会在CPU未调度前循环查看发送队列里面是否有数据,如果有数据则直接将该数据包从队列拿下来后通过函数:dev_hard_start_xmit函数进入网卡驱动层。但是如果在CPU使用结束后还扔然又未发送的数据包那该怎么处理呢?很显然,发送一个软中断通知CPU当前还有未发送的数据包,然后让内核线程softirqd继续执行数据包发送过程。

网卡驱动数据包发送:

Linux内核是如何将用户态数据包通过中断来配合网卡将数据包发送到目标机器上
image-20230611151820238

网卡驱动层最主要的就是将RingBuffer中内核使用的环形队列中的数据映射到网卡能通过DMA访问的内存区域,这里也涉及一次拷贝。当所有准备就绪后,网卡驱动会调用网卡真正的发送方法来进行发送,而该方法就是通过DMA技术从网卡能访问到的RingBuffer中获取要发送的数据,然后直接发送。

硬中断清理内存:

当网卡驱动将数据包准确无误的发送出去之后,网卡设备会触发一个硬中断来实施RingBuffer内存清理工作,具体工作原理如下:网卡驱动程序会将其DMA内存区域相关的SKB引用清理掉,同时也会把内核相关的引用清理掉。

注意:这里发送的软中断是NET_RX_SOFTIRQ整个网络包收发过程中只有这个地方才会发送这个软中断.

理论上到这里基本上就算本次报传输完成了,但是删除最后真正的包的需要等到对方ACK回包之后才会进行彻底删除。

Linux内核是如何将用户态数据包通过中断来配合网卡将数据包发送到目标机器上
image-20230611152023897

三、零拷贝

通过上面分整个流程分析,我们可以知道在整个过程中有这么几个拷贝:

  1. 传输层将用户态数据包拷贝到内核态SKB中。
  2. 传输层执行完毕将数据包透传到网络层时是拷贝的一个新的SKB
  3. 网卡驱动程序执行DMA拷贝。

上面三个拷贝过程中第二个以及第三个我认为都是不可以省略的,所以说零拷贝并不是说的一次拷贝都没有就直接将用户态的数据包直接通过网卡发送到目标机器上,而是说减少用户态到内核态在传输层的那一次拷贝,也就是第一个拷贝:传输层将用户态数据拷贝到内核态SKB中。所以零拷贝应该是尽量减少拷贝的次数,而不是说一次拷贝都没有。

如果上述描述有问题 欢迎大佬指出👍



原文始发于微信公众号(社恐的小马同学):Linux内核是如何将用户态数据包通过中断来配合网卡将数据包发送到目标机器上

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/269704.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!