一文读懂零拷贝技术|splice使用
服务端要向客户端连接发送一个文件,一般过程如下:
服务端首先调用 read()
函数读取文件内容。服务端通过调用 write()/send()
函数将文件内容发送给客户端连接。
上面过程如下图所示:
从上图可以看出,在发送文件的过程中,首先需要将文件页缓存(Page Cache)从内核态复制到用户态缓存中,然后再从用户态缓存复制到客户端的 Socket 缓冲区中。
其实在上面的过程中,复制文件数据到用户态缓存这个操作是多余的,我们完全可以直接把文件页缓存的数据复制到 Socket 缓冲区即可,这样就可以减少一次拷贝数据的操作。
为了实现这样的功能,内核提供了一个名为 splice()
的系统调用,使用 splice()
系统调用可以避免从内核态拷贝数据到用户态。
不需要将内核态的数据拷贝到用户态缓存的技术被称为:
零拷贝技术
。
下面我们将介绍 splice()
系统调用的原理和实现。
splice 使用实例
如果服务端要发送文件给客户端,使用 read()/write()
方式来实现的话,代码如下所示:
/**
* 发送文件给客户端(read/write版本)
*/
int send_file_to_client(int client_fd, char *file)
{
int fd;
struct stat fstat;
int blocks, remain;
char buf[4096]; // 每次发送4096个字节
fd = open(file, O_RDONLY);
if (fd == -1) {
return -1;
}
stat(file, &fstat); // 用于获取文件的大小
blocks = fstat.st_size / 4096; // 需要发送的次数
remain = fstat.st_size % 4096; // 如果文件的大小不是4096的倍数,要额外发送这些数据
for (i = 0; i < blocks; i++) {
read(fd, buf, 4096); // 读取文件内容
write(client_fd, buf, 4096); // 发送文件内容给客户端
}
if (remain > 0) {
read(fd, buf, remain);
write(client_fd, buf, remain);
}
return 0;
}
上面代码的流程比较简单,如下:
首先通过调用 stat()
系统调用获取文件的大小。然后通过调用 read()
系统调用读取文件内容。最后通过调用 write()
系统调用将文件内容发送给客户端连接。
从上面的代码可以看出,使用 read()/write()
方式发送文件给客户端,首先需要将文件内容读到用户态缓存中,然后才能发送给客户端连接。
然而,将文件内容读取到用户态缓存这个过程是多余的,我们看看怎么使用 splice()
系统调用来避免将文件内容拷贝到用户态缓存。
使用 splice()
发送文件时,需要创建一个管道作为中转,代码如下:
/**
* 发送文件给客户端(splice版本)
*/
int send_file_to_client(int client_fd, char *file)
{
int fd;
struct stat fstat;
int blocks, remain;
int pipefd[2];
fd = open(file, O_RDONLY);
if (fd == -1) {
return -1;
}
stat(file, &fstat);
blocks = fstat.st_size / 4096;
remain = fstat.st_size % 4096;
pipe(pipefd); // 创建管道作为中转
for (i = 0; i < blocks; i++) {
// 1. 将文件内容读取到管道
splice(fd, NULL, pipefd[1], NULL, 4096, SPLICE_F_MOVE|SPLICE_F_MORE);
// 2. 将管道的数据发送给客户端连接
splice(pipefd[0], NULL, client_fd, NULL, 4096, SPLICE_F_MOVE|SPLICE_F_MORE);
}
if (remain > 0) {
splice(fd, NULL, pipefd[1], NULL, remain, SPLICE_F_MOVE|SPLICE_F_MORE);
splice(pipefd[0], NULL, client_fd, NULL, remain, SPLICE_F_MOVE|SPLICE_F_MORE);
}
return 0;
}
从上面代码可以看出,使用 splice()
发送文件时,我们并不需要将文件内容读取到用户态缓存中,但需要使用管道作为中转(关于 管道
的原理可以参考这篇文章:《图解 | Linux进程通信 - 管道实现》)。
其实这里的管道只是作为一个通道,并不会产生数据拷贝的,如下图所示:
对比 read()/write()
版本的原理图,可以看出 splice()
版本省去了拷贝文件内容到用户态缓存这个步骤。
总结
本文主要介绍了使用 read()/write()
方式传输文件与使用 splice()
方式传输文件的原理,也提供了这两种方式的实例代码。
当然,从原理上看,使用 splice()
方式传输文件会比 read()/write()
方式性能要高。但如果真实测试这两种方式,会发现性能相差并不大。这是由于 splice()
方式虽然减少了数据拷贝过程,但是其处理逻辑比 read()/write()
方式更为复杂,所以性能提升并不理想,有兴趣的读者可以自己测试一下。
下一篇文章,我们将会介绍 splice()
的实现过程。
微信扫码关注该文公众号作者