这里,图书馆的书架,也就是硬盘上的文件系统格式已经搭建好了,现在我们还需要一个图书馆管理与借阅系统,也就是文件管理模块。
进程要想往文件系统里面写数据,需要很多层的组件一起合作:
接下来我们逐层解析。
解析系统调用是了解内核架构最有力的一把钥匙,这里我们只要重点关注这几个最重要的系统调用就可以了:
想要操作文件系统,第一件事情就是挂载文件系统。
register_filesystem(&ext4_fs_type); static struct file_system_type ext4_fs_type = { .owner = THIS_MODULE, .name = "ext4", .mount = ext4_mount, .kill_sb = kill_block_super, .fs_flags = FS_REQUIRES_DEV, };
mount系统调用的定义如下:
SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name, char __user *, type, unsigned long, flags, void __user *, data) { ...... ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options); ...... }
接下来的调用链为:do_mount->do_new_mount->vfs_kern_mount。
struct vfsmount * vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data) { ...... mnt = alloc_vfsmnt(name); ...... root = mount_fs(type, flags, name, data); ...... mnt->mnt.mnt_root = root; mnt->mnt.mnt_sb = root->d_sb; mnt->mnt_mountpoint = mnt->mnt.mnt_root; mnt->mnt_parent = mnt; list_add_tail(&mnt->mnt_instance, &root->d_sb->s_mounts); return &mnt->mnt; }
vfs_kern_mount先是创建struct mount结构,每个挂载的文件系统都对应于这样一个结构
struct mount { struct hlist_node mnt_hash; struct mount *mnt_parent; struct dentry *mnt_mountpoint; struct vfsmount mnt; union { struct rcu_head mnt_rcu; struct llist_node mnt_llist; }; struct list_head mnt_mounts; /* list of children, anchored here */ struct list_head mnt_child; /* and going through their mnt_child */ struct list_head mnt_instance; /* mount instance on sb->s_mounts */ const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */ struct list_head mnt_list; ...... } __randomize_layout; struct vfsmount { struct dentry *mnt_root; /* root of the mounted tree */ struct super_block *mnt_sb; /* pointer to superblock */ int mnt_flags; } __randomize_layout;
接下来,我们来看调用 mount_fs 挂载文件系统。
struct dentry * mount_fs(struct file_system_type *type, int flags, const char *name, void *data) { struct dentry *root; struct super_block *sb; ...... root = type->mount(type, flags, name, data); ...... sb = root->d_sb; ...... }
这里调用大的是ext4_fs_type的mount函数,也就是咱们上面提到的ext4_mount,从文件系统里面读取超级块。在文件系统的实现中,每个在硬盘上的结构,在内存中也对应相同格式的结构。当所有的数据结构都读到内存里面,内核就可以通过操作这些数据结构,来操作文件系统了。
接下来,我们从分析 Open 系统调用说起。
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode) { ...... return do_sys_open(AT_FDCWD, filename, flags, mode); } long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode) { ...... 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; }
要打开一个文件,首先要通过get_unused_fd_flags得到一个没有用的文件描述符。如何获取这个文件描述符呢?
在每一个进程的task_struct中,有一个指针files,类型是files_struct
struct files_struct *files;
files_struct 里面最重要的是一个文件描述符列表,每打开一个文件,就会在这个列表中分配一项,下标就是文件描述符。
struct files_struct { ...... struct file __rcu * fd_array[NR_OPEN_DEFAULT]; };
对于任何一个进程,默认情况下,文件描述符0表示stdin标准输入,文件描述符1表示stdout标准输出,文件描述符2表示stderr标准错误输出。另外,再打开的文件,都会从这个列表中找一个空闲位置分配给它。
文件描述符列表的每一项都是一个指向struct file的指针,也就是说,每每块一个文件,都会有一个struct file对应。
do_sys_open中调用do_filp_open,就是创建这个struct file结构,然后fd_install(fd, f);是将文件描述符和这个结构关联起来
struct file *do_filp_open(int dfd, struct filename *pathname, const struct open_flags *op) { ...... set_nameidata(&nd, dfd, pathname); filp = path_openat(&nd, op, flags | LOOKUP_RCU); ...... restore_nameidata(); return filp; }
do_filp_open里面首先初始化了 struct nameidata这个结构。我们知道,文件都是一串的路径名称,需要逐个解析。这个结构就是解析和查找路径的时候做辅助作用。
在 struct nameidata里面有一个关键的成员变量struct path
struct path { struct vfsmount *mnt; struct dentry *dentry; } __randomize_layout;
其中struct vfsmount和文件系统的挂载有关。另一个struct dentry,除了上面说的用于标识目录之外,还可以表示文件名,还会建立文件名以及inode之间的关联。
接下来就是调用path_openaat,主要做了下面几件事情:
static struct file *path_openat(struct nameidata *nd, const struct open_flags *op, unsigned flags) { ...... file = get_empty_filp(); ...... s = path_init(nd, flags); ...... while (!(error = link_path_walk(s, nd)) && (error = do_last(nd, file, op, &opened)) > 0) { ...... } terminate_walk(nd); ...... return file; }
例如,文件“/root/hello/world/data”,link_path_walk 会解析前面的路径部分“/root/hello/world”,解析完毕的时候 nameidata 的 dentry 为路径名的最后一部分的父目录“/root/hello/world”,而 nameidata 的 filename 为路径名的最后一部分“data”。
最后一部分的解析和处理,我们交给 do_last。
static int do_last(struct nameidata *nd, struct file *file, const struct open_flags *op, int *opened) { ...... error = lookup_fast(nd, &path, &inode, &seq); ...... error = lookup_open(nd, &path, file, op, got_write, opened); ...... error = vfs_open(&nd->path, file, current_cred()); ...... }
在这里面,我们需要先查找文件路径最后一部分对应的dentry。如何查找呢?
linux为了提高目录项对象的处理效率,设计与实现了目录项高速缓存dentry cache,简称dcache。它主要由两个数据结构组成:
这两个列表之间会产生复杂的关系:
所以,do_last()在查找dentry的时候,当然先从缓存中查找,调用的是lookup_fast。
如果缓存中没有找到,就需要真的到文件系统里面去找了,lookup_open会创建一个新的dentry,并且调用上一级目录的lnode的inode_operations的lookup函数,对于ext4来说,调用的是ext4_lookup,会到物理文件系统中找inode。最终找到后将新的dentry赋予path变量
static int lookup_open(struct nameidata *nd, struct path *path, struct file *file, const struct open_flags *op, bool got_write, int *opened) { ...... dentry = d_alloc_parallel(dir, &nd->last, &wq); ...... struct dentry *res = dir_inode->i_op->lookup(dir_inode, dentry, nd->flags); ...... path->dentry = dentry; path->mnt = nd->path.mnt; } const struct inode_operations ext4_dir_inode_operations = { .create = ext4_create, .lookup = ext4_lookup, ...
do_last()的最后一步是调用vfs_open真正打开文件
int vfs_open(const struct path *path, struct file *file, const struct cred *cred) { struct dentry *dentry = d_real(path->dentry, NULL, file->f_flags, 0); ...... file->f_path = *path; return do_dentry_open(file, d_backing_inode(dentry), NULL, cred); } static int do_dentry_open(struct file *f, struct inode *inode, int (*open)(struct inode *, struct file *), const struct cred *cred) { ...... f->f_mode = OPEN_FMODE(f->f_flags) | FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE; path_get(&f->f_path); f->f_inode = inode; f->f_mapping = inode->i_mapping; ...... f->f_op = fops_get(inode->i_fop); ...... open = f->f_op->open; ...... error = open(inode, f); ...... f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC); file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping); return 0; ...... } const struct file_operations ext4_file_operations = { ...... .open = ext4_file_open, ...... };
vfs_open里面最终要做的事情是,调用f_op->open,也就是调用ext4_file_open。另外一件重要的事情是将打开文件的所有信息,填写到struct file这个结构里面。
struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; spinlock_t f_lock; enum rw_hint f_write_hint; atomic_long_t f_count; unsigned int f_flags; fmode_t f_mode; struct mutex f_pos_lock; loff_t f_pos; struct fown_struct f_owner; const struct cred *f_cred; ...... struct address_space *f_mapping; errseq_t f_wb_err; }