上一篇:https://blog.youkuaiyun.com/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