从文件描述符中检索文件名(C语言)

130

在C语言中,是否可以根据文件描述符(Linux)获取文件名?


我认为,应该选择zneak的答案,因为他的解决方案具有更好的可移植性,并且没有注意到访问问题。 - Sergei Krivonos
它不支持Ubuntu 14.04(内核3.16.0-76-generic)。我猜在Linux上也不支持。 - felipou
对于macOS,请参见D.Nathanael在另一个问题中的答案 - Jonathan Leffler
8个回答

152
您可以在/proc/self/fd/NNN上使用readlink,其中NNN是文件描述符。这将给出文件名,当它被打开时,它的名字 - 但是,如果文件自那时以来已经被移动或删除,它可能不再准确(尽管Linux在某些情况下可以跟踪重命名)。要验证,请获取给定的文件名的stat和您拥有的文件描述符的fstat,并确保st_devst_ino相同。
当然,并非所有文件描述符都指向文件,对于那些你会看到一些奇怪的文本字符串,例如 pipe:[1538488]。由于所有真实的文件名都将是绝对路径,因此您可以轻松确定哪些是这些文件描述符。此外,正如其他人所指出的那样,文件可以具有指向它们的多个硬链接 - 这只会报告它打开的一个。如果您想查找给定文件的所有名称,您将需要遍历整个文件系统。

9
只要原始文件仍然存在引用(例如打开的“fd”),则inode编号就不能被重复使用。任何在关闭文件后或在打开文件之前使用inode编号的软件都会面临竞争条件。 - R.. GitHub STOP HELPING ICE
4
危险,威尔·罗宾逊!这并不总是有效的 - 如果你使用了 setuid() 技巧,那么你的进程可能无法访问 /proc/self/fd。请参见:http://permalink.gmane.org/gmane.linux.kernel/1302546 - David Given
2
@bdonlan: 如果 /proc 没有挂载怎么办? - user2284570
1
@user2284570,这个答案是针对Linux的。我不知道NetBSD是否支持procfs - 如果您的共享主机没有提供它,那么很可能是因为NetBSD根本不支持它,并使用其他机制代替。您可能希望发布另一个问题,重点关注NetBSD,以查看是否有人知道NetBSD如何公开此信息(您可能还想尝试zneak下面的答案,因为OS X与BSD比Linux更相似)。 - bdonlan
1
@bdonlan:NetBSD支持/proc,但不强制挂载。每次我提到这个问题时,答案都是“切换到更高成本的提供商,你就会得到/proc”。因此,我正在寻找一种无需/proc的解决方案。 - user2284570
显示剩余7条评论

116

我在Mac OS X上遇到了这个问题。我们没有/proc虚拟文件系统,因此已接受的解决方案无法使用。

相反,我们有一个用于fcntlF_GETPATH命令:

 F_GETPATH          Get the path of the file descriptor Fildes.  The argu-
                    ment must be a buffer of size MAXPATHLEN or greater.

要获取与文件描述符相关联的文件,您可以使用此代码片段:

#include <sys/syslimits.h>
#include <fcntl.h>

char filePath[PATH_MAX];
if (fcntl(fd, F_GETPATH, filePath) != -1)
{
    // do something with the file path
}

由于我从不记得MAXPATHLEN定义在哪里,所以我认为使用syslimits中的PATH_MAX就可以了。


2
你期望什么?除非它是UNIX套接字,否则它没有关联的文件。 - zneak
最好测试 == 1 并在 if 语句中处理失败,还有下面涉及到 filePath 的内容。 - Kalle Richter
4
@uchuugaka 是的,所有东西都是文件,但并非所有东西都是具有名称和位置的_目录项_,位于文件系统树中。文件由inode表示,它可以存在而没有任何目录项引用它。 - lgeorget
10
在 <sys/param.h> 中:#define MAXPATHLEN PATH_MAX - geowar
3
我刚刚测试了一下,如果文件被移动并再次调用,它仍然是正确的(意思是:您会得到文件的新路径)。但是,在Linux上不支持这个功能(在Ubuntu 14.04上进行了测试-F_GETPATH未定义)。 - felipou
显示剩余3条评论

36

17
正如Tyler所指出的,不能直接且可靠地实现你所需的功能,因为一个FD可以对应于0个文件名(在各种情况下)或者大于1个(多个“硬链接”是通常描述后一种情况的方式)。如果你仍然需要带有所有限制条件(速度上和可能得到0、2等多个结果而不是1个上)的功能,那么你可以这样做:首先,使用fstat获取FD的状态信息--在结果中的struct stat中,可以得知文件位于哪个设备上,有多少个硬链接以及是否为特殊文件等等。这也许已经回答了你的问题--例如,如果硬链接数为0,你就会知道磁盘上实际上没有相应的文件名。
如果状态信息给你希望,那么你需要在相关设备上“遍历树形目录”,找到所有的硬链接(如果你不需要多个硬链接,只需一个即可)。为此,你需要使用readdir(当然还需要使用opendir等函数)递归打开子目录,直到你在从中收到的struct dirent中找到与原始struct stat中具有相同inode号的项(此时,如果你需要整个路径而不仅仅是名称,你需要向后遍历目录链来重建它)。
如果这种一般方法可行,但是你需要更详细的C代码,请告诉我们。编写这样的代码并不难(尽管如果它对你的应用程序的目的来说不可行,即你无法承受必然缓慢的性能或者可能得到!=1的结果,我宁愿不写)。

11
在轻易否定之前,建议查看lsof命令的源代码。虽然可能存在限制,但是lsof似乎能够确定文件描述符和文件名。这些信息存在于/proc文件系统中,因此从您的程序中获取这些信息应该是可行的。

6
您可以使用fstat()函数通过结构体stat获取文件的inode。然后,使用readdir()函数可以将您找到的inode与目录中存在的inode(struct dirent)进行比较(假设您已知道该目录,否则您必须搜索整个文件系统),并找到相应的文件名。 有点难懂吧?

0

在OpenBSD上没有官方API可以做到这一点,但是通过一些非常复杂的解决方法,仍然可以使用以下代码实现,注意需要链接-lkvm-lc。使用FTS遍历文件系统的代码来自this answer

#include <string>
#include <vector>

#include <cstdio>
#include <cstring>

#include <sys/stat.h>
#include <fts.h>

#include <sys/sysctl.h>
#include <kvm.h>

using std::string;
using std::vector;

string pidfd2path(int pid, int fd) {
  string path; char errbuf[_POSIX2_LINE_MAX];
  static kvm_t *kd = nullptr; kinfo_file *kif = nullptr; int cntp = 0;
  kd = kvm_openfiles(nullptr, nullptr, nullptr, KVM_NO_FILES, errbuf); if (!kd) return "";
  if ((kif = kvm_getfiles(kd, KERN_FILE_BYPID, pid, sizeof(struct kinfo_file), &cntp))) {
    for (int i = 0; i < cntp; i++) {
      if (kif[i].fd_fd == fd) {
        FTS *file_system = nullptr; FTSENT *child = nullptr; FTSENT *parent = nullptr;
        vector<char *> root; char buffer[2]; strcpy(buffer, "/"); root.push_back(buffer);
        file_system = fts_open(&root[0], FTS_COMFOLLOW | FTS_NOCHDIR, nullptr);
        if (file_system) {
          while ((parent = fts_read(file_system))) {
            child = fts_children(file_system, 0);
            while (child && child->fts_link) {
              child = child->fts_link;
              if (!S_ISSOCK(child->fts_statp->st_mode)) {
                if (child->fts_statp->st_dev == kif[i].va_fsid) {
                  if (child->fts_statp->st_ino == kif[i].va_fileid) {
                    path = child->fts_path + string(child->fts_name);
                    goto finish;
                  }
                }
              }
            }
          }
          finish:
          fts_close(file_system); 
        }
      }
    }
  }
  kvm_close(kd);
  return path;
}

int main(int argc, char **argv) {
  if (argc == 3) {
    printf("%s\n", pidfd2path((int)strtoul(argv[1], nullptr, 10), 
      (int)strtoul(argv[2], nullptr, 10)).c_str());
  } else {
    printf("usage: \"%s\" <pid> <fd>\n", argv[0]);
  }
  return 0;
}

如果函数无法找到文件(例如,因为它已经不存在),它将返回一个空字符串。如果文件被移动,在我的经验中,当将文件移动到垃圾箱时,如果FTS尚未搜索过该位置,则会返回文件的新位置。对于具有更多文件的文件系统,速度会变慢。

在整个文件系统的目录树中进行深度搜索,如果没有找到文件,则可能会出现竞争条件,尽管由于其高性能,这种情况仍然很少见。我知道我的OpenBSD解决方案是C++而不是C。如果您愿意,可以将其更改为C,大部分代码逻辑将保持不变。如果有时间,我希望很快就能用C重写它。与macOS一样,此解决方案随机获得一个硬链接(需要引用),以便在Windows和其他只能获得一个硬链接的平台上具有可移植性。如果您不关心跨平台并且想获取所有硬链接,则可以删除while循环中的break并返回一个向量。{{link1:DragonFly BSD和NetBSD与当前问题的macOS解决方案相同(完全相同的代码)},我已经手动验证过了。如果macOS用户希望从任何进程打开的文件描述符中获取路径,并插入进程ID,而不仅仅限于调用者,同时还可以获取所有可能的硬链接,而不仅仅是随机的一个,请参见{{link2:此答案}}。它应该比遍历整个文件系统要快得多,类似于Linux和其他更直接和简洁的解决方案。{{link3:FreeBSD用户可以在此问题中找到他们要寻找的内容},因为该问题中提到的操作系统级错误已经针对新的操作系统版本得到了解决。

这里有一个更通用的解决方案,它只能检索由调用进程打开的文件描述符的路径,但它应该可以在大多数类Unix系统上直接使用,与前面的解决方案相同,对于硬链接和竞争条件存在相同的问题,尽管由于if-then、for循环等较少,因此执行速度略快:

#include <string>
#include <vector>

#include <cstring>

#include <sys/stat.h>
#include <fts.h>

using std::string;
using std::vector;

string fd2path(int fd) {
  string path;
  FTS *file_system = nullptr; FTSENT *child = nullptr; FTSENT *parent = nullptr;
  vector<char *> root; char buffer[2]; strcpy(buffer, "/"); root.push_back(buffer);
  file_system = fts_open(&root[0], FTS_COMFOLLOW | FTS_NOCHDIR, nullptr);
  if (file_system) {
    while ((parent = fts_read(file_system))) {
      child = fts_children(file_system, 0);
      while (child && child->fts_link) {
        child = child->fts_link; struct stat info = { 0 }; 
        if (!S_ISSOCK(child->fts_statp->st_mode)) {
          if (!fstat(fd, &info) && !S_ISSOCK(info.st_mode)) {
            if (child->fts_statp->st_dev == info.st_dev) {
              if (child->fts_statp->st_ino == info.st_ino) {
                path = child->fts_path + string(child->fts_name);
                goto finish;
              }
            }
          }
        }
      }
    }
    finish: 
    fts_close(file_system); 
  }
  return path;
}

一个更快的解决方案,也仅限于调用进程,但应该更有效率,您可以将所有对fopen()和open()的调用都包装在一个帮助函数中,该函数存储基本上是与std::unordered_map等效的C语言版本,并将文件描述符与传递给fopen()/open()包装器的绝对路径版本配对(以及仅适用于Windows且不适用于UWP的相当无聊的_wopen_s()等支持UTF-8的东西),这可以通过realpath()在类Unix系统上完成,或通过GetFullPathNameW()(*W表示支持UTF-8)在Windows上完成。 realpath()将解析符号链接(这在Windows上并不常用),而realpath()/GetFullPathNameW()将把您打开的现有文件从相对路径转换为绝对路径。使用文件描述符和绝对路径存储的C等效的std::unordered_map(您可能需要编写自己使用malloc()和最终free()的int和c-string数组),这将比任何其他动态搜索文件系统的解决方案更快,但它具有不同的、不太吸引人的限制,即它不会记录在文件系统中移动的文件,但至少您可以使用自己的代码检查文件是否被删除,它也不会记录文件是否自从您打开并将路径存储到内存中的描述符后替换,因此可能会提供过时的结果。如果您想看到这个示例代码,请告诉我,尽管由于文件位置的改变,我不建议使用这个解决方案。

2
10层嵌套还用了goto?!?!?! 快来看看这个:倒置“if”语句以减少嵌套 - Andrew Henle
@AndrewHenle 我想这是个人喜好的问题,但我通常不喜欢有多个返回点需要更多的函数调用来释放内存,这会使意外双重释放或错过内存泄漏更容易发生,因为没有在正确的位置释放,或者太多、太少等。即使在失败的情况下,因为文件被删除而找不到它,它在我的系统上也只需要不到一秒钟的时间,而我现在的文件系统中有几万个文件。 - user4821390

0

不可能的。一个文件描述符在文件系统中可能有多个名称,或者根本没有名称。

编辑:假设您谈论的是一个普通的 POSIX 系统,没有任何特定于操作系统的 API,因为您没有指定操作系统。


4
如果是这样,我的答案适用。Linux 没有实现这个功能的设施。Linux(POSIX)文件描述符不一定指向文件,即使它们确实指向文件,它们也指向索引节点,而不是文件名。描述符可以指向已删除的文件(因此没有名称,这是创建临时文件的常见方式),或者它可能指向具有多个名称(硬链接)的索引节点。 - Tyler McHenry
3
尝试查看lsof源代码。 :) 当我有这个相同的问题一段时间时,我就是这么做的。 lsof使用黑魔法和献祭山羊 - 你不能指望复制它的行为。更具体地说,lsof与Linux内核紧密耦合,并且不通过任何用户空间代码可用的API来实现其功能。 - Tyler McHenry
32
Linux有一个非可移植的proc API可以实现这个功能。确实存在一定限制,但是说它不可能是完全错误的。 - bdonlan
1
@Tyler - lsof在用户空间运行。因此,它所做的任何事情都有一个API可用于用户空间代码 :) - bdonlan
1
@bdonlan 这个软件有太多选项了,足以让任何理智的人崩溃。 - Duck
显示剩余5条评论

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