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

时间:2016-02-18 15:58:14

标签: linux linux-kernel system-calls

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

  

注意

     

不检查close()的返回值是一个常见但严重的编程错误。很可能在最后的write(2)上首次报告先前close()操作的错误。关闭文件时不检查返回值可能会导致数据无声丢失。使用NFS和磁盘配额尤其可以观察到这种情况。 [...]

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

那么:有没有办法可以在write(2)上重复触发I / O错误,其中有效的文件描述符引用文件,最好是在Linux上,但* BSD会很好同样。

2 个答案:

答案 0 :(得分:2)

与manpage状态一样,这主要是NFS共享的一个问题。引用nfs.sourceforge.net:

  

因此,当应用程序打开存储在NFS中的文件时,NFS客户端会检查它是否仍然存在于服务器上,并通过发送GETATTR或ACCESS操作被允许进入开启者。当应用程序关闭文件时,NFS客户端会将所有挂起的更改写回文件,以便下一个开启者可以查看更改。这也使NFS客户端有机会通过close()的返回代码向应用程序报告任何服务器写入错误。

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

答案 1 :(得分:1)

由于NFS共享可能不像您可能要查找的那样可以重现测试用例,因此您还可以使用自定义返回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,这意味着,只要有人刷新或关闭此文件,系统调用就会返回-EIO。你可能想稍微调整一下这个程序,但它应该提供一种非常可重现的方法来测试程序在遇到EIO时的行为。