性能优化系列目录:
性能优化理论篇 | 彻底弄懂系统平均负载
性能优化理论篇 | swap area是个什么东西
性能优化理论篇 | Cache VS Buffer,傻傻分不清 ?
在很多IO场景中,我们经常需要确保数据已经安全的写到磁盘上,以便在系统宕机重启之后还能读到这些数据。
为了编写尽可能确保数据能够安全落盘的程序,了解整个I/O缓冲系统架构至关重要。在整个I/O子系统架构中,数据在最终到达稳定存储之前可能会经过多层,如下图所示:
最上层是正在运行的应用程序,应用程序在处理数据时,通常会将数据暂时存储在内存中的缓冲区中。这些缓冲区可以是应用程序直接创建的,也可以由应用程序所调用的库来管理。但不论数据是在应用程序缓冲区中还是通过库进行缓冲,数据都存在于应用程序的地址空间中。
下一层是内核,当应用程序将数据写入文件时,数据并不会立即被写入磁盘,而是先被存储在操作系统内核管理的页面缓存中。内核页面缓存的设计是为了提高系统的效率,避免频繁的磁盘I/O操作。
现代硬盘通常配有自己的缓存(写回缓存,Write-back Cache),数据在最终写入磁盘前,可能会先被存储在硬盘设备的写回缓存中。如果此时发生断电或系统故障,数据也会丢失。
最后,最底层是非易失性存储例如磁盘中。当数据到达这一层时,被认为是“安全的”。
为了进一步说明缓冲的各层,我们假设有一个应用程序:它监听网络套接字的连接,将从每个客户端接收到的数据写入文件。在关闭连接之前,服务器确保接收到的数据已写入磁盘,并向客户端发送确认。
在接受客户端的连接后,应用程序需要将数据从网络套接字读入缓冲区。下面的函数从网络套接字中读取指定量的数据并将其写入文件。调用者已经从客户端确定了预期的数据量,并打开了一个文件流以写入数据。下面的(略微简化的)函数预期会在返回之前将从网络套接字读取的数据保存到磁盘。
int sock_read(int sockfd, FILE *outfp, size_t nrbytes) {
int ret;
size_t written = 0;
char *buf = malloc(MY_BUF_SIZE); //@1
if (!buf)
return -1;
while (written < nrbytes) { //@2
ret = read(sockfd, buf, MY_BUF_SIZE);
if (ret <= 0) {
if (errno == EINTR)
continue;
return ret;
}
written += ret;
ret = fwrite((void *)buf, ret, 1, outfp);
if (ret != 1)
return ferror(outfp);
} //@3
//@4
ret = fflush(outfp); //@5
if (ret != 0)
return -1;
ret = fsync(fileno(outfp)); //@6
if (ret < 0)
return -1;
return 0;
}
@1处是应用程序缓冲区的示例,这是我们自己在代码中显示创建的,对应上图中的Application Buffers,从套接字读取的数据放入此缓冲区。
由于传输的数据量已经确定,并且考虑到网络通信的特点(数据可能是突发的),我们决定使用libc库的流函数(fwrite() 和 fflush(),对应上图中的“Library Buffers”)进一步缓冲数据。
@2到@3之间的这段代码负责从套接字读取数据并将其写入文件流。程序执行到@4处,所有数据都已写入文件流缓冲区。
在@5处,程序调用fflush()函数,强制刷新文件流缓冲区,将数据传输到操作系统的内核缓冲区(Kernel Buffers)。
然后,在@6处,程序调用fsync()函数,将内核缓冲区中的数据强制刷新到物理存储设备(如磁盘)上,直到现在,数据才被保存到上图所示的“稳定存储”层。
特别注意:
-
fwrite返回成功,只是意味着 数据已被成功复制到用户空间的文件流缓冲区中(libc管理的缓冲区)。
-
如果希望确保数据被写入到内核的页面缓存,可以在调用fwrite之后调用fflush。fflush函数才会保证将文件流缓冲区的数据写入到内核的页面缓存。
-
如果希望确保数据被写入到磁盘,还需要调用fsync()函数,将内核缓冲区(页面缓存)中的数据强制刷新到物理存储设备。
下面这张图更详细的展示了各种缓冲区的转换条件。
图中间从上到下,我们可以看到stdio库函数把用户数据传输到stdio缓冲区(这些都在用户内存空间中维护)。当这个缓冲区满时,stdio库调用write()系统调用,把数据传输给内核缓冲区缓存(在内核内存中维护)。最后内核发起磁盘操作,将数据传输至磁盘。
图的左侧显示了任意时候对缓冲区进行显式强制刷新的调用。右侧则显示了用于自动(隐式)刷新的调用,通过禁止stdio缓冲、或使用同步文件输出系统调用(open时设置标志位O_SYNC),这样每个write()都立即刷新到磁盘。