性能优化理论篇 | 如何保证数据安全落盘,5分钟彻底弄懂 一次write中的各种缓冲区 !

性能优化系列目录:
性能优化理论篇 | 彻底弄懂系统平均负载
性能优化理论篇 | 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()都立即刷新到磁盘。

  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值