Linux 文件系统详解 要讲文件系统主要有三个方面的内容需要了解:
用户视角下的文件系统,即一个树状的目录结构,包括各个不同磁盘的mount关系,各个文件的属性和权限以及文件本身(一个数组形状的字节合集)。
Linux系统下的文件系统,即内核中文件相关的数据结构,包括super block, inode, dentry等,还有磁盘上文件的页缓存。
磁盘上物理组织文件的视角,磁盘上的各个块之间的关系以及各个inode等。
用户视角下的文件系统 用户视角下文件系统其实就是一个树状的目录,如图所示。其中主磁盘即rootfs,在系统启动阶段被装载在根目录,而其他文件系统例如U盘或者移动硬盘固态等,就会装载在一个特定的节点上,例如图中home和usb1就是两个挂载点,挂在了两个文件系统。整体形成一棵目录树。
进程视角下的文件系统 task_struct中和文件相关内容 进程在Linux kernel中主要是用task_struct这个结构来表示,一个进程有一个task_struct,即操作系统中所谓的TCB。在task_struct中有很多字段和文件相关,这里只列出几个关键的:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct task_struct { struct fs_struct *fs ; struct files_struct *files ; struct nameidata *nameidata ; ...... }
首先每个进程会有自己的进程上下文,即当前的工作目录和根目录,记录在fs_struct结构体中。同时会打开多个文件,记录在files_struct结构体中。此外还有一个nameidata
字段,struct nameidata 是 Linux 内核中用于路径查找的辅助结构体。它在文件系统中起着关键作用,帮助根据路径名找到目标节点的 dentry 和 inode。
fs_struct结构体 前面说到了一个进程内有一个fs_struct结构体,我们来看看它包含什么
1 2 3 4 5 6 7 8 9 10 struct fs_struct { int users; spinlock_t lock; seqcount_spinlock_t seq; int umask; int in_exec; struct path root , pwd ; } __randomize_layout;
files_struct结构体 内核中还有一个files_struct结构体,同样看看里面包含了哪些内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct files_struct { atomic_t count; bool resize_in_progress; wait_queue_head_t resize_wait; struct fdtable __rcu *fdt ; struct fdtable fdtab ; spinlock_t file_lock ____cacheline_aligned_in_smp; unsigned int next_fd; unsigned long close_on_exec_init[1 ]; unsigned long open_fds_init[1 ]; unsigned long full_fds_bits_init[1 ]; struct file __rcu * fd_array [NR_OPEN_DEFAULT ]; };
具体如图所示
struct fdtable结构体 上面files_struct结构体中有一个关键数据结构fdtable,即文件描述符表,其在结构体内部有一个内嵌的fdtable结构体fdtab,同时还有一个指针fdt指向一个fdtable结构体。初始化时候fdt指针是指向内嵌在结构体files_struct中的fdtab成员的,并且该结构用了RCU同步保护机制。具体结构定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct fdtable { unsigned int max_fds; struct file __rcu **fd ; unsigned long *close_on_exec; unsigned long *open_fds; unsigned long *full_fds_bits; struct rcu_head rcu ; };
同时类似,初始时内嵌成员fdtab中struct file __rcu **fd
就指向files_struct中的fd_array成员等,具体看图,这样做的好处是不用kmalloc这样耗时的操作,先预先分配NR_OPEN_DEFAULT个。
struct file 结构体 最后就是内核视角的文件结构,即file结构体,在用户视角下一个文件其实就是一个int值,也叫文件描述符fd,但是对应内核数据结构其实就是file结构体数组的一个下表,真实的file结构体如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 struct file { union { struct callback_head f_task_work ; struct llist_node f_llist ; unsigned int f_iocb_flags; }; spinlock_t f_lock fmode_t f_mode; atomic_long_t f_count; struct mutex f_pos_lock ; loff_t f_pos; unsigned int f_flags; struct fown_struct f_owner ; const struct cred *f_cred ; struct file_ra_state f_ra ; struct path f_path ; struct inode *f_inode ; const struct file_operations *f_op ; u64 f_version; #ifdef CONFIG_SECURITY void *f_security; #endif void *private_data; #ifdef CONFIG_EPOLL struct hlist_head *f_ep ; #endif struct address_space *f_mapping ; errseq_t f_wb_err; errseq_t f_sb_err; } __randomize_layout __attribute__((aligned(4 )));
OK了,我们现在大概知道一个文件在用户视角和在内核视角的区别与联系了,接下来我们就可以看VFS的实现了。首先用户进程要操作文件,首先需要使用open函数对其打开,然后通过read和write对其进行处理。(这里的read, write都是库函数,也可以直接调用系统调用进行处理)
VFS虚拟文件系统 VFS其实作用其实就是隐藏了底层不同文件系统的差异,向上提供统一的数据结构和接口。就比如用户执行cp命令,把U盘的文件拷贝到本地,其无需考虑U盘的文件系统格式和操作函数,也无需考虑本地根目录或者安装的硬盘的文件系统格式和操作函数,而是统一地用write,read这样的操作来实现,极大地方便了用户。
VFS实现主要抽象出了四大组件对象,包括文件(file),目录(dentry),索引结点(inode),超级块(super block)。
文件:struct file,存放进程已经打开的文件信息和对其进行操作的函数,类似户口,并不能直接看到文件数据。
表示一个打开的文件实例 ,包含偏移量 (f_pos
)、打开标志 (f_flags
)、所属进程的 cred 等。
每次 open()
(或 dup()
)都会产生/引用一个新的 struct file
。
即使两个 file 指针指向同一个 dentry,它们的偏移量、flags 可以不同。
目录项:存在文件的特定识别名和关联信息。用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。 多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存。简单来说说目录项纪录了已访问过的文件和目录的内存影像,是整个文件系统树结构的一个子集 。
表示一个目录项(名字和 inode 的映射关系)。
同一个文件路径对应一个唯一的 dentry(dcache 缓存)。
多个 dentry 可以指向同一个 inode(硬链接)。
索引结点:每个索引结点的编号可以唯一地标识本文件系统中的文件,不同文件系统上可以有相同的索引号。它纪录了文件的名字、索引节点指针以及与其他目录项的层级关联关系。 多个目录项关联起来,就会形成目录结构,因为目录也是一个特殊的文件。且索引节点其是磁盘上索引节点的内存影像和拓展。
表示一个文件系统对象(文件/目录/设备节点)的元数据(权限、大小、时间戳等)。
在内存中是唯一的:一个 inode number 在一个超级块里只有一个 struct inode
。
超级块:存放安装和挂载的文件系统基本信息。超级块是磁盘上超级块的内存影像和拓展。
具体关系如图所示,并且都是多对一的关系,即多个文件描述符可以指向相同的文件对象(不同进程打开相同的文件),多个文件对象可以指向相同的目录项(比如同一个文件被打开多次,会产生多个struct file(fd), 但是dentry相同)和inode节点,多个目录项可以指向相同的索引节点(硬链接):
PS: 这里顺便说一下软链接,软链接是一个单独的文件,里面存放路径字符串。软链接文件有自己的 inode(类型 S_IFLNK
),存放目标路径。软链接有独立的 dentry,dentry->d_inode
指向软链接自己的 inode。open("symlink")
通常触发路径解析,VFS 调用 ->follow_link
(旧 API,内核新版本改为 ->get_link
),解析目标路径,再返回目标文件的 dentry/inode。
对于查找一个文件,我们以下图作为例子,假设需要找/home/lqm/file1
,先在磁盘上找到/
根目录对应的索引节点和内容,取到内存构造dentry,和inode的内存映像,然后找到下一级目录l2的索引节点,这样依次类推。
起点 :内核通过进程的 fs_struct
知道当前根目录(可能是 /
,也可能是 chroot/jail 的某个子目录)和当前工作目录 pwd
。 对于绝对路径 /home/lqm/file1
,查找从根目录 inode 开始。
第一级 /
:
找到根目录 inode(假设编号 0
或 1
),把它读入内存形成 struct inode
。
内核为 /
目录项建立一个 struct dentry
,并与 inode 关联。
读出root的目录页,到page_cache, 然后用look_up查看对应下一级是否存在
第二级 home
:
在根目录数据块中找到 home
这个目录项(dentry 名字 -> inode 号)。
读出 home
的 inode,建立 dentry("home")
,挂到 /
的 dentry 树下。
第三级 lqm
:同样,找到 inode X3
,建立 dentry("lqm")
。
第四级 file1
:
在 lqm
目录数据里找到 file1
→ inode Y4
。
读出 Y4
的 inode,建立 dentry("file1")
。
此时路径解析完成,dentry("file1")
+ inode(Y4)
已经在内核内存里。
打开文件的源码解析 看了那么多理论,show me the code,我们就看看进程调用open之后,创建file_struct等过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 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); } #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__) #define SYSCALL_DEFINEx(x, sname, ...) \ SYSCALL_METADATA(sname, x, __VA_ARGS__) \ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) #define __SYSCALL_DEFINEx(x, name, ...) \ __diag_push(); \ __diag_ignore(GCC, 8, "-Wattribute-alias" , \ "Type aliasing is used to sanitize syscall arguments" );\ asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \ __attribute__((alias(__stringify(__se_sys##name)))); \ ALLOW_ERROR_INJECTION(sys##name, ERRNO); \ static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\ asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \ asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \ { \ long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\ __MAP(x,__SC_TEST,__VA_ARGS__); \ __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \ return ret; \ } \ __diag_pop(); \ static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) #endif static inline long __do_sys_open(int dfd, 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); }
经过上面分析这些宏,其实最终是到do_sys_open函数,我们来看看这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 long do_sys_open (int dfd, const char __user *filename, int flags, umode_t mode) { struct open_how how = build_open_how(flags, mode); return do_sys_openat2(dfd, filename, &how); } static long do_sys_openat2 (int dfd, const char __user *filename, struct open_how *how) { struct open_flags op ; int fd = build_open_flags(how, &op); struct filename *tmp ; if (fd) return fd; tmp = getname(filename); if (IS_ERR(tmp)) return PTR_ERR(tmp); fd = get_unused_fd_flags(how->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 { fd_install(fd, f); } } putname(tmp); return fd; }
先关注如何获取空闲fd的,即get_unused_fd_flags(how->flags)
,就是前面说的利用file_struct结构体中的字段来获取到一个空闲的fd的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 int get_unused_fd_flags (unsigned flags) { return __get_unused_fd_flags(flags, rlimit(RLIMIT_NOFILE)); } EXPORT_SYMBOL(get_unused_fd_flags); int __get_unused_fd_flags(unsigned flags, unsigned long nofile){ return alloc_fd(0 , nofile, flags); } static int alloc_fd (unsigned start, unsigned end, unsigned flags) { struct files_struct *files = current->files; unsigned int fd; int error; struct fdtable *fdt ; spin_lock(&files->file_lock); repeat: fdt = files_fdtable(files); fd = start; if (fd < files->next_fd) fd = files->next_fd; if (fd < fdt->max_fds) fd = find_next_fd(fdt, fd); error = -EMFILE; if (fd >= end) goto out; error = expand_files(files, fd); if (error < 0 ) goto out; if (error) goto repeat; if (start <= files->next_fd) files->next_fd = fd + 1 ; __set_open_fd(fd, fdt); if (flags & O_CLOEXEC) __set_close_on_exec(fd, fdt); else __clear_close_on_exec(fd, fdt); error = fd; #if 1 if (rcu_access_pointer(fdt->fd[fd]) != NULL ) { printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n" , fd); rcu_assign_pointer(fdt->fd[fd], NULL ); } #endif out: spin_unlock(&files->file_lock); return error; }
得到空闲fd号之后,构建对应的struct file结构体
1 2 3 4 5 6 7 struct file *f = do_filp_open(dfd, tmp, &op);if (IS_ERR(f)) { put_unused_fd(fd); fd = PTR_ERR(f); } else { fd_install(fd, f); }
那么关键就是构建file struct这个结构体的函数do_filp_open, 然后核心就是使用kmem_cache_zalloc去分配一个结构体,这个是一个slab cache,再初始化相应字段,最后就返回了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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, NULL ); 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; } static void __set_nameidata(struct nameidata *p, int dfd, struct filename *name){ struct nameidata *old = current->nameidata; p->stack = p->internal; p->depth = 0 ; p->dfd = dfd; p->name = name; p->path.mnt = NULL ; p->path.dentry = NULL ; p->total_link_count = old ? old->total_link_count : 0 ; p->saved = old; current->nameidata = p; } static struct file *path_openat (struct nameidata *nd, const struct open_flags *op, unsigned flags) { struct file *file ; int error; file = alloc_empty_file(op->open_flag, current_cred()); if (IS_ERR(file)) return file; if (unlikely(file->f_flags & __O_TMPFILE)) { error = do_tmpfile(nd, flags, op, file); } else if (unlikely(file->f_flags & O_PATH)) { error = do_o_path(nd, flags, file); } else { const char *s = path_init(nd, flags); while (!(error = link_path_walk(s, nd)) && (s = open_last_lookups(nd, file, op)) != NULL ) ; if (!error) error = do_open(nd, file, op); terminate_walk(nd); } ... } struct file *alloc_empty_file (int flags, const struct cred *cred) { struct file *f ; if (get_nr_files() >= files_stat.max_files && !capable(CAP_SYS_ADMIN)) { if (percpu_counter_sum_positive(&nr_files) >= files_stat.max_files) goto over; } f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL); ... return f; }