如何在C语言中检测文件写入错误?

4
我有一个嵌入式环境,用户可能会插入或拔出USB闪存驱动器。我想知道驱动器是否已被移除,或者在尝试写入驱动器时是否存在其他问题。然而,Linux只是将信息保存在其缓冲区中,并返回没有指示错误的结果。
我使用的计算机配备了2.4.26内核和libc 2.3.2。
我是这样挂载驱动器的:
i = mount(MEMORY_DEV_PATH, MEMORY_MNT_PATH, "vfat", MS_SYNCHRONOUS, NULL);
这样可以正常工作:
50:/root # mount
/dev/scsi/host0/bus0/target0/lun0/part1 on /mem type vfat (rw,sync)
50:/root #

稍后,我尝试将文件复制到它上面:

int ifile, ofile;
ifile = open("/tmp/tmpmidi.mid", O_RDONLY);
if (ifile < 0)
{
    perror("open in");
    break;
}
ofile = open(current_file_name.c_str(), O_WRONLY | O_SYNC);
if (ofile < 0)
{
    perror("open out");
    break;
}
#define BUFSZ 256
char buffer[BUFSZ];
while (1)
{
    i = read(ifile, buffer, BUFSZ);
    if (i < 0)
    {
        perror("read");
        break;
    }
    j = write(ofile, buffer, i);
    if (j < 0)
    {
        perror("write");
        break;
    }
    if (i != j)
    {
        perror("Sizes wrong");
        break;
    }
    if (i < BUFSZ)
    {
        printf("Copy is finished, I hope\n");
        close(ifile);
        close(ofile);
        break;
    }
}

如果使用一个写保护的USB存储器执行这段代码,则结果为:
Copy is finished, I hope

在控制台上,内核不断发出错误消息。我相信,如果我只是拔掉了USB驱动器(而没有卸载它),同样的事情也会发生。
我还尝试过调整devfs。我找到了如何使用注册事件自动挂载驱动器的方法,但似乎从未触发注销事件,当我拔出存储设备时。
如何在我的程序中确定我是否成功创建了一个文件?

7月4日更新: 我犯了一个愚蠢的错误,没有检查close()的结果。不幸的是,文件可以在没有错误的情况下关闭。所以这并没有帮助。那么fsync()呢?听起来很好,但它也没有捕捉到错误。
如果我有/sys文件,可能会有一些有趣的信息。我相信直到2.6.?,才会添加这个文件。
关于我的闪存驱动器质量的评论可能是有道理的。它是早期的产品之一。实际上,写保护开关现在似乎非常少见。
我认为我必须使用最彻底的选项:创建一个文件,卸载和重新挂载驱动器,并检查文件是否存在。如果这不能解决我的问题,那么肯定有什么问题!提醒自己:确保你尝试创建的文件不存在!
顺便说一句,这确实是一个C++程序。你可以通过我本来想简化的.c_str()看出来。


你尝试过使用C标准库的I/O函数(fopenfreadfwrite等)而不是POSIX函数吗? - Emile Cormier
你可能也想在关闭时检查错误。 - Vaughn Cato
5个回答

3
如果您的应用程序想要将一个或多个文件保存到 USB 存储设备中,并在小 LED 灯灭后立即让用户拔出存储设备,则需要执行以下操作:
  1. 在写入文件之前使用mount()挂载USB存储设备

    对此,您不需要挂载SYNC,这有助于低质量的USB存储设备。

  2. 使用正确的代码保存文件

    您尝试使用C语言底层I/O的方法是非常不正确的。特别地,任何read()write()都可以返回短计数(short count),即在请求大小范围内的任何正值,包括1和请求的大小本身,而这不表示出现了任何错误。

  3. 对USB存储设备上的文件调用fsync()

    验证其是否返回成功。如果返回成功,则说明数据已经被写入USB存储设备。

  4. 对USB存储设备上文件所在的目录调用fsync()

    验证其是否返回成功。如果返回成功,则说明文件元数据已经被写入USB存储设备,即使用户强制拔出USB存储设备,该文件也应该可以被访问。

  5. 使用umount()卸载USB存储设备

    这将阻塞,直到USB存储设备可以被拔出,因此您的应用程序应该在umount()返回之前表现出正在保存文件的样子。

    如果您已经执行了上述两个fsync()操作,则umount()应该非常快。取决于文件系统,内核可能需要进行一些簿记工作,但无论如何都不会花费太长时间。

否则,其他方式是不可靠的。您可以通过挂载带有同步访问的 VFAT 分区来做出某些假设,但这基本上只是徒劳。

如果您不想以 root 权限运行应用程序,则可以编写一个简单的特权服务来管理挂载和卸载。如果您怀疑可能需要多个应用程序,请强烈建议这样做,因为只有集中的挂载器/卸载器才能确定媒体何时就绪。(如果另一个应用程序同时写入同一USB介质,则它可以延迟返回“已卸载”消息。顺便说一下,当您不需要同步挂载USB介质时,这种方法效果非常好)。我个人会在/var/run/中使用一个UNIX域数据报套接字,例如/var/run/mounter.socket,用于进程间通信。

最后,如果您的Linux内核配置正确,并且已经挂载了/sys/分区,则可以扫描所有可移动媒体所在的/sys/block/sd?目录:

  • /sys/block/sd?/removable中将包含一个非零的十进制数字字符串。
  • /sys/block/sd?/size中包含设备大小,以512字节为单位的十进制数字字符串。
  • /sys/block/sd?/device/vendor中包含供应商名称作为字符串。
  • /sys/block/sd?/device/model中包含型号名称作为字符串。

这些条目是硬件级别的,并且只要USB存储器连接并接通电源就可以使用;无论它是否被挂载。如果用户拔出USB存储器,整个设备目录树将立即消失。


2

如果您想检测所有写入错误,您需要检查的不仅仅是 write() 的返回代码 - 您还必须调用 fsync()(并检查返回值),并且还要检查由 close() 报告的错误。


+1 "check for errors reported by close()"有人可能认为这不是必要的,因为当关闭文件失败时,你能做什么呢?然而,这个问题只是一个例子,说明为什么你需要这样做。文档明确将不这样做视为“严重的编程错误”。 - Damon

1
简短回答:问题出在操作系统和/或USB驱动器上。Linux有时会愉快地将USB驱动器挂载为可写,即使写保护开关已设置。然而,当它实际尝试写入时,驱动器会拒绝,并且这被处理为一个有缺陷的驱动器。(我不确定为什么会发生这种情况。我的猜测是闪存驱动器没有向操作系统报告其只读性。这可能取决于您使用的闪存驱动器的品牌和型号。)
我可以在3.2.0-26内核(具体来说是Ubuntu 12)上使用(非常老旧的)USB键盘获得相同的行为。我没有问题将写保护的USB键盘挂载为读写,并且如果我将文件复制到其中,'cp'不会抱怨。该文件甚至可能会因为缓冲而显示在目录中,但实际上从未写入任何内容。我在syslog中收到了很多错误消息。
如果我是你,我会尝试实际写入闪存驱动器并确保它成功,然后再假设驱动器实际上是可写的。具体来说,我会:
  1. 将驱动器挂载为可写。

  2. 在驱动器上创建一个具有唯一名称的新文件。

  3. 卸载驱动器以清空缓冲区。

  4. 再次挂载驱动器。

  5. 检查新文件是否仍然存在,并包含您写入其中的数据。

  6. 删除测试文件。

我对硬件和驱动程序状态不够了解,无法告诉您是否可以从API中检测到不可写的驱动器 - 可能有,但我不知道。但即使有,这种情况也会检测到不正常工作的USB驱动器。

更新:

我进行了更多的研究,发现如果设置了写保护开关,则在文件句柄上执行的fsync()将失败。 因此,我撤回上述建议。 相反,这是我的测试程序:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int
main(int argc, char *argv[]) {
    char *path;
    int f;
    size_t stat;
    const char *message = "Hello, world.\n";

    if (argc != 2) {
        printf("Need filename\n");
        exit(1);
    }

    path = argv[1];

    f = open(path, O_CREAT | O_WRONLY | O_SYNC, S_IRWXU);
    if (f < 0) {
        perror("open out");
        exit(1);
    }/* if */

    stat = write(f, message, strlen(message));
    if (stat < 0) {
        perror("write");
        exit(1);
    }/* if */

    stat = fsync(f);
    if (stat) {
        perror("fsync");
        exit(1);
    }

    stat = close(f);
    if (stat) {
        perror("close");
        exit(1);
    }/* if */

    printf("(Apparently) successfully wrote '%s'\n", path);
    return 0;
}

如果设备不可写,则在fsync()调用上应该失败。


0
这是 POSIX 的做法。如果 write 的返回值为 -1,那么你可以确定出了什么严重的问题。但是,如果 write 返回 0,也可能出现了问题。检查 errno 变量,看看它是否与此处显示的预定义写入错误之一相匹配:http://www.kernel.org/doc/man-pages/online/pages/man2/write.2.html

OP正在使用C语言,而不是C++。iostream是C++标准库的一部分。 - Emile Cormier
不完全正确。 :) OP正在使用POSIX文件I/O函数,而不是C标准库函数。 - Emile Cormier
他说他正在使用libc。这包括stdio.h。 - user457586
1
看他的代码:我看到了 openreadwrite 而不是 fopenfreadfwriteferror 不应用于检查 POSIX 文件 I/O 函数的状态。 - Emile Cormier
@EmileCormier 好的。已经更新为POSIX! - user457586

0
如果您找不到解决方案,可以尝试这个丑陋的方法。在编写并关闭输出文件后,您可以尝试以读模式打开它,并检查其大小是否正确。如果您确实想确保它具有正确的内容,则可以验证它与刚刚编写的文件具有相同的校验和。这假设操作系统将直接从USB驱动器中读取文件,而不是某种缓存。

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