路径名查找

本文深入探讨了Linux内核中文件路径名查找的过程,包括如何将人类易读的路径名转换为内核内部表示,涉及目录项、vfsmount和inode的概念。详细解释了路径名分析、权限检查、符号链接处理及路径名查找函数的应用。

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

当进程要使用一个文件时,如open()、mkdir()、rename()或stat()等,就要首先进行路径名查找,即是将人类易于识别的字符串形式路径名,转换为一个文件在内核中的内部表示,也就是目录项、vfsmount和inode等。

 

执行这一任务的标准过程就是分析路径名并把它拆分成一个文件名序列。除了最后一个项以外,所有的文件名都必定是目录。如果路径名的第一个字符是“/”,例如:/usr/bin/tree,这个路径名就是一个绝对路径名,因此从current->fs->root(进程的根目录)所标识的目录开始搜索。否则,路径名就是一个相对路径,从 currrent->fs->pwd(进程的当前目录)所标识的目录开始搜索。

 

在对初始目录(进程的根目录或进程的当前目录)的索引节点进行处理的过程中,代码要检查与文件名匹配的目录项,以获得相应的索引节点。然后,从缓存或磁盘读出那个索引节点所表示的目录文件,并检查与第二个名字匹配的目录项,以获得相应的索引节点。对于包含在路径中的每个名字,这个过程反复执行。

 

目录项高速缓存极大地加速了这一过程,因为它把最近最常使用的目录项对象保留在内存中。正如我们以前看到的,每个这样的对象使特定目录中的一个文件名与它相应的索引节点相联系。因此在很多情况下,路径名的分析可以避免从磁盘读取中间目录文件的内容。但是,事情并不像看起来那么简单,因为必须考虑如下的Unix和VFS文件系统的特点:

- 对每个目录的访问权必须进行检查,以验证是否允许进程读取这一目录的内容。
- 文件名可能是与任意一个路径名对应的符号链接;在这种情况下,分析必须扩展到那个路径名的所有分量。
- 符号链接可能导致循环引用;内核必须考虑这个可能性,并能在出现这种情况时将循环终止。
- 文件名可能是一个已挂载文件系统的挂载点。这种情况必须检测到,这样,查找操作必须延伸到新的文件系统。
- 路径名查找应该在发出系统调用的进程的命名空间中完成。由具有不同命名空间的两个进程使用的相同路径名,可能指定了不同的文件。

 

内核提供了在不同的条件下调用的用于路径名查找的函数,他们分别为path_lookup()、kern_path()、user_path_at(),

其定义如下:

//fs/namei.c
int path_lookup(const char *name, unsigned int flags,
           struct nameidata *nd)
{
    return do_path_lookup(AT_FDCWD, name, flags, nd);
}
 
int kern_path(const char *name, unsigned int flags, struct path *path)
{
    struct nameidata nd;
    int res = do_path_lookup(AT_FDCWD, name, flags, &nd);
    if (!res)
       *path = nd.path;
    return res;
}
 
int user_path_at(int dfd, const char __user *name, unsigned flags,
        struct path *path)
{
    struct nameidata nd;
    char *tmp = getname(name);
    int err = PTR_ERR(tmp);
    if (!IS_ERR(tmp)) {
 
       BUG_ON(flags & LOOKUP_PARENT);
 
       err = do_path_lookup(dfd, tmp, flags, &nd);
       putname(tmp);
       if (!err)
           *path = nd.path;
    }
    return err;
}


 

user_path_at()会首先调用getname(name),将用户空间的路径名参数复制到内核空间的临时缓冲区中。他们最终都会调用do_path_lookup()函数来实际完成路径名的查找工作,这个函数接受四个参数:

dfd:使用的基目录;name:指向要解析的文件路径名的指针;flags:标志的值,表示将会怎样访问查找的文件;nd:nameidata数据结构的地址,这个结构存放了查找操作的结果。

 

可以使用的dfd定义如下,注释中的解释也比较清楚明白了:

//include/linux/fcntl.h
#define AT_FDCWD         -100     /* Special value used to indicate openat should use the current working directory. */
#define AT_SYMLINK_NOFOLLOW 0x100  /* Do not follow symbolic links.  */
#define AT_REMOVEDIR     0x200   /* Remove directory instead of
                                           unlinking file.  */
#define AT_SYMLINK_FOLLOW   0x400   /* Follow symbolic links.  */


 

do_path_lookup()返回时,查找的结果存放在参数nd指向的nameidata结构中,其定义如下:

//include/linux/namei.h
struct nameidata {
    struct path   path; /* 查找到的路径 */
/* 路径名的最后一个分量(当LOOKUP_PARENT标志被设置时使用) */
    struct qstr   last; 
    struct path   root; /*进程根路径 */
    unsigned int  flags; /* 查找标志 */
/* 路径名的最后一个分量的类型(当LOOKUP_PARENT标志被设置时使用) */
    int    last_type;
    unsigned   depth; /* 符号链接嵌套的当前级别;它必须小于6 */
    /* 与嵌套的符号链接关联的路径名数组 */
    char *saved_names[MAX_NESTED_LINKS + 1];
 
    /* Intent data */
    union { 
       struct open_intent open;
    } intent;  /* 单个成员联合体,指定如何访问文件 */
};


 

path字段会保存最后一个路径分量的信息(目录项对象和vfsmount对象)。这个字段“描述”由给定路径名表示的文件。

 

由于do_path_lookup()函数返回的nameidata结构中的目录项对象和vfsmount对象代表了查找操作的结果,因此在do_path_lookup()的调用者完成使用查找结果之前,这个两个对象都不能被释放。因此,do_path_lookup()增加这两个对象引用计数器的值。 如果调用者想释放这些对象,则调用path_put()函数,传递给它的参数就是path结构的地址。

 

flags字段存放查找操作中使用的某些标志的值,这些标志中的大部分可由调用者在do_path_lookup()的flags参数中进行设置:

//include/linux/namei.h
/* 如果最后一个分量是符号链接,则解释(追踪)它 */
#define LOOKUP_FOLLOW       1
/* 最后一个分量必须是目录 */
#define LOOKUP_DIRECTORY 2
/* 在路径名中还有文件名要检查 */
#define LOOKUP_CONTINUE     4
/* 查找最后一个分量名所在的目录 */
#define LOOKUP_PARENT       16
/* dentry 中的内容不被信任,强制执行一个真实的查找,即从父目录的文件目录项中查找 */
#define LOOKUP_REVAL     64
/*
 * Intent data
 */
/* 试图打开一个文件 */
#define LOOKUP_OPEN      0x0100
/* 试图创建一个文件(如果不存在) */
#define LOOKUP_CREATE       0x0200
#define LOOKUP_EXCL      0x0400
#define LOOKUP_RENAME_TARGET    0x0800


 

下面,我们来详细查看do_path_lookup ()函数,do_path_lookup()定义如下:

//fs/namei.c
/* Returns 0 and nd will be valid on success; Retuns error, otherwise. */
static int do_path_lookup(int dfd, const char *name,
              unsigned int flags, struct nameidata *nd)
{
    int retval = path_init(dfd, name, flags, nd);
    if (!retval)
       retval = path_walk(name, nd);
    if (unlikely(!retval && !audit_dummy_context() && nd->path.dentry &&
              nd->path.dentry->d_inode))
       audit_inode(name, nd->path.dentry);
    if (nd->root.mnt) {
       path_put(&nd->root);
       nd->root.mnt = NULL;
                                }
    return retval;
}


 

do_path_lookup ()执行如下操作:

1、调用path_init()来初始化nd参数的某些字段,path_init()定义如下:

//fs/namei.c
static int path_init(int dfd, const char *name, unsigned int flags, struct nameidata *nd)
{
    int retval = 0;
    int fput_needed;
    struct file *file;
 
    nd->last_type = LAST_ROOT; /* if there are only slashes... */
    nd->flags = flags;
    nd->depth = 0;
    nd->root.mnt = NULL;
 
    if (*name=='/') {
       set_root(nd);
       nd->path = nd->root;
       path_get(&nd->root);
    } else if (dfd == AT_FDCWD) {
       struct fs_struct *fs = current->fs;
       read_lock(&fs->lock);
       nd->path = fs->pwd;
       path_get(&fs->pwd);
       read_unlock(&fs->lock);
    } else {
       struct dentry *dentry;
 
       file = fget_light(dfd, &fput_needed);
       retval = -EBADF;
       if (!file)
           goto out_fail;
 
       dentry = file->f_path.dentry;
 
       retval = -ENOTDIR;
       if (!S_ISDIR(dentry->d_inode->i_mode))
           goto fput_fail;
 
       retval = file_permission(file, MAY_EXEC);
       if (retval)
           goto fput_fail;
 
       nd->path = file->f_path;
       path_get(&file->f_path);
 
       fput_light(file, fput_needed);
    }
    return 0;
 
fput_fail:
    fput_light(file, fput_needed);
out_fail:
    return retval;
}


 

a. last_type字段置位LAST_ROOT(如果路径名是一个“/”或“/”序列,那么这是必需的)

b.把flags字段设置为参数flags的值。

c.把depth字段设为0。

d.判断路径名为绝对路径还是相对路径。若为绝对路径,则调用set_root(nd)将root字段设置为current->fs ->root,并增加其引用计数。并将path字段同样设为nd->root,在此增加其引用计数。否则,若dfd设置了AT_FDCWD,则为相对路径名。则将path字段设置为进程的当前路径current->fs ->pwd,并增加该路径引用计数。否则,使用的基目录就是进程文件文件描述符表中的某个文件,而此时dfd参数则正是该目录文件的文件描述符,则将path字段设为该目录文件的路径。

总之,也就是将查找的路径名的基路径找到,并赋给path字段。

 

2、调用path_walk(name, nd)处理

//fs/namei.c
static int path_walk(const char *name, struct nameidata *nd)
{
    struct path save = nd->path;
    int result;
 
    current->total_link_count = 0;
 
    /* make sure the stuff we saved doesn't go away */
    path_get(&save);
 
    result = link_path_walk(name, nd);
    if (result == -ESTALE) {
       /* nd->path had been dropped */
       current->total_link_count = 0;
       nd->path = save;
       path_get(&nd->path);
       nd->flags |= LOOKUP_REVAL;
       result = link_path_walk(name, nd);
    }
 
    path_put(&save);
 
    return result;
}


 

path_walk(name, nd)执行如下操作:

a.将查找的路径名的基目录的路径保存在临时变量save中

b.将当前进程的total_link_count字段设置为0。

c.增加查找的路径名的基目录的路径的引用计数,确保保存的查找的路径名的基目录的路径不会消失。

d.调用link_path_walk()函数处理正在进行的查找操作:

result = link_path_walk(name, nd);

如果返回值为-ESTALE,则设置查找标志LOOKUP_REVAL并再次查找。

(这个函数是路径名查找操作的核心,随后有更详细说明。)

e.减少查找的路径名的基目录的路径的引用计数。

 

3、产生审计信息,即记录对于文件的访问。说明:2.6 Linux内核有用日志记录事件的能力,比如记录系统调用和文件访问。然后,管理员可以评审这些日志,确定可能存在的安全裂口,比如失败的登录尝试,或者用户对系统文件不成功的访问。这种功能称为Linux审计系统。

include/linux/audit.h
static inline int audit_dummy_context(void)
{
    void *p = current->audit_context;
    return !p || *(int *)p;
}
 
static inline void audit_inode(const char *name, const struct dentry *dentry) {
    if (unlikely(!audit_dummy_context()))
       __audit_inode(name, dentry);
}


 

4、若nd->root.mnt不为空(当路径名为绝对路径时,在path_init()设置),则减少对根路径的引用并设置nd->root.mnt为NULL。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值