我如何(可重现)从 close(2) 触发 EIO 错误?

How can I (reproducible) trigger an EIO error from close(2)?

close(2) 的 Linux 手册页指出:

NOTES

Not checking the return value of close() is a common but nevertheless serious programming error. It is quite possible that error on a previous write(2) operation are first reported at the final close(). Not checking the return value when closing the file may lead to silent loss of data. This can especially be observed with NFS and with disk quota. [...]

现在我想知道这是否真的是这样:很多软件项目检查close()的return值,但这是一个问题?我试图通过在文件中使用小 ext2 文件系统并写入磁盘容量边界附近的文件来生成这样的错误,但我唯一得到的是 ENOSPC 来自 write(2)系统调用。

所以:有没有一种方法可以在 close(2) 上使用引用 文件 的有效文件描述符重复触发 I/O 错误,最好是在 Linux,但 *BSD 也可以。

如联机帮助页所述,这主要是 NFS 共享的问题。引用 nfs.sourceforge.net:

So, when an application opens a file stored in NFS, the NFS client checks that it still exists on the server, and is permitted to the opener, by sending a GETATTR or ACCESS operation. When the application closes the file, the NFS client writes back any pending changes to the file so that the next opener can view the changes. This also gives the NFS client an opportunity to report any server write errors to the application via the return code from close().

这就是检查 close() 的 ret 值变得重要的地方。当您关闭 (fd) 时,磁盘文件系统在正常情况下不会提供 EIO。但是您可以尝试在程序关闭文件时拔出 USB 记忆棒。

由于 NFS 共享可能无法像您正在寻找的那样重现测试用例,您还可以使用始终如一地 returns EIO 的自定义 FUSE 文件系统。

基于 python-llfuse 的 this example,以下是您可以执行此操作的方法:

# See header in original example for a copy of the MIT license
import os, sys, stat, errno, llfuse, time
class TestFs(llfuse.Operations):
    now = time.time()
    name = b"file"
    data = b"text\n"
    fino = llfuse.ROOT_INODE + 1
    def getattr(self, inode, ctx=None):
        entry = llfuse.EntryAttributes()
        if inode == llfuse.ROOT_INODE:
            entry.st_mode, entry.st_size = (stat.S_IFDIR | 0o755), 0
        elif inode == self.fino:
            entry.st_mode, entry.st_size = (stat.S_IFREG | 0o644), len(self.data)
        else:
            raise llfuse.FUSEError(errno.ENOENT)
        entry.st_atime_ns = entry.st_ctime_ns = entry.st_mtime_ns = self.now
        entry.st_gid, entry.st_uid = os.getgid(), os.getuid()
        entry.st_ino = inode
        return entry
    def setattr(self, inode, attr, fields, fh, ctx):
        return self.getattr(inode) # ignore it
    def lookup(self, parent_inode, name, ctx=None):
        if parent_inode != llfuse.ROOT_INODE or name != self.name:
            raise llfuse.FUSEError(errno.ENOENT)
        return self.getattr(self.fino)
    def opendir(self, inode, ctx):
        if inode != llfuse.ROOT_INODE:
            raise llfuse.FUSEError(errno.ENOENT)
        return inode
    def readdir(self, fh, off):
        assert fh == llfuse.ROOT_INODE
        if off == 0:
            yield (self.name, self.getattr(self.fino), 1)
    def open(self, inode, flags, ctx):
        if inode != self.fino:
            raise llfuse.FUSEError(errno.ENOENT)
        return inode
    def read(self, fh, off, size):
        return self.data[off:off+size]
    def write(self, fh, off, buf):
        return len(buf)
    def flush(self, fh):
        raise llfuse.FUSEError(errno.EIO)
if __name__ == '__main__':
    testfs = TestFs()
    llfuse.init(testfs, "/tmp/mp")
    try:
        llfuse.main(workers=1)
    finally:
        llfuse.close()

现在,让我们试试看:

$ mkdir /tmp/mp
$ python example.py &
$ ls /tmp/mp
file
$ cat /tmp/mp/file
text
cat: /tmp/mp/file: Input/output error
$ cat /dev/null >/tmp/mp/file
cat: write error: Input/output error

解释这个例子是如何工作的:它本质上只是一堆存根实现,除了这两行:

def flush(self, fh):
    raise llfuse.FUSEError(errno.EIO)

flush 在刷新或关闭操作发生时被调用 - 我们只是提高 EIO,这意味着,每当有人刷新或关闭此文件时,系统调用将 return -EIO。您可能想要稍微调整一下这个程序,但它应该提供一种非常可重现的方法来测试遇到 EIO 时程序的行为。