如何closures文件?

经过多年的经验,我感到与Posix的和平。

然后,我读了大约2002年的Linus Torvalds的这个消息:

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

没有。

以上是

(a)不可携带

(b)不是目前的做法

“不可移植的”部分来自于(正如有人指出的),内核在错误的情况下closures了FD的线程环境,FD可能已被有效地重新使用(由内核)用于其他线程,第二次closuresFD是一个BUG。

不仅循环,直到EBADF不可移植,但任何循环,由于竞争条件,我可能会注意到,如果我没有“理所当然地把这样的事情理所当然”。

但是,在GCC C ++标准库实现中,我们有basic_file_stdio.cc

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

这个图书馆的主要目标是Linux,但似乎没有听取Linus的意见。

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

这是一个标准的库错误,对吧? 在每个由C ++应用程序closures的文件上。

编辑:为了避免在一些大师带着答案出现之前引起太多的警告,我应该注意, close似乎只能在特定情况下阻止,也许这些都不适用于常规文件。 我并不清楚所有的细节,但是不应该在没有selectfcntlsetsockopt情况下close EINTR 。 不过,这种可能性使得通用库代码更加危险。

Solutions Collecting From Web of "如何closures文件?"

关于POSIX, R .. 对相关问题的回答非常清楚简洁: close()是不可重启的特例,不应该使用循环。

这令我感到惊讶,所以我决定描述我的发现,接下来是我的结论,并选择了解决方案。

这不是一个真正的答案。 想想这更像是一个程序员的意见,包括这个意见背后的推理。


POSIX.1-2001和POSIX.1-2008描述了可能发生的三种可能的错误值: EBADFEINTREIOEINTREIO之后的描述符状态是“未指定的” ,这意味着它可能已经关闭也可能没有关闭。 EBADF表示fd不是有效的描述符。 换句话说,POSIX.1明确推荐使用

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

没有任何重试循环来关闭文件描述符。

(即使是奥斯汀集团的缺陷#519 R ..也没有帮助从close()错误中恢复:即使描述符本身保持打开, EINTR错误之后是否EINTR可能执行任何I / O操作。


对于Linux, close()系统调用是在fs / open.c中定义的, fs / file.c中的 __do_close()管理描述符表锁定,而filp_close()返回到fs / open.c中去处理细节。

总之,描述符条目首先被无条件地从表中移除,然后是文件系统特定的刷新( f_op->flush() ),接着是通知(dnotify / fsnotify钩子),最后是删除任何记录或文件锁。 (大多数本地文件系统,如ext2,ext3,ext4,xfs,bfs,tmpfs等都没有->flush() ,所以给定一个有效的描述符, close()不会失败,只有ecryptfs,exofs,fuse,cifs ,并且nfs在Linux-3.13.6中有->flush()处理程序,据我所知。)

这意味着在Linux中,如果close()期间在文件系统特定的->flush()处理程序中发生写入错误, 则无法重试 ; 文件描述符总是关闭,就像Torvalds说的那样。

FreeBSD的close()手册页描述了完全相同的行为。

OpenBSD和Mac OS X的 close()手册页都没有描述描述符在错误情况下是否关闭,但我相信它们共享FreeBSD的行为。


在我看来很清楚,没有必要或不需要循环安全地关闭文件描述符。 但是, close()可能仍然会返回一个错误。

errno == EBADF表示文件描述符已经关闭。 如果我的代码出乎意料地遇到了这个问题,那么表明代码逻辑中存在严重的错误,并且该过程应该优雅地退出; 我宁愿我的流程死亡,而不是生产垃圾。

任何其他的errno值都表示在完成文件状态时出错。 在Linux中,将所有剩余的数据清空到实际的存储空间肯定是一个错误。 尤其是,我可以想象,如果没有空间来缓冲数据, EIO如果数据不能被发送或写入到实际的设备或介质, EPIPE如果连接到存储器丢失, ENOSPC如果存储已经完全没有保留的未刷新的数据,等等。 如果该文件是一个日志文件,我会有进程报告失败并正常退出。 如果文件内容仍在内存中,我将删除(取消链接)整个文件,然后重试。 否则,我会向用户报告失败。

(请记住,在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-y系统上是安全的。 如果我遇到一个不是,我可以简单地用一个自定义版本替换上面,包装在一个合适的#ifdef为该操作系统。 这个保持errno不变的原因只是我的编码风格的一个怪癖; 它使得短路错误路径更短(重复代码更少)。

如果我正在关闭一个包含重要用户信息的文件,我将在关闭之前对其执行fsync()fdatasync() 。 这确保了数据点击存储,但也比正常操作造成延迟; 因此我不会为普通的数据文件做这件事。

除非我将unlink()关闭文件,否则我将检查closefd()返回值,并相应地执行。 如果我可以轻松地重试,我会,但最多一次或两次。 对于日志文件和生成/流文件,我只是警告用户。

我想提醒任何人读到这一点, 我们不能做任何事情完全可靠 ; 这是不可能的。 在我看来,我们所能做的就是尽可能可靠地发现错误何时发生。 如果我们可以轻松地,而且可以忽略的资源使用重试,我们应该。 在所有情况下,我们都应该确保通知(关于错误)传播给实际的用户。 让人类担心在重做手术之前是否需要做其他一些可能很复杂的动作。 毕竟,许多工具只是作为一个更大的任务的一部分,最好的行动通常取决于那个更大的任务。