系统调用的内核实现,一文讲透open函数内核真实实现。
本文接续上文,继续探寻在内核中的系统调用如何具体实现,本文以open函数为例,保证你一次性弄懂。本文采用linux3.2.1内核代码,并推荐使用Source Insight,这会让这个过程更容易。
上一篇:https://blog.csdn.net/weixin_42523774/article/details/103341058
· 本文是Linux文件系列的第三篇,上文《系统调用如何进入内核层次,深入glibc寻找open函数真实实现。》,讲到了从应用程序到如何通过glibc中进入内核的过程,本文接着上文,讲系统调用在内核中如何实现,还是以open函数为例,后续会介绍read和write函数的内核实现。
· 本文采用linux3.2.1内核代码[下载处],并推荐使用Source Insight,这会让这个过程更容易。
· 我想先提醒一下,这些系统调用的操作者是某一个进程,open操作其实是在read和write操作之前的一个预备动作,如果是真实文件,这个操作就是将文件从硬盘(或flash)中读取到内存中,当然这个过程中要防止多个进程的同时操作,因此会有很多锁作为保护,这也是代码理解的难点。
· 首先将搜寻结构图展示出来,让大家先知其全貌,后续在一步一步细讲:(前面直接套用的函数就没写注释)
compat_sys_openat
|-->do_sys_open
|-->do_filp_open
|-->do_filp_open
|-->path_openat
|-->path_init # nd初始化。
|-->link_path_walk # 真实寻找。
| |-->for(;;) {
| | |-->may_lookup # 查询文件权限是否允许访。
| | |-->hash = init_name_hash(); # 算出该文件名的哈希值,和文件名长度。
| | |-->## 判断文件名是否使用了"."或者"..",来标明文件类型type ##
| | |-->d_hash # 查询是否有哈希表存在。
| | |-->walk_component # 依据刚刚识别的类型,做单次搜索。
| | | |-->handle_dots # "." 和 ".." 文件名处理。
| | | |-->do_lookup # 其他文件的搜索。
| | | | |-->__d_lookup_rcu # 不带rcu搜寻。
| | | | |-->__d_lookup # 带rcu,可能引起阻塞搜寻。
| | | | |-->d_alloc_and_lookup # 上两步搜不到,就要通过硬盘文件系统搜寻。
| | | |-->should_follow_link # 查看是否可以继续链接文件,前面提到过,对链接次数有限制。
| | |-->nested_symlink # 限制递归调用不能超过8次,符号链接不能超过40次。
| | |-->can_lookup # 判断是否可以继续查找,可以则继续。
| | |-->terminate_walk(nd); # 查找完成操作,包括解RCU锁。
| |-->}
|-->do_last # 查找完成,做打开文件操作
1.续接前文
· 书接上文,我们找到了内核代码位置include\asm-generic\unistd.h的如下语句:
#define __NR_openat 56
__SC_COMP(__NR_openat, sys_openat, compat_sys_openat)
· 根据这个分别找到了如下的宏定义:
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SYSCALL(nr, call) [nr] = (call),
· 这里我们就明白了,我们这个宏定义,将 compat_sys_openat函数的地址赋给了一个变量,然后让swi指令去调用这个函数,我们在fs/compat.c找到了这个函数:
asmlinkage long
compat_sys_openat(unsigned int dfd, const char __user *filename, int flags, int mode)
{
return do_sys_open(dfd, filename, flags, mode);
}
· asmlinkage 表示这个是通过汇编指令链接过来的,do_sys_open这就是open函数的真实实现,入参分别 AT_FDCWD, file, oflag, mode,下面我们看看它做了什么。
2.do_sys_open
· 本文将整个代码结构都展示出来,大部分内容通过代码中加注释的方式来解释,关键部分在代码后用文字说明。
long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
struct open_flags op; // 创建一个标志集合
/* 从flags和mode中分离出lookup event bitmap,返回值lookup表示正在做的事件(寻找目录和结构),op中存有找到之后期望做的事件(执行或创建)*/
int lookup = build_open_flags(flags, mode, &op);
char *tmp = getname(filename); // 将文件名从用户空间复制到内核空间
int fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
fd = get_unused_fd_flags(flags); // 根据flags获取一个对应类型的未使用的文件号
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op, lookup); // 执行文件打开过程
if (IS_ERR(f)) { // 判断是否出错
put_unused_fd(fd); // 释放刚用get_unused_fd_flags申请的文件号
fd = PTR_ERR(f); // 将指针转化为错误码返回
} else {
fsnotify_open(f); // 通知其他相关项,该文件已经打开
fd_install(fd, f); // 安装文件指针到fd数组
}
}
putname(tmp); // 释放内核态缓存
}
return fd;
}
· do_sys_open函数首先做了做了设置open_flags标志和转换用户空间的文件名到内核空间,核心功能是调用do_filp_open,我们进一步寻找:
3.do_filp_open
struct file *do_filp_open(int dfd, const char *pathname,
const struct open_flags *op, int flags)
{
struct nameidata nd;
struct file *filp;
/* 核心就是path_openat,就是沿着打开文件名的整个路径,一层层解析,最后得到文件对象 */
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(dfd, pathname, &nd, op, flags);
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
return filp;
}
· 这里的核心就是path_openat,就是沿着打开文件名的整个路径,一层层解析,最后得到文件对象。但是为什么最多会调用3次?
· 由操作系统发展趋势一文中讲过,我们讲过当前的操作系统的内存管理系统中,CPU只能间接通过将硬盘数据缓存到内存中才能使用。而用来缓存文件的区域叫dentry cache ,这是一个哈希表。
· 第一次搜索采用rcu-walk方式搜索dentry cache,这种方式不会阻塞等待(RCU才会阻塞),更高效,但是可能会失败(因为文件inode本身还有顺序锁和自旋锁的保护);
· 第二次搜索则是采用ref-walk方式搜索dentry cache,考虑rcu锁的情形,但是可能会阻塞。
· 如果这样仍然失败,说明内存中没有该文件,这样就需要第三次,直接通过硬盘文件系统,进入硬盘慢速搜索,要带LOOKUP_REVAL标志。
4.path_openat
static struct file *path_openat(int dfd, const char *pathname,
struct nameidata *nd, const struct open_flags *op, int flags)
{
struct file *base = NULL;
struct file *filp;
struct path path;
int error;
filp = get_empty_filp(); // 找到一个未使用的文件结构返回
if (!filp)
return ERR_PTR(-ENFILE);
/* 初始化nd中的 intent结构的标志 */
filp->f_flags = op->open_flag;
nd->intent.open.file = filp;
nd->intent.open.flags = open_to_namei_flags(op->open_flag);
nd->intent.open.create_mode = op->mode;
/* 初始化nd中的其他参数,通过开头是不是"/"和dfd是否是AT_FDCWD,识别是绝对路径还是相对路径,设置RCU */
error = path_init(dfd, pathname, flags | LOOKUP_PARENT, nd, &base);
if (unlikely(error))
goto out_filp;
current->total_link_count = 0; // 重置链接的次数,后续做判断
/* 命名解析函数,针对路径循环搜索,直到找到最后一个不是目录的文件,次步骤需要细讲---> */
error = link_path_walk(pathname, nd);
if (unlikely(error))
goto out_filp;
/* 处理打开文件的最后操作,如果这个文件是个链接文件,则需要找到真实文件之后再执行do_last */
filp = do_last(nd, &path, op, pathname);
while (unlikely(!filp)) { /* trailing symlink */
struct path link = path;
void *cookie;
if (!(nd->flags & LOOKUP_FOLLOW)) {
path_put_conditional(&path, nd);
path_put(&nd->path);
filp = ERR_PTR(-ELOOP);
break;
}
nd->flags |= LOOKUP_PARENT;
nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
error = follow_link(&link, nd, &cookie);
if (unlikely(error))
filp = ERR_PTR(error);
else
filp = do_last(nd, &path, op, pathname);
put_link(nd, &link, cookie);
}
out:
if (nd->root.mnt && !(nd->flags & LOOKUP_ROOT))
path_put(&nd->root);
if (base)
fput(base);
release_open_intent(nd);
return filp;
out_filp:
filp = ERR_PTR(error);
goto out;
}
· nameidata是搜索用到的主要结构,首先要调用path_init将其初始化,然后调用link_path_walk搜寻文件,找到文件之后调用do_last来做文件打开操作,如果是个链接文件还需要找到真实文件,再执行do_last。
· 下面深入介绍 path_init 和 link_path_walk。
5.path_init
static int path_init(int dfd, const char *name, unsigned int flags,
struct nameidata *nd, struct file **fp)
{
int retval = 0;
int fput_needed;
struct file *file;
nd->last_type = LAST_ROOT; /* if there are only slashes... */
nd->flags = flags | LOOKUP_JUMPED;
nd->depth = 0;
/* flags中表明是从根目录搜寻,然后查看文件权限上是否允许,如果允许,则对nd、RCU和顺序锁进行初始化后就返回 */
if (flags & LOOKUP_ROOT) {
struct inode *inode = nd->root.dentry->d_inode;
if (*name) {
if (!inode->i_op->lookup)
return -ENOTDIR;
retval = inode_permission(inode, MAY_EXEC);
if (retval)
return retval;
}
nd->path = nd->root;
nd->inode = inode;
if (flags & LOOKUP_RCU) {
br_read_lock(vfsmount_lock);
rcu_read_lock();
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
} else {
path_get(&nd->path);
}
return 0;
}
nd->root.mnt = NULL;
/*通过字符的开头是"/",也可以表示绝对路径,则也要做相关初始化*/
if (*name=='/') {
if (flags & LOOKUP_RCU) {
br_read_lock(vfsmount_lock);
rcu_read_lock();
set_root_rcu(nd);
} else {
set_root(nd);
path_get(&nd->root);
}
nd->path = nd->root;
/*开头不是"/",且dfd == AT_FDCWD表示使用相对路径*/
} else if (dfd == AT_FDCWD) {
if (flags & LOOKUP_RCU) {
struct fs_struct *fs = current->fs;
unsigned seq;
br_read_lock(vfsmount_lock);
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);
}
/*最后一种特殊情况是依据文件号打开方式,本文不考虑此情况*/
} else {
struct dentry *dentry;
file = fget_raw_light(dfd, &fput_needed);
retval = -EBADF;
if (!file)
goto out_fail;
dentry = file->f_path.dentry;
if (*name) {
retval = -ENOTDIR;
if (!S_ISDIR(dentry->d_inode->i_mode))
goto fput_fail;
retval = inode_permission(dentry->d_inode, MAY_EXEC);
if (retval)
goto fput_fail;
}
nd->path = file->f_path;
if (flags & LOOKUP_RCU) {
if (fput_needed)
*fp = file;
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
br_read_lock(vfsmount_lock);
rcu_read_lock();
} else {
path_get(&file->f_path);
fput_light(file, fput_needed);
}
}
nd->inode = nd->path.dentry->d_inode;
return 0;
fput_fail:
fput_light(file, fput_needed);
out_fail:
return retval;
}
· 这里就是依据文件是绝对路径(从根目录开始搜寻)还是相对路径(从当前目录开始搜索)做区分,对nd、rcu和顺序锁做不同的初始化。AT_FDCWD在这里终于用到了,就是表示是相对路径的。
· 下面回到上一步,在看看初始化之后调用的link_path_walk做了什么:
6.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;
/* 到此,我们有了一个依据初始化的nameidata,开始搜寻循环 . */
for(;;) {
unsigned long hash;
struct qstr this;
unsigned int c;
int type;
err = may_lookup(nd); // 查询文件权限是否允许访问
if (err)
break;
this.name = name;
c = *(const unsigned char *)name;
/* 算出该文件名的哈希值,和文件名长度. */
hash = init_name_hash();
do {
name++;
hash = partial_name_hash(c, hash);
c = *(const unsigned char *)name;
} while (c && (c != '/'));
this.len = name - (const char *) this.name;
this.hash = end_name_hash(hash);
/* 判断文件名是否使用了"."或者"..",是则标明type */
type = LAST_NORM;
if (this.name[0] == '.') switch (this.len) {
case 2:
if (this.name[1] == '.') {
type = LAST_DOTDOT;
nd->flags |= LOOKUP_JUMPED;
}
break;
case 1:
type = LAST_DOT;
}
/* 文件名没有"."或者"..",则是普通文件或目录,则查看是否有hash缓存表,有表示内存中存在缓存,没有直接退出 */
if (likely(type == LAST_NORM)) {
struct dentry *parent = nd->path.dentry;
nd->flags &= ~LOOKUP_JUMPED;
if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
err = parent->d_op->d_hash(parent, nd->inode,
&this);
if (err < 0)
break;
}
}
/* 如果尾部是"/",则open要打开的是目录,则直接退出 */
if (!c)
goto last_component;
while (*++name == '/'); // 当有多个"/"时,则搜索时去掉
if (!*name)
goto last_component;
/* 依据刚刚识别的类型,做不同的操作,此步骤再细说---> */
err = walk_component(nd, &next, &this, type, LOOKUP_FOLLOW);
if (err < 0)
return err;
/* 限制递归调用不能超过8次,符号链接不能超过40次 */
if (err) {
err = nested_symlink(&next, nd);
if (err)
return err;
}
/* 判断是否可以继续查找,可以则继续 */
if (can_lookup(nd->inode))
continue;
err = -ENOTDIR;
break;
/* here ends the main loop */
last_component:
nd->last = this;
nd->last_type = type;
return 0;
}
terminate_walk(nd); // 查找完成操作,包括解RCU锁
return err;
}
· 到这里我们看到循环了,这就是逐次搜索文件名的循环。
· 循环中,首先判断文件名有没有".“或者”…",来设置文件类型;然后查看是否有hash表,有表示内存中存在缓存,没有直接退出。
· 然后继续调用walk_component做某一次的搜寻操作。
7.walk_component
static inline int walk_component(struct nameidata *nd, struct path *path,
struct qstr *name, int type, int follow)
{
struct inode *inode;
int err;
/* "." 和 ".." 符号是特殊的,".." 尤其特殊,这是因为必须知道当前文件所在目录的父目录 */
if (unlikely(type != LAST_NORM))
return handle_dots(nd, type); //处理"."和".."文件名,成功则设置nd->path.dentry和nd->inode,如果父目录是挂载点找到挂载点目录再处理。
/* 普通文件的搜索 */
err = do_lookup(nd, name, path, &inode);
if (unlikely(err)) {
terminate_walk(nd);
return err;
}
/* 如果没有找到,返回对应的错误码 */
if (!inode) {
path_to_nameidata(path, nd);
terminate_walk(nd);
return -ENOENT;
}
/* 查看是否链接次数超过最大值,前面提到过,对链接次数有限制 */
if (should_follow_link(inode, follow)) {
if (nd->flags & LOOKUP_RCU) {
if (unlikely(unlazy_walk(nd, path->dentry))) {
terminate_walk(nd);
return -ECHILD;
}
}
BUG_ON(inode != path->dentry->d_inode);
return 1;
}
path_to_nameidata(path, nd); // 将path中的dentry放到nd->path中
nd->inode = inode;
return 0;
}
· 这里先判断文件名是不是".“和”. .",如果是的话就处理退出;
· 如果是普通文件,就执行do_lookup,后面继续深入分析;
· 然后就是针对搜寻情况做一些异常判断,请看代码注释。
8.do_lookup
static int do_lookup(struct nameidata *nd, struct qstr *name,
struct path *path, struct inode **inode)
{
struct vfsmount *mnt = nd->path.mnt;
struct dentry *dentry, *parent = nd->path.dentry;
int need_reval = 1;
int status = 1;
int err;
/* 文件可能存在RCU锁,这表示可能有别的进程使用,则有可能被加载到 dcache 中 */
if (nd->flags & LOOKUP_RCU) {
unsigned seq;
*inode = nd->inode;
dentry = __d_lookup_rcu(parent, name, &seq, inode); // 搜索,只使用内存屏障作为防护手段
if (!dentry)
goto unlazy;
/* 在子目录的read_seqcount_begin的内存屏障就足够,这里判断父目录和子目录的seq是否一致*/
if (__read_seqcount_retry(&parent->d_seq, nd->seq))
return -ECHILD;
nd->seq = seq;
/* 找到的目录是否需要重新生效,需要就执行denter的对应函数*/
if (unlikely(dentry->d_flags & DCACHE_OP_REVALIDATE)) {
status = d_revalidate(dentry, nd);
if (unlikely(status <= 0)) {
if (status != -ECHILD)
need_reval = 0;
goto unlazy;
}
}
/* 找到的目录是否需要搜索 */
if (unlikely(d_need_lookup(dentry)))
goto unlazy;
path->mnt = mnt; //将搜索到的mnt和dentry值保存,下次继续搜索
path->dentry = dentry;
/* 判断是否此文件是挂载点,是则需要在mount_hashtable中找挂载点,再找子目录 */
if (unlikely(!__follow_mount_rcu(nd, path, inode)))
goto unlazy;
if (unlikely(path->dentry->d_flags & DCACHE_NEED_AUTOMOUNT))
goto unlazy;
return 0;
unlazy:
if (unlazy_walk(nd, dentry))
return -ECHILD;
} else {
dentry = __d_lookup(parent, name); // 搜索,需要使用RCU锁,在hash表中搜索。
}
/* 如果搜到的dentry标记了要再搜一次,这种情况就是该文件是另一个文件系统的挂载点 */
if (dentry && unlikely(d_need_lookup(dentry))) {
dput(dentry);
dentry = NULL;
}
retry: // 没有找到dentry,则需要再搜索一次
if (unlikely(!dentry)) {
struct inode *dir = parent->d_inode;
BUG_ON(nd->inode != dir);
mutex_lock(&dir->i_mutex);
dentry = d_lookup(parent, name); // 再搜索一次
if (likely(!dentry)) {
dentry = d_alloc_and_lookup(parent, name, nd);
if (IS_ERR(dentry)) {
mutex_unlock(&dir->i_mutex);
return PTR_ERR(dentry);
}
/* known good */
need_reval = 0;
status = 1;
} else if (unlikely(d_need_lookup(dentry))) {
dentry = d_inode_lookup(parent, dentry, nd);
if (IS_ERR(dentry)) {
mutex_unlock(&dir->i_mutex);
return PTR_ERR(dentry);
}
/* known good */
need_reval = 0;
status = 1;
}
mutex_unlock(&dir->i_mutex);
}
if (unlikely(dentry->d_flags & DCACHE_OP_REVALIDATE) && need_reval)
status = d_revalidate(dentry, nd);
if (unlikely(status <= 0)) {
if (status < 0) {
dput(dentry);
return status;
}
if (!d_invalidate(dentry)) {
dput(dentry);
dentry = NULL;
need_reval = 1;
goto retry;
}
}
path->mnt = mnt;
path->dentry = dentry;
err = follow_managed(path, nd->flags);
if (unlikely(err < 0)) {
path_put_conditional(path, nd);
return err;
}
if (err)
nd->flags |= LOOKUP_JUMPED;
*inode = path->dentry->d_inode;
return 0;
}
· 这里首先会调用__d_lookup_rcu 或者 __d_lookup进入dcache中搜索看看是否存在相同文件,如果没找到,则会调用d_lookup再搜索一遍确认一次(可能在阻塞的这段时间被添加进了dcache)。如果真的没有,就要调用d_alloc_and_lookup 和 d_inode_lookup 使用硬盘文件系统的接口做慢速搜索。
9. dentry cache搜索函数__d_lookup_rcu
struct dentry *__d_lookup_rcu(struct dentry *parent, struct qstr *name,
unsigned *seq, struct inode **inode)
{
unsigned int len = name->len;
unsigned int hash = name->hash;
const unsigned char *str = name->name;
struct hlist_bl_head *b = d_hash(parent, hash);
struct hlist_bl_node *node;
struct dentry *dentry;
/* hash表的循环查找,实质上是for循环,要经过多次的比较,如果某项不同就跳过,继续搜索*/
hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
struct inode *i;
const char *tname;
int tlen;
if (dentry->d_name.hash != hash)
continue;
seqretry:
*seq = read_seqcount_begin(&dentry->d_seq);
if (dentry->d_parent != parent)
continue;
if (d_unhashed(dentry))
continue;
tlen = dentry->d_name.len;
tname = dentry->d_name.name;
i = dentry->d_inode;
prefetch(tname);
if (read_seqcount_retry(&dentry->d_seq, *seq))
goto seqretry;
if (unlikely(parent->d_flags & DCACHE_OP_COMPARE)) {
if (parent->d_op->d_compare(parent, *inode,
dentry, i,
tlen, tname, name))
continue;
} else {
if (dentry_cmp(tname, tlen, str, len))
continue;
}
*inode = i;
return dentry;
}
return NULL;
}
· 这里就是dcache中的搜索函数,dcache hash表采用for循环,每一项要经过多次的比较,如果某项不同就跳过,继续搜索,直到找到全部符合的文件,否者就是没找到。搜不到这不代表这个文件就不在dcache中,因为条件中有顺序锁seq的判断,在SMP的多核架构中可能会出现这种情况,这就需要回到第3步的do_filp_open中继续第二次的path_openat,这是就会调用__d_lookup函数,各位可以自行看看,内容也就增加了rcu和自旋锁的限制,这会引起阻塞。
10.硬盘搜索函数d_alloc_and_lookup
static struct dentry *d_alloc_and_lookup(struct dentry *parent,
struct qstr *name, struct nameidata *nd)
{
struct inode *inode = parent->d_inode;
struct dentry *dentry;
struct dentry *old;
/* Don't create child dentry for a dead directory. */
if (unlikely(IS_DEADDIR(inode)))
return ERR_PTR(-ENOENT);
/* 这里首先创建一个dentry对象 */
dentry = d_alloc(parent, name);
if (unlikely(!dentry))
return ERR_PTR(-ENOMEM);
/* 然后调用文件系统的lookup函数搜索 */
old = inode->i_op->lookup(inode, dentry, nd);
if (unlikely(old)) {
dput(dentry);
dentry = old;
}
return dentry;
}
· 到这里就比较简单了,先分配一个dentry对象,然后调用硬盘文件系统的lookup接口去搜索。
11.总结
· 回过头来,我们最好再看看这个调用过程,核心就在最后。
· 经过这次的分析,我们实际上深入了虚拟文件系统的核心部分,知道了内核是如何搜寻一个文件的。爽得很!!!!!!!!!!(这么多感叹号,是为了发泄这两周对我的折磨)
· 但是爽归爽,其实中间有些地方一带而过了,其实我们中间忽略了VFS的一些基础性的架构介绍,需要把这条走过的路巩固一下,请见下篇《文件系统原理 和 VFS架构》(一股 百家讲坛 的感觉 ~~~)。
------------------如果有所收获的话,请帮忙点个赞吧!
下一篇:https://blog.csdn.net/weixin_42523774/article/details/103739139
更多推荐
所有评论(0)