【问题标题】:What goes on behind the curtains during disk I/O?磁盘 I/O 期间幕后发生了什么?
【发布时间】:2012-11-01 04:43:10
【问题描述】:

当我在文件中寻找某个位置并写入少量数据(20 字节)时,幕后发生了什么?

我的理解

据我所知,可以从磁盘写入或读取的最小数据单位是一个扇区(传统上是 512 字节,但该标准现在正在改变)。这意味着要写入 20 个字节,我需要读取整个扇区,在内存中修改其中的一部分并将其写回磁盘。

这是我期望在无缓冲 I/O 中发生的事情。我也希望缓冲 I/O 做大致相同的事情,但要巧妙地处理它的缓存。所以我会认为,如果我通过随机查找和写入来消除局部性,缓冲和非缓冲 I/O 应该具有相似的性能......也许非缓冲会稍微好一些。

再说一次,我知道缓冲 I/O 只缓冲一个扇区是很疯狂的,所以我也可能期望它的性能非常糟糕。

我的应用程序

我正在存储由 SCADA 设备驱动程序收集的值,该驱动程序接收超过十万点的远程遥测数据。文件中有额外数据,每条记录为 40 字节,但在更新期间只需要写入 20 字节。

实施前基准

为了检查我是否不需要想出一些精巧的过度设计的解决方案,我使用几百万条随机记录进行了测试,该记录写入一个可能包含总共 200,000 条记录的文件。每个测试都为具有相同值的随机数生成器提供种子以保证公平。首先,我擦除文件并将其填充到总长度(大约 7.6 兆),然后循环几百万次,将随机文件偏移量和一些数据传递给两个测试函数之一:

void WriteOldSchool( void *context, long offset, Data *data )
{
    int fd = (int)context;
    lseek( fd, offset, SEEK_SET );
    write( fd, (void*)data, sizeof(Data) );
}

void WriteStandard( void *context, long offset, Data *data )
{
    FILE *fp = (FILE*)context;
    fseek( fp, offset, SEEK_SET );
    fwrite( (void*)data, sizeof(Data), 1, fp );
    fflush(fp);
}

也许没有惊喜?

OldSchool 方法名列前茅 - 很多。它的速度提高了 6 倍以上(148 万对每秒 232000 条记录)。为了确保没有遇到硬件缓存,我将数据库大小扩大到 2000 万条记录(文件大小为 763 meg)并得到了相同的结果。

在您指出对fflush 的明显调用之前,让我说删除它没有任何效果。我想这是因为当我寻找足够远的地方时必须提交缓存,这是我大部分时间都在做的事情。

那么,发生了什么事?

在我看来,每当我尝试写入时,缓冲的 I/O 必须读取(并可能写入全部)文件的一大块。因为我几乎从不利用它的缓存,所以这是非常浪费的。

另外(我不知道磁盘上硬件缓存的细节),如果缓冲的 I/O 在我只更改一个扇区时尝试写入一堆扇区,那会降低硬件缓存的有效性.

有没有磁盘专家可以比我的实验结果更好地评论和解释这一点? =)

【问题讨论】:

  • 我假设 fseek 会导致 fflush(因此没有差异),因为缓冲区必须在移动之前被刷新。
  • 读、写将是文件系统定义的块大小。在 linux ext4 上,默认为 4KB。同样,文件缓存(缓冲文件)在 PAGE_SIZE 单元上工作,同样很可能是 4KB。
  • 您可以在打开fp后调用setbuf(fp, NULL);来禁用stdio级缓冲。
  • 顺便说一句,sizeof(Data) 是指针的大小,而不是数据的大小。
  • @janneb 抱歉,这实际上是作为 C++ 项目编译的,但由于它主要处理 C 函数,因此我将其标记为这样。我对 sizeof 使用了 C++ 语法。

标签: c file-io buffer


【解决方案1】:

确实,至少在我的带有 GNU libc 的系统上,看起来 stdio 在写回更改的部分之前正在读取 4kB 块。对我来说似乎是假的,但我想当时有人认为这是个好主意。

我通过编写一个简单的 C 程序来打开一个文件,写入少量数据,然后退出;然后在 strace 下运行它,查看它实际触发了哪些系统调用。在偏移 10000 处写入,我看到了这些系统调用:

lseek(3, 8192, SEEK_SET)                = 8192
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1808) = 1808
write(3, "hello", 5)                    = 5

您似乎希望在这个项目中坚持使用低级 Unix 风格的 I/O,是吗?

【讨论】:

  • 4kB 可能是您文件系统的块大小(我认为这是 ext4 中的默认值)。如果你愿意,你可以改变它。
  • @dave,在这个测试中文件系统块大小确实是 4kB——我的内存页面大小也是如此——但我根本没有要求 libc 读取。在这种情况下,没有任何理由这样做。使用原始的 lseek/write 系统调用可以正常工作,无需额外的内核用户内存副本。
  • 我有一个理论为什么它会这样读:因为 fs 的块大小是 4kB 保证另一个进程没有修改文件的唯一方法是读入然后写入再次退出。使用 write 会绕过这个检查。
  • @dave:嗯,这是一个有趣的理论。不过我不明白:读-修改-写周期引入了 more 数据竞争,而不是更少。底层系统调用提供了一定程度的原子性,只有内核才能提供。
  • @paddy:哦,这完全是在讨论用户空间中发生的事情。内核仍然需要从磁盘读取整个块,用你的新写入修改它,然后(稍后)将更改写回。然而,内核可以高效且安全地做到这一点,而用户空间通常不能。
【解决方案2】:

C 标准库函数执行额外的缓冲,通常针对流式读取进行优化,而不是随机 IO。在我的系统上,我没有观察到 Jamey Sharp 看到的虚假读取我只在偏移量未与页面大小对齐时看到虚假读取 - 这可能是 C 库总是试图保持它的 IO 缓冲区对齐到 4kb 什么的。

在您的情况下,如果您在相当小的数据集上进行大量随机读取和写入,您最好使用pread/pwrite 来避免必须进行系统调用,或者简单地使用mmaping数据集并将其写入内存(如果您的数据集适合内存,则可能是最快的)。

【讨论】:

  • 尝试寻找不是 4kB 的倍数或其他可能的块大小的偏移量——这就是我使用偏移量 10,000 的原因。我怀疑你会看到和我一样的虚假读数。
  • 谢谢,我不知道pread/pwrite。我实际上是在 Windows 平台上(我已标记为 VS-2010,但已被删除 - 我想我应该在我的问题中说明它)。我发现这个 (stackoverflow.com/questions/766477/…) 表明使用 CreateFile API 的异步 I/O 可能是可行的方法。我一直回避的东西。 lseek + write 的吞吐量对我来说绰绰有余......我承认我是在具有 2 驱动器 RAID-0 配置的系统上执行此操作的。
  • @paddy,异步 I/O 是我羡慕 Windows 的少数几个地方之一。 (Linux 在这方面很糟糕;几乎唯一可行的情况是 Oracle 需要的情况。)如果 lseek + write 对你来说足够快,我肯定会这样做——但如果你这样做,AIO 值得考虑撞墙。
  • @JameySharp 我将有一个单独的线程,其唯一目的是处理包含要写入磁盘的所有消息的工作队列。在这方面,我不确定异步 I/O 会提供什么优势。我想我还是会做一些测试(但也许在我完成工作之后)——至少它会迫使我学习新的东西。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-12-07
  • 2014-05-20
  • 1970-01-01
  • 2011-04-28
  • 2010-11-30
  • 1970-01-01
  • 2012-10-10
相关资源
最近更新 更多