上篇中我们已经获取到路径名 filename(用户空间传递过来的),fd 文件描述符。
如果获取的 fd 有效,那么接着调用 do_filp_open() 函数。
struct file * do_filp_open(int dfd, struct filename * pathname,
const struct open_flags * op) {
struct nameidata nd;
int flags = op->lookup_flags;
struct file * filp;
set_nameidata(&nd, dfd, pathname);
filp = path_openat(&nd, op, flags | LOOKUP_RCU);
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(&nd, op, flags);
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
restore_nameidata();
return filp;
}
参数 dfd 是相对路径的基准目录对应的文件描述符,参数 name 指向文件路径,参数 op
是查找标志。
在路径查找中有个很重要的数据结构 nameidata 用来向解析函数传递参数,保存解析结果。
struct nameidata {
struct path path;
struct qstr last;
struct path root;
struct inode * inode; //path.dentry.d_inode
unsigned int flags;
unsigned seq, m_seq;
int last_type;
unsigned depth;
int total_link_count;
struct saved {
struct path link;
void * cookie;
const char * name;
struct inode * inode;
unsigned seq;
} * stack, internal[EMBEDDED_LEVELS];
struct filename * name;
struct nameidata * saved;
unsigned root_seq;
int dfd;
};
成员 last 存放需要解析的文件路径的分量(以前提到的组件),是一个快速字符串(quick string),
不仅包字符串,还包含长度和散列值。
成员 path 存放解析得到的挂载描述符和目录项,成员 iode 存放目录项对应的索引节点。
path 保存已经成功解析到的信息,last 用来存放当前需要解析的信息,如果 last 解析成功
那么就会更新 path。
如果文件路径的分量是一个符号链接,那么接下来需要解析符号链接的目标。成员
stack 是一个栈,用来保存文件路径没有解析的部分。成员 depth 是的深度。假设目录b
是一个符号链接,目标是 “e/f”,解析文件路径 “a/b/c/d.txt”,解析到 b,发现 b 是符号链
接下来要解析符号链接 b 的目标 “e/f”,需要把文件路径中没有解析的部分 “c/d.txt” 保存到栈中
,等解析完符号链接后继续解析。
函数 do_flp_open 三次调用函数 path_openat以解析文件路径。
-
第一次解析传入标志
LOOKUP_RCU,使用RCU查找(rcu-walk)方式。在散列 表中根据{父目录, 名称}查找目录的过程中,使用RCU保护散列桶的链表,使用 序列号保护目录,其他处理器可以并行地修改目录,RCU查找方式速度最快。 -
如果在第一次解析的过程中发现其他处理器修改了正在查找的目录,返回错误号
-ECHILD,那么第二次使用引用查找(ref-walk)REF方式,在散列表中根据{父目录, 名称} 查找目录的过程中,使用RCU保护散列桶的链表,使用自旋锁保护目录,并且把目录的引用计数加1。 引用查找方式速度比较慢。 -
网络文件系统的文件在网络的服务器上,本地上次查询得到的信息可能过期,和服务器的当前状态 不一致。如果第二次解析发现信息过期,返回错误号
-ESTALE,那么第三次解析传入标志LOOKUP_REVAL, 表示需要重新确认信息是否有效。
static void set_nameidata(struct nameidata * p, int dfd, struct filename * name)
{
struct nameidata * old = current->nameidata;
p->stack = p->internal;
p->dfd = dfd;
p->name = name;
p->total_link_count = old ? old->total_link_count: 0;
p->saved = old;
current->nameidata = p;
}
调用 set_nameidata() 保护当前进程现场信息。
接着调用 filp = path_openat(&nd, op, flags | LOOKUP_RCU);
static struct file * path_openat(struct nameidata * nd,
const struct open_flags * op, unsigned flags) {
const char * s;
struct file * file;
int opened = 0;
int error;
// 获取一个空的 file 描述符。
file = get_empty_filp();
//获取失败,返回。
if (IS_ERR(file))
return file;
//设置 file 描述符的查找标志。
file->f_flags = op->open_flag;
// 如果是本次目标是创建一个临时文件,这里就不深入了,只研究正常的文件打开操作。
if (unlikely(file->f_flags & __ O_TMPFILE)) {
error = do_tmpfile(nd, flags, op, file, &opened);
goto out2;
}
// 路径初始化,确定查找的起始目录,初始化结构体 nameidata 的成员 path。
s = path_init(nd, flags);
//如果获取的路径(待查找的路径)无效,则释放 file 描述符,返回错误。
if (IS_ERR(s)) {
put_filp(file);
return ERR_CAST(s);
}
// 调用函数 link_path_walk 解析文件路径的每个分量,最后一个分量除外。
// 调用函数 do_last,解析文件路径的最后一个分量,并且打开文件。
while (! (error = link_path_walk(s, nd)) && (error = do_last(nd, file, op, &opened)) > 0) {
nd->flags &= ~(LOOKUP_OPEN | LOOKUP_CREATE | LOOKUP_EXCL);
// 如果最后一个分量是符号链接,调用 trailing_symlink 函数进行处理
// 读取符号链接文件的数据,新的文件路径是符号链接链接文件的数据,然后继续 while
// 循环,解析新的文件路径。
s = trailing_symlink(nd);
if (IS_ERR(s)) {
error = PTR_ERR(s);
break;
}
}
// 结束查找,释放解析文件路径的过程中保存的目录项和挂载描述符。
terminate_walk(nd);
out2:
if (! (opened & FILE_OPENED)) {
BUG_ON(!error);
put_filp(file);
}
if (unlikely(error)) {
if (error == -EOPENSTALE) {
if (flags & LOOKUP_RCU)
error = -ECHILD;
else
error = -ESTALE;
}
file = ERR_PTR(error);
}
return file;
}
从路径初始化函数 path_init 开始分析。
static const char * path_init(struct nameidata * nd, unsigned flags)
{
int retval = 0;
//获取路径名(用户空间传递过来的)
const char * s = nd->name->name;
// 如果路径名为空,清除 LOOKUP_RCU 标志。
if (! *s)
flags &= ~LOOKUP_RCU;
nd->last_type = LAST_ROOT; // if there are only slashes...
nd->flags = flags | LOOKUP_JUMPED | LOOKUP_PARENT;
nd->depth = 0;
// 如果设置 `LOOKUP_ROOT`,表示 nameidata 中的 root 字段是由调用者提供的,因此即使在
// 不需要它的时候也不应该释放它。暂时跳过该分支。
if (flags & LOOKUP_ROOT) {
struct dentry * root = nd->root.dentry;
struct vfsmount * mnt = nd->root.mnt;
struct inode * inode = root->d_inode;
if (*s) {
if (!d_can_lookup(root))
return ERR_PTR(-ENOTDIR);
retval = inode_permission2(mnt, inode, MAY_EXEC);
if (retval)
return ERR_PTR(retval);
}
nd->path = nd->root;
nd->inode = inode;
// 如果是 RCU 模式,则保存序列锁(处理竞争)。
if (flags & LOOKUP_RCU) {
rcu_read_lock();
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
nd->root_seq = nd->seq;
nd->m_seq = read_seqbegin(&mount_lock);
}
else {
//如果是 REF 模式,则获取 path 的计数引用(处理竞争)。
path_get(&nd->path);
}
return s;
}
nd->root.mnt = NULL;
// mount_lock 是一个全局 seqlock,有点像 rename_lock。它可以用来检查任何
// 挂载点的任何修改。
nd->m_seq = read_seqbegin(&mount_lock);
// 如果是以 / 开头,也就是说明是绝对路径。
if (*s == '/') {
// RCU 模式。
if (flags & LOOKUP_RCU) {
//开始 RCU 的读临界区。
rcu_read_lock();
//把 nd->root 设置为当前进程的根目录。
set_root_rcu(nd);
nd->seq = nd->root_seq;
}
else {
//以自旋锁的方式(阻塞方式)设置 nd->root 为 fs->root。
set_root(nd);
path_get(&nd->root);
}
//保存已解析到的 挂载描述符和目录项。
nd->path = nd->root;
}
else if (nd->dfd == AT_FDCWD) {
//如果为相对路径,没有指定了相对路径的基准目录。
if (flags & LOOKUP_RCU) {
struct fs_struct * fs = current->fs;
unsigned seq;
rcu_read_lock();
do {
seq = read_seqcount_begin(&fs->seq);
//获取当前进程的工作目录。
nd->path = fs->pwd;
// 初始化 nd->seq 的值为 d_seq,用于后面的同步。
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
}while(read_seqcount_retry(&fs->seq, seq));// 检查期间 fs->pwd 有没有发生改变,如果有返回 1,然后重试。
}
else {
// 如果为 REF 模式,则以自旋锁的方式设置 nd->path,并且增加 current->fs 的引用计数。
get_fs_pwd(current->fs, &nd->path);
}
}
else {
//如果为相对路径,并且已经指定了相对路径的基准目录。
// Caller must check execute permissions on the starting path component
struct fd f = fdget_raw(nd->dfd);
struct dentry * dentry;
if (!f.file)
return ERR_PTR(-EBADF);
dentry = f.file->f_path.dentry;
if (*s) {
// 检查是否为目录。
if (!d_can_lookup(dentry)) {
fdput(f);
return ERR_PTR(-ENOTDIR);
}
}
nd->path = f.file->f_path;
if (flags & LOOKUP_RCU) {
rcu_read_lock();
nd->inode = nd->path.dentry->d_inode;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
}
else {
// 增加引用计数。
path_get(&nd->path);
nd->inode = nd->path.dentry->d_inode;
}
fdput(f);
return s;
}
nd->inode = nd->path.dentry->d_inode;
if (! (flags & LOOKUP_RCU))
return s;
// 检查期间 nd->path.dentry 有没有发生改变。
if (likely(!read_seqcount_retry(&nd->path.dentry->d_seq, nd->seq)))
return s;
if (! (nd->flags & LOOKUP_ROOT))
nd->root.mnt = NULL;
rcu_read_unlock();
return ERR_PTR(-ECHILD);
}
这里,我们发现 “如果为相对路径,并且已经指定了相对路径的基准目录。” 这种情况,并没有退出
RCU 的读临界区(调用 rcu_read_unlock();)。
接下来就是 link_path_walk 的调用,这个函数也是路径查找的核心函数。
/*
* Name resolution.
* This is the basic name resolution function, turning a pathname into
* the final dentry. We expect 'base' to be positive and a directory.
*
* Returns 0 and nd will have valid dentry and mnt on success.
* Returns error and drops reference to input namei data on failure.
*/
static int link_path_walk(const char * name, struct nameidata * nd)
{
int err;
while (*name == '/')
name++;
if (! *name)
return 0;
// At this point we know we have a real path component.
for (; ; ) {
u64 hash_len;
int type;
//假设当前目录分量的父目录为 t,这里检查目录 t 的访问权限。
err = may_lookup(nd);
if (err)
return err;
//根据目录 t 和当前分量,计算当前分量的散列值和长度
hash_len = hash_name(name);
type = LAST_NORM;
//如果当前分量的第一个字符为
// “.”,那么当前分量有可能是:.(当前目录)、..(上一层目录)
if (name[0] == '.')
//获取当前分量的长度
switch (hashlen_len(hash_len))
{
//当前分量为:..
case 2:
if (name[1] == '.') {
type = LAST_DOTDOT;
nd->flags |= LOOKUP_JUMPED;
}
break;
//当前分量为:.
case 1:
type = LAST_DOT;
}
//当前分量为正常目录
if (likely(type == LAST_NORM)) {
//获取 父目录的目录项
struct dentry * parent = nd->path.dentry;
nd->flags &= ~LOOKUP_JUMPED;
// 判断父目录是否需要做 hash 操作,这里我的猜测是某些文件系统有加密的
// 行为,需要使用自己的 d_hash 操作才能获取正确的名字与长度这些信息。
if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
struct qstr this = {
{
.hash_len = hash_len
},
.name = name
};
err = parent->d_op->d_hash(parent, &this);
if (err < 0)
return err;
hash_len = this.hash_len;
name = this.name;
}
}
//设置当前要解析分量的长度
nd->last.hash_len = hash_len;
//设置当前当前要解析分量的路径名
nd->last.name = name;
//设置当前要解析分量的类型
nd->last_type = type;
//name 指针加上当前分量的长度
name += hashlen_len(hash_len);
//如果此时 * name 为 '\0' 表示 last 指向的分量是最后一个分量,没有下一个分量可解析了。
if (! *name)
goto OK;
// If it wasn't NUL, we know it was '/'. Skip that
// slash, and continue until no more slashes.
// 否则跳过 “/”,name 指向下一个分量的首字符。
do {
name++;
}
while(unlikely(*name == '/'));
// 如果解析的路径类似: “a/b/c/” 这种以 “/” 结尾的路径。
// 那么跳过 “/” 后,name 指向 “\0” ,所以说处理方式跟 “a/b/c” 是一样的。
// 这个分支处理的是:name 没有指向下一个分量。
if (unlikely(! *name)) {
OK:
// 这里如果为 0,表示当前解析的是路径的最后一个分量(已经处理完所有的符号链接了)。
// 那么处理完成,最后一个分量交给函数 do_last 来处理。
// 这边我们只是给 nd->last 赋值了。
if (!nd->depth)
return 0;
// 流程到这里,我们知道目前处理的是某个符号链接所指向路径的最后一个分量了。
// 否则取出上一层未解析的剩余路径赋值给 name
// 比如 “a/b/c/d.txt”,如果 “c” 是符号链接并且指向 “e/f”。
// 此时我们处于解析符号链接 “c” 的过程并且当前分量 nd->last 指向 “f” 分量。
// 那么把 name 指向下一个分量 “d.txt” 。
name = nd->stack[nd->depth - 1].name;
// trailing symlink, done
// 对于上一个例子,如果符号链接 “c” 后面没有 “d.txt”,那么 “f”
// 就为最后一个分量了,于是在这里返回,然后交给 do_last 函数处理分量 “f”。
if (!name)
return 0;
// last component of nested symlink
// 调用函数 walk_component 解析分量 “f”。
err = walk_component(nd, WALK_GET | WALK_PUT);
}
else {
// 这个分支处理的是:name 有指向下一个分量。
// 解析路径(包括符号链接指向的路径)的某一个中间节点,比如:“c”。
err = walk_component(nd, WALK_GET);
}
if (err < 0)
return err;
// 这里 err 的值为 1,后面会分析到。
if (err) {
//如果当前的分量是符号链接,那么获取符号链接指向的路径。
//对于上面的例子,有两种情况:
//(1)当前分量是 “c”,我们知道它是个符号链接,取出指向的路径 “e/f。
//(2)当前分量是 “f” 也是一个符号链接。那么取出“e” 指向的路径(假设为 “g/h”) ,嵌套符号链接的情况。
const char * s = get_link(nd);
if (IS_ERR(s))
return PTR_ERR(s);
err = 0;
// 符号链接 “c” 或者 “e” 指向 “\0”,那么跳过不处理,一般来说不会出现这种情况。
if (unlikely(!s)) {
// --nd->depth
put_link(nd);
}
else {
// 对于情况 (1),我们保存 “d.txt”,情况 (2):由于把
// nd->stack[nd->depth - 1].name 取出来赋值给了name ,所以还是保存 “d.txt”。
nd->stack[nd->depth - 1].name = name;
name = s;
continue;
}
}
//如果当前分量不是目录,那么停止解析,返回错误。
if (unlikely(!d_can_lookup(nd->path.dentry))) {
if (nd->flags & LOOKUP_RCU) {
// 切换到 REF 模式进行查找。
if (unlazy_walk(nd, NULL, 0))
return - ECHILD;
}
return -ENOTDIR;
}
// 如果 walk_component 没有返回错误,比如处理完目录“f”(情况 1),或者“h”(情况 2),
// 此时 name 指向 “d.txt”,所以接下来解析 “d.txt”。最后 !nd->depth 条件成立,返回
// 0, 交给 do_last 函数处理。
}
}
现在,我们可以肯定当前的子路径一定是一个中间节点(文件夹或符号链接),既然是中间节点,
那么就需要“走过”这个节点。咱们来看看 walk_component 是怎么“走过”中间节点的。
static int walk_component(struct nameidata * nd, int flags)
{
struct path path;
struct inode * inode;
unsigned seq;
int err;
// "." and ".." are special - ".." especially so because it has
// to be able to know about the current root directory and
// parent relationships.
// 如果不是 LAST_NORM 类型,就交由 handle_dots 处理。
if (unlikely(nd->last_type != LAST_NORM)) {
err = handle_dots(nd, nd->last_type);
if (flags & WALK_PUT)
put_link(nd);
return err;
}
// 快速查找
err = lookup_fast(nd, &path, &inode, &seq);
if (unlikely(err)) {
if (err < 0)
return err;
// 如果快速查找模式失败,则进行慢速查找模式。
err = lookup_slow(nd, &path);
if (err < 0)
return err;
// 此时我们已经退出 RCU 模式了。
seq = 0; // we are already out of RCU mode
err = -ENOENT;
// 判断是否是一个 negative 目标,比如当文件被删除后就会成为 negative。
if (d_is_negative(path.dentry))
goto out_path_put;
inode = d_backing_inode(path.dentry);
}
// 这种情况处于解析符号链接指向目标的最后一个分量,所以处理完后释放符号链接。
if (flags & WALK_PUT)
put_link(nd);
// 如果查找到的分量又是一个符号链接,是否跟下去。这里 flags & WALK_GET 为 true。
err = should_follow_link(nd, &path, flags & WALK_GET, inode, seq);
if (unlikely(err))
return err;
// 把 path 更新到 nd->path。
path_to_nameidata(&path, nd);
nd->inode = inode;
nd->seq = seq;
return 0;
out_path_put:
path_to_nameidata(&path, nd);
return err;
}
walk_component 中有几个比较重要的函数,一个是 RCU 模式下的快速查找函数 lookup_fast,
一个是 REF 模式下的 lookup_slow 函数,以及处理目录“..”或者“.”函数 handle_dots。
以及符号链接处理函数 should_follow_link。
static inline void path_to_nameidata(const struct path * path,
struct nameidata * nd)
{
// 如果不是出于 RCU 模式,那么就减少 nd->path 指向旧的 dentry 和 mnt 的引用计数。
if (! (nd->flags & LOOKUP_RCU)) {
dput(nd->path.dentry);
if (nd->path.mnt != path->mnt)
mntput(nd->path.mnt);
}
// 更新 nd->path。
nd->path.mnt = path->mnt;
nd->path.dentry = path->dentry;
}
下一篇介绍 Linux 的虚拟文件系统。