通过文件描述符获取目录路径

11

我需要在Linux中使用文件描述符来引用一个目录的路径。这个路径不必是规范化的,只需要是可用的,以便我能将它传递给其他函数。因此,我需要像 fstatat() 这样的函数所传递的参数一样,能够调用像 getxattr() 这样没有 f-XYZ-at() 变体的函数。

到目前为止,我想出了以下解决方案;尽管它们都不是特别优雅。

最简单的解决方法是通过调用 openat(),然后使用像 fgetxattr() 这样的函数。虽然这种方法有效,但并不适用于所有情况。因此需要另一种方法来填补空缺。

下一个解决方案涉及在 proc 中查找信息:

if (!access("/proc/self/fd",X_OK)) {
    sprintf(path,"/proc/self/fd/%i/",fd);
}

当然,在没有proc的系统中,包括一些chroot环境中,这完全会出现问题。

最后一个选择是更便携但可能存在竞争条件的解决方案,如下所示:

DIR* save = opendir(".");
fchdir(fd);
getcwd(path,PATH_MAX);
fchdir(dirfd(save));
closedir(save);

显而易见的问题在于,在多线程应用程序中,修改工作目录可能会产生副作用。

然而,它可以工作的事实是有说服力的:如果我可以通过调用fchdir()然后跟着getcwd()来获取目录的路径,为什么我不能直接获取信息:fgetcwd()或其他类似函数。显然内核正在跟踪必要的信息。

那么我该如何访问它呢?


回答

Linux在内核中实现getcwd的方式是这样的:它从涉及的目录条目开始,并将该目录的父目录名称添加到路径字符串中,重复此过程直到到达根目录。理论上,这个相同的机制可以在用户空间中实现。

感谢Jonathan Leffler指出了这个算法。以下是链接到该函数内核实现的链接:https://github.com/torvalds/linux/blob/v3.4/fs/dcache.c#L2577


如果在fchdirgetcwd之间重命名目录,或者在getcwd和将结果传递给其他函数之间重命名目录,预期的行为是什么?您仍将拥有该目录的有效文件描述符,但是您获取的名称将不匹配。 - bignose
然后函数失败了。这不是一个主要的问题。 - tylerl
fchdir()必须具有路径名称才能更改cwd并不是显而易见的,它可以通过内核fd结构中的i编号来完成。 - msw
@msw 事实仍然存在,即在调用fchdir之后,您可以调用getcwd并获得有效路径。无论它如何进行映射,映射都会发生。 - tylerl
@tylerl - 现在已经很晚了,所以我可能会漏掉一些东西,但是我想你总可以为解决方案3派生一个子进程。虽然不太优雅,但子进程可以消除潜在的副作用,并且你可以通过管道或其他方式将目录名称传递回来。 - Duck
显示剩余2条评论
2个回答

8
内核以不同于您的方式来思考目录 - 它以inode号码为单位进行思考。它记录了目录的inode号码(和设备号码),这就是它作为当前目录所需的全部内容。您有时指定一个名称给它,意味着它会跟踪到对应该名称的inode号码,但它只保留inode号码,因为那是它所需的全部内容。
因此,您将需要编写一个适当的函数。您可以使用open()直接打开一个目录,以获取可由fchdir()使用的文件描述符;在许多现代系统上,您不能对其执行其他任何操作。您也可能无法打开当前目录;您应该测试该结果。这种情况很少见,但并非不存在。(SUID程序可能会chdir()到SUID权限允许的目录,但然后放弃SUID权限,使进程无法读取该目录;getcwd()调用在这种情况下也会失败-因此您必须检查错误!)此外,如果在您的(可能长时间运行的)进程打开它时删除了一个目录,则随后的getcwd()将失败。
始终检查系统调用的结果;即使它们失败,通常也有情况。有例外-getpid()是典型的例子-但它们很少。 (好吧:并不是那么少-getppid()是另一个例子,它在手册中非常接近getpid();getuid()和相关函数也不远。)
多线程应用程序是一个问题;在这些情况下使用chdir()不是一个好主意。您可能需要fork()并使子进程评估目录名称,然后以某种方式将其传递回父进程。
bignose问道:

这很有趣,但似乎与查询者报告的经验相矛盾:getcwd知道如何从fd获取路径。这表明系统至少在某些情况下知道如何从fd到路径;您能否编辑您的答案以解决此问题?

为此,了解一种机制-或至少是一种-通过该机制,getcwd()函数可以编写。忽略“没有权限”的问题,它工作的基本机制是:
  • 在根目录'/'上使用stat(这样您就知道何时停止向上移动)。
  • 在当前目录'.'上使用stat(这样您就知道自己在哪里);这会为您提供当前的inode。
  • 直到达到根目录:
  • 扫描父目录'..',直到找到与当前inode相同的条目;这会为您提供目录路径的下一个组件名称。
  • 然后将当前inode更改为父目录中'.'的inode。
  • 当您到达根目录时,您可以构建路径。

以下是该算法的实现。这是旧代码(最初为1986年;最后一次非美容性更改是1998年),它没有像应该那样使用fchdir()。如果您要遍历NFS自动安装的文件系统,则它也会工作得很糟糕-这就是为什么我不再使用它的原因。但是,这大致相当于getcwd()使用的基本方案。(哦,我看到了一个18个字符的字符串(“../123456789.abcd”)-好吧,在编写它时,我所使用的机器只有非常旧的14个字符文件名-而不是现代的flex名称。就像我说的那样,这是旧代码!我大约15年或更长时间没有见过这些文件系统了。还有一些用于处理更长名称的代码。使用时要小心。)


/*
@(#)File:           $RCSfile: getpwd.c,v $
@(#)Version:        $Revision: 2.5 $
@(#)Last changed:   $Date: 2008/02/11 08:44:50 $
@(#)Purpose:        Evaluate present working directory
@(#)Author:         J Leffler
@(#)Copyright:      (C) JLSS 1987-91,1997-98,2005,2008
@(#)Product:        :PRODUCT:
*/

/*TABSTOP=4*/

#define _POSIX_SOURCE 1

#include "getpwd.h"

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#if defined(_POSIX_SOURCE) || defined(USG_DIRENT)
#include "dirent.h"
#elif defined(BSD_DIRENT)
#include <sys/dir.h>
#define dirent direct
#else
What type of directory handling do you have?
#endif

#define DIRSIZ      256

typedef struct stat   Stat;

static Stat root;

#ifndef lint
/* Prevent over-aggressive optimizers from eliminating ID string */
const char jlss_id_getpwd_c[] = "@(#)$Id: getpwd.c,v 2.5 2008/02/11 08:44:50 jleffler Exp $";
#endif /* lint */

/* -- Routine: inode_number */

static ino_t   inode_number(char *path, char *name)
{
    ino_t           inode;
    Stat            st;
    char            buff[DIRSIZ + 6];

    strcpy(buff, path);
    strcat(buff, "/");
    strcat(buff, name);
    if (stat(buff, &st))
        inode = 0;
    else
        inode = st.st_ino;
    return(inode);
}

/*
    -- Routine: finddir
    Purpose:    Find name of present working directory

    Given:
        In:  Inode of current directory
        In:  Device for current directory
        Out: pathname of current directory
        In:  Length of buffer for pathname

    Maintenance Log
    ---------------
    10/11/86  JL    Original version stabilised
    25/09/88  JL    Rewritten to use opendir/readdir/closedir
    25/09/90  JL    Modified to pay attention to length
    10/11/98  JL    Convert to prototypes

*/
static int finddir(ino_t inode, dev_t device, char *path, size_t plen)
{
    register char  *src;
    register char  *dst;
    char           *end;
    DIR            *dp;
    struct dirent  *d_entry;
    Stat            dotdot;
    Stat            file;
    ino_t           d_inode;
    int             status;
    static char     name[] = "../123456789.abcd";
    char            d_name[DIRSIZ + 1];

    if (stat("..", &dotdot) || (dp = opendir("..")) == 0)
        return(-1);
    /* Skip over "." and ".." */
    if ((d_entry = readdir(dp)) == 0 ||
        (d_entry = readdir(dp)) == 0)
    {
        /* Should never happen  */
        closedir(dp);
        return(-1);
    }

    status = 1;
    while (status)
    {
        if ((d_entry = readdir(dp)) == 0)
        {
            /* Got to end of directory without finding what we wanted */
            /* Probably a corrupt file system */
            closedir(dp);
            return(-1);
        }
        else if ((d_inode = inode_number("..", d_entry->d_name)) != 0 &&
                 (dotdot.st_dev != device))
        {
            /* Mounted file system */
            dst = &name[3];
            src = d_entry->d_name;
            while ((*dst++ = *src++) != '\0')
                ;
            if (stat(name, &file))
            {
                /* Can't stat this file */
                continue;
            }
            status = (file.st_ino != inode || file.st_dev != device);
        }
        else
        {
            /* Ordinary directory hierarchy */
            status = (d_inode != inode);
        }
    }
    strncpy(d_name, d_entry->d_name, DIRSIZ);
    closedir(dp);

    /**
    ** NB: we have closed the directory we are reading before we move out of it.
    ** This means that we should only be using one extra file descriptor.
    ** It also means that the space d_entry points to is now invalid.
    */
    src = d_name;
    dst = path;
    end = path + plen;
    if (dotdot.st_ino == root.st_ino && dotdot.st_dev == root.st_dev)
    {
        /* Found root */
        status = 0;
        if (dst < end)
            *dst++ = '/';
        while (dst < end && (*dst++ = *src++) != '\0')
            ;
    }
    else if (chdir(".."))
        status = -1;
    else
    {
        /* RECURSE */
        status = finddir(dotdot.st_ino, dotdot.st_dev, path, plen);
        (void)chdir(d_name);    /* We've been here before */
        if (status == 0)
        {
            while (*dst)
                dst++;
            if (dst < end)
                *dst++ = '/';
            while (dst < end && (*dst++ = *src++) != '\0')
                ;
        }
    }

    if (dst >= end)
        status = -1;
    return(status);
}

/*
    -- Routine: getpwd

    Purpose:    Evaluate name of current directory

    Maintenance Log
    ---------------
    10/11/86  JL    Original version stabilised
    25/09/88  JL    Short circuit if pwd = /
    25/09/90  JL    Revise interface; check length
    10/11/98  JL    Convert to prototypes

    Known Bugs
    ----------
    1.  Uses chdir() and could possibly get lost in some other directory
    2.  Can be very slow on NFS with automounts enabled.

*/
char    *getpwd(char *pwd, size_t plen)
{
    int             status;
    Stat            here;

    if (pwd == 0)
        pwd = malloc(plen);
    if (pwd == 0)
        return (pwd);

    if (stat("/", &root) || stat(".", &here))
        status = -1;
    else if (root.st_ino == here.st_ino && root.st_dev == here.st_dev)
    {
        strcpy(pwd, "/");
        status = 0;
    }
    else
        status = finddir(here.st_ino, here.st_dev, pwd, plen);
    if (status != 0)
        pwd = 0;
    return (pwd);
}

#ifdef TEST

#include <stdio.h>

/*
    -- Routine: main
    Purpose:    Test getpwd()

    Maintenance Log
    ---------------
    10/11/86  JL    Original version stabilised
    25/09/90  JL    Modified interface; use GETCWD to check result

*/
int main(void)
{
    char            pwd[512];
    int             pwd_len;

    if (getpwd(pwd, sizeof(pwd)) == 0)
        printf("GETPWD failed to evaluate pwd\n");
    else
        printf("GETPWD: %s\n", pwd);
    if (getcwd(pwd, sizeof(pwd)) == 0)
        printf("GETCWD failed to evaluate pwd\n");
    else
        printf("GETCWD: %s\n", pwd);
    pwd_len = strlen(pwd);
    if (getpwd(pwd, pwd_len - 1) == 0)
        printf("GETPWD failed to evaluate pwd (buffer is 1 char short)\n");
    else
        printf("GETPWD: %s (but should have failed!!!)\n", pwd);
    return(0);
}

#endif /* TEST */

这很有趣,但似乎与提问者报告的经验相矛盾:即 getcwd 知道如何从 fd 获取路径。这表明系统至少在某些情况下知道如何从 fd 转换为路径;您能否编辑您的答案以解决这个问题? - bignose
2
这非常有趣,让我开始思考了。我浏览了内核源代码,确实就是这样内核进行getcwd查找的。所以你说得很准确。我已经追踪到fs/dcache.c中的一个叫做__d_path的函数。你可以自己看一下:http://lxr.linux.no/#linux+v2.6.33/fs/dcache.c#L1905 - tylerl
@tylerl:这是内核的核心所在,但有一个巨大的区别。如果您在某个目录上有多个NFS挂载的文件系统(例如/net/host1/net/host2/net/host3,其中这些都是来自三台其他机器的目录),那么内核会避免自动挂载主机,而所示代码可能会自动挂载所有三个主机,即使它只需要其中一个。这是一个真实的问题,对于一个[tag:clearcase]系统,在/vobs下有大约100个不同的VOB,在/vobs/vob83/src/libxyz下评估getcwd()会导致许多VOB被挂载。 - Jonathan Leffler

4

乔纳森的答案非常好,展示了它是如何工作的。但它没有展示解决你描述的情况的方法。

我也会使用像你描述的东西:

DIR* save = opendir(".");
fchdir(fd);
getcwd(path,PATH_MAX);
fchdir(dirfd(save));
closedir(save);

但是,为了避免线程中的竞态条件,可以fork另一个进程来做这件事。

这听起来可能很费力,但如果你不经常这样做,应该还好。

思路大致如下(没有可运行代码,只是一个基本想法):

int fd[2];
pipe(fd);
pid_t pid;
if ((pid = fork()) == 0) {
    // child; here we do the chdir etc. stuff
    close(fd[0]); // read end
    char path[PATH_MAX+1];
    DIR* save = opendir(".");
    fchdir(fd);
    getcwd(path,PATH_MAX);
    fchdir(dirfd(save));
    closedir(save);
    write(fd[1], path, strlen(path));
    close(fd[1]);
    _exit(EXIT_SUCCESS);
} else {
    // parent; pid is our child
    close(fd[1]); // write end
    int cursor=0;
    while ((r=read(fd[0], &path+cursor, PATH_MAX)) > 0) {
        cursor += r;
    }
    path[cursor]='\0'; // make it 0-terminated
    close(fd[0]);
    wait(NULL);
}

我不确定这是否能解决所有问题,我也没有进行任何错误检查,所以你需要添加相应的内容。


我看不出这种简单方法为什么不行。如果使用线程(如果您使用fork()),是否存在危险? - étale-cohomology
@étale-cohomology 将工作目录从一个线程更改会影响其他线程,因此 fchdir() 可能会对试图确定当前工作目录或访问相对于当前工作目录的文件的线程产生影响。 - glglgl
谢谢回复!但是如果您首先使用fork(),其他线程不会受到影响,对吧? - étale-cohomology
@étale-cohomology 没有,这就是我这样做的原因。 - glglgl
在Linux上,你可以使用线程来代替fork,并在fchdir()之前调用unshare(CLONE_FS)为该线程提供独立的工作目录。在macOS上,你可以类似地使用pthread_fchdir_np - Simon Kissane

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