Linux中打开的文件处理器会发生什么如果所指向的文件被移动或删除?

135
在Linux操作系统中,如果指向的文件被以下方式更改:
  • 移动 -> 文件句柄是否仍然有效?
  • 删除 -> 是否导致EBADF错误,表示无效的文件句柄?
  • 替换为新文件 -> 文件句柄指向此新文件吗?
  • 替换为新文件的硬链接 -> 我的文件句柄是否"跟随"该链接?
  • 替换为新文件的软链接 -> 我的文件句柄现在是否命中此软链接文件?
我之所以问这样的问题:是因为我正在使用热插拔硬件(例如USB设备等)。用户或其他人可能会重新连接设备(以及其/dev/file)。
处理这种情况的最佳实践是什么?
7个回答

191
如果文件被移动(在同一文件系统中)或重命名,则文件句柄仍然保持打开状态,可以继续用于读写该文件。
如果删除了该文件,则文件句柄仍然保持打开状态,也可以继续使用该文件(这并非某些人的预期)。直到最后一个句柄关闭之前,该文件才不会真正被删除。
如果新文件替换了该文件,则取决于具体情况。如果文件的内容被覆盖,文件句柄仍然有效并可以访问新内容。如果现有文件被取消链接,并使用相同名称创建一个新文件,或者如果将新文件移动到使用rename()的现有文件上,则与删除相同(参见上文)- 即文件句柄将继续引用该文件的原始版本。
通常情况下,一旦文件被打开,该文件就被打开了,无法通过更改目录结构来改变它 - 它们可以移动、重命名文件或在文件中放置其他内容,但它仍然保持打开状态。
在Unix中没有删除,只有unlink(),这是有意义的,因为它不一定会删除文件 - 只是从目录中删除链接。
另一方面,如果基础设备消失(例如,USB拔掉),则文件句柄将不再有效,并且很可能在任何操作上产生IO /错误。但仍需关闭文件句柄,即使设备重新插入,也应该关闭文件句柄,因为在这种情况下保持文件打开是不明智的。

我想你的第二点同样适用于如果包含文件的目录被删除了。是这样吗? - Drew Noakes
2
我只关心一件事:如果您使用cp命令覆盖文件,是第一种情况还是第二种情况? - xuhdev
1
“文件直到最后一个句柄关闭才会真正被删除。” 很有趣。谢谢。 - Geremia

10

文件句柄指向一个inode而不是路径,因此你大部分的场景仍然像你所假设的那样工作,因为句柄仍然指向文件。

具体来说,在删除场景中,这个函数被称为“unlink”,它破坏了文件名(dentry)和文件之间的“链接”。当你打开一个文件,然后删除它时,文件实际上仍然存在,直到它的引用计数降为零,也就是当你关闭句柄时。

编辑: 在硬件的情况下,你已经打开了一个特定设备节点的句柄,如果你拔掉了设备,内核将会失败所有对它的访问,即使设备重新连接。你需要关闭设备并重新打开它。


5
我不确定其他操作,但就删除而言:只有在文件的最后一个打开句柄被关闭时(也就是说,在文件系统中)才会发生删除。因此,不可能在应用程序下删除文件。
一些应用程序(我记不太清楚了)依赖这种行为,通过创建、打开和立即删除文件,使得文件与应用程序的寿命完全一致——可以使其他应用程序知道第一个应用程序的生命周期,而无需查看进程映射等内容。
其他操作可能也存在类似的考虑。

4
如果您想检查文件处理程序(文件描述符)是否正常,可以调用此函数。
/**
 * version : 1.1
 *    date : 2015-02-05
 *    func : check if the fileDescriptor is fine.
 */

#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>

/**
 * On success, zero is returned.  On error, -1  is  returned,  and  errno  is  set
 *      appropriately.
 */
int check_fd_fine(int fd) {
    struct stat _stat;
    int ret = -1;
    if(!fcntl(fd, F_GETFL)) {
        if(!fstat(fd, &_stat)) {
            if(_stat.st_nlink >= 1)
                ret = 0;
            else
                printf("File was deleted!\n");
        }
    }
    if(errno != 0)
        perror("check_fd_fine");
    return ret;
}

int main() {
    int fd = -1;
    fd = open("/dev/ttyUSB1", O_RDONLY);
    if(fd < 0) {
        perror("open file fail");
        return -1;
    }
    // close or remove file(remove usb device)
//  close(fd);
    sleep(5);
    if(!check_fd_fine(fd)) {
        printf("fd okay!\n");
    } else {
        printf("fd bad!\n");
    }
    close(fd);
    return 0;
}

1
if(!fcntl(fd, F_GETFL)) { 检查的意义是什么?我猜你在那里寻找 EBADF。 (你也可能忘记将 errno 初始化为0)。 - woky
这对我不起作用。我尝试使用open(O_WRONLY|O_APPEND)方法,但是当我的描述符打开时,st_nlink始终保持>=1。 - imbearr

2
以下实验表明MarkR的回答是正确的。
code.c:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <stdio.h>

void perror_and_exit() {
  perror(NULL);
  exit(1);
}

int main(int argc, char *argv[]) {
  int fd;
  if ((fd = open("data", O_RDONLY)) == -1) {
    perror_and_exit();
  }
  char buf[5];
  for (int i = 0; i < 5; i++) {
    bzero(buf, 5);
    if (read(fd, buf, 5) != 5) {
      perror_and_exit();
    }
    printf("line: %s", buf);
    sleep(20);
  }
  if (close(fd) != 0) {
    perror_and_exit();
  }
  return 0;
}

数据:

1234
1234
1234
1234
1234

使用gcc code.c编译a.out。运行./a.out。当您看到以下输出时:
line: 1234

使用rm data来删除data。但./a.out将继续运行而不会产生错误,并生成以下完整输出:
line: 1234
line: 1234
line: 1234
line: 1234
line: 1234

我在Ubuntu 16.04.3上进行了这个实验。


2

一个已删除文件的内存信息(你提供的所有示例都是已删除文件的实例)以及磁盘上的inode会一直存在,直到该文件被关闭。

硬件热插拔是完全不同的问题,如果磁盘上的inode或元数据发生任何变化,你不应该期望你的程序能够长时间保持运行。


1
在/proc/目录下,您将找到当前活动的每个进程列表,只需找到您的PID,所有相关数据都在那里。一个有趣的信息是文件夹fd/,您将找到进程当前打开的所有文件处理程序。
最终,您将在设备下找到一个符号链接(在/dev/甚至/proc/bus/usb/下),如果设备挂起,则链接将失效,无法刷新此句柄,进程必须关闭并重新打开它(即使重新连接)。
此代码可以读取您的PID链接当前状态。
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>

int main() {
    // the directory we are going to open
    DIR           *d;

    // max length of strings
    int maxpathlength=256;

    // the buffer for the full path
    char path[maxpathlength];

    // /proc/PID/fs contains the list of the open file descriptors among the respective filenames
    sprintf(path,"/proc/%i/fd/",getpid() );

    printf("List of %s:\n",path);

    struct dirent *dir;
    d = opendir(path);
    if (d) {
        //loop for each file inside d
        while ((dir = readdir(d)) != NULL) {

            //let's check if it is a symbolic link
            if (dir->d_type == DT_LNK) {

                const int maxlength = 256;

                //string returned by readlink()
                char hardfile[maxlength];

                //string length returned by readlink()
                int len;

                //tempath will contain the current filename among the fullpath
                char tempath[maxlength];

                sprintf(tempath,"%s%s",path,dir->d_name);
                if ((len=readlink(tempath,hardfile,maxlength-1))!=-1) {
                    hardfile[len]='\0';
                        printf("%s -> %s\n", dir->d_name,hardfile);

                } else
                    printf("error when executing readlink() on %s\n",tempath);

            }
        }

        closedir(d);
    }
    return 0;
}

这段最终的代码很简单,你可以尝试使用linkat函数。

int
open_dir(char * path)
{
  int fd;

  path = strdup(path);
  *strrchr(path, '/') = '\0';
  fd = open(path, O_RDONLY | O_DIRECTORY);
  free(path);

  return fd;
}

int
main(int argc, char * argv[])
{
  int odir, ndir;
  char * ofile, * nfile;
  int status;

  if (argc != 3)
    return 1;

  odir = open_dir(argv[1]);
  ofile = strrchr(argv[1], '/') + 1;

  ndir = open_dir(argv[2]);
  nfile = strrchr(argv[2], '/') + 1;

  status = linkat(odir, ofile, ndir, nfile, AT_SYMLINK_FOLLOW);
if (status) {
  perror("linkat failed");
}


  return 0;
}

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