最近,学妹在开发一套远程升级工具,需要将本地的软件包传输到远程机器上,然而她发现文件传输比较慢。于是,我想到了零拷贝技术,遂写了此文也算对零拷贝做个详细了解。
问题:现在有一个用户需要读取磁盘文件上的内容然后将其通过网络发送出去,假设使用IO系统调用read/write。
1、IO原理
要了解什么是零拷贝,首先得知道什么是IO,以及有哪些类型的IO。一般而言,IO分为:标准IO库、IO系统调用、网络IO库。
IO系统调用
Linux标准访问文件方式是通过两个系统调用实现的:read()和write(),这两个系统调用在用户态都是没有缓冲。当用户进程使用read 和 write 读写Linux的文件时,进程会从用户态进入内核态,通过I/O操作读取文件中的数据。
//read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。
ssize_t read(int fd, void * buf, size_t count);
//write()会把参数buf所指的内存写入count个字节到参数放到所指的文件内。
ssize_t write (int fd, const void * buf, size_t count);
标准IO库
标准IO库是基于IO系统调用实现的,优化了对系统调用的使用方式。引入标准IO库主要是对IO系统调用进行封装。而且,read 和 write 等底层系统调用需要在用户态和内核态之间切换,如果每次读写的数据很少,那么切换带来的开销将大大降低IO的效率,所以标准IO库在用户态也引入了缓冲机制,提升了性能。
常见的标准IO库函数:fopen、fclose、fwrite、fread、ffulsh、fseek等等。
假如按照上面IO系统调用方式,要写入数据到文件上时,内核先将数据写入到内核中所设的缓冲当中;假如这个缓冲储存器的长度是100字节,调用系统函数write时,假设每次要写入的数据的长度为10个字节,那么要调用10次write函数才能将内核缓冲区写满(内核是由缓冲区的),由此可以看出,上下文切换的次数是很多的。
如果按照标准IO库调用方式,一次调用可以将数据尽可能多的写入内核缓存,然后由内核态将数据复制到用户态缓存。因为read 和 write 等底层系统调用需要在用户态和内核态之间切换,如果每次读写的数据很少,那么切换带来的开销将大大降低IO的效率,所以标准IO库在用户态也引入了缓冲机制,提升了性能。采用内核缓存可以减少磁盘IO的次数,提升磁盘IO的效率。
如上图所示,不管采用哪种方式,从磁盘读取文件,再到网络发送。涉及到4次上下文切换和4次拷贝。如果想继续优化,则需要减少上下文切换次数,也就是要减少系统调用的次数。解决方案就是把 read、write 两次系统调用合并成一次,在内核中完成磁盘与网卡的数据交换,则提高了性能。
零拷贝
我们知道,数据的拷贝是需要借助内核实现的,而所谓的零拷贝其实是指在用户态和内核态没有了数据拷贝工作。如下图所示,减少了拷贝次数,也就减少了用户态和内核态的上下文切换次数。
当然了,如果网卡支持 SG-DMA(Direct Memory Access)技术,还可以再去除 Socket 缓冲区的拷贝,这样一共只有 2 次内存拷贝。这样效率会更高,如下图所示:在用户态发起一次调用,则内核从磁盘加载数据到内核,然后写入网卡队列。写入成功了,则再内核通知socket结果,然后socket调用返回用户态。
那么,我们再多说两句。在Linux下,使用sendfile实现零拷贝的调用,其经历了两次发展变化。
- Linux 2.1内核引入sendfile函数:
sendfile通过一次系统调用完成了文件的传送,通过sendfile发送文件只需要一次系统调用,当调用sendfile时数据的拷贝路径如下:
第一次拷贝:将数据从磁盘读取到内核缓冲区中;
第二次拷贝:将数据从内核缓冲区拷贝到socket buffer中;
第三次拷贝:将数据从socket buffer拷贝到网卡设备中发送;
- Linux2.4 内核对sendfile做了进一步的改进:
改进后的数据拷贝处理路径如下:
- 第一次拷贝:将文件从磁盘拷贝到内核缓冲区中,不再将内核缓冲区的数据拷贝到socket buffer,而是向socket buffer中写入当前要发送的数据在内核缓冲区中的位置和偏移量;
- 第二次拷贝:根据socket buffer中的位置和偏移量,直接将内核缓冲区的数据copy到网卡设备中;
总结:
每次IO请求,内核态和用户态的切换开销以及数据的拷贝开销会严重降低性能,所以零拷贝技术可以来省掉用户态和内核态之间多余的数据拷贝,大大提高了应用程序的性能,并且减少了内核态和用户态的上下文的切换。对于零拷贝需要记住以下2点:
- 零拷贝可以将读取磁盘文件网络传输的上下文切换的次数从4次降低到2次;数据拷贝次数从4次降低到2次;
- 零拷贝是针对内核来说,数据在内核模式下是无拷贝的,并不是指整个过程数据没有拷贝;