浅谈 Linux 的 Zero Copy 技术

mmap 文件映射

  通常情况下,我们可以使用read()write()去访问文件,除此之外,Linux 还提供了mmap()系统调用,它可以将文件映射到进程的地址空间,这样程序就可以通过访问内存的方式去访问文件了。那么与read()write()相比,使用mmap()去访问文件能带来什么好处呢?
  使用mmap()一个明显的好处就是减少一次 I/O 拷贝,譬如说,当我们使用read()读取文件时,通常的做法是这样:

1
2
char buffer[SIZE];
ssize_t n = read(fd, buffer, SIZE);

  这个过程实际上发生了两次 I/O 拷贝,第一次是将磁盘中的文件内容拷贝到 OS 的文件系统缓冲区,第二次是将 OS 缓冲区的数据拷贝到用户缓冲区。而使用mmap()读取文件时,只会发生第一次拷贝操作,也就是将文件内容拷贝到 OS 文件系统缓冲区,完成这个拷贝操作之后,mmap()还会执行其它一些复杂的操作,例如将相应的 OS 缓冲区映射到进程的地址空间。
  尽管mmap()可以减少一次 I/O 拷贝,但由于mmap()的实现很复杂,调用mmap()将会带来额外的开销,因此在一些情况下,没有使用mmap()的必要:

  • 访问小文件时,直接使用read()write()将更加高效。
  • 单个进程对文件执行顺序访问时(sequential access),使用mmap()几乎不会带来性能上的提升。譬如说,使用read()顺序读取文件时,文件系统会使用 read-ahead 的方式提前将文件内容缓存到文件系统的缓冲区,因此使用read()将很大程度上可以命中缓存。

  那么,在什么情况下使用mmap()去访问文件会更高效呢?

  • 对文件执行随机访问时,如果使用read()write(),则意味着较低的 cache 命中率。这种情况下使用mmap()通常将更高效。
  • 多个进程同时访问同一个文件时(无论是顺序访问还是随机访问),如果使用mmap(),那么 OS 缓冲区的文件内容可以在多个进程之间共享,从操作系统角度来看,使用mmap()可以大大节省内存。

sendfile()

  Web Server 处理静态页面请求时,通常是从磁盘中读取网页的内容,然后发送给客户端:

1
2
read(fd, buffer, len);
write(sockfd, buffer, len);


  正如我们前面说到的,使用read()读取文件时,将发生两次 I/O 拷贝。然而,数据发送的过程也发生了两次 I/O 拷贝,第一次是write()将用户缓冲区的数据写入内核的 socket 发送缓冲区,成功写入之后write()会返回,在write()返回之后,内核会将 socket 发送缓冲区的数据拷贝到网卡驱动。可以看到,整个过程发生了四次 I/O 拷贝操作。
  然而除了考虑 I/O 拷贝带来的开销,我们还要考虑系统 context switch 带来的开销,当程序调用read()时,系统会从用户态切换到内核态,而当read()返回时,又会导致系统从内核态切换到用户态,所以调用read()发生两次 context switch,同理,调用write()也会发生两次 context switch。
  Linux 提供了sendfile()用来减少我们前面提到的 I/O 拷贝和 context switch 的次数:
1
sendfile(sockfd, fd, NULL, len);


  使用sendfile()发送文件时,实际发生了三次 I/O 拷贝,第一次是将磁盘中的文件内容拷贝到 OS 的文件系统缓冲区,第二次是将 OS 缓冲区的数据拷贝到 socket 的发送缓冲区,最后一次是将 socket 发送缓冲区的数据发送到网卡驱动。可以看到,与使用read()write()发送文件相比,使用sendfile()减少了一次 I/O 拷贝和两次 context switch。
  如果使用的网卡支持 scatter-gather 特性,那么还可以再减少一次 I/O 拷贝:


  这种情况下,使用sendfile()发送文件只会发生两次 I/O 拷贝,第一次是将磁盘中的文件拷贝到 OS 的文件系统缓冲区,而第二次是将 OS 缓冲区的数据直接拷贝到网卡驱动。可以使用下面的命令查看网卡是否支持 scatter-gather 特性:
1
2
3
4
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
tx-scatter-gather: on
tx-scatter-gather-fraglist: off [fixed]

参考资料