如何关闭文件?

18

经过多年的经验,我感觉使用Posix很舒适。

然后我读到了来自林纳斯·托瓦兹(Linus Torvalds)的这条信息,时间为2002年:(链接)

int ret;
do {
    ret = close(fd);
} while(ret == -1 && errno != EBADF);

不是的。

上述内容为:

(a) 不具有可移植性

(b) 不是当前实践方法

“不具有可移植性”的原因在于(正如某人指出的那样),在内核关闭FD时,若处于一个支持线程的环境中,该FD可能已被内核(针对其他线程)有效地重新使用,第二次关闭FD将导致错误。

不仅仅是循环直到EBADF不具有可移植性,任何循环都不具备可移植性,这是由于一个竞争条件,如果我没有“默认”这些东西,我大概会注意到。

然而,在GCC C ++标准库实现的basic_file_stdio.cc中,我们有:

    do
      __err = fclose(_M_cfile);
    while (__err && errno == EINTR);

这个库的主要目标是Linux,但似乎并没有遵守Linus。

据我了解,EINTR 只会在系统调用阻塞后发生,这意味着内核在开始被中断的工作之前接收到释放描述符的请求。所以没有必要循环。确实,SA_RESTART 信号行为不适用于 close,默认情况下会生成这样的循环,因为这是不安全的。

这就是标准库的一个bug,对于每个被C++应用程序关闭的文件都是如此,是吗?

编辑: 为了避免在一些大师回答之前引起过多的警惕,我应该指出,close 看起来只在特定情况下允许阻塞,也许从未应用于普通文件。我不清楚所有细节,但您不应该看到由 fcntlsetsockopt 打开 close 导致的 EINTR。尽管如此,这种可能性使得通用库代码更加危险。


请参见我获取链接的https://dev59.com/7mYr5IYBdhLWcg3w29hI。 - Potatoswatter
我认为EINTR会来自隐式的fflush,这种情况下close不会被调用。为了谨慎起见,应该显式地调用fflush。但是,根据您提供的链接,也许您指的是close而不是fclose - Jim Balter
@JimBalter 是的,我的问题具体是关于 close,而不是 stdio 或 iostreams,它们只是一个典型的用例和一个可靠的示例来进行工作。 - Potatoswatter
既然fclose调用close可能会产生EINTR,那么假设Nicholas Wilson在该链接中的答案是正确的,则可以关闭两个文件。 - Jim Balter
1
@JimBalter 嗯,看起来在任何平台上,包括Linux,使用C或C++标准库或使用Posix的close函数都无法可靠地关闭一个文件而不是零个或两个,除非深入研究特定于平台的文档。这有点糟糕。 - Potatoswatter
显示剩余3条评论
1个回答

8
关于POSIX,R..对相关问题的回答非常清晰简洁: close()是一个不可重启的特殊情况,不应使用循环。

这让我感到惊讶,所以我决定描述我的发现,接着在结尾给出我的结论和选择的解决方案。

这并不是真正的答案。把它看作是一位程序员的意见,包括该意见背后的推理。


POSIX.1-2001POSIX.1-2008描述了可能发生的三个errno值:EBADFEINTREIO。在EINTREIO之后,描述符状态为“未指定”,这意味着它可能已经关闭,也可能没有关闭。EBADF表示fd不是有效的描述符。换句话说,POSIX.1明确建议使用。

    if (close(fd) == -1) {
        /* An error occurred, see 'errno'. */
    }

没有任何重试循环来关闭文件描述符。
(即使是Austin Group的defect #519 R..提到的,也无法帮助从close()错误中恢复:它未指定在EINTR错误后是否可能进行任何I/O,即使描述符本身仍然打开。)
在Linux中,close()系统调用在fs/open.c中定义,__do_close()fs/file.c中管理描述符表锁定,而filp_close()则在fs/open.c中处理详细信息。
总之,描述符条目首先被无条件地从表中删除,然后进行特定于文件系统的刷新(f_op->flush()),接着是通知(dnotify/fsnotify hook),最后删除任何记录或文件锁。(大多数本地文件系统如ext2、ext3、ext4、xfs、bfs、tmpfs等都没有->flush(),因此只要有一个有效的描述符,close()就不会失败。据我所知,只有ecryptfs、exofs、fuse、cifs和nfs在Linux-3.13.6中具有->flush()处理程序。)这意味着在Linux中,如果在特定于文件系统的->flush()处理程序中发生写入错误,close()期间没有重试的方法;文件描述符总是关闭,就像Torvalds所说的那样。
FreeBSD的close()手册页面描述了完全相同的行为。

无论是OpenBSD还是Mac OS Xclose()手册都没有描述在出现错误时描述符是否关闭,但我相信它们共享FreeBSD的行为。


我认为并不需要循环来安全关闭文件描述符。但是,close()可能仍会返回错误。
errno == EBADF 表示文件描述符已经关闭。如果我的代码意外遇到这种情况,则表明代码逻辑存在重大错误,进程应该优雅地退出;我宁愿让我的进程死亡也不要产生垃圾。
任何其他errno值都表示在完成文件状态时发生了错误。在Linux中,这绝对是与刷新任何剩余数据到实际存储相关的错误。特别是,如果没有缓冲数据的空间,则可以想象出ENOMEM,如果无法将数据发送或写入实际设备或介质,则为EIO,如果与存储的连接丢失,则为EPIPE,如果存储已经满了而没有保留未刷新的数据,则为ENOSPC等。如果文件是日志文件,则应该让进程报告失败并优雅地退出。如果文件内容仍然在内存中,则应删除(unlink)整个文件并重试。否则,我会向用户报告失败。

请记住,在Linux和FreeBSD中,在出现错误的情况下不会“泄漏”文件描述符;即使发生错误,它们也保证会被关闭。我假设我可能使用的所有其他操作系统都是以同样的方式运作。

从现在开始我将使用类似以下的辅助函数

#include <unistd.h>
#include <errno.h>

/**
 * closefd - close file descriptor and return error (errno) code
 *
 * @descriptor: file descriptor to close
 *
 * Actual errno will stay unmodified.
*/
static int closefd(const int descriptor)
{
    int saved_errno, result;

    if (descriptor == -1)
        return EBADF;

    saved_errno = errno;

    result = close(descriptor);
    if (result == -1)
        result = errno;

    errno = saved_errno;
    return result;
}

我知道上述在Linux和FreeBSD上是安全的,而且我认为它在所有其他POSIX系统上也是安全的。如果我遇到一个不安全的系统,我可以简单地用适当的#ifdef为该OS包装一个自定义版本来替换上述内容。这个保持errno不变的原因只是我的编码风格的怪癖;它使得错误路径的短路更短(重复代码更少)。
如果我关闭了一个包含重要用户信息的文件,我会在关闭之前执行fsync()fdatasync()以确保数据已经存储,但这也会导致与正常操作相比的延迟;因此,我不会为普通数据文件执行此操作。
除非我将unlink()关闭的文件,否则我将检查closefd()的返回值,并根据情况采取行动。如果可以轻松重试,我会重试,但最多只能重试一两次。对于日志文件和生成/流式传输文件,我只会警告用户。
我想提醒所有阅读到这里的人,我们无法做出完全可靠的任何东西;这是不可能的。我们能做的,也应该做的,是尽可能可靠地检测出错误。如果我们可以轻松地并且使用资源很少地重试,那就应该这样做。在所有情况下,我们都应该确保将有关错误的通知传播给实际的人类用户。让人类担心在重新尝试操作之前是否需要执行其他可能复杂的操作。毕竟,很多工具只是作为更大任务的一部分使用,而最佳行动方案通常取决于该更大任务。

介绍和免责声明本身就值得一个加1 :) 。到目前为止,我得到的带回家的教训是:1)避免裸fd,并具体说明正在关闭什么,以预测特定的意外语义。2)始终针对NFS故障进行测试。系统管理员需要可靠性,但使用此功能会自食其果。也许为了最佳可靠性,日志文件等的管理应检测NFS文件系统并透明地生成本地镜像。 - Potatoswatter

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