MMAP

在 Linux 中,减少拷贝次数的一种方法是调用 mmap() 来代替调用 read,比如:

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

流程

首先,应用程序调用了 mmap() 之后,数据会先通过 DMA 拷贝到操作系统内核的缓冲区中去。

接着,应用程序跟操作系统共享这个缓冲区,这样,操作系统内核和应用程序存储空间就不需要再进行任何的数据拷贝操作。

应用程序调用了 write() 之后,操作系统内核将数据从原来的内核缓冲区中拷贝到与 socket 相关的内核缓冲区中。

接下来,数据从内核 socket 缓冲区拷贝到协议引擎中去,这是第三次数据拷贝操作。

作用

通过使用 mmap() 来代替 read(), 已经可以减半操作系统需要进行数据拷贝的次数。

当大量数据需要传输的时候,这样做就会有一个比较好的效率。

mmap 的问题

但是,这种改进也是需要代价的,使用 mmap() 其实是存在潜在的问题的。

当对文件进行了内存映射,然后调用 write() 系统调用,如果此时其他的进程截断了这个文件,那么 write() 系统调用将会被总线错误信号 SIGBUS 中断,因为此时正在执行的是一个错误的存储访问。

这个信号将会导致进程被杀死。

解决方案

解决这个问题可以通过以下这两种方法:

为 SIGBUS 安装一个新的信号处理器,这样,write() 系统调用在它被中断之前就返回已经写入的字节数目,errno 会被设置成 success。

但是这种方法也有其缺点,它不能反映出产生这个问题的根源所在,因为 BIGBUS 信号只是显示某进程发生了一些很严重的错误。

第二种方法是通过文件租借锁来解决这个问题的,这种方法相对来说更好一些。

我们可以通过内核对文件加读或者写的租借锁,当另外一个进程尝试对用户正在进行传输的文件进行截断的时候,内核会发送给用户一个实时信号:RT_SIGNAL_LEASE 信号,这个信号会告诉用户内核破坏了用户加在那个文件上的写或者读租借锁,那么 write() 系统调用则会被中断,并且进程会被 SIGBUS 信号杀死,返回值则是中断前写的字节数,errno 也会被设置为 success。文件租借锁需要在对文件进行内存映射之前设置。

使用 mmap 是 POSIX 兼容的,但是使用 mmap 并不一定能获得理想的数据传输性能。

数据传输的过程中仍然需要一次 CPU 拷贝操作,而且映射操作也是一个开销很大的虚拟存储操作,这种操作需要通过更改页表以及冲刷 TLB (使得 TLB 的内容无效)来维持存储的一致性。

但是,因为映射通常适用于较大范围,所以对于相同长度的数据来说,映射所带来的开销远远低于 CPU 拷贝所带来的开销。

个人收获

  1. 每一种方式都有其使用的场景,比如零拷贝就比较适合数据量特别大的场景。(kafka, rocketmq)

  2. 大家都爱吹牛,零拷贝也是要拷贝的。

参考资料

https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy2/