友情提示:您需要一个 kernel 3.15.6,下载地址:
我们将以 Linux 系统调用 open 为主线,参观游览 Kernel 的文件系统,一窥 Kernel 文件系统精妙的设计和严谨的实现。因受篇幅限制,我们此次观光只涉足 Kernel 的虚拟文件系统(vfs),对于具体的文件系统就不深入进去了。
各位,准备好了吗?我们已经迫不及待要开始这次奇幻之旅了!
“前往 Kernel 的旅客请注意,您所乘坐的 OPEN1024 航班已经开始登机......”
好了,我们的 OPEN1024 航班已经通过 Linux 系统调用穿越了中断门来到了内核空间,并且通过系统调用表(sys_call_table)到达了系统调用 open 的主程序 sys_open,这就开始我们的旅程吧。
【fs/open.c】sys_open()
-
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
-
{
-
if (force_o_largefile())
-
flags |= O_LARGEFILE;
-
-
return do_sys_open(AT_FDCWD, filename, flags, mode);
-
}
SYSCALL_DEFINE3 是用来定义系统调用的宏,展开后类似于这样:
-
long sys_open(const char __user *filename, int flags, umode_t mode)
形参 filename 实际上就是路径名;flags 表示打开模式,诸如只读、新建等等;mode 代表新建文件的权限,所以仅仅在创建新文件时(flags 为 O_CREAT 或 O_TMPFILE)才使用,具体还有哪些标志位请参考 Linux man 手册()。接下来,除了 flags 会在 64 位 Kernel 的情况下强制加上 O_LARGEFILE 标志位,其余的参数都原封不动的传递给 open 的主函数 do_sys_open。唯一需要注意的是 AT_FDCWD,其定义在 include/uapi/linux/fcntl.h,是一个特殊值(-100),该值表明当 filename 为相对路径的情况下将当前进程的工作目录设置为起始路径。相对而言,你可以在另一个系统调用 openat 中为这个起始路径指定一个目录,此时 AT_FDCWD 就会被该目录的描述符所替代。
现在来看 open 的主函数 do_sys_open。
【fs/open.c】sys_open()->do_sys_open()
-
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
-
{
-
struct open_flags op;
-
int fd = build_open_flags(flags, mode, &op);
-
struct filename *tmp;
-
-
if (fd)
-
return fd;
-
-
tmp = getname(filename);
-
if (IS_ERR(tmp))
-
return PTR_ERR(tmp);
-
-
fd = get_unused_fd_flags(flags);
-
if (fd >= 0) {
-
struct file *f = do_filp_open(dfd, tmp, &op);
-
if (IS_ERR(f)) {
-
put_unused_fd(fd);
-
fd = PTR_ERR(f);
-
} else {
-
fsnotify_open(f);
-
fd_install(fd, f);
-
}
-
}
-
putname(tmp);
-
return fd;
-
}
首先检查并包装传递进来的标志位(962),随后将用户空间的路径名复制到内核空间(968),在顺利取得空闲的文件表述符的情况下调用 do_filp_open 完成对路径的搜寻和文件的打开(974),如果一切顺利,就为这个文件描述符安装文件(980),然后大功告成并将文件描述符返回用户空间。在此之前还不忘使用 fsnotify 机制来唤醒文件系统中的监控进程(979)。
build_open_flags 就是对标志位进行检查,然后包装成 struct open_flags 结构以供接下来的函数使用。因为这些标志位大多涉及到对最终目标文件的操作,所以这个函数也等到我们用到这些标志位的时候再回过头来看。
接下来就是 getname
,这个函数定义在 fs/namei.c,主体是 getname_flags,我们捡重点的分析,无关紧要的代码以 ... 略过:
【fs/namei.c】sys_open()->do_sys_open()->getname()->getname_flags()
-
static struct filename *
-
getname_flags(const char __user *filename, int flags, int *empty)
-
{
-
struct filename *result, *err;
...
-
result = __getname();
...
-
kname = (char *)result + sizeof(*result);
-
result->name = kname;
-
result->separate = false;
-
max = EMBEDDED_NAME_MAX;
-
-
recopy:
-
len = strncpy_from_user(kname, filename, max);
...
-
if (len == EMBEDDED_NAME_MAX && max == EMBEDDED_NAME_MAX) {
-
kname = (char *)result;
-
-
result = kzalloc(sizeof(*result), GFP_KERNEL);
...
-
result->name = kname;
-
result->separate = true;
-
max = PATH_MAX;
-
goto recopy;
-
}
...
-
}
首先通过第 144 行 __getname
在内核缓冲区专用队列里申请一块内存用来放置路径名,其实这块内存就是一个 4KB 的内存页。这块内存页是这样分配的,在开始的一小块空间放置结构体 struct filename,之后的空间放置字符串。152 行初始化字符串指针 kname,使其指向这个字符串的首地址,相当于 kname = (char *)((struct filename *)result + 1)。然后就是拷贝字符串(158),返回值 len 代表了已经拷贝的字符串长度。如果这个字符串已经填满了内存页剩余空间(170
),就说明该字符串的长度已经大于 4KB - sizeof(struct filename) 了,这时就需要将结构体 struct filename 从这个内存页中分离(180)并单独分配空间(173),然后用整个内存页保存该字符串(171)。
回到 do_sys_open,现在需要为新打开的文件分配空闲文件描述符。get_unused_fd_flags 主要作用就是在当前进程的 files 结构中找到空闲的文件描述符,并初始化该描述符对应的 file 结构。
一切准备就绪,就进入 do_filp_open 了。
【fs/namei.c】sys_open->do_sys_open->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;
-
-
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,在这里 Kernel 向我们展示了“路径行走(path walk)”的两种策略:rcu-walk(3242)和 ref-walk(3244)。在 rcu-walk 期间将会禁止抢占,也决不能出现进程阻塞,所以其效率很高;ref-walk 会在 rcu-walk 失败、进程需要随眠或者需要取得某结构的引用计数(reference count)的情况下切换进来,很明显它的效率大大低于 rcu-walk。最后 REVAL(3246)其实也是 ref-walk,在以后我们会看到,该模式是在已经完成了路径查找,打开具体文件时,如果该文件已经过期(stale)才启动的,所以 REVAL 是给具体文件系统自己去解释的。其实 REVAL 几乎不会用到,在内核的文件系统中只有 nfs 用到了这个模式。
path_openat 主要作用是首先为 struct file 申请内存空间,设置遍历路径的初始状态,然后遍历路径并找到最终目标的父节点,最后根据目标的类型和标志位完成 open 操作,最终返回一个新的 file 结构。我们分段来看:
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat
-
static struct file *path_openat(int dfd, struct filename *pathname,
-
struct nameidata *nd, const struct open_flags *op, int flags)
-
{
...
-
file = get_empty_filp();
-
if (IS_ERR(file))
-
return file;
-
-
file->f_flags = op->open_flag;
...
-
error = path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base);
-
if (unlikely(error))
-
goto out;
...
首先需要分配一个 file 结构,成功的话 get_empty_filp 会返回一个指向该结构的指针,在这个函数里会对权限、最大文件数进行检查。我们忽略有关 tempfile 的处理,直接来看 path_init。path_init 是对真正遍历路径环境的初始化,主要就是设置变量 nd。这个 nd 是 do_filp_open 里定义的局部变量,是一个临时性的数据结构,用来存储遍历路径的中间结果,其结构体定义如下:
【include/linux/namei.h】
-
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;
-
char *saved_names[MAX_NESTED_LINKS + 1];
-
};
其中,path 保存当前搜索到的路径;last 保存当前子路径名及其散列值;root 用来保存根目录的信息;inode 指向当前找到的目录项的 inode 结构;flags 是一些和查找(lookup)相关的标志位;seq 是相关目录项的顺序锁序号; m_seq 是相关文件系统(其实是 mount)的顺序锁序号; last_type 表示当前节点类型;depth 用来记录在解析符号链接过程中的递归深度;saved_names 用来记录相应递归深度的符号链接的路径。我们结合 path_init
的代码来看这些成员的初始化。
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat->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) {
...
-
}
-
-
nd->root.mnt = NULL;
-
-
nd->m_seq = read_seqbegin(&mount_lock);
-
if (*name=='/') {
...
-
set_root(nd);
...
-
nd->path = nd->root;
-
} else if (dfd == AT_FDCWD) {
...
-
get_fs_pwd(current->fs, &nd->path);
...
-
} else {
...
-
}
-
-
nd->inode = nd->path.dentry->d_inode;
-
return 0;
-
}
首先将 last_type 设置成 LAST_ROOT,意思就是在路径名中只有“/”。为方便叙述,我们把一个路径名分成三部分:起点(根目录或工作目录)、子路径(以“/”分隔的一系列子字符串)和最终目标(最后一个子路径),Kernel 会一个子路径一个子路径的遍历整个路径。所以 last_type 表示的是当前子路径(不是 dentry 或 inode)的类型。last_type 一共有五种类型:
【include/linux/namei.h】
-
/*
-
* Type of the last component on LOOKUP_PARENT
-
*/
-
enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND};
LAST_NORM 就是普通的路径名;LAST_ROOT 是 “/”;LAST_DOT
和 LAST_DOTDOT
分别代表了 “.” 和 “..”;LAST_BIND 就是符号链接。
下面接着来看 path_init,LOOKUP_ROOT 可以提供一个路径作为根路径,主要用于两个系统调用 open_by_handle_at 和 sysctl,我们就不关注了。然后是根据路径名设置起始位置,如果路径是绝对路径(以“/”开头)的话,就把起始路径指向进程的根目录(1849);如果路径是相对路径,并且 dfd 是一个特殊值(AT_FDCWD),那就说明起始路径需要指向当前工作目录,也就是 pwd(1856);如果给了一个有效的 dfd,那就需要吧起始路径指向这个给定的目录(1871)。
path_init
返回之后 nd 中的 path 就已经设定为起始路径了,现在可以开始遍历路径了。在此之前我们先探讨一下 Kernel 的文件系统,特别是 vfs 的组织结构。vfs 是具体文件系统(如 ext4、nfs、fat)和 Kernel 之间的桥梁,它将各个文件系统抽象出来并提供一个统一的机制来组织和管理各个文件系统,但具体的实现策略则由各个文件系统来实现,这很好的屏蔽的各个文件系统的差异,也非常容易扩展,这就是 Linux 著名格言“提供机制而不是策略”的具体实践。vfs 中有两个个很重要的数据结构 dentry 和 inode,dentry 就是“目录项”保存着诸如文件名、路径等信息;inode 是索引节点,保存具体文件的数据,比如权限、修改日期、设备号(如果是设备文件的话)等等。文件系统中的所有的文件(目录也是一种特殊的文件)都必有一个 inode 与之对应,而每个 inode 也至少有一个 dentry 与之对应(也有可能有多个,比如硬链接)。结合下图我们可以更清晰的理解这个架构:
【dentry-inode 结构图】
首先有这样一个文件:/home/user1/file1,它的目录项中 d_parent 指针指向它所在目录的目录项 /home/user1(1),而这个目录项中有一个双向链表 d_subdirs,里面链接着该目录的子目录项(2),所以 /home/user1/file1 目录项里的 d_u 也加入到了这个链表(3),这样一个文件上下关系就建立起来了。同样,/home/user1 的 d_parent 将指向它的父目录 /home,并且将自己的 d_u 链接到 /home 的 d_subdirs。file1 的目录项中有一个 d_inode 指针,指向一个 inode 结构(4),这个就是该文件的索引节点了,并且 file1 目录项里的 d_alias 也加入到了 inode 的链表 i_dentry 中(5),这样 dentry 和 inode 的关系也建立起来了。前面讲过,如果一个文件的硬连接不止一个的话就会有多个 dentry 与 inode 相关联,请看图中 /home/user2/file2,它和 file1 互为硬链接。和 file1 一样,file2 也把自己的 d_inode 指向这个 inode 结构(6)并且把 d_alias 加入到了 inode 的链表 i_dentry 里(7)。这样无论是通过 /home/user1/file1 还是 /home/user2/file2,访问的都是同一个文件。还有,目录也是允许硬链接的,只不过不允许普通用户创建目录的硬链接。
但是 Kernel 并不直接使用这样的结构来进行路径的遍历,为了提高效率 Kernel 使用散列数组来组织这些 dentry 和 inode,这已经超出我们的讨论范围了,所以知道有这么个东西就好了。现在我们可以回到 path_openat 接着我们的旅行了。
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat
...
-
current->total_link_count = 0;
-
error = link_path_walk(pathname->name, nd);
-
if (unlikely(error))
-
goto out;
...
total_link_count 是用来记录符号链接的深度,每穿越一次符号链接这个值就加一,最大允许 40 层符号链接。接下来 link_path_walk 会带领我们走向目标,并在到达最终目标所在目录的时候停下来(最终目标需要交给另一个函数 do_last 单独处理)。下面我们就来看看这个函数是怎样一步一步接近目标的。
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk
-
static int link_path_walk(const char *name, struct nameidata *nd)
-
{
...
-
while (*name=='/')
-
name++;
-
if (!*name)
-
return 0;
...
首先略过连续的“/”(可以试试这个命令“ls /////dev/”,看看有什么效果),如果此时路径就结束了那就相当于整个路径只有一个“/”(1741),还记得在 init_path() 里 nd->last_type 的初始值是什么么?没错这就是 LAST_ROOT。那么如果路径没有结束呢,那就说明我们至少拥有了一个真正的子路径,这就需要进入 1745 行这个大大的循环体来一步一步的走下去,所以连函数名都叫做“路径行走”嘛。
【fs/namei.c】sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk
...
-
for(;;) {
...
-
err = may_lookup(nd);
...
-
type = LAST_NORM;
-
if (name[0] == '.') switch (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;
-
if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
-
err = parent->d_op->d_hash(parent, &this);
-
if (err < 0)
-
break;
-
}
-
}
-
-
nd->last = this;
-
nd->last_type = type;
-
-
if (!name[len])
-
return 0;
-
/*
-
* If it wasn't NUL, we know it was '/'. Skip that
-
* slash, and continue until no more slashes.
-
*/
-
do {
-
len++;
-
} while (unlikely(name[len] == '/'));
-
if (!name[len])
-
return 0;
-
-
name += len;
...
首先是例行安全检查(1750),然后就看子路径名是否是“.”或“..”并做好标记(1759-1768)。如果不是“.”或“..”那么这就是一个普通的路径名,此时还要看看这个当前目录项是否需要重新计算一下散列值(1772)。现在可以先把子路径名更新一下(1779),如果此时已经到达了最终目标,那么“路径行走”的任务就完成了(1782 和 1791)。如果路径还没到头,那么现在就一定是一个“/”,再次略过连续的“/”(1790)并让 name 指向下一个子路径(1794),为下一次循环做好了准备。
到现在为止我们第一天的行程已经结束了,好好休息准备明天的旅程吧。
阅读(1914) | 评论(0) | 转发(0) |