通过fd获取目录路径

时间:2010-03-21 04:13:27

标签: c linux unix posix

我遇到了在Linux中给出其文件描述符的路径引用目录的需要。路径不必是规范的,它必须是功能性的,以便我可以将它传递给其他功能。因此,使用与传递给fstatat()之类的函数相同的参数,我需要能够调用getxattr()之类的函数,而f-XYZ-at()函数没有openat()变体。

到目前为止,我已经提出了这些解决方案;虽然没有一个特别优雅。

最简单的解决方案是通过调用fgetxattr()然后使用if (!access("/proc/self/fd",X_OK)) { sprintf(path,"/proc/self/fd/%i/",fd); } 之类的函数来避免此问题。这有效,但不是在所有情况下都有效。因此需要另一种方法来填补空白。

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

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

当然,这完全打破了没有proc的系统,包括一些chroot环境。

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

fchdir()

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

然而,它的工作原理很有吸引力:如果我可以通过调用getcwd()然后调用fgetcwd()来获取目录的路径,为什么我不能直接获取信息:getcwd或其他什么。很明显,内核正在跟踪必要的信息。

那我怎么做呢?


答案

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

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

2 个答案:

答案 0 :(得分:8)

内核认为目录与你的方式不同 - 它根据inode数量来考虑。它记录了目录的inode编号(和设备编号),这就是当前目录所需的全部内容。您有时为其指定名称这一事实意味着它会跟踪并追踪与该名称对应的inode编号,但它仅保留inode编号,因为这就是它所需要的全部内容。

因此,您必须编写合适的函数代码。您可以使用open()直接打开目录,以获取fchdir()可以使用的文件描述符;在许多现代系统中,你无法用它做任何其他事情。您也可能无法打开当前目录;你应该测试那个结果。发生这种情况的情况很少见,但并非不存在。 (SUID程序可能chdir()到SUID权限允许的目录,但随后删除SUID权限,使进程无法读取目录; getcwd()调用也会在这种情况下失败 - 所以你必须错误检查一下!)另外,如果在您的(可能是长时间运行的)进程打开时删除了一个目录,那么后续的getcwd()将会失败。

始终检查系统调用的结果;通常情况下他们可能会失败,即使他们这样做非常不方便。有例外 - getpid()是规范的例子 - 但它们很少而且很远。 (好的:不是那么远 - getppid()是另一个例子,它在手册中非常接近getpid();并且getuid()和亲戚在手册中也不远了。)

多线程应用程序是一个问题;使用chdir()并不是一个好主意。您可能必须fork()并让孩子评估目录名称,然后以某种方式将其传达回父母。


bignose问:

  

这很有趣,但似乎违背了querent的报道经验:getcwd知道如何从fd获取路径。这表明系统至少在某些情况下知道如何从fd转到路径;你可以编辑你的答案来解决这个问题吗?

为此,有助于理解 - {或者至少一种机制 - 可以编写getcwd()函数。忽略“不允许”的问题,它运作的基本机制是:

  • 在根目录'/'上使用stat(因此您知道何时停止向上)。
  • 在当前目录中使用stat'。' (所以你知道你在哪里);这会给你一个当前的inode。
  • 直到您到达根目录:
  • 扫描父目录'..',直到找到与当前inode具有相同inode的条目;这将为您提供目录路径的下一个组件名称。
  • 然后将当前inode更改为'。'的inode。在父目录中。
  • 当您到达root时,您可以构建路径。

以下是该算法的实现。它是旧代码(最初是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 */

答案 1 :(得分:4)

Jonathan的回答非常精细,展示了它是如何运作的。但它没有显示您描述的情况的解决方法。

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

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

但是,为了避免使用线程中的竞争条件,请分叉另一个进程以便执行此操作。

这可能听起来很贵,但如果你不经常这样做,那应该没问题。

这个想法是这样的(没有可运行的代码,只是一个原始想法):

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);
}

我不确定这是否会解决所有问题,我也不会做任何错误检查,所以这就是你应该添加的内容。