【问题标题】:Get path from file descriptor when path is longer than PATH_MAX当路径长于 PATH_MAX 时从文件描述符中获取路径
【发布时间】:2025-12-05 12:10:01
【问题描述】:

我从 fanotify 接收文件系统事件。有时我想获取正在访问的文件的绝对路径。

通常,这不是问题 - fanotify_event_metadata 包含文件描述符 fd,因此我可以在 /proc/self/fd/<fd> 上调用 readlink 并获取我的路径。

但是,如果路径超过PATH_MAX,则无法再使用readlink - 它会以ENAMETOOLONG 失败。我想知道在这种情况下是否有办法获取文件路径。

显然,我可以fstat 从 fanotify 获得的描述符并遍历整个文件系统以查找具有相同设备 ID 和 inode 号的文件。但是这种方法在性能方面对我来说是不可行的(即使我优化它以忽略比PATH_MAX 短的路径)。

我尝试通过使用O_PATH 重新打开fd 并调用openat(fd, "..", ...) 来获取父目录。显然,这失败了,因为fd 没有引用目录。我还尝试在 readlink 调用失败后检查缓冲区的内容(希望它包含部分路径)。那也没用。

到目前为止,我已经设法在打开它们的进程的工作目录中获取文件的长路径(fanotify 事件包含目标进程的pid,因此我可以读取/proc/<pid>/cwd 并获取路径从那里的根)。但这是部分解决方案。

有没有一种方法可以在不遍历整个文件系统的情况下从文件描述符中获取绝对路径?最好是适用于内核 2.6.32/glibc 2.11 的那个。

更新:对于好奇的人。我已经弄清楚为什么使用足够大的缓冲区来存储整个路径的调用 readlink("/proc/self/fd/<fd>", ... 不起作用。

do_proc_readlink的实现。请注意,它不直接使用提供的buffer。相反,它会分配一个页面,并在调用d_path 时将其用作临时缓冲区。换句话说,无论buffer 有多大,d_path 总是会被限制在一个页面的大小。在 amd64 上是 4096 字节。同PATH_MAX-ENAMETOOLONG 本身在用完提到的页面时由prepend 返回。

【问题讨论】:

  • 没有链接和/或./..元素的最终解析路径是否比PATH_MAX长?如果没有,realpath() 可能适合您:man7.org/linux/man-pages/man3/realpath.3.html
  • 路径不应该比PATH_MAX 字符短吗?这听起来很奇怪。
  • @AndrewHenle, realpath 首先需要一条路径。我只有一个文件描述符和一个 PID。
  • @unwind 不。我不能代表所有文件系统,但在 ext4 之类的系统上,您几乎可以继续创建嵌套目录,直到 inode 用完为止。
  • @NikitaKakuev realpath 首先需要一条路径。 readlink() 也是如此。你是如何让那个工作的?

标签: c linux fanotify


【解决方案1】:

readlink 可以与长于PATH_MAX 的链接目标一起使用。有两个限制:链接本身的名称必须短于PATH_MAX(检查,"/proc/self/fd/<fd>" 大约是 20 个字符)并且提供的输出缓冲区必须足够大。您可能想先调用lstat 来确定输出缓冲区应该有多大,或者在缓冲区不断增长的情况下重复调用readlink

【讨论】:

  • 这是我尝试的第一件事(由man 2 readlink 建议),但没有成功。尽管缓冲区的大小,我得到ENAMETOOLONG 的长路径。
  • @NikitaKakuev:再次检查一下:您确实将实际缓冲区大小作为第三个参数传递了吗?
【解决方案2】:

PATH_MAX 的限制源于 unix(或 linux,从现在开始)需要绑定传递给内核的参数的大小。文件层次结构可以增长的深度没有限制,并且始终有可能访问所有文件,而与它们在文件系统层次结构中的深度无关。实际上受到限制的是您可以从内核传递或接收代表文件名的字符串的长度。这意味着您不能创建(因为您必须传递目标路径)长度超过此长度的符号链接,但您可以轻松地创建比此限制更长的路径。

当你将文件名传递给内核时,你可以这样做有两个原因,命名一个文件(或设备,或套接字,或 fifo,或其他),打开它等等。你这样做和你的文件名首先进入一个将路径转换为 ​​inode 的例程(这是内核实际管理的)。该例程从文件系统层次结构中的两个可能点开始扫描。这些点是根 inode 的 inode 引用和进程当前工作目录的 inode 引用。选择哪个 inode 用作离开 inode 取决于路径开头是否存在前导 / 字符。从这一点开始,每次将处理多达PATH_MAX 个字符,但这可能会导致我们深入到无法仅一步到达根目录...

假设您使用路径更改当前目录,并执行chdir A/B/C/D/E/.../Z。在那里,您创建新目录并执行相同的操作,chdir AA/AB/AC/AD/AE/.../AZ,然后是chdir BA/BB/BC/BD/... 等等......系统中没有任何东西可以阻止您深入文件系统(您可以自己尝试,我之前已经完成并测试过)您可以增长到远远大于PATH_MAX 的地图。但这仅意味着您不能直接从文件系统根目录到达那里。只要系统允许,您可以分步前往那里,具体取决于您修复根目录的位置(通过chroot(2) 系统调用)或当前目录(通过chdir(2) 系统调用)

您可能已经注意到(或没有)没有系统调用可以从根目录获取您当前的工作目录路径...这有几个原因:

  • 根 inode 和当前工作 inode 是两个本地到进程的概念。同一系统中的两个进程可以有不同的工作目录,也可以有不同的根目录,直到它们无法共享任何共同点,也无法从一个目录到达另一个。
  • inode 路径可能不明确。好吧,这对于目录来说不是这样,因为不允许两个硬链接指向同一个目录 inode(这在较旧的 unice 中是可能的,如果你有的话,必须使用 mknod(2) 系统调用创建目录访问某些 hp-ux v6 或旧的 Unix SysV R4,您可以使用 ... 条目创建目录---指向目录的祖父或类似的东西,只是作为 root 并且知道如何使用 mknod(2) 系统调用)这个想法是,当两个链接指向同一个 inode 时,哪个(或两个)都指向根,哪一个是从根 inode 到当前目录的正确路径?
  • 当前的 inode 和 root 可以被一条足够远的路径分开,不适合 PATH_MAX 限制。
  • 获取根目录可能涉及多个不同的文件系统(和文件系统类型)。所以这不是只知道磁盘中存储的数据就可以得到的,你必须知道挂载表。

由于这些原因,内核中没有直接支持知道文件的根路径。而且没有办法获得路径(这就是pwd(1) 命令所做的),而不是跟随.. 条目并到达父目录并在那里搜索一个指向当前目录的inode 号的链接...并重复此操作,直到父 inode 与上次访问的 inode 相同。只有这样你才会在根目录中(你的根目录,这与其他进程的根目录一般不同)

试试这个练习:

i=0
while [ "$i" -lt 10000 ]
do
    mkdir dir-$i
    cd dir-$i
    i=$(expr "$i" + 1)
done

看看你可以从你的层次结构中的根目录走多远。

注意 1

另一个无法从打开的描述符中获取文件路径的原因是您只能访问 inode(您用于 open(2) 的路径与实际的根路径无关,因为您可以使用符号链接和相对于工作目录,或者在打开调用和要访问路径之间更改根目录,它甚至可以不存在,因为你可以有unlink(2)d 它)inode 信息没有引用 inode 的路径,因为一个文件可以有多个(甚至数百万个)路径。在 inode 中,您只有一个 ref 计数,这意味着在该 inode 上实际完成的路径数。

【讨论】:

  • 这是一个……有趣的观点。但请查看我的问题的更新。事实证明,我并没有遇到“PATH_MAX 的限制”。问题是readlinkprocfs 的实现总是使用单页缓冲区。如果路径不适合此缓冲区,则返回 -ENAMETOOLONG。如果do_proc_readlink 分配了更大的缓冲区,即使它大于 PATH_MAX,我也会得到完整路径。
  • 单页缓冲区是什么意思。系统调用的缓冲区不必与页面边界对齐,因此缓冲区很容易跨越两个连续的页面(只有两个字节的路径,这是一个 char 文件名和最后一个 '\0' 字符,你可以得到一个使用两个系统页面的路径)