当磁盘已满时,使用fwrite可靠地计算写入的字节数

4
有没有可靠的方法使用C标准I/O(fwrite)在磁盘已满的情况下计算实际写入的字节数? 我一直很苦恼如何让它正常工作。问题似乎是fwrite被缓存,有时会认为它写入了比设备实际可以接受的更多的字节。 使用一个小缓冲区,与设备块大小相同,当实际写入不足一个缓冲区时,fwrite 将报告它已经写满了一个缓冲区,因此计数结束时会比正确值多一个块。我通过测试错误并且只在没有错误时增加总数来解决这个问题。 但是,使用较大的缓冲区,fwrite会写入部分缓冲区,这样我就无法计算。所以我检查了部分写入,并加上了中断循环的代码。最终得到了以下程序(MCVE):
#include <stdio.h>
#include <string.h>

//#define BUF_SIZE 4096
#define BUF_SIZE 8192
//#define BUF_SIZE 16384

int main(void)
{
    unsigned long long ct = 0;
    size_t written;
    unsigned char *buf;

    buf = malloc(BUF_SIZE);
    memset(buf, 0xFF, BUF_SIZE);

    while (1) {
        written = fwrite(buf, 1, BUF_SIZE, stdout);
        if (written < BUF_SIZE) {
            ct += written;
            break;
        }
        fflush(stdout);
        if (ferror(stdout))
            break;
        ct += written;
    }

    fprintf(stderr, "%llu bytes written\n", ct);

    return 0;
}

该设备有4k块,剩余空间为68k或72k。我尝试了4k、8k和16k的缓冲区大小。
但该死的东西仍然不起作用。当剩余空间为72k且使用8k缓冲区时,它写入了72k,然后认为又写入了4k并添加了它。
我想我可以只使用与块大小相等的缓冲区大小。但我甚至不确定这会可靠地工作。
有人知道如何使其在所有情况下都能正常工作吗?我想最好是完全绕过缓冲问题,改用POSIX I/O(open和write)。
编辑:nsilent22的建议正确地解决了问题,并将循环减少到了两行:
    setbuf(stdout, NULL);
    ...
    while ((written = fwrite(buf, 1, BUF_SIZE, stdout)) > 0)
        ct += written;

嗯... 这是一个不错的问题。这是一个标准输入输出API无法解决的地方,这很有道理:在UNIX中,文件不应该出现非致命写入错误,应用程序通常也不会期望收到ENOSPC错误。 - fuz
1
使用setbuf和NULL一起是一个选项吗? - nsilent22
@nsilent22:哦,我没有考虑过那个。似乎它可以正确地工作。既然我已经写了一个大缓冲区,那么我想它不会影响性能。感谢你的建议。你想把它发表为答案吗?这可能是最好的解决方案。 - Tom Zych
发布。希望有所帮助。 - nsilent22
如果您需要进行一些系统测试,那么缓冲的C库就不是正确的工具了;只需绕过它并使用系统函数、同步写入等。您仍然可能存在一些不确定性。如果这是生产代码,您实际上不应该关心;写入的数据已经损坏了,无论发生在文件的哪个位置(对于错误报告,“为什么”就足够了)。 - too honest for this site
2个回答

1

我认为最好的方法是完全绕过缓冲问题,而是使用POSIX I/O(openwrite)。

你找到了解决方案!缓冲确实会妨碍你。 fread 正确地返回写入流的元素数量,但是流的一部分可能尚未刷新,如果设备已满,则后续的 fflush() 可能会失败。没有可移植的方法可以找出未刷新的字节数。

你可以 fclose() 文件,重新打开它(以二进制模式),并寻找结尾来找出,但最好一开始就使用低级别的Posix I/O。

将流缓冲设置为未缓冲应该有效,但如果文件很大,则可能会导致显着的性能损失。

你确实会写大量数据来尝试覆盖硬盘。我很久以前就写了这样的一个实用程序...一定要使用无缓冲流,并使用低级Posix API和一个大缓冲区,其大小应为2的幂,并用随机数填充以打败可能会尝试压缩或以其他方式共享数据块的操作系统或硬件算法。任何伪随机多项式都可以,但在每次写入时请更改缓冲区内容。

请注意,如果您的系统是32位的,则可能会遇到文件大小限制为2G或4G的问题。由于文件系统的使用,也可能存在此类限制。您可以通过创建多个文件来解决这些问题。


文件会非常大;我将使用它来擦除一个300GB的驱动器。但是,如果我正在写4k、8k或16k缓冲区,关闭流缓冲会减慢速度吗?我的印象是,如果你正在进行大量小写操作,缓冲主要是有用的。而且,使用POSIX的write函数不会产生同样的效果吗? - Tom Zych
@TomZych:你知道这并不一定会像你想的那样“擦除”吗?现代硬盘驱动器 - 更糟糕的是固态硬盘使用替换算法,这些算法不一定会写入先前数据所在的块。它们还具有用于缺陷块的内部备用块以及额外允许写入“完整”介质、耐磨平衡等的SSD。对于驱动器,请使用机械/热擦除或(如果支持)“安全擦除”功能。通常只加密存储敏感数据。这样您就不需要擦除了。 - too honest for this site
@Olaf 是的,我已经阅读了 Gutmann 的论文,谢谢。只是尽力保护我不想毁坏的硬盘。这还是一个练习使用 C 语言的好机会,现在我已经很少用它了。 - Tom Zych
这些都很有用,但我更感兴趣的是是否可以使用标准I/O实现,因此nsilent22的答案更为准确。结果发现,非缓冲的标准I/O速度与POSIX I/O差不多。谢谢。 - Tom Zych
@TomZych:未缓冲的标准I/O的速度与POSIX I/O差不多。你很幸运,fwrite直接将其请求映射到write调用中。并非所有C库都会为未缓冲流执行此操作。 - chqrlie

1
考虑使用 setbuf 函数并将参数设置为 NULL 作为缓冲区。这将关闭流缓冲。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接