Nginx篇06-Sendfile指令及其原理

本文最后更新于:March 23, 2020 pm

nginx中http模块中的sendfile指令及其原理。

1、sendfile()介绍

nginx的http模块中有一个sendfile指令,默认是开启状态,官网的文档对其解释是:

Enables or disables the use of sendfile().

Starting from nginx 0.8.12 and FreeBSD 5.2.1, aio can be used to pre-load data for sendfile():

1
2
3
4
5
location /video/ {
sendfile on;
tcp_nopush on;
aio on;
}

In this configuration, sendfile() is called with the SF_NODISKIO flag which causes it not to block on disk I/O, but, instead, report back that the data are not in memory. nginx then initiates an asynchronous data load by reading one byte. On the first read, the FreeBSD kernel loads the first 128K bytes of a file into memory, although next reads will only load data in 16K chunks. This can be changed using the read_ahead directive.

简单来说就是启用sendfile()系统调用来替换read()和write()调用,减少系统上下文切换从而提高性能,当 nginx 是静态文件服务器时,能极大提高nginx的性能表现,而当 nginx 是反向代理服务器时,则没什么用了。下面我们来分析一下这个sendfile的工作原理:

2、原理分析

首先我们需要知道sendfile()和read()、write()之间最大的区别就是前者是属于系统调用,而后者是属于函数调用,我们来看下面这幅图

我们不难看出,nginx是属于Applicaiton的,而read()、write()属于函数调用,也就是在Lib Func这一层,sendfile()系统调用,位于System Call这一层,而想要对硬盘进行操作,是kernel才有的权限,上面的那些层都需要往下调用。

作为对比我们先来看一下正常情况下如果nginx调用read()和write()函数的操作过程:

我们都知道数据是存储在硬盘上面的,当数据被调用的时候会被加载进内存再被层层递进最后被CPU使用,这里个这个过程我们进行简化。

  • 步骤一:首先nginx调用read函数,这时data从harddisk从被加载进Kernel Buffer(Hard Disk)中,此时是从一开始的用户态(user mode)陷入内核态(kernel mode)才能完成操作
  • 步骤二:接着由于data需要被write()函数进行操作,所以data从Kernel Buffer(Hard Disk)传输到User Buffer中,此时从内核态(kernel mode)切换回用户态(user mode)
  • 步骤三:再接着data被write()函数从user buffer写入到Kernel Buffer(Socket Engine),此时从用户态(user mode)陷入内核态(kernel mode)
  • 步骤四:data从Kernel Buffer(Socket Engine)传输到Socket Engine,此时从内核态(kernel mode)切换回用户态(user mode)

这里需要说明两点,一是用户态和内核态之间的切换是需要执行上下文切换操作的,这是十分耗费系统资源和时间的操作,二是因为read()、write()属于函数调用,它们是没有权限在kernel中操作,无法将data直接从Kernel Buffer(Hard Disk)传输到Kernel Buffer(Socket Engine)。

那么使用sendfile()呢?,由于是系统调用,所以在步骤二和步骤三的时候就可以不需要再将数据传输到User Buffer,直接在kernel中进行操作,省去了两次状态切换,也就是省去了两次的上下文切换,从而大幅度提升了性能。

我们来看一下下面的这幅图(灵魂画手上线→_→)

最后我们再来解释一下,为什么当 nginx 是反向代理服务器时,sendfile()就没什么用了呢。

顾名思义,sendfile()的作用是发送文件,也就是接收数据的一段是文件句柄,发送数据的那一端是socket。而在做反向代理服务器的时候,两端都是socket,此时无法使用sendfile(),也就不存在性能提升这一说了。