一文快速弄懂零拷贝原理

一、零拷贝概念

零拷贝(Zero-Copy)是一种 I/O 操作优化技术,可以快速高效地将数据从文件系统移动到网络接口,而不需要将其从内核空间复制到用户空间。其在 FTP 或者 HTTP 等协议中可以显著地提升性能。但是需要注意的是,并不是所有的操作系统都支持这一特性,目前只有在使用 NIO 和 Epoll 传输时才可使用该特性。

需要注意,它不能用于实现了数据加密或者压缩的文件系统上,只有传输文件的原始内容。这类原始内容也包括加密了的文件内容。

二、传统I/O操作存在的性能问题

如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。

传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。

代码通常如下,一般会需要两个系统调用:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

代码很简单,虽然就两行代码,但是这里面发生了不少的事情。

img

首先,期间共发生了 4 次用户态与内核态的上下文切换 ,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。

上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。

其次,还发生了 4 次数据拷贝 ,其中两次是 DMA(直接内存访问) 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:

  • 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
  • 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
  • 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
  • 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。
    这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。

所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。

三、零拷贝技术原理

零拷贝主要是用来解决操作系统在处理 I/O 操作时,频繁复制数据的问题。关于零拷贝主要技术有 mmap+writesendfilesplice等几种方式。

3.1、虚拟内存

在了解零拷贝技术之前,先了解虚拟内存的概念。

所有现代操作系统都使用虚拟内存,使用虚拟地址取代物理地址,主要有以下几点好处:

  • 多个虚拟内存可以指向同一个物理地址。
  • 虚拟内存空间可以远远大于物理内存空间。

利用上述的第一条特性可以优化,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样在 I/O 操作时就不需要来回复制了。

如下图展示了虚拟内存的原理。

image-20210812181924274

3.2、mmap/write 方式

使用mmap/write方式替换原来的传统I/O方式,就是利用了虚拟内存的特性。下图展示了mmap/write原理:

image-20210812201839908

整个流程的核心区别就是,把数据读取到内核缓冲区后,应用程序进行写入操作时,直接把内核的Read Buffer的数据复制到Socket Buffer以便写入,这次内核之间的复制也是需要CPU的参与的。

上述流程就是少了一个 CPU COPY,提升了 I/O 的速度。不过发现上下文的切换还是4次并没有减少,这是因为还是要应用程序发起write操作。

那能不能减少上下文切换呢?这就需要sendfile方式来进一步优化了。

3.3、sendfile 方式

从 Linux 2.1 版本开始,Linux 引入了 sendfile来简化操作。sendfile方式可以替换上面的mmap/write方式来进一步优化。

sendfile将以下操作:

  mmap();
  write();

替换为:

 sendfile();

这样就减少了上下文切换,因为少了一个应用程序发起write操作,直接发起sendfile操作。

下图展示了sendfile原理:

image-20210812201905046

sendfile方式只有三次数据复制(其中只有一次 CPU COPY)以及2次上下文切换。

那能不能把 CPU COPY 减少到没有呢?这样需要带有 scatter/gather的sendfile方式了。

3.4、带有 scatter/gather 的 sendfile方式

Linux 2.4 内核进行了优化,提供了带有 scatter/gather 的 sendfile 操作,这个操作可以把最后一次 CPU COPY 去除。其原理就是在内核空间 Read BUffer 和 Socket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中,这样就不需要复制。其本质和虚拟内存的解决方法思路一致,就是内存地址的记录。

下图展示了scatter/gather 的 sendfile 的原理:

image-20210812201922193

scatter/gather 的 sendfile 只有两次数据复制(都是 DMA COPY)及 2 次上下文切换。CUP COPY 已经完全没有。不过这一种收集复制功能是需要硬件及驱动程序支持的。

3.5、splice 方式

splice 调用和sendfile 非常相似,用户应用程序必须拥有两个已经打开的文件描述符,一个表示输入设备,一个表示输出设备。与sendfile不同的是,splice允许任意两个文件互相连接,而并不只是文件与socket进行数据传输。对于从一个文件描述符发送数据到socket这种特例来说,一直都是使用sendfile系统调用,而splice一直以来就只是一种机制,它并不仅限于sendfile的功能。也就是说 sendfile 是 splice 的一个子集。

在 Linux 2.6.17 版本引入了 splice,而在 Linux 2.6.23 版本中, sendfile 机制的实现已经没有了,但是其 API 及相应的功能还在,只不过 API 及相应的功能是利用了 splice 机制来实现的。

和 sendfile 不同的是,splice 不需要硬件支持。

四、总结

无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA copy是都少不了的。因为两次 DMA 都是依赖硬件完成的。所以,所谓的零拷贝,都是为了减少 CPU copy 及减少了上下文的切换。

下图展示了各种零拷贝技术的对比:

方式CPU拷贝次数DMA拷贝次数系统调用上下文切换次数
传统方法(I/O)22read/write4
内存映射12mmap/write4
sendfile12sendfile2
scatter/gather copy02sendfile2
splice02splice0
### YOLOv1 原理与实现 #### 2.1 YOLOv1 简介 YOLO (You Only Look Once) v1 是一种用于实时对象检测的单阶段神经网络框架。该方法通过将输入图像划分为多个网格单元,并让每个网格负责预测固定数量的边界框及其对应的类别概率来完成物体检测任务[^3]。 #### 2.2 核心算法原理与具体操作步骤 YOLOv1 将整个图片分成 S×S 的网格结构,如果某个物体中心落在这个格子内部,则此格子就负责预测该物体的位置以及其属于各个类别的条件概率 P(C_i|Object)。对于每一个网格,模型会输出 B 个边框坐标(x, y, w, h),其中 x 和 y 表示相对于网格左上角位置的比例偏移量;w 和 h 则表示宽度和高度相对于整张图的比例尺寸。此外还会给出这些候选区域存在目标的概率 C。 为了提高定位准确性并减少冗余预测,在训练过程中引入了 IOU(Intersection Over Union)作为评价标准之一,即真实标签包围盒与预测结果之间的重叠程度比率。当两个矩形框完全吻合时IOU=1,而没有任何交集则为0。因此优化过程旨在最大化正样本间的平均IOU值的同时最小化负样本得分。 #### 数学模型和公式 在数学建模方面,YOLOv1 设计了一种特殊的损失函数用来衡量预测值同标注数据间差异: \[ L_{coord} \sum_{i=0}^{S^2}\sum_{j=0}^{B}(1^{obj}_{ij})[(x_i-\hat{x}_i)^2+(y_i-\hat{y}_i)^2]\] 这里 \(L_{coord}\) 控制着坐标的权重系数;\(1^{obj}_{ij}\) 是指示变量,只有当第 i 个 grid cell 中确实含有 object 并且 jth 边界框负责预测它的时候才取 1 。上述表达式计算的是所有含object 的grid cells里所对应的最佳box 对应的真实center point 同预测值之间差距平方之和。 另外还有关于宽高误差项、置信度误差项及分类误差项等部分共同构成了最终的整体loss function 形式。 ```python def compute_loss(predictions, targets): """ 计算YOLOv1损失函数 参数: predictions -- 模型预测的结果向量 targets -- 数据集中给定的目标真值 返回: total_loss -- 总体损失数值 """ # 初始化各项损失分量 coord_mask = ... conf_pos_mask = ... conf_neg_mask = ... class_mask = ... # ... (省略中间具体的mask构建逻辑) # 计算各部分损失 loss_coord = torch.sum(coord_mask * (pred_box_xy - true_box_xy)**2) loss_conf_pos = torch.sum(conf_pos_mask * (pred_box_confidence - true_box_confidence)**2) loss_conf_neg = torch.sum(conf_neg_mask * pred_noobj_confidence**2) loss_class = torch.sum(class_mask * F.cross_entropy(pred_class_probabilities, target_classes)) # 加权求和得到总损失 total_loss = lambda_coord*loss_coord + loss_conf_pos + loss_conf_neg + loss_class return total_loss ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悬浮海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值