Linux路径名查找

本文详细解析了文件系统中路径行走的概念及实现,介绍了两种路径行走模式:rcu-walk和ref-walk,阐述了路径行走的具体步骤,包括路径解析、权限检查、目录项缓存查找等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  对于文件系统的大部分操作(open、stat、unlink等等)都要涉及多多少少的路径解析。路劲解析是根据路径名(name string)得到对应的dentry对象,通过路径行走的方式。本文就来介绍路径行走。

路径行走解释

  路径是由一个开始字符(根目录、当前工作目录或者fd指向的目录),和一串其他的文件名组成的,path中的每一个文件名用/分割开。名称查找(Name Lookup)就是希望根据已知的path和name的开始指针找到path中的最后一个文件名或者最后一个文件的父文件名。根据name指针找path中要求的当前父亲文件的孩子文件,如果孩子并不是最后一个path组件(component),孩子文件称为新的父文件,迭代这个过程,就像是在path上行走一样,因此被称为路径行走。
  这个过程要求父亲必须是一个目录,并且我们对父亲文件的inode节点拥有读权限。为了下一步查找把孩子文件变成父亲文件的时候,要求更多的检测规程。符号链接实质上是链接所指向文件的替代,符号链接要求递归的路径行走。挂载点必须被跟踪(因此改变vfsmount的后续路径元素引用),从挂载点路径切换到特定的安装vfsmount的根目录。这些不同的修改行为取决于具体的路径行走标志。

路径行走必须做下面几件事:

  • 找到行走的起点
  • 执行权限和有效性检查
  • 执行目录项缓存元组上的的哈希name查找
  • 遍历挂载点
  • 遍历符号链接
  • 查找或者创建要求路径中的文件

  路径行走有两个模式:rcu_walk模式和ref_walk模式。ref-walk是传统的使用自旋锁(d_lock)去并发修改目录项。比起rcu-walk这个模式,ref-walk是操作简单,但是在在路劲行走的过程中,对于每一个目录项的操作可能需要睡眠、得到锁等等这些操作。rcu-walk是基于顺序锁的目录项查找,可以不用存储任何共享数据在目录项或inode里,查找中间的元素。rcu-walk并不适合与任何一种情况,比如说:如果文件系统必须沉睡或者执行不重要的操作,就需要切换到ref-walk模式。
  在 rcu-walk 期间将会禁止抢占,也决不能出现进程阻塞,所以其效率很高;ref-walk 会在 rcu-walk 失败、进程需要随眠或者需要取得某结构的引用计数(reference count)的情况下切换进来,很明显它的效率大大低于

路径行走的过程总结

路径行走到底是在找什么呢?说的简单一点,就是在找路径所指向的文件的父目录项。因为整个查找过程是一个分量一个分量的查找的,就像是在路径上行走一样,因此成为路径行走。行走结束,得到对应的dentry。

1.中间数据

nd变量:用来保存找到的当前分量dentry我们需要的信息,而且这个分量一定是一个普通的、真正的目录。
path变量:既然nd变量只能保存真正目录的信息,如果我们得到了一个分量,我们在确定这个分量是真正的目录之前,找到的dentry信息就用path变量保存。
name变量:指向路径中当前的分量开始位置

2.大致过程

在内核空间申请一页空间,把存储在用户空间的路径信息拷贝到内核空间,初次创建nd结构体。
初始化nd结构体,理想情况下,nd保存的是name指针指向的分量的上一个分量信息,原因还是因为nd结构体要求保存真正的目录的信息。如果路径是绝对路径,nd结构体初始化为根目录的dentry信息,如果路径是相对路径,nd结构体初始化位当前进程cwd的dentry信息。
开始”行走“第一个分量,这个分量信息在初始化的时候填充完成,因此第一次行走要做的工作并不多。然后移动name指针指向第二个分量,如果这个分量是普通目录,行走这个分量:

根据分量名的hash值在内核缓冲区查找dentry
如果没有找到,调用文件系统自身的lookup,

并循环执行上面过程。
如果这个分量是.目录或者..目录,则:

如果是.目录,直接返回0。对于当前目录并不需要做任何操作
如果是..目录,找到父目录的dentry,填充nd结构体

如果这个分量是符号链接:
找到符号链接指向的文件的dentry,填充nd结构体。

代码解释

  函数user_path_parent主要执行path walking,我们下面来看看这个函数的内核实现。函数原型如下:

static struct filename *
user_path_parent(int dfd, const char __user *path, struct nameidata *nd, 
         unsigned int flags)

  函数返回值是filename结构体,参数分别是系统调用传入的dfd参数,存储在用户空间的path,带填充的nameidata结构体(这个结构体在路径行走的过程中,主要用来保存一些临时数据),最后是一个标志位。我们看到参数里面有一个nameidata结构体,这个结构体在整个路径行走过程中至关重要,所以还是在看函数之前还是看看这个结构体。定义如下:

struct nameidata {
    struct path path;               //存储文件挂载点和dentry地址
    struct qstr last;               //路径名最后分量
    struct path root;               //存在文件所在文件系统根的信息
    struct inode    *inode;         //path.dentry.d_inode 
    unsigned int    flags;          //标志
    unsigned    seq, m_seq;      //seq 是相关目录项的顺序锁序号; m_seq 是相关文件系统(其实是mount)的顺序锁序号
    int     last_type;          //路径最后的文件类型
    unsigned    depth;              //解析符号链接过程中的递归深度
    char *saved_names[MAX_NESTED_LINKS + 1];            //相应递归深度的符号链接的路径
};
//其中type的取值是一个枚举类型,如下
enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND};
//分别代表了普通文件,根文件,.文件,..文件和符号链接文件

  下面我们还需要看看filename结构体是什么样子,因为我们需要分析的函数的返回值是这个类型的,我们必须知道我们的函数是返回了一个什么东西。定义如下:

struct filename {
        const char      *name;    //真实的名字
    const __user char   *uptr;       //原来的用户指针
    struct audit_names  *aname;
    bool            separate;       //name是不是应该被释放
};

  这里面的前两个指针变量不是很理解,没关系,等会分析完函数就明白了。接下来我们才来分析user_path_parent函数,函数代码如下:

struct filename *s = getname(path);
    int error;

    /* only LOOKUP_REVAL is allowed in extra flags */
    //设置LOOKUP_REVAL标志
    flags &= LOOKUP_REVAL;

    if (IS_ERR(s))
        return s;

    error = filename_lookup(dfd, s, flags | LOOKUP_PARENT, nd);
    if (error) {
        putname(s);
        return ERR_PTR(error);
    }

    return s;

  很明显,可以看到除了一些出错处理,函数user_path_parent函数主要调用了getname函数和filename_loopup函数。所以我们还是分析这两个函数。显示分析getname函数:

    result = __getname();
    if (unlikely(!result))
        return ERR_PTR(-ENOMEM);

    /*
     * First, try to embed the struct filename inside the names_cache
     * allocation
        */
    kname = (char *)result + sizeof(*result);
    result->name = kname;
    result->separate = false;
    max = EMBEDDED_NAME_MAX;
recopy:
    len = strncpy_from_user(kname, filename, max);
    if (unlikely(len < 0)) {
        err = ERR_PTR(len);
        goto error;
    }

  函数getname第一步就是调用__getname函数,为了避免本文边的很冗长,这个函数我们暂且不具体分析。他的功能就是在内核空间分配4096字节的空间,然后把地址赋给result,kname保存分配到的这篇空间的尾地址,并且把这个地址赋给filename的name成员变量,这里我们可以猜测一下,不管将来name里面保存了什么变量,肯定是存储在这片空间的末尾的,并且设置这个name不能被释放。EMBEDDED_NAME_MAX这个宏变量的是这页空间存储完filename结构体以后,还剩多少字节空间,把这个值赋给max变量。开始把存储在用户空间的path拷贝到内核空间,并且返回拷贝字节数,并且判断拷贝动作是否成功,若出错进行出错处理。

 if (len == EMBEDDED_NAME_MAX && max == EMBEDDED_NAME_MAX) {
        kname = (char *)result;
        result = kzalloc(sizeof(*result), GFP_KERNEL);
        if (!result) {
            err = ERR_PTR(-ENOMEM);
            result = (struct filename *)kname;
            goto error;
        }

        result->name = kname;
        result->separate = true;

        max = PATH_MAX;
    /* The empty path is special. */
    if (unlikely(!len)) {
        if (empty)
            *empty = 1;
        err = ERR_PTR(-ENOENT);
        if (!(flags & LOOKUP_EMPTY))
            goto error;
    }

    err = ERR_PTR(-ENAMETOOLONG);
    if (unlikely(len >= PATH_MAX))
        goto error;

    result->uptr = filename;
    result->aname = NULL;
    audit_getname(result);
    return result;

  判断如果拷贝到内核空间的path把新分配的这页占满了,kname指向这个空间开始的位置(kname时刻指向path开始的位置),然后调用kalloc函数分配再分配一页空间。成功以后,把kname的地址(也就是path存储在内核空间的首地址赋给result的name域,设置name可以被释放。)并且把之前春初在user空间的地址存储在result的utpr域 (现在明白了这个原来的用户是什么意思了吧)。获取到aname域,并且填充。最后返回result。这个getname函数我们就分析完成了。下面来看看filename_lookup函数:

    err = path_init(dfd, name, flags | LOOKUP_PARENT, nd, &base);

    if (unlikely(err))
        return err;

    current->total_link_count = 0;              //设置当前进程描述符符号链接数

    //路径名查找操作的核心

    err = link_path_walk(name, nd);

    ...

  这个函数功能就比较复杂了,我们先来大概讲讲这些函数的功能,并且展开一部分函数仔细分析。首先是path_init函数,顾名思义就是初始化path函数,我们前面说了整个路径查找的过程其实就是填充nameidata的过程,所以这个函数肯定就是初始化nameidata,然后是就函数link_path_walk,这个函数是整个路径名查找过程的核心功能实现函数,经过这个函数,我们就找到了路径中的最后一个目录的dentry和inode信息了。然后后面的我还没有研究清楚是做什么的函数,所以这里暂不分析。我们接下来看看path_init函数,

static int path_init(int dfd, const char *name, unsigned int flags,
             struct nameidata *nd, struct file **fp)
{
    int retval = 0;

    nd->last_type = LAST_ROOT; /* if there are only slashes... */
    nd->flags = flags | LOOKUP_JUMPED;
    nd->depth = 0;    

    if (flags & LOOKUP_ROOT) {
        }

  首先将 last_type 设置成 LAST_ROOT,意思就是在路径名中只有“/”。为方便叙述,我们把一个路径名分成三部分:起点(根目录或工作目录)、子路径(以“/”分隔的一系列子字符串)和最终目标(最后一个子路径),Kernel 会一个子路径一个子路径的遍历整个路径。所以 last_type 表示的是当前子路径(不是 dentry 或 inode)的类型。LOOKUP_ROOT标志 可以提供一个路径作为根路径,主要用于两个系统调用 open_by_handle_at 和 sysctl,这里不做分析了。

  nd->root.mnt = NULL;

    nd->m_seq = read_seqbegin(&mount_lock);
    if (*name=='/') {                   
        if (flags & LOOKUP_RCU) {
            rcu_read_lock();
            nd->seq = set_root_rcu(nd
else {
            set_root(nd);
            path_get(&nd->root);
        }
        nd->path = nd->root;                    
    } else if (dfd == AT_FDCWD) {                   //表示path应该是当前进程的cwd           
            if (flags & LOOKUP_RCU) {
            struct fs_struct *fs = current->fs;             //获取进程的fs结构体,             
            unsigned seq;

            rcu_read_lock();

            do {
                seq = read_seqcount_begin(&fs->seq);
                nd->path = fs->pwd;
                nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
            } while (read_seqcount_retry(&fs->seq, seq));
        } else {
            get_fs_pwd(current->fs, &nd->path);
        }
} 

  设置vfsmount域是空,初始化相关文件系统的的顺序锁序号。然后根据给定的path内容,设置其实位置。如果给定的是绝对路径(第一个字符是’/’),就把path指向进程的根目录,如果dfd==AF_FDCWD,表示path是调用进程的当前工作路径,获取current(这个在前面分析do_fork的博文里面解释了这个宏表示当前进程)的当前工作路径,把path执行这个路径。

else {                       //这个else选择表示 path不是一个绝对路径,而且dfd不是一个特殊的值
        /* Caller must check execute permissions on the starting path component */
        struct fd f = fdget_raw(dfd);               
        struct dentry *dentry;

        if (!f.file)
            return -EBADF;

        dentry = f.file->f_path.dentry;            

        if (*name) {
            if (!d_can_lookup(dentry)) {
                fdput(f);
                return -ENOTDIR;
            }

        nd->path = f.file->f_path;                  
        if (flags & LOOKUP_RCU) {
            if (f.flags & FDPUT_FPUT)
                *fp = f.file;
            nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
            rcu_read_lock();
        } else {
            path_get(&nd->path);
            fdput(f);
        }
    }

    nd->inode = nd->path.dentry->d_inode;              
    return 0;

  如果传入的path不是一个绝对路径,而且dfd不是一个特殊的值。根据这个dfd得到对应的fd结构体,然后获取这个dfd所指向文件的dentry结构体,把path指向这个目录。
  所以大概来说,path_init函数就是根据传入的不同参数初始化nd的path域。
分析完了path_init函数,我们接下来分析path walk的核心函数——link_path_walk函数。

static int link_path_walk(const char *name, struct nameidata *nd)
{
    struct path next;
    int err;

    while (*name=='/')             
        name++;                     
    if (!*name)                    
        return 0;

    /* At this point we know we have a real path component. */

    for(;;) {

        struct qstr this;
        long len;
        int type;

        err = may_lookup(nd);
        if (err)
            break;

        len = hash_name(name, &this.hash);

        //填充quick string结构体
        this.name = name;
        this.len = len;

        type = LAST_NORM;               

        if (name[0] == '.') switch (len) {
            case 2:
                if (name[1] == '.') {
                    type = LAST_DOTDOT;

  函数头解释:函数返回一个int值可以判断查找操作是否出错,参数name就是从用户空间复制来的path,nd表示查找过程中用来存储临时数据的nameidata结构体。
  首先跳过多个连续的/(内核是有这样的容错的,你可以试试输入一个 ls ///和ls /输出是不是一样的),这样做是为了是的name指针指向一个真正的path中的第一个文件名 判断name是不是空的,空返回。因为如果是/开始的,在path_init函数中nd填充的就是根文件的信息,如果此时name为空表示path就是根目录,那么就不需要pathwalk已经找到了对应的dentry和其他的信息。现在开始一个很大的死循环,可以肯定我们所有的查找操作都是在这个循环里。在查找之前要先检测权限。如果权限检测出错,跳出循环。计算name(系统调用传入的path),填充quick string结构体。初始化name的第一个文件的文件类型,(分为普通文件,.文件和..文件)。

                if (name[1] == '.') {
                    type = LAST_DOTDOT;
                    nd->flags |= LOOKUP_JUMPED;
                }
                break;
            case 1:
                type = LAST_DOT;
        }
        if (likely(type == LAST_NORM)) {            //说明path的第一个component并不是.或者..开始的,那就接着查询>下一个
            struct dentry *parent = nd->path.dentry;
            nd->flags &= ~LOOKUP_JUMPED;                //清除查找jumped标志
            if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
                err = parent->d_op->d_hash(parent, &this);
                if (err < 0)
                    break;
            }
        }

        nd->last = this;            //设置nameidata的路径名最后分量是当前路径,
        nd->last_type = type;           //设置type
        do {
            len++;     
        } while (unlikely(name[len] == '/'));
        if (!name[len])
            return 0;

        //使name再次指向下一个path component
        name += len; 

如果第一个分量是普通文件,得到path中的第一个分量的dentry(这个在path_init这个函数在填充nd的path时候填充好了),nd中的last设置为当前的文件,。name[len]访问待查找路径中的下一个分量,判断如果下一个分量的第一个字符是空的,表示当前的文件是带查找路径的最后一个分量。跳过两个分量中间的斜线(这里和开始的时候相似,有可能用户输入了多个斜线),跳过以后再进行一次前面的判断。如果不空,name指针就指向下一个分量的开始位置(注意这里len并不是第一个分量的长度了,应该是前一个分量加上一个或者多个斜线),为下一次循环做准备。这里要注意修改name指针的时候,必须要确定后指针移动后指向的空间是可以访问的,在最高特权级下访问了不该访问的内存是一个不可挽回的错误

捋一下,刚才的过程大概就是“走过”路径的第一个分量,如果路径以.或者..开头,设置nd的type成员是对应的类型。如果path是以/开始的,跳过前面的不止一个/,访问完第一个分量(访问是指根据该分量的信息填充nd的last和last_type成员),name指针指向下一个分量。

现在我们已经知道了name此时指向的空间存储的是一个中间分量,调用walk_component“走过这个节点”,walk_component定义如下:

static inline int walk_component(struct nameidata *nd, struct path *path,
        int follow)
{
    struct inode *inode;
    int err;

    if (unlikely(nd->last_type != LAST_NORM))
        return handle_dots(nd, nd->last_type);

    //填充path结构体,在rcuwalk模式下,
    err = lookup_fast(nd, path, &inode);
    if (unlikely(err)) {
        if (err < 0)
            goto out_err;

        err = lookup_slow(nd, path);
        if (err < 0)
            goto out_err;
            inode = path->dentry->d_inode;
    }

在遍历的时候我们看到了,子目录被分成三种情况,.目录或者..目录、普通目录或者一个符号链接。在walk_component函数开始的时候就判断了是不是.目录或者..目录,如果是执行handle_dots,这个函数不展开分析了,大概执行的功能是:如果是.目录就直接返回0,如果是..目录表示要走进父目录,返回的时候,nd结构体已经“站在了”父目录上(nd结构体中填充的是父目录的信息)。
如果当前的目录是一个普通目录,我们前面说了路径行走有两个策略:先在效率高的rcu-walk模式下“行走”,如果失败了就在效率较低的ref-walk模式下“行走。lookshihouup_fast函数应该就是指的两个查找策略,先调用lookup_fast,当返回值大于0的时候,才会调用lookup_slow函数当我们先来分析lookup_fast(rcu-walk模式),这里有必要澄清一下,ref-walk模式并不是一定可以找到的,有可能也会失败。

static int lookup_fast(struct nameidata *nd,
               struct path *path, struct inode **inode)
{

    if (nd->flags & LOOKUP_RCU) {
        unsigned seq;
        //找到nameidata last的目录项,__d_lookup_rcu函数查找dentry必须要求拥有rcu锁
        dentry = __d_lookup_rcu(parent, &nd->last, &seq);

        //如果找到的dentry是空的,则进行切换到ref_mode模式进行再次查找
        if (!dentry)
            goto unlazy;

       *inode = dentry->d_inode;
        if (read_seqcount_retry(&dentry->d_seq, seq))
            return -ECHILD;

                if (__read_seqcount_retry(&parent->d_seq, nd->seq))
            return -ECHILD;
        nd->seq = seq;

        if (unlikely(dentry->d_flags & DCACHE_OP_REVALIDATE)) {
            status = d_revalidate(dentry, nd->flags);
            if (unlikely(status <= 0)) {
                if (status != -ECHILD)
                    need_reval = 0;
                goto unlazy;
            }
        }
        path->mnt = mnt;
        path->dentry = dentry;
        if (unlikely(!__follow_mount_rcu(nd, path, inode)))
            goto unlazy;

        reurn 0

调用__d_lookup_rcu函数在哈希桶中找到对应的dentry(这是哪个dentry还记得吗?这个是name指向的那个目录分量的dentry),如果找到了就返回dentry,如果没找到就跳转到unlazy标记处(切换到ref-walk模式继续查找)。根据这个dentry得到对应的inode,进行一系列的检查操作,这样是为了确保在读取的时候,并没有其他进程对这些结构进行修改操作(rcu-walk模式并没有加锁),更新的临时变量path,这时候不能直接修改nd变量,因为不能确定这个分量是不是目录,nd记录的信息必须是目录,然后结束。
其中有很多个跳转到unlazy标志的语句,我们前面说了,跳到unlazy标志表示rcu模式查找失败,用ref模式进行查找。ref模式的fast查找还是在内核缓冲区查找相应的dentry,和上述过程类似,这就不深入讲了。下来大概看看lookup_slow函数:

static int lookup_slow(struct nameidata *nd, struct path *path)
{
    ...
    mutex_lock(&parent->d_inode->i_mutex);
    dentry = __lookup_hash(&nd->last, parent, nd->flags);
    mutex_unlock(&parent->d_inode->i_mutex);
    ...
}

看到这里,大家应该就明白了,为什么这种查找方法很慢呢,因为这种查找是互斥操作,进程可能会阻塞。而且lookup_hash函数还是再会回到dcache中在找一遍,如果没有找到的话就调用文件系统自己的lookup函数从头开始找,所以这种方式比lookup_fast方式比起来慢多了。
看完了这两个lookup函数,我们还是回到walk_component函数,剩下的也不多了。

      if (should_follow_link(path->dentry, follow)) {
        if (nd->flags & LOOKUP_RCU) {
            if (unlikely(nd->path.mnt != path->mnt ||
                     unlazy_walk(nd, path->dentry))) {
                err = -ECHILD;
                goto out_err;
            }
        }
        BUG_ON(inode != path->dentry->d_inode);
        return 1;
    }
    path_to_nameidata(path, nd);
    nd->inode = inode;
    return 0;

询问是否需要跟踪符号链接,还记得我们之前说的分类吗?在路径行走的时候,子分量被分为三种:.开头的目录、普通目录和符号链接。如果这个自分量是符号链接,根据这个链接找到指向的目标文件,用目标文件替代该链接文件。如果这个分量不是符号链接,这时候我们可以确定这个分量就是一个普通的目录,根据path的值,填充nameidata,也就是nd变量。此时nd已经“站在”了子分量上,通过这样的递归调用,最终nd回“站在”最后一个目录上。
到此路径名查找就结束了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值