sendfile相关「建议收藏」

sendfile相关「建议收藏」考虑将一个本地文件通过socket发送出去的问题。我们通常的做法是:打开文件fd和一个socket,然后循环地从文件fd中read数据,并将读取的数据send到socket中。这样,每次读写我们都需要两次系统调用,并且数据会被从内核拷贝到用户空间(read),再从用户空间拷贝到内核(send)。而sendfile就将整个发送过程封装在一个系统调用中,避免了多次系统调用,避免了数据在内核空间

大家好,又见面了,我是你们的朋友全栈君。考虑将一个本地文件通过socket发送出去的问题。我们通常的做法是:打开文件fd和一个socket,然后循环地从文件fd中read数据,并将读取的数据send到socket中。这样,每次读写我们都需要两次系统调用,并且数据会被从内核拷贝到用户空间(read),再从用户空间拷贝到内核(send)。

而sendfile就将整个发送过程封装在一个系统调用中,避免了多次系统调用,避免了数据在内核空间和用户空间之间的大量拷贝。

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

虽然这个系统调用接收in和out两个fd,但是有所限制,in只能是普通文件,out只能是socket(这个限制不知道后来的内核版本有没有放宽)。

一、什么是“零拷贝”

零拷贝(zero-copy)基本思想是:数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现CPU的零参与,彻底消除CPU在这方面的负载。实现零拷贝用到的最主要技术是DMA数据传输技术和内存区域映射技术。

先看普通网络服务守护进程的一般服务方法:

表面上看来,系统的负荷似乎只是两个系统调用,而没什么开销。如果这么认为,那么这就大错特错了。在这两个函数的背后,数据至少已经被复制了 4 次,以及几乎一样的用户空间到内核空间的切换次数(上下文切换)。下图可以很好的说明这一个过程:


sendfile相关「建议收藏」

分析上图中的步骤:

第一步,系统调用 read() 导致了用户空间到内核空间的切换。这时,DMA模块将磁盘上的文件内容拷贝(CMA COPY)到内核缓冲区,这就完成了第 1 次复制。

第二步,将内核缓冲区中的内容拷贝到用户缓冲区(kernel buffer –> user buffer),这样又导致了内核空间到用户空间的上下文切换。这时,我们需要的数据已经存放在 tmp_buf 中,这是第 2 次复制。

第三步,在调用 write() 时,又导致了用户空间到内核空间的上下文切换。数据从用户空间缓冲区再次被复制到内核空间缓冲区。这时完成了第 3 次的复制。这里需要注意的是,这次所用的内核缓冲区是与 socket 相关的缓冲区,而非“第一步”中的缓冲区。

第四步,write() 系统调用返回,这时导致了第 4 次的上下文切换。这一次是 DMA 模块将内核中的数据(socket buffer)拷贝到协议引擎中。这些动作是与代码的执行是独立并且是异步发生的。反过来说,假如是非独立且同步的,那么函数返回时那么数据也同时被发送。事实上并非如此,函数返回并不能说明数据被发送出去了,甚至是 write() 的返回都无法保证传输的开始!为什么这么说呢?这是因为,调用的返回,只是表明以太网驱网卡的动程序在其传输队列中有空位,并且已经接受我们的数据用于传输。这时候,有一种情况可能是,还有许多数据还排在我们这些数据的前头,除非我们的数据拥有特权或优先级很高(如果以太网驱动程序是采用队列优先级的方法来发送数据且我们的数据的优先级确实很高),否则数据按照 FIFO 这种先进先出的方法传送。所以在上图中,红色虚线箭头正表示了我们的传输可能发生延迟,若是实线,则刚好发送而没延迟。

正如上面所分析,整个过程中存在着数据冗余。某些冗余可以消除以减少开销并提升性能。有些硬件支持完全绕开内存,可以将数据直接传递给其它设备,这样的特性避免了系统内存中的数据副本,虽然这是一种很好的选择,但并非所有的硬件都能如此。此外,来自硬盘的数据必须重新打包(地址连续)才能用于网络传输,这让情况也会变得有些复杂。

为了减少开销,我们就从消除内核缓冲区和用户缓冲区之间的复制入手。

消除的一种方法就是使用 mmap() 系统调用来替代 read() 系统调用,例如:

tmp_buf = mmap(file, len);  write(socket, tmp_buf, len);

工作原理如下图:

sendfile相关「建议收藏」

从上图可见:

第一步,调用 mmap() 后,文件的内容通过 DMA 模块复制到内核缓冲区中,该缓冲区之后与用户进程共享,这样一来,就无需再进行内核缓冲区和用户缓冲区的切换,也就是它们之间的复制就不会再发生。

第二步,在调用 write() 后,内核缓冲区中的数据被复制到与 socket 相关联的内核缓冲区中。

第三步,DMA 模块将数据由 socket 的缓冲区复制到协议引擎,这时第 3 次复制发生。

通过调用 mmap() 而不是 read(),我们已经将内核需要执行的复制操作减半。当有大量的数据传输时,有相当好的效果。但是性能改进的同时,也潜藏着一定的代价与陷阱。比如,在对文件进行内存映射后调用 write(),而这时有另外一个进程将映射的文件截断,此时 write() 系统调用会被进程接收到的 SIGBUS 信号而中断,SIGBUS 信号往往意味着尝试进行非法地址访问。对 SIGBUS 信号的默认处理方式是杀死当前进程并生成 core dump 文件 — 这对于网络服务器来说是极不期望的!

有两种方式可以解决该问题:

第一种是为 SIGBUS 信号设置处理程序,并在处理中简单的执行 return 语句。这样,write() 系统调用将返回被信号中断前已写的字节数,同时设置 errno 变量。但是这样的做法并不值得鼓励,因为收到 SIGBUS 信号意味着发生了严重错误!

第二种是采用文件租约的方式。文件租约是指,通过对文件描述符执行租借,你可以和内核对某个文件达成租约,从内核可以获得读/写租约。当另外的一个进程试图将你正在传输的文件截断时,内核会向你发出实时信号 RT_SIGNAL_LEASE 。该信号通知你的进程,内核即将终止你在该文件上曾经获得的租约。这样,当 write() 访问非法地址时,并即被随后到来的 SIGBUS 杀诉之前,write() 系统调用会被 RT_SIGNAL_LEASE 信号中断。write() 的返回值就是被中断之前已写的字节数,全局变量 errno 设置为成功。下面是一段示例租约的代码:

if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {      perror(“kernel lease set signal”);      return -1;  }  /* l_type can be F_RDLCK F_WRLCK */  if(fcntl(fd, F_SETLEASE, l_type)){      perror(“kernel lease set type”);      return -1;  }

在文件进行映射之前 (通过 mmap() 系统调用),应该先获得租约,并在 write() 结束之后结束租约。这通过在 fcntl() 函数中指定租约类型为 F_UNLCK 来实现。

sendfile

sendfile() 的目的是简化通过网络在两个本地之间的数据传输过程。sendfile() 系统调用的引入,不仅减少了数据的复制,还减少了上下文切换的次数。 使用方法如下:

#include <sys/sendfile.h>  ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

它的原理如下图所示:

sendfile相关「建议收藏」

在上图中,

第一步,sendfile() 导致文件内容通过 DMA 模块复制到某个内核缓冲区,之后被复制到与 soket 相关联的的缓冲区中。

第二步,DMA 模块将 socket 缓冲区中的数据复制到协议亲引擎,这时进行第 3 次复制。

在调用 sendfile() 期间,如果有另外一个进程将文件截断,且进程没有为 SIGBUS 注册任何的信号处理函数时,sendfile() 调用仍会返回进程被信号中断前已发送的字节数,并将全局变量 errno 设置为成功。然而,类似的,如果在调用 sendfile() 前,从内核里获得了文件租约,那么 sendfile() 在返回前也会收到 RT_SIGNAL_LEASE。

到此为止,我们已经能够避免内核的多次复制了,然而我们还存在一个多余的副本,这里就是 socket buffer。那么,这个副本是否可以消除? 确实可以,但这需要特定硬件的支持。为了消除内核产生的冗余数据,需要网络适配器支持聚合操作特性。该特性被支持意味着要发送的数据无须存放在地址连续的内存空间中,相反可以存放在各个内存位置。在 2.4 版本的内核中,socket 缓冲区描述符发生了变动,以适合聚合(将零散分布的数据聚合起来发送)操作的要求 –这就是 Linux 中所谓的“零拷贝”。这种方式不但减少了多个上下文的切换,而且消除了数据冗余。从用户层程序的角度来看,没有发生任何改动,所有的代码还是和以前一样。这里所描述原理如下图所示:

sendfile相关「建议收藏」

在上图中,

第一步,sendfile() 系统调用导致 DMA 模块将文件内容复制到内核缓冲区中。

第二步,数据并未复制到 socket 缓冲区中,取而代之的是,只有记录数据位置和长度的描述符被加入到 socket 缓冲区中。DMA 模块将内核缓冲区中的数据传递给协议引擎,从而消除了遗留的最后一次复制。

由于数据实际上仍然要通过从硬盘拷贝到内存,再由内存再发送到设备,有人可能会觉得这不是真正的“零拷贝”。然而,从操作系统的角度来看,这就是“零拷贝”,因为内核空间不存在冗余数据(既不会存在将数据存放内核一块数据缓冲区中,又将数据存放在 socket 缓冲区中)。应用“零拷贝”特性,除了避免了重复复制外,还可以获得其他性能的优势,例如更少的上下文切换,更少的 CPU cache污染和没有必要的 CPU 必要校验。

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

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/140915.html原文链接:https://javaforall.net

(0)
上一篇 2022年5月8日 下午9:40
下一篇 2022年5月8日 下午10:00


相关推荐

  • centos7搭建hadoop集群之rsync和xsync[通俗易懂]

    centos7搭建hadoop集群之rsync和xsync[通俗易懂]文章记录于各个服务器(或者虚拟机等)已经配置了ssh免密登录,可执行下面操作,未配置ssh免密登录,可参考:https://blog.csdn.net/yhblog/article/details/84029535此文章是基于centos7minimal版本的,纯净系统,所以还需要安装rsync工具(确保所有节点都必须安装rsync)否则报错:安装成功:启动rsync服务sys…

    2022年6月2日
    35
  • 矩阵论:向量范数和矩阵范数的区别_矩阵范数的定义

    矩阵论:向量范数和矩阵范数的区别_矩阵范数的定义http://blog.csdn.net/pipisorry/article/details/51030563向量范数0-范数,向量中非零元素的个数。1-范数:,即向量元素绝对值之和,matlab调用函数norm(x,1) 。2-范数:,Euclid范数(欧几里得范数,常用计算向量长度),即向量元素绝对值的平方和再开方,matlab调用函数norm(x,2)。∞-范数:,即所有向量元素绝对值中

    2025年12月8日
    4
  • 2015 多校联赛 ——HDU5373(模拟)

    2015 多校联赛 ——HDU5373(模拟)

    2021年9月8日
    58
  • 启动mysql报错10038_解决navicat远程连接mysql报错10038的问题

    navicat远程连接mysql报错10038一般由以下两个原因:一:本地防火墙问题在本地安装了mysql、navicat并打开了mysql服务的情况下,来设置防火墙。首先右击或者点击入站规则,找到新建规则,点击。点击端口。在特定本地端口中填入3306.一直点击下一步。这里可以给一个好分别的名称即可。之前再尝试连接即可,若仍然不可以,可能是服务器方面的问题。二:服务器3306端口未打开首先需要在安…

    2022年4月11日
    110
  • listView1.Items.Add()与 listView1.Items[i].SubItems.Add()区别

    listView1.Items.Add()与 listView1.Items[i].SubItems.Add()区别listView1.Items是ListViewItem的集合打开资源管理器,看看windows目录,使用Details方式,一个ListViewItem就是一行,比如目录名称,修改时间,类型,大小,这整个一条记录是一个ListViewItem而listView1.Items[i].SubItems是ListViewSubItem的集合比如刚才的目录名称或

    2022年7月26日
    7
  • redis(五)Jedis连接redis[通俗易懂]

    redis(五)Jedis连接redis[通俗易懂]一、创建项目1.1、创建项目,导入依赖<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.0.0</version></dependency>注意:远程服务器器可以连通p

    2025年10月11日
    10

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号