Linux FS文件夹下函数分析

一、从系统调用开始

linux系统通过向内核发出系统调用(systemcall)实现了用户态进程和硬件设备之间的大部分接口。系统调用是操作系统提供的服务,用户程序通过各种系统调用,来引用内核提供的各种服务,系统调用的执行让用户程序陷入内核,该陷入动作由swi软中断完成。

在/kernel/kernel_sdk/arch/arm/kernel/entry-common.S中可以找到关于swi中断的入口,具体源码不进行分析,总之该中断实现对“现场”进行保护最后进行返回,我们只关注其查找系统调用表并调用的过程。如下图是系统调用过程的一个关键点,总之就是通过改中断获取到了系统调用时传入的系统调用号,然后获取系统调用表到tbl中,最后从表中找到对应函数进行调用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
位于kernel/kernel_sdk/arch/arm/kernel/calls.S 中有系统调用函数声明,可以参考如下函数进行理解,总之就是从一个数组中获取对应函数的函数指针。

#undef __SYSCALL
#define __SYSCALL(nr, call) [nr] = (call),

void *sys_call_table[NR_syscalls] = {
    [0 ... NR_syscalls-1] = sys_ni_syscall,
#include <asm/unistd.h>
};

在这里插入图片描述
文件 kernel/kernel_sdk/arch/arm/include/asm/unistd.h 中包含了包含系统调用号相关定义
在这里插入图片描述
使用系统调用的标准宏定义如下,SYSCALL_DEFINE后面的数字就是该函数需要传入的参数个数
在这里插入图片描述
以open为例:则其名为sys_open,传参分别为filename,flags,mode
在这里插入图片描述

二、常用操作

1.mount

mount是进入文件系统的,获取超级块,根目录的最初的起点,以下源码略一些东西

SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,char __user *, type, unsigned long, flags, void __user *, data)
{
    int ret;char *kernel_type;char *kernel_dev;void *options;
    kernel_type = copy_mount_string(type); /* 要被挂载的文件系统类型 */
    kernel_dev = copy_mount_string(dev_name); /* 设备,有些文件系统不需要依附于硬件,
    											比如debugfs,ramfs */
    options = copy_mount_options(data); /* 操作 */
    ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options);
}

long do_mount(const char *dev_name, const char __user *dir_name,
        const char *type_page, unsigned long flags, void *data_page)
{
	/* 中间一堆flag操作和安全校验全部略过 */
    retval = user_path(dir_name, &path); /* 这一步同open查找路径,详情看下面,总之就是将用户层
    									指定的路径名转化为path中的vfsmount挂载点 */
    retval = do_new_mount(&path, type_page, flags, mnt_flags,
                      dev_name, data_page);
    return retval;
}

static int do_new_mount(struct path *path, const char *fstype, int flags,
            int mnt_flags, const char *name, void *data)
{
    struct file_system_type *type;
    struct vfsmount *mnt; int err;
    type = get_fs_type(fstype); /* 通过字符串的文件系统类型名获取文件系统类型 */
    mnt = vfs_kern_mount(type, flags, name, data);
    err = do_add_mount(real_mount(mnt), path, mnt_flags); /* add a mount into a namespace's mount tree */
    return err;
}

vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
    struct mount *mnt;
    struct dentry *root;
    mnt = alloc_vfsmnt(name); /* 申请一个mount的内存空间,并初始化,
    							如果name非空, mnt-> mnt_devname会被赋值 */
	root = mount_fs(type, flags, name, data); 
	/* root = type->mount(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;
    return &mnt->mnt;
}

2.super

获取超级块是一个文件系统的重中之重,这里以ramfs为例,在mount_fs的过程中会调用对应文件系统的mount,而对应文件系统的mount一堆会伴随着超级块的读取填充,获取了超级块就可以获取对一个文件系如创建inode的方法,以及其inode指向的各种操作函数。这里对ramfs的mount进行分析

struct dentry *ramfs_mount(struct file_system_type *fs_type,
    int flags, const char *dev_name, void *data)
{
    return mount_nodev(fs_type, flags, data, ramfs_fill_super);
}

struct dentry *mount_nodev(struct file_system_type *fs_type,
    int flags, void *data,
    int (*fill_super)(struct super_block *, void *, int))
{
    int error;
    /* 这里申请一个超级块的内存空间并加入链表 */
    struct super_block *s = sget(fs_type, NULL, set_anon_super, flags, NULL); 
    

error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
	/* 对超级块进行填充,实际调用函数 ramfs_fill_super */
    return dget(s->s_root); /* 返回挂载目录 */
}

int ramfs_fill_super(struct super_block *sb, void *data, int silent)
{
    struct inode *inode;

    sb->s_maxbytes      = MAX_LFS_FILESIZE;
    sb->s_blocksize     = PAGE_SIZE;
    sb->s_blocksize_bits    = PAGE_SHIFT;
    sb->s_magic     = RAMFS_MAGIC;
    sb->s_op        = &ramfs_ops; /* 操作函数 */
    sb->s_time_gran     = 1;

    inode = ramfs_get_inode(sb, NULL, S_IFDIR, 0); /* 这里对根节点进行填充 */
    sb->s_root = d_make_root(inode); /* 对跟目录项进行填充 */

    return 0;
}

超级块结构

部分,省略一些

struct super_block {
    struct list_head    s_list;     /* 所有超级块组成双链表 */
    dev_t           s_dev;      /* 设备标识 */
    unsigned char       s_blocksize_bits;
    unsigned long       s_blocksize;
loff_t          s_maxbytes; 
/* 这三个成员变量分别对应这文件系统块大小的位数,块大小,最大文件大小 */
    struct file_system_type *s_type; /* 文件系统类型 */
    const struct super_operations   *s_op; /* 操作函数 */
    unsigned long       s_flags; /* 文件系统的超级块的状态位 */
    unsigned long       s_magic; /* 每一个超级块有一个唯一的魔术数标记 */
    struct dentry       *s_root; /* 超级块内的指向根目录的dentry结构体 */
    struct list_head    s_mounts;   /* 所有挂载的文件系统组成的链表 */
    struct block_device *s_bdev; /* 指向的块设备 */
    struct backing_dev_info *s_bdi; /* bdi设备 */
    struct mtd_info     *s_mtd; /* mtd设备 */
    char s_id[32];              /* Informational name */
    u8 s_uuid[16];              /* UUID */
    const struct dentry_operations *s_d_op; /* default d_op for dentries */
    struct list_head    s_inodes;   /* 改超级块下所有inode组成链表/
    struct list_head    s_inodes_wb;    /* writeback inodes */
};

3.open

open是Linux开启一切文件的开始,其函数如下,连creat也是open的一个变化,其中AT_FDCWD是一个常数为-100
在这里插入图片描述
一些flag的含义
│O_RDONLY│读文件 │
│O_WRONLY│写文件 │
│O_RDWR │即读也写 │
│O_APPEND│即读也写,但每次写总是在文件尾添加 │
│O_CREAT │若文件存在,此标志无用;若不存在,建新文件 │
│O_TRUNC │若文件存在,则长度被截为0,属性不变 │
│O_BINARY│此标志可显示地给出以二进制方式打开文件 │
│O_TEXT │此标志可用于显示地给出以文本方式打开文件│

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); /* 该函数负责校验flags并对flags进行某种设置,
												这里还没有真的区获取文件描述符的值 */

struct filename *tmp;
---------------ex-start--------------
struct filename {
    	const char      *name;  /* pointer to actual string */
    	const __user char   *uptr;  /* original userland pointer */
   		struct audit_names  *aname; /* 正常情况最后是个NULL */
    	int         refcnt; /* 该文件的引用计数,在调用getname时会被置为1,
    							只有为0时,在最后会将申请的iname的空间释放掉 */
    	const char      iname[]; /* getname过程中会被使用,实际上name一般指向iname */
};
---------------ex-ebd----------------

    if (fd)
        return fd;

    tmp = getname(filename); /* 该函数实现将用户层的路径和文件名拷贝到内核层,
    		放入到filename结构体中,该结构体如上,最终名字在name指向的地址中 */
    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); /* 打开文件,获取file结构体,
        												文件操作的函数指针等 */
        if (IS_ERR(f)) {
            put_unused_fd(fd);
            fd = PTR_ERR(f);
        } else {
            fsnotify_open(f);
            fd_install(fd, f); /* 文件描述符与文件结构体(真正的文件操作函数等)关联,
            					实际就是设置fd对应的数组的值为f。*/
			 
        }
    }
    putname(tmp);/* 释放文件名占用的空间 */
    return fd;
}

    实际上sys_open最关键的几步如下
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
      struct file *f = do_filp_open(dfd, tmp, &op);	
      fd_install(fd, f);
}

文件描述符分配实现

函数get_unused_fd_flags调用__alloc_fd,这里涉及到一个关键结构体:files_struct,源码在kernel/kernel_sdk/fs/file.c中
在这里插入图片描述

struct files_struct {
  /*
   * read mostly part
   */
    atomic_t count;
    bool resize_in_progress;
    wait_queue_head_t resize_wait;

    struct fdtable __rcu *fdt;
    struct fdtable fdtab;
  /*
   * written part on a separate cache line in SMP
   */
    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 {
    unsigned int max_fds;
    struct file __rcu **fd;      /* current fd array */
    unsigned long *close_on_exec;
    unsigned long *open_fds;
    unsigned long *full_fds_bits;
    struct rcu_head rcu;
};

/* 初始化时候的状态 */
struct files_struct init_files = {
    .count      = ATOMIC_INIT(1),
    .fdt        = &init_files.fdtab,
    .fdtab      = {
        .max_fds    = NR_OPEN_DEFAULT, /* 32 */
        .fd     = &init_files.fd_array[0],
        .close_on_exec  = init_files.close_on_exec_init,
        .open_fds   = init_files.open_fds_init,
        .full_fds_bits  = init_files.full_fds_bits_init,
    },
    .file_lock  = __SPIN_LOCK_UNLOCKED(init_files.file_lock),
};

files_struct: 进程打开文件的表结构,next_fd表示下一个可用进程描述符,并不一定真正可用,假如0-10描述符都被使用了,中间释放了3文件描述符,再打开文件,此时将使用3作为新的文件描述符,内核认为next_fd为4,next_fd只是表示可能可用的下一个文件描述符,下次查找可用描述符时从next_fd开始查找,而不需要从头开始找。

fdtable: 真正记录哪些文件描述符被使用了,哪些是空闲的,实际是一个文件描述符位图,每1bit表示了一个文件描述符,例如bit 0为1表示描述符1被使用了,bit 3为0表示描述符3可以使用。fd数组记录了file信息,数组下标就是文件描述符的值。细节后面再介绍。

file: 文件的真正信息,文件描述符只是个数组下标,通过下标查找file结构体信息,f_op记录的是文件读写及其他操作的真正函数,不同的文件系统,读写函数不一样,申请文件描述后,内核会将文件描述符与文件结构体(file读写函数等)关联起来。具体怎识别文件系统获取读写函数不在本文介绍。
在这里插入图片描述
上图描述了在初始化时,相互之间的关系(不包括位表,实际带位表间下面的fdtable介绍,这里只是一种简单关系),进程调用files_struct,然后找到fdtab,在找到fd对应的file,下图描述了当flie array被用满时需要拓展的情况,拓展后,“旧fdtab”被释放,*fdt和fdtab指向“新fdtab”
在这里插入图片描述

fdtable介绍

如下简要示意了下文件描述符位图结构
在这里插入图片描述
full_fds_bits每1bit代表的是一个32位的数组,也就是说代表了32位描述符;上面只画了32位,内核中的位图是一片连续的内存空间,最低bit表示数值0,下一比特表示1,依次类推;full_fds_bits每1bit只有0和1两个值,0表示有该组有可用的文件描述符,1表示没有可用的文件描述符,例如位图bit 0代表的是0-31共32个文件描述符,bit1代表的是32-63共32个文件描述符,假如0-31文件描述符都被使用了,那么位图bit0则应该标记为1,如果32-63中有一个未使用的文件描述符,则bit1被标记为0,当32-63中的所有文件描述符都被使用的时候,才标记为1。

open_fds是真正的文件描述符位图,也是一片连续的内存空间,每bit代表一个文件描述符(注意full_fds_bits每bit代表的是一组文件描述符),标记为0的bit表示该文件描述符没用被使用,标记为1的比特表示该文件描述符已经被使用,例如从内存其实地址开始计算,第35比特为1,则表示文件描述符35已经被使用了。

在open的最后调用fd_install函数实际上调用的是__fd_install,关键点如图画框的部分,其fd实际上就是ftb指向fd数组的下标。而ftd->fd[fd]实际上最后指向了文件(struct file)本身
在这里插入图片描述
注:对于一个进程,默认的文件描述符0就是 stdin,文件描述符1就是 stdout,文件描述符2就是 stderr

打开一个file

接下来解析file打开的过程,在open函数中调用了do_filp_open,其源码如下
在这里插入图片描述
其中需要注意一个结构体:nameidata。路径的查找是一个递归的过程。在递归寻找目标节点的过程中,需要借助一个搜索辅助结构 nameidata,这是一个临时结构,仅仅用在寻找目标节点的过程中,可以看到在最终filp返回前,nameidata会被释放掉。

而该函数的核心就是path_openat,就是沿着打开文件名的整个路径,一层层解析,最后得到文件对象

· 这里的核心就是path_openat,就是沿着打开文件名的整个路径,一层层解析,最后得到文件对象。但是为什么最多会调用3次?
· 由操作系统发展趋势一文中讲过,我们讲过当前的操作系统的内存管理系统中,CPU只能间接通过将硬盘数据缓存到内存中才能使用。而用来缓存文件的区域叫dentry cache ,这是一个哈希表。
· 第一次搜索采用rcu-walk方式搜索dentry cache,这种方式不会阻塞等待,更高效,但是可能会失败(因为文件inode本身还有顺序锁和自旋锁的保护);
· 第二次搜索则是采用ref-walk方式搜索dentry cache,考虑rcu锁的情形,但是可能会阻塞。
· 如果这样仍然失败,说明内存中没有该文件,这样就需要第三次,直接通过硬盘文件系统,进入硬盘慢速搜索,要带LOOKUP_REVAL标志。

路径行走有两个模式:rcu_walk模式和ref_walk模式。ref-walk是传统的使用自旋锁(d_lock)去并发修改目录项。比起rcu-walk这个模式,ref-walk是操作简单,但是在在路劲行走的过程中,对于每一个目录项的操作可能需要睡眠、得到锁等等这些操作。rcu-walk是基于顺序锁的目录项查找,可以不用存储任何共享数据在目录项或inode里,查找中间的元素。rcu-walk并不适合与任何一种情况,比如说:如果文件系统必须沉睡或者执行不重要的操作,就需要切换到ref-walk模式。

path_openat将一些校验去掉,比较关键部分的源码如下

static struct file *path_openat(struct nameidata *nd,
            const struct open_flags *op, unsigned flags)
{
    const char *s;
    struct file *file;
    int opened = 0;
    int error;

    file = get_empty_filp(); /* 找到一个未使用的文件结构返回,如果没有就申请 */
file->f_flags = op->open_flag;

s = path_init(nd, flags); /* 初始化nd参数,通过开头是不是"/"和dfd是否是AT_FDCWD,
							识别是绝对路径还是相对路径*/

/* link_path_walk:解析函数,针对路径循环搜索,直到找到最后一个不是目录的文件,该过程非常复杂,
主要执行的是函数walk_component又分为fast_lookup(使用rcu机制,在内存中进行快速读取)和
slow_lookup(这里可能调用inode -> i_op -> lookup(inode, dentry, flags),去对应的文件系统中[涉及
到硬件]读取数据), kernel/kernel_sdk/fs/namei.c和kernel/kernel_sdk/fs/dcache.c较大部分都在做这
部分,后面做单独解析
do_last:处理打开文件的最后操作,如果这个文件是个链接文件,则需要找到真实文件之后再执行do_last
*/
    while (!(error = link_path_walk(s, nd)) &&
        (error = do_last(nd, file, op, &opened)) > 0) {
        nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
        s = trailing_symlink(nd);
        if (IS_ERR(s)) {
            error = PTR_ERR(s);
            break;
        }
    }
    terminate_walk(nd);
    return file;
}

其中,do_last主要做了三件事:lookup_fast,lookup_open和vfs_open,其中lookup_fast先从缓存(dcache-目录项缓存)中查找,如果找到就不在执行lookup_open,否则lookup_open会在对应的文件系统中找到对应文件(这里涉及到实际硬件挂载的文件系统),最后执行vfs_open,vfs_open最终调用do_dentry_open最终指向对应文件的inode的fops的open,完成最后的open操作。也就是说,实际上打开一个文件获取其inode实际上是通过路径获取目录项,在获取对应的文件的inode,最终将inode赋值给file的过程,其file_op实际上是通过inode的flie_op获取的。
在这里插入图片描述

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);

    if (IS_ERR(dentry))
        return PTR_ERR(dentry);

    file->f_path = *path;
return do_dentry_open(file, d_backing_inode(dentry), NULL, cred);
}

static inline struct inode *d_backing_inode(const struct dentry *upper)
{
    struct inode *inode = upper->d_inode;
    return inode;
}

static int do_dentry_open(struct file *f,
              struct inode *inode,
              int (*open)(struct inode *, struct file *),
              const struct cred *cred)
{
    path_get(&f->f_path);
f->f_inode = inode; /* 将获取的inode给到file结构体中 */
f->f_mapping = inode->i_mapping; /* 下面具体介绍i_mapping */


    f->f_op = fops_get(inode->i_fop);

    if (!open)
        open = f->f_op->open;
if (open) {
        error = open(inode, f); /* 执行open操作 */
}
    return 0;
}
i_mapping

i_mapping 对应struct address_space *mapping;它是用于管理文件(struct inode)映射到内存的页面(struct page)的,其实就是每个file都有这么一个结构,将文件系统中这个file对应的数据与这个file对应的内存绑定到一起;与之对应,address_space_operations 就是用来操作该文件映射到内存的页面,比如把内存中的修改写回文件、从文件中读入数据到页面缓冲等。file结构体和inode结构体中都有一个address_space结构体指针,实际上,file->f_mapping是从对应inode->i_mapping而来,inode->i_mapping->a_ops是由对应的文件系统类型在生成这个inode时赋予的。

也就是说address_space结构与文件的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构(该inode结构也会在对应的file结构体中引用),其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个 address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。 (目前关于内存的部分还不太了解,图先放这,以后再做分析)
在这里插入图片描述
在这里插入图片描述
同样以ubifs为例,可以发现其address_space的相关操作
在这里插入图片描述

注:什么是RCU

RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用。
RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的,使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)。
RCU适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是RCU发挥作用的最佳场景

一图总结open

在这里插入图片描述

4.read_write

a.read

读源码如下:

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
	struct fd f = fdget_pos(fd); /* __fdget_pos –> __fdget -> __fcheck_files: 
								struct 	fdtable *fdt = rcu_dereference_raw(files->fdt);
								return rcu_dereference_raw(fdt->fd[fd]); 
								通过层层调用,最终实际上就是从ftd->fd表通过fd号找到file。
								struct fd 包含file和flag*/
    ssize_t ret = -EBADF;

    if (f.file) {
        loff_t pos = file_pos_read(f.file); /* return file->f_pos */
        ret = vfs_read(f.file, buf, count, &pos);
----------------------ex-start--------------------------
    /* vfs_read */
    if (file->f_op->read)
        return file->f_op->read(file, buf, count, pos);
    else if (file->f_op->read_iter) /* 代替aio(异步) */
        return new_sync_read(file, buf, count, pos);
    else
     return -EINVAL;
-----------------------ex-end---------------------------

        if (ret >= 0)
            file_pos_write(f.file, pos); /* file->f_pos = pos; */
        fdput_pos(f);
    }
    return ret;
}

在这里插入图片描述
以ubi文件系统为例,在读操作过程中会调用read_iter,然后指到ubifs对应操作,最终还是会执行generic_file_read_iter去找缓存页,这里涉及到整个kernel/mm/filemap.c文件,即涉及到内存的知识,等到以后再进行分析
在这里插入图片描述

b.write

读源码如下:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count)
{
    struct fd f = fdget_pos(fd);
    ssize_t ret = -EBADF;

    if (f.file) {
        loff_t pos = file_pos_read(f.file);
        ret = vfs_write(f.file, buf, count, &pos); /* 执行主体 */
----------------------ex-start--------------------------
    if (file->f_op->write)
        return file->f_op->write(file, p, count, pos);
    else if (file->f_op->write_iter)
        return new_sync_write(file, p, count, pos);
    else
        return -EINVAL;
-----------------------ex-end---------------------------

        if (ret >= 0)
            file_pos_write(f.file, pos);
        fdput_pos(f);
    }

    return ret;
}

同样的,写函数最终也是会调用对应文件写的操作函数或者找到对应文件系统的写动能之中。通常来说,对一个文件系统而言会最终调用generic_file_write_iter,该函数也在 kernel/mm/filemap.c 中
这里以依旧以ubi文件系统举例,直接看上面的图,调用ubifs_write_iter,最终还是调用了函数generic_file_write_iter
在这里插入图片描述

c.writeback

写文件的时候其实是通过文件系统写到page cache中,然后再由相应的线程在适当的时机将page cache中的数据写到磁盘中。
//mm/filemap.c
generic_file_write_iter
| -> __generic_file_write_itere
|->| -> generic_perform_write
|->|->|-> a_ops->write_begin
|->|->|-> iov_iter_copy_from_user_atomic(page, i, offset, bytes)
|->|->|-> a_ops->write_end
经历上述步骤之后,可能由于用户使用了sync或者写回机制的触发,有如下情况

通过sync的写回

generic_file_fsync
|-> sync_inode_metadata
|->|-> sync_inode
|->|->|-> writeback_single_inode
|->|->|->|-> __writeback_single_inode
|->|->|->|->|-> do_writepages
|->|->|->|->|->|-> mapping->a_ops->writepages
|->|->|->|->|-> write_inode

int generic_file_fsync(struct file *file, loff_t start, loff_t end,
               int datasync)
{
    struct inode *inode = file->f_mapping->host;
    err = __generic_file_fsync(file, start, end, datasync);
    return blkdev_issue_flush(inode->i_sb->s_bdev, GFP_KERNEL, NULL);
     /* 向底层发送flush指令,这将触发磁盘中的cache写入介质的操作
     (这样就能保证在正常情况下数据都被落盘了)*/
}

int __generic_file_fsync(struct file *file, loff_t start, loff_t end,
                 int datasync)
{
    struct inode *inode = file->f_mapping->host;
	/* 同步文件脏数据。等文件脏数据写入完成以后,调用具体文件系统的fsync方法,
	该方法主要是回写文件元数据,即inode信息。在此之前,需要对inode加锁,即mutex_lock。 */
    err = filemap_write_and_wait_range(inode->i_mapping, start, end);
    inode_lock(inode);
    ret = sync_mapping_buffers(inode->i_mapping); /* 这个函数就是写入数据的过程,
    											过程非常复杂这里不进行展开 */
    if (!(inode->i_state & I_DIRTY_ALL))
        goto out; 
	/* 从这里我们得知,实际上fsync比fdatasync多了一个同步metadata 
		(fdatasync函数类似于fsync,但它只影响文件的数据部分。
		而除数据外,fsync还会同步更新文件的属性) */
    if (datasync && !(inode->i_state & I_DIRTY_DATASYNC))
        goto out;

    err = sync_inode_metadata(inode, 1); /* 同步inode数据 */
    if (ret == 0)
        ret = err;
out:
    inode_unlock(inode);
    return ret;
}
通过回写工作队列bdi_writeback将page cache写到磁盘中

//mm/backing_dev.c
bdi_init
|-> wb_init
|->|-> INIT_DELAYED_WORK(&wb->dwork, wb_workfn);

//fs/fs-writeback.c
wb_workfn
|-> wb_do_writeback
|->|-> wb_writeback
|->|->|-> writeback_sb_inodes
|->|->|->|-> __writeback_single_inode
|->|->|->|->|-> do_writepages
|->|->|->|->|->|-> mapping->a_ops->writepages
|->|->|->|->|-> write_inode

在这里初始化的时候安装了回写wb->dwork,并且wb->dwork的回调函数和前面系统sync系统调用一样最终都会调用__writeback_single_inode来回写page cache。
blk_init_queue
|-> blk_init_queue_node
|->|-> blk_alloc_queue_node
|->|->|-> bdi_init(&q->backing_dev_info);
|->|->|-> setup_timer(&q->backing_dev_info.laptop_mode_wb_timer, laptop_mode_timer_fn, …

//mm/page-writeback.c
laptop_mode_timer_fn
|-> bdi_start_writeback //fs/fs-writeback.c
|->|-> __bdi_start_writeback
|->|->|-> bdi_queue_work
|->|->|->|-> mod_delayed_work(bdi_wq, &bdi->wb.dwork, 0);

在blk_init_queue时会初始化一个timer,并且timer的回调函数是laptop_mode_timer_fn,他里面会通过bdi_queue_work来调度回写wb->dwork。这样这个回写wb->dwork就会不断的被这个timer定时的调度执行了。

5.Ioctl

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
    int error;
    struct fd f = fdget(fd);/* 获取文件 */
	error = do_vfs_ioctl(f.file, fd, cmd, arg); /* 如果不是系统指定的几个参数,
											则会运行 vfs_ioctl(filp, cmd, arg) */
----------------------ex-start--------------------------
    int error = -ENOTTY;
    if (!filp->f_op->unlocked_ioctl)
        goto out;
	error = filp->f_op->unlocked_ioctl(filp, cmd, arg); 
	/* 运行驱动的ioctl */
	    if (error == -ENOIOCTLCMD)
	        error = -ENOTTY;
	 out:
	    return error;
-----------------------ex-end---------------------------

    fdput(f);
    return error;
}

/* 一些默认的ioctl参数 */
#define FIOCLEX     0x5451 /* 即File IOctl Close on Exec,对文件设置专用标志,
						通知内核当exec()系统调用发生时自动关闭打开的文件 */
#define FIONCLEX    0x5450 /* 即File IOctl Not CLose on Exec,与FIOCLEX标志相反,
							清除由FIOCLEX命令设置的标志 */
# define FIOQSIZE   0x5460 /* 获得一个文件或者目录的大小,当用于设备文件时,
							返回一个ENOTTY错误。 */

由以上定义可以看出,FIOCLEX、FIONCLEX、FIOQSIZE和FIONBIO的幻数为“T”。

三、三大缓冲区

三大缓冲为了提升效率:inode缓冲区、dentry缓冲区、块缓冲

1.目录缓冲区dcache

dentry cache中每一项的内容是一个路径到inode的映射关系,其数量众多,通过hash表的形式来管理是很自然的。既然是hash表,那就得有作为"key"的元素,在dentry的数据结构中,是通过类型为"qstr"的name来充当key值,进而计算出hash表的索引(即"value")

const struct qstr *name
unsigned int hash = name->hash;
struct hlist_bl_head *b = d_hash(hash);

static inline struct hlist_bl_head *d_hash(unsigned int hash)
{
    return dentry_hashtable + (hash >> (32 - d_hash_shift));
}

既然是依据name来做hash,那相同name的、不同name的,都可能在同一hash链表里(俗称“碰撞”)。name不同的,字符串比对一下就能区别,而name相同的,其"parent"肯定不同(同一目录下不可能有2个同名的文件),所以是具有唯一性的
如果一个dentry的引用计数(d_lockref.count)不为0,说明还有进程在引用它(比如通过"open"操作),此时dentry处于"in use"状态。
而当其引用计数变为0,表明不再被使用(比如文件被"close"了),则将切换到"unused"的状态,但此时其指向的内存inode依然有效,因为这些inode对应的文件之后还可能被用到。
当内存紧张时,这些unused dentry所占据的内存是可以被回收的,根据局部性原理,我们应当选择最近未被使用的dentry作为回收的对象。同page cache类似,通过slab cache分配得到的dentry在进入unused状态后,会通过LRU(Least Recently Used 最近最少使用)链表的形式被管理,最新加入的unused dentry被放在链表的头部,启动内存shrink的操作时,链表尾部的dentry将被率先回收,调用如下dput->dentry_lru_add->d_lru_add。
如果尝试open一个路径,但最后发现此路径对应的文件在磁盘上是不存在的,此时路径对应的的dentry也会以"negative entry"的形式记录在dcache里,这样下次在试图访问这个不存在的路径时,可以立即返回错误,不用再去磁盘瞎折腾一番(失败的案例同样有价值)

可通过查询"/proc/sys/fs/dentry-state"来获取申请的dentry的数目可未被使用的数量,如下申请2636,800未被使用,age_limit目前是个定值45
在这里插入图片描述
在这里插入图片描述
当创建一个新的dentry时,d_alloc()会为其申请内存空间,而后通过d_add()将这个新的dentry和对应的inode关联起来,并放入dcache的hash表中。

对dentry的使用以dget()和dput()来实现引用计数的加减,当引用计数变为0时,加入LRU链表,再次被使用后,从LRU链表移除。d_drop()直接将一个dentry从hash表中移除(比如dentry失效时),而d_delete()的目标则是归还inode,并将一个dentry置于"negative"状态

在这里插入图片描述
查找dcache的核心函数是"link_path_walk",在路径的遍历中,每一层目录/文件都被视为一个"component"(组成)
component的查询和判定主要依靠hash表的比对,不需要修改dentry的内容,本质上这是一个“读”的过程,但考虑到并行的需要,需要对途径的dentry的引用计数加1,而后再减1(就像击鼓传花,或者人浪一样),由于涉及到reference count值的更新,所以这种方式被称为"ref-walk"。

a.核心结构

struct dentry {
    /* RCU查找字段 */
    unsigned int d_flags;       /* protected by d_lock */
    seqcount_t d_seq;       /* per dentry seqlock */
    struct hlist_bl_node d_hash; /* 哈希链表:从中能够快速获取与给定的文件名和
    								目录名对应的目录项对象 */
    struct dentry *d_parent;    /* parent directory */
    struct qstr d_name;
    struct inode *d_inode;      /* Where the name belongs to - NULL is
                     * negative */
    unsigned char d_iname[DNAME_INLINE_LEN];    /* small names */

    /* Ref查找还涉及以下内容 */
    struct lockref d_lockref;   /* per-dentry lock and refcount */
    const struct dentry_operations *d_op;
    struct super_block *d_sb;   /* The root of the dentry tree */
    unsigned long d_time;       /* used by d_revalidate */
    void *d_fsdata;         /* fs-specific data */

    union {
        struct list_head d_lru;     /* “未使用”链表:即上面描述的,被close状态下的目录项。
        所有未使用 目录项对象都存放在一个LRU的双向链表。
        LRU链表的首元素和尾元素的地址存放在变量dentry_unused中的next 域和prev域中。
        目录项对象的d_lru域包含的指针指向该链表中相邻目录的对象。 */
        wait_queue_head_t *d_wait;  /* in-lookup ones only */
    };
    struct list_head d_child;   /* child of parent list */
    struct list_head d_subdirs; /* our children */
    /*
     * d_alias and d_rcu can share memory
     */
    union {
        struct hlist_node d_alias;  /* inode alias list */
        struct hlist_bl_node d_in_lookup_hash;  /* only for in-lookup ones */
        struct rcu_head d_rcu;
    } d_u;
};

b.目录缓存初始化

实际上就是相当于给dentry_cache和dentry_hashtable申请空间,并把哈希表进行一个初始化

static void __init dcache_init(void)
{
    unsigned int loop;
    dentry_cache = KMEM_CACHE(dentry,
        SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|SLAB_MEM_SPREAD|SLAB_ACCOUNT);
    dentry_hashtable = alloc_large_system_hash(参数略);
    for (loop = 0; loop < (1U << d_hash_shift); loop++)
        INIT_HLIST_BL_HEAD(dentry_hashtable + loop);
}

c.申请一个目录项

那么何时调用该函数?再上面分析open时了解了,open再经由路径找文件时实际上经过了3个步骤:rcu-walk直接读去dcache,如果读取不到,就会进行申请(link_path_walk -> walk_component -> lookup_slow -> d_alloc_parallel -> d_alloc),同样的在最后阶段去硬件中读取文件信息时同样会进行申请(do_last-> lookup_open-> d_alloc_parallel->d_alloc,在最后给这个文件建立目录项)

struct dentry *d_alloc(struct dentry * parent, const struct qstr *name)
{
    struct dentry *dentry = __d_alloc(parent->d_sb, name);/* 核心 */
    if (!dentry)
        return NULL;
    dentry->d_flags |= DCACHE_RCUACCESS;
    spin_lock(&parent->d_lock);
    __dget_dlock(parent);
    dentry->d_parent = parent;
    list_add(&dentry->d_child, &parent->d_subdirs); /* 这里相当于把目录的子目录加入到
    													父目录的“孙辈”链表中 */
    spin_unlock(&parent->d_lock);
    return dentry;
}

struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
{
    struct dentry *dentry;
    char *dname;
    int err;

	dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL);
	……略一些,以下都是对目录项的初始化
    dentry->d_name.len = name->len;
    dentry->d_name.hash = name->hash;
    memcpy(dname, name->name, name->len);
    dname[name->len] = 0;
    dentry->d_name.name = dname;

    dentry->d_lockref.count = 1;
    dentry->d_flags = 0;
    spin_lock_init(&dentry->d_lock);
    seqcount_init(&dentry->d_seq);
    dentry->d_inode = NULL;
    dentry->d_parent = dentry;
    dentry->d_sb = sb;
    dentry->d_op = NULL;
    dentry->d_fsdata = NULL;
    INIT_HLIST_BL_NODE(&dentry->d_hash);
    INIT_LIST_HEAD(&dentry->d_lru);
    INIT_LIST_HEAD(&dentry->d_subdirs);
    INIT_HLIST_NODE(&dentry->d_u.d_alias);
    INIT_LIST_HEAD(&dentry->d_child);
    d_set_d_op(dentry, dentry->d_sb->s_d_op);
	/* 某些文件系统可能对目录项有额外的操作,比如debugfs中会有 
	sb->s_d_op = &debugfs_dops;这类操作 */
    if (dentry->d_op && dentry->d_op->d_init) {
        err = dentry->d_op->d_init(dentry);
        if (err) {
            if (dname_external(dentry))
                kfree(external_name(dentry));
            kmem_cache_free(dentry_cache, dentry);
            return NULL;
        }
    }

    return dentry;
}

d.查找一个目录项

以下只分析ref-walk过程使用的函数源码
在do_last-> lookup_open-> d_lookup会被调用。d_lookup在父dentry的子级中搜索相关名称。如果找到dentry,则会增加其引用计数并返回dentry,如果dentry不存在,则返回NULL。

struct dentry *d_lookup(const struct dentry *parent, const struct qstr *name)
{
    struct dentry *dentry;
    unsigned seq;

    do {
        seq = read_seqbegin(&rename_lock); /* 在查找过程中,
        路径可能会被运行在其他CPU上的线程重命名(比如从"/a/b"更改为"/a/c/b"),
        你没法防止这种情况的发生,只能通过seqlock检测,如果确实被更改了,
        就放弃之前的查找结果,再次尝试。因为这个锁主要用来处理“重命名”的问题,
        所以在代码中被称为"rename_lock"。 */
        dentry = __d_lookup(parent, name);
        if (dentry)
            break;
    } while (read_seqretry(&rename_lock, seq));
    return dentry;
}

struct dentry *__d_lookup(const struct dentry *parent, const struct qstr *name)
{
    unsigned int hash = name->hash;
    struct hlist_bl_head *b = d_hash(hash);
    struct hlist_bl_node *node;
    struct dentry *found = NULL;
	struct dentry *dentry;

    hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
			/* 在整个哈希表中进行查找 */
        if (dentry->d_name.hash != hash)
            continue; /* name哈希值不对,不是要找的 */

        spin_lock(&dentry->d_lock);
        if (dentry->d_parent != parent)
            goto next; /* parent不对,不是要找的 */
        if (d_unhashed(dentry))
            goto next;

        if (!d_same_name(dentry, parent, name))
            goto next; /* 全名对比不对,不是要找的 */

        dentry->d_lockref.count++;
        found = dentry; /* 到这里才是真的找到了 */
        spin_unlock(&dentry->d_lock);
        break;
next:
        spin_unlock(&dentry->d_lock);
    }
    rcu_read_unlock();

    return found;
}

2.索引节点缓存icache

只要在内存中建立了一个dentry,那么它指向的inode也会在内存中被"cached",这就构成了inode cache(简称icache),icache的每一项内容都是一个已挂载的文件系统中的文件inode
icache机制的实现是以inode对象的slab分配器缓存为基础的,因此要从物理内存中申请或释放一个inode对象,都必须通过kmem_cache_alloc()函数和kmem_cache_free()函数来进行
Inode对象的slab分配缓存由一个kmem_cache_t类型的指针变量inode_cachep来定义。这个slab分配器缓存是在inode cache的初始化函数inode_init()中通过kmem_cache_create()函数来创建的。

a.inode数据结构

struct inode {
    umode_t         i_mode; /* 文件的访问权限(eg:rwxrwxrwx) */
    unsigned short      i_opflags;
    kuid_t          i_uid; /* inode拥有者id */
    kgid_t          i_gid; /* inode拥有者组id */
    unsigned int        i_flags; /* inode标志,可以是S_SYNC,S_NOATIME,S_DIRSYNC等 */

    const struct inode_operations   *i_op; /* inode操作 */
    struct super_block  *i_sb; /* 所属的超级快 */
    struct address_space    *i_mapping; /* address_space并不代表某个地址空间,
    而是用于描述页高速缓存中的页面的一个文件对应一个address_space,
    一个address_space与一个偏移量能够确定一个高速缓存中的页面。
    i_mapping通常指向i_data,不过两者是有区别的,i_mapping表示应该向谁请求页面,
    i_data表示被改inode读写的页面。 */

    /* Stat数据,未通过路径行走访问 */
    unsigned long       i_ino; /* inode号 */
    /*
     * 文件系统只能直接读取i_nlink。应使用以下功能进行修改:
     *    (set|clear|inc|drop)_nlink
     *    inode_(inc|dec)_link_count
     */
    union {
        const unsigned int i_nlink; /* 硬链接个数 */
        unsigned int __i_nlink;
    };
    dev_t           i_rdev; /* 如果inode代表设备,i_rdev表示该设备的设备号,否则该值应该为0 */
    loff_t          i_size; /* 文件大小 */
    struct timespec     i_atime; /* 最近一次访问文件的时间 */
    struct timespec     i_mtime; /* 最近一次修改文件的时间 */
    struct timespec     i_ctime; /* 最近一次修改inode的时间 */
    spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
    unsigned short          i_bytes; /* 文件中位于最后一个块的字节数 */
    unsigned int        i_blkbits; /* 以bit为单位的块的大小 */
    blkcnt_t        i_blocks; /* 文件使用块的数目 */

#ifdef __NEED_I_SIZE_ORDERED
    seqcount_t      i_size_seqcount; /* 对i_size进行串行计数 */
#endif

    /* Misc */
    unsigned long       i_state; /* inode状态,可以是I_NEW,I_LOCK,I_FREEING等 */
    struct rw_semaphore i_rwsem;

	/* inode第一次为脏的时间 以jiffies为单位 */
    unsigned long       dirtied_when;   /* jiffies of first dirtying */
    unsigned long       dirtied_time_when;

    struct hlist_node   i_hash; /* 散列表 */
    struct list_head    i_io_list;  /* backing dev IO list */
    struct list_head    i_lru;      /* inode LRU list */
    struct list_head    i_sb_list; /* 超级块链表 */
    struct list_head    i_wb_list;  /* backing dev writeback list */
    union {
        struct hlist_head   i_dentry; /* 所有引用该inode的目录项形成的链表 */
        struct rcu_head     i_rcu;
    };
    u64         i_version; /* 版本号 inode每次修改后递增 */
    atomic_t        i_count; /* 引用计数 */
    atomic_t        i_dio_count;
    atomic_t        i_writecount; /* 记录有多少个进程以可写的方式打开此文件 */
    const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
    struct file_lock_context    *i_flctx;
	struct address_space    i_data;
	/* 公用同一个驱动的设备形成链表,比如字符设备,在open时,
	会根据i_rdev字段查找相应的驱动程序,并使i_cdev字段指向找到的cdev,
	然后inode添加到struct cdev中的list字段形成的链表中 */
    struct list_head    i_devices;
    union {
        struct pipe_inode_info  *i_pipe; /* 如果文件是一个管道则使用i_pipe */
        struct block_device *i_bdev; /* 如果文件是一个块设备则使用i_bdev */
        struct cdev     *i_cdev; /* 如果文件是一个字符设备这使用i_cdev */
        char            *i_link;
        unsigned        i_dir_seq;
    };

    void            *i_private; /* fs or device private pointer */
};

b.inode缓存初始化

实际上就是相当于给inode_cache和inode_hashtable申请空间,并把哈希表进行一个初始化,与dcache是相同的套路

void __init inode_init(void)
{
    unsigned int loop;
    /* inode slab cache */
    inode_cachep = kmem_cache_create("inode_cache",sizeof(struct inode), 0, (SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|SLAB_MEM_SPREAD|SLAB_ACCOUNT), init_once);
    inode_hashtable = alloc_large_system_hash("Inode-cache", sizeof(struct hlist_head),  ihash_entries, 14, 0, &i_hash_shift , &i_hash_mask, 0, 0);
    for (loop = 0; loop < (1U << i_hash_shift); loop++)
        INIT_HLIST_HEAD(&inode_hashtable[loop]);
}
/* 这个函数用于配置字符设备,块设备或者管道,在后面会讲 */
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
    inode->i_mode = mode;
    if (S_ISCHR(mode)) {
        inode->i_fop = &def_chr_fops;
        inode->i_rdev = rdev;
    } else if (S_ISBLK(mode)) {
        inode->i_fop = &def_blk_fops;
        inode->i_rdev = rdev;
    } else if (S_ISFIFO(mode))
        inode->i_fop = &pipefifo_fops;
    else if (S_ISSOCK(mode))
        ;   /* leave it no_open_fops */
    else
        printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
                  " inode %s:%lu\n", mode, inode->i_sb->s_id,
                  inode->i_ino);
}

c.创建一个新的节点

struct inode *new_inode(struct super_block *sb)
{
    struct inode *inode;
    spin_lock_prefetch(&sb->s_inode_list_lock);
    inode = new_inode_pseudo(sb);
    if (inode)
        inode_sb_list_add(inode); 
/* list_add(&inode->i_sb_list, &inode->i_sb->s_inodes); 将该索引节点加入到超级块的全部索引节点的链表中*/
    return inode;
}

struct inode *new_inode_pseudo(struct super_block *sb)
{
    struct inode *inode = alloc_inode(sb); /* 申请一个索引节点 */
    if (inode) {
        spin_lock(&inode->i_lock);
        inode->i_state = 0;
        spin_unlock(&inode->i_lock);
        INIT_LIST_HEAD(&inode->i_sb_list);
    }
    return inode;
}

static struct inode *alloc_inode(struct super_block *sb)
{
    struct inode *inode;
    if (sb->s_op->alloc_inode)
        inode = sb->s_op->alloc_inode(sb); /* 通常的在有文件系统时,都由超级块提供创建一个新的索引节点的方法 */
    else
        inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL); /*没有就使用默认的从内存申请 */
    if (!inode)
        return NULL;
    /* 对申请的节点进行初始化 */
    if (unlikely(inode_init_always(sb, inode))) {
        if (inode->i_sb->s_op->destroy_inode)
            inode->i_sb->s_op->destroy_inode(inode);
        else
            kmem_cache_free(inode_cachep, inode);
        return NULL;
    }
    return inode;
}

d.查找索引节点

通常不会使用这个函数通过找索引节点的方式去找文件,而是使用目录项去找文件

inode的hash值定位,然后开始finde_inode */
    struct inode *inode;
again:
    spin_lock(&inode_hash_lock);
inode = find_inode_fast(sb, head, ino);
---------------ex-start--------------
hlist_for_each_entry(inode, head, i_hash) {
        if (inode->i_ino != ino)
            continue;
        if (inode->i_sb != sb)
            continue;
        spin_lock(&inode->i_lock);
        if (inode->i_state & (I_FREEING|I_WILL_FREE)) {
            __wait_on_freeing_inode(inode);
            goto repeat;
        }
        __iget(inode);
        spin_unlock(&inode->i_lock);
        return inode; /* 本质就是从双链表中找东西 */
 }
---------------ex-end-----------------
    spin_unlock(&inode_hash_lock);

    if (inode) {
        wait_on_inode(inode);
        if (unlikely(inode_unhashed(inode))) {
            iput(inode);
            goto again;
        }
    }
    return inode;
}

3.块缓冲区

a.Buffer cache和Page cache

Buffer cache(块缓存)

块缓冲,通常1K,对应于一个磁盘块,用于减少磁盘IO由物理内存分配,通常空闲内存全是bufferCache应用层面,不直接与BufferCache交互,而是与PageCache交互(见下)

读文件:从块设备中读取到bufferCache后,以后从中读取
写文件:方法一,写bufferCache,后写磁盘,方法二,写bufferCache,后台程序合并写磁盘

Buffer cache 也叫块缓冲,是对物理磁盘上的一个磁盘块进行的缓冲,其大小为通常为1k,磁盘块也是磁盘的组织单位。设立buffer cache的目的是为在程序多次访问同一磁盘块时,减少访问时间。系统将磁盘块首先读入buffer cache 如果cache空间不够时,会通过一定的策略将一些过时或多次未被访问的buffer cache清空。程序在下一次访问磁盘时首先查看是否在buffer cache找到所需块,命中可减少访问磁盘时间。不命中时需重新读入buffer cache。对buffer cache 的写分为两种,一是直接写,这是程序在写buffer cache后也写磁盘,要读时从buffer cache 上读,二是后台写,程序在写完buffer cache 后并不立即写磁盘,因为有可能程序在很短时间内又需要写文件,如果直接写,就需多次写磁盘了。这样效率很低,而是过一段时间后由后台写,减少了多次访磁盘 的时间。

Buffer cache 是由物理内存分配,linux系统为提高内存使用率,会将空闲内存全分给buffer cache ,当其他程序需要更多内存时,系统会减少cahce大小。

Page cache(页面缓存)

页缓冲/文件缓冲,通常4K,由若干个磁盘块组成(物理上不一定连续),也即由若干个bufferCache组成

读文件:可能不连续的几个磁盘块—>bufferCache—>pageCache—>应用程序进程空间
写文件:pageCache—>bufferCache—>磁盘

Page cache 也叫页缓冲或文件缓冲,是由好几个磁盘块构成,大小通常为4k,在64位系统上为8k,构成的几个磁盘块在物理磁盘上不一定连续,文件的组织单位为一页, 也就是一个page cache大小,文件读取是由外存上不连续的几个磁盘块,到buffer cache,然后组成page cache,然后供给应用程序。

Page cache在linux读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问。具体说是加速对文件内容的访问,buffer cache缓存文件的具体内容——物理磁盘上的磁盘块,这是加速对磁盘的访问。

文件系统通过块访问设备,块,是扇区之上的抽象概念。扇区是设备访问的最小单元,而文件系统最小寻址单元是块(扇区只是物理概念,块才是内核上的概念)。块一般是扇区的倍数(扇区是块的基本单元),但必须小于一个页大小,因此,块大小一般是512B,1KB,4KB。

文件系统的缓冲区对应着块。一个块就是一个缓冲区。一个磁盘块被调入内存的时候,它会被调入一个缓冲区中,这个缓冲区在内存中表示就是块。

即整体来说,Linux 文件缓冲区分为page cache和buffer cache,每一个 page cache 包含若干 buffer cache。然后又有以下几点
 内存管理系统和 VFS 只与 page cache 交互,内存管理系统负责维护每项 page cache 的分配和回收,同时在使用“内存映射”方式访问时负责建立映射。
 VFS 负责 page cache 与用户空间的数据交换。
 而具体文件系统则一般只与 buffer cache 交互,它们负责在存储设备和 buffer cache 之间交换数据,具体的文件系统直接操作的就是disk部分,而具体的怎么被包装被用户使用是VFS的责任(VFS将buffer cache包装成page给用户)。
 每一个page有N个buffer cache,struct buffer_head结构体中一个字段b_this_page就是将一个page中的buffer cache连接起来的结构

b.核心结构

struct buffer_head {
unsigned long b_state;      /* 缓冲区的状态标志 */

    struct buffer_head *b_this_page;/* 页面中缓冲区(一般一个页面会有多个块组成,
   									 一个页面中的块是以一个循环链表组成在一起的,
   								 该字段指向下一个缓冲区首部的地址。)见下图 */
    struct page *b_page;        /* 指向包含该块的页描述符 */

    sector_t b_blocknr;     /* start block number 存放逻辑块号起始,
    						表示块在磁盘或分区上的逻辑块号*/
    size_t b_size;          /* 块大小 */
    char *b_data;           /* pointer to data within the page 指向该块对应的数据的指针 */

    struct block_device *b_bdev; /* 表示包含该块的块设备,通常是一个磁盘或分区 */
    bh_end_io_t *b_end_io;      /* I/O completion */
    void *b_private;        /* reserved for b_end_io */
    struct list_head b_assoc_buffers; /* associated with another mapping */
    struct address_space *b_assoc_map;  /* mapping this buffer is
                           associated with */
    atomic_t b_count;       /* users using this buffer_head */
};

在这里插入图片描述
在这里插入图片描述
对于具体的Linux文件系统,会以block(磁盘块)的形式组织文件,为了减少对物理块设备的访问,在文件以块的形式调入内存后,使用块高速缓存进行管理。每个缓冲区由两部分组成,第一部分称为缓冲区首部,用数据结构buffer_head表示,第二部分是真正的存储的数据。由于缓冲区首部不与数据区域相连,数据区域独立存储。因而在缓冲区首部中,有一个指向数据的指针和一个缓冲区长度的字段

c.buff缓存初始化

void __init buffer_init(void)
{
    unsigned long nrpages;

    bh_cachep = kmem_cache_create("buffer_head",
            sizeof(struct buffer_head), 0,
                (SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|
                SLAB_MEM_SPREAD),
                NULL);
    /*
     * Limit the bh occupancy to 10% of ZONE_NORMAL
     */
    nrpages = (nr_free_buffer_pages() * 10) / 100;
    max_buffer_heads = nrpages * (PAGE_SIZE / sizeof(struct buffer_head)); /* 计算出最大的buffer_head数量 */
    hotcpu_notifier(buffer_cpu_notify, 0);
}

d.申请与读取

其过程为:
sb_bread
| __bread_gfp
| | __getblk_gfp
| | | __find_get_block :获取buffer_head
| | | __getblk_slow:上一步没获取到走这里
| | | | grow_buffers
| | | | | grow_dev_page
| | | | | | alloc_page_buffers:申请页缓存
| | | | | | | alloc_buffer_head:申请buffer_head
| | | | | | | set_bh_page: bh与page绑定
| |
| | __bread_slow:通过io读取对应块数据

四、字符设备

1.字符设备初始化

void __init chrdev_init(void)
{
    cdev_map = kobj_map_init(base_probe, &chrdevs_lock);
}

struct kobj_map {
    struct probe {
        struct probe *next;
        dev_t dev; /* 设备号 */
        unsigned long range; /* 设备号的范围 */
        struct module *owner;
        kobj_probe_t *get;
        int (*lock)(dev_t, void *);
        void *data; /* 指向struct cdev对象 */
    } *probes[255];
    struct mutex *lock;
};

struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct mutex *lock)
{
    struct kobj_map *p = kmalloc(sizeof(struct kobj_map), GFP_KERNEL);
    struct probe *base = kzalloc(sizeof(*base), GFP_KERNEL);
    int i;

    if ((p == NULL) || (base == NULL)) {
        kfree(p);
        kfree(base);
        return NULL;
    }

    base->dev = 1;
    base->range = ~0;
    base->get = base_probe;
    for (i = 0; i < 255; i++)
        p->probes[i] = base;
    p->lock = lock;
    return p;
}

在这里插入图片描述
这个cdev_map是一个struct kobj_map类型的指针,其中包含着一个struct probe指针类型、大小为255的数组,数组的每个元素指向的一个probe结构封装了一个设备号和相应的设备对象(这里就是cdev),可见,这个cdev_map封装了系统中的所有的cdev结构和对应的设备号。但是如果设备号多于255怎么处理呢?
当主设备号超过255时,会进行probe复用,此时probe->next就派上了用场,比如probe[200]可以表示设备号200,455…3895等所有对255取余是200的数字。
在这里插入图片描述

2.字符设备管理

cdev_map:实际是结构体kobj_map (probe[255])用于管理字符设备结构体cdev
chrdevs[255],该数组用于管理设备号

static struct char_device_struct {
    struct char_device_struct *next; /* 该结构为一个单向链表,当在chrdevs经过计算之后
								    在同一个数组内,则会将新的节点插入到char_device_struct中。
								    其实和上面的probe的管理方式是相同的 */
    unsigned int major; /* 该节点所属主字符设备号 */
    unsigned int baseminor; /* 该节点次设备号开始位置 */
    int minorct; /* 次设备号数量 */
    char name[64]; /* 设备号名称 */
    struct cdev *cdev;      /* will die 丢弃不用 */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; /* 该数组用于管理设备号 */

static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
               int minorct, const char *name)
{
    struct char_device_struct *cd, **cp;
    int ret = 0;
    int i;

    cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);

    mutex_lock(&chrdevs_lock);

	if (major == 0) {
	/* 主设备号填0时会动态查找一个设备号 */
        for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
            if (chrdevs[i] == NULL)
                break;
        }

        if (i < CHRDEV_MAJOR_DYN_END) /* 这个数是个定值234 */
            pr_warn("CHRDEV \"%s\" major number %d goes below the dynamic allocation range\n",name, i);        
		/* 在这里,如果动态申请的主设备号小于234,会有个警告,动态申请的主设备号从254开始 */
        major = i;
    }

    cd->major = major;
    cd->baseminor = baseminor;
    cd->minorct = minorct;
    strlcpy(cd->name, name, sizeof(cd->name));

    i = major_to_index(major);/* major % 255 */

    for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
        if ((*cp)->major > major ||
            ((*cp)->major == major &&
             (((*cp)->baseminor >= baseminor) ||
              ((*cp)->baseminor + (*cp)->minorct > baseminor))))
            break;
	/* 这里保证找到的节点要么主设备号大于当前设备号,要么主设备号相同的情况下,
	保证次设备号起始或起始加次设备号个数大于此次要申请的起始点 */

    /* Check for overlapping minor ranges.  */
    if (*cp && (*cp)->major == major) {
        int old_min = (*cp)->baseminor;
        int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
        int new_min = baseminor;
        int new_max = baseminor + minorct - 1;
         /* 实际上这里就是比较次设备号不能在原先已有的设备号的范围内,
         比如上一次已经申请了次设备号1,2,3.那么新的设备号不能在1,2,3
         的范围内并且又由于上面的操作,能够保证之前申请的次设备号不被覆盖,
         也就是说,不存在申请过了1,2,3在去从1开始申请的情况 */
        if (new_max >= old_min && new_max <= old_max) {
            ret = -EBUSY;
            goto out;
        }
        if (new_min <= old_max && new_min >= old_min) {
            ret = -EBUSY;
            goto out;
        }
    }

    cd->next = *cp;
    *cp = cd;
    mutex_unlock(&chrdevs_lock);
    return cd;
}

当主设备号为0~254范围时,将根据索引之间访问相应的chrdev数组指针,如果chrdev[i]其为空则说明没有分配,则申请char_device_struct结构大小内存将其地址赋值为chrdev[i]范围
当主设备号范围大于255时,计算方法为(index % CHRDEV_MAJOR_HASH_SIZE)求其余数,这样当index为255时,其访问数组为chrdev[ 255 % CHRDEV_MAJOR_HASH_SIZE ] = chrdev[0],和index 0指向同样的节点,此时chrdev[0]保存的时0 设备号主节点,需要将index为255新节点插入到该节点中,需要将其next指向0设备号节点,并将其chrdev[0],数组中,chrdev[0]永远指向其最近新申请的主设备号,同样如此,256和1 保持在一个节点上,257和2保持在一个节点上,如此以来,如下所图
在这里插入图片描述
chrdevs最终的排序结果可能由如下结果
在这里插入图片描述

3.字符设备注册

static inline int register_chrdev(unsigned int major, const char *name,
                  const struct file_operations *fops)
{
    return __register_chrdev(major, 0, 256, name, fops);
}

int __register_chrdev(unsigned int major, unsigned int baseminor,
              unsigned int count, const char *name,
              const struct file_operations *fops)
{
	/* 这里去掉冗余操作 */
    struct char_device_struct *cd;
    struct cdev *cdev;
    cd = __register_chrdev_region(major, baseminor, count, name); /* 这个函数就是用于申请和管理设备号的,在静态或者动态申请设备号的函数中都会进行调用 */
    cdev = cdev_alloc(); /* 申请一个cdev的空间,并加入到 */
    cdev->owner = fops->owner;
    cdev->ops = fops;
    kobject_set_name(&cdev->kobj, "%s", name);
    err = cdev_add(cdev, MKDEV(cd->major, baseminor), count); /* 将设备添加到kobj_map里 */
    cd->cdev = cdev;
    return major ? 0 : cd->major;
}

4.字符设备打开

源码如下,这里关于chardev_open需要溯源到根文件系统,rootfs的实质使用的是ramfs,过程后面分析,这里只需要知道在ramfs进行inode填充时,将def_chr_fops注册到了字符设备的inode的节点fop中 rootfs_mount ->ramfs_get_inode-> init_special_inode在这里插入图片描述

const struct file_operations def_chr_fops = {
    .open = chrdev_open,
    .llseek = noop_llseek,
};

static int chrdev_open(struct inode *inode, struct file *filp)
{
    const struct file_operations *fops;
    struct cdev *p;
    struct cdev *new = NULL;
    int ret = 0;

    spin_lock(&cdev_lock);
    p = inode->i_cdev; /* 得到相应的字符设备结构 */
    if (!p) {
        struct kobject *kobj;
        int idx;
        spin_unlock(&cdev_lock);
        kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); /* 如果此字符设备结构无效,则从设备对象管理中查找 */
        if (!kobj)
            return -ENXIO;
        new = container_of(kobj, struct cdev, kobj);
        spin_lock(&cdev_lock);
        p = inode->i_cdev; /* 再次尝试获得正确的字符设备结构 */
        if (!p) {
            inode->i_cdev = p = new;
            list_add(&inode->i_devices, &p->list);
            new = NULL;
        } else if (!cdev_get(p))
            ret = -ENXIO;
    } else if (!cdev_get(p)) /* 使用 cdev_get() 函数判断相应设备结构的内核设备对象是否有效 */
        ret = -ENXIO;
    spin_unlock(&cdev_lock);
    cdev_put(new); /* 如果到此字符设备还无效的话,则返回错误 */
    if (ret)
        return ret;

    ret = -ENXIO;
    fops = fops_get(p->ops); /* 获取该inode的ops */
    if (!fops)
        goto out_cdev_put;

    replace_fops(filp, fops); /* 把inode的ops给到file */
    if (filp->f_op->open) {
        ret = filp->f_op->open(inode, filp); /* 打开对应字符驱动的open */
        if (ret)
            goto out_cdev_put;
    }

    return 0;

 out_cdev_put:
    cdev_put(p);
    return ret;
}

五、块设备

1.块设备初始化

上文中我们分析了字符设备驱动程序的抽象结构体cdev和管理cdev的结构体cdev_map,在块设备中会相对复杂一些,因为涉及到一个概念:伪文件系统bdevfs。在此之下主要有三个结构体:对块设备或设备分区的抽象结构体block_device,对磁盘的通用描述gendisk以及磁盘分区描述hd_struct。其中block_device和hd_struct一一互相关联,而gendisk统一管理众多hd_struct。当虚拟文件系统需要使用该块设备时,则会利用block_device去gendisk中寻找对应的hd_struct从而实现读写等访问操作。除了这三个结构体以外,同字符设备驱动一样,块设备也有对gendisk的管理结构体bdev_map,同样是kobj_map结构体。

这里首先说明一下伪文件系统。在前文中我们已经分析了文件系统,而文件系统的精髓所在是让用户可以通过文件描述符来对指定的inode进行一系列的操作。伪文件系统和普通文件系统的区别在于,其inode对用户不可访问,即仅在内核态可见,从用户层的视角来看该文件系统并不存在。伪文件系统的作用是对一些操作系统中的元素进行封装,和普通的文件统一接口,如块设备bdevfs,管道文件pipefs,套接字socketfs等。通过这种方式的统一封装,才实现了Linux一切皆文件的思想。

bdevfs对应的超级块名为blockdev_superblock,初始化工作在系统初始化时调用bdev_cache_init()完成。所有表示块设备的 inode 都保存在伪文件系统 bdevfs 中以方便块设备的管理。Linux 将块设备的 block_device 和 bdev 文件系统的块设备的 inode通过 struct bdev_inode 进行关联

下面是bdevfs初始化的源码

static struct file_system_type bd_type = {
    .name       = "bdev",
    .mount      = bd_mount,
    .kill_sb    = kill_anon_super,
};

struct bdev_inode {
    struct block_device bdev;
    struct inode vfs_inode;
};

struct super_block *blockdev_superblock __read_mostly;

void __init bdev_cache_init(void)
{
    int err;
    static struct vfsmount *bd_mnt;
bdev_cachep = kmem_cache_create("bdev_cache", 
sizeof(struct bdev_inode),
                   0, (SLAB_HWCACHE_ALIGN|SLAB_RECLAIM_ACCOUNT|
                   SLAB_MEM_SPREAD|SLAB_ACCOUNT|SLAB_PANIC),
                   init_once);
    err = register_filesystem(&bd_type); /* 注册伪文件系统,实际上df –T是看不到这个文件系统的 */
    if (err)
        panic("Cannot register bdev pseudo-fs");
    bd_mnt = kern_mount(&bd_type); /* 这里进行伪文件系统挂载 */
    if (IS_ERR(bd_mnt))
        panic("Cannot create bdev pseudo-fs");
    blockdev_superblock = bd_mnt->mnt_sb; 
}

a.block_device(块设备信息)

下面先看看block_device结构体,其实和char_device有很多相似之处,如设备号bd_dev,打开用户数统计bd_openers等,从这里可以看到块设备的抽象结构体会直接和超级块以及对应的特殊inode关联,而且和hd_struct一一关联。其中bd_disk指向对应的磁盘gendisk,需要使用时通过hd_struct获取对应的磁盘分区信息并使用,请求队列bd_queue会传递给gendisk。

struct block_device {
    dev_t           bd_dev;  /* 对应底层设备的设备号 */
    int         bd_openers; /* 该设备同时被多少进程打开 */
    struct inode *      bd_inode;   /* 块设备的inod,可利用bd_dev通过bdget获得 */
    struct super_block *    bd_super;
 	 … …
	/* 首先block_device既可以是gendisk的抽象,又可以是hd_struct(分区)的抽象。
	当作为分区的抽象时,bd_contains指向了该分区所属的gendisk对应的block_device。
	当作为gendisk的抽象时,bd_contains指向自身的block_device
     */
    struct block_device *   bd_contains;
    unsigned        bd_block_size; /* 块的大小 */
    struct hd_struct *  bd_part; /* 指向分区指针,对于gendisk,指向内置的分区0 */
    /* number of times partitions within this device have been opened. */
    unsigned        bd_part_count; /* 该设备的所有分区同时被打开的次数 */
    int         bd_invalidated; /* 置1表示内存中的分区信息无效,
    							下次打开设备时需要重新扫描分区表 */
    struct gendisk *    bd_disk; /* 通用磁盘抽象,当该block_device作为分区抽象时,
    							指向该分区所属的gendisk,当作为gendisk的抽象时,指向自身 */
    struct request_queue *  bd_queue;
    struct list_head    bd_list;
    unsigned long       bd_private;
};

b.gendisk(通用磁盘描述)

gendisk是对通用磁盘的一个描述,与真正的底层物理设备相关联。其详细内容如下,major 是主设备号,first_minor 表示第一个分区的从设备号,minors 表示分区的数目。disk_name 给出了磁盘块设备的名称。struct disk_part_tbl 结构里是一个 struct hd_struct 的数组,用于表示各个分区。struct block_device_operations fops 指向对于这个块设备的各种操作。struct request_queue queue 表示在这个块设备上的请求队列。
所有的块设备,不仅仅是硬盘 disk,都会用一个 gendisk 来表示,然后通过调用链 add_disk()->device_add_disk()->blk_register_region(),将 dev_t 和一个 gendisk 关联起来并保存在 bdev_map 中。

struct gendisk {
    int major;          /* 主设备号 */
    int first_minor; /* 第一个次设备号 */
    int minors;  /* 表示分区的个数,分区号从1开始,0表示gendisk本身 */

    char disk_name[DISK_NAME_LEN];  /* 磁盘的名称,用于在sysfs和/proc/partitions中表示该磁盘 */
    char *(*devnode)(struct gendisk *gd, umode_t *mode);

    unsigned int events;        /* supported events */
    unsigned int async_events;  /* async events, subset of all */

    struct disk_part_tbl __rcu *part_tbl; /* 分区表 */
    struct hd_struct part0; /* 用于表示gendisk本身 */

    const struct block_device_operations *fops; /* 向底层具体设备的操作函数,
    				一般由用户驱动程序实现,如ramdisk驱动实现的fops为brd_fops */
    struct request_queue *queue; /* 该disk关联的请求队列 */
void *private_data;
… … 
};

c.hd_struct(分区)

hd_struct用于描述一个具体的磁盘分区,其详细内容如下

struct hd_struct {
    sector_t start_sect; /* 该分区的起始扇区号 */
    sector_t nr_sects; /* 该分区的扇区个数,也就是分区容量 */
    seqcount_t nr_sects_seq;
    sector_t alignment_offset;
    unsigned int discard_alignment;
    struct device __dev;
    struct kobject *holder_dir;
    int policy, partno; /* 该分区的分区号 */
    struct partition_meta_info *info;
#ifdef CONFIG_FAIL_MAKE_REQUEST
    int make_it_fail;
#endif
    unsigned long stamp;
    atomic_t in_flight[2];
#ifdef  CONFIG_SMP
    struct disk_stats __percpu *dkstats;
#else
    struct disk_stats dkstats;
#endif
    struct percpu_ref ref;
    struct rcu_head rcu_head;
};

d.三者之间的关系

block_device就相当于每个程序猿的档案信息(如姓名、电话、邮件、职位以及leader等等),gendisk相当于一个项目组,而hd_struct相当于项目组中的每一个程序猿。如何解释呢?

想象一下,对于人力管理者(相当于VFS)来说,他其实并不关心底下干活的是哪个程序猿,是高富帅还是矮矬穷,是美女还是帅哥,他只需要知道你的档案信息(block_device)就可以了,因为只要有了你的档案信息,在需要你的时候就随时可以找到你

而对于一个项目组(gendisk)来说,里面一个或多个程序猿(hd_struct)。因为项目组至少有一个leader吧,而leader本质上也是一个程序猿(相当于struct hd_struct part0)。当一个项目比较庞大时,可能一个leader会带领多个兄弟(就像一个硬盘管理着多个分区),然而如果是一个迷你项目,可能只需要项目组leader一个人就搞定了(就像一个磁盘不进行分区)。

当人力管理需要找某个程序猿(hd_struct或者part0)时,只需要找到他的档案信息(block_device)就可以了,因为二者是一对一的关系,而且根据程序猿找到他的项目组(gendisk)是不是也是一件很容易的事情?同理,一旦找到了项目组(gendisk),那么里面的所有程序猿(hd_struct)是不是也非常明朗了?总之一句话,档案信息(block_device)充当了人力管理(相当于VFS)和项目组成员(gendisk、hd_struct)之间的桥梁。
在这里插入图片描述
在这里插入图片描述
如下图为一个实际的块设备mmcblk2,分了20个分区,每一个分区都有对应的块设备抽象,最终实际都指向mmcblk2这个总的设备抽象,然后再mmcblk2中管理着gendisk分区表,分别对应下述20个分区,比如第一个分区,大小1024块。
在这里插入图片描述

2.块设备的管理和注册

上面已经讲到,块设备是由一个伪文件系统进行管理的,每一个块设备都有其对应的bdev_inode,那么其注册方式必然符合文件系统的方法。已知blockdev_superblock是该文件系统的超级块,只要找到对应的inode申请就可以进行溯源。下面以mtd层注册一个块设备为例进行回溯。

struct block_device *bdget(dev_t dev)
{
    struct block_device *bdev;
    struct inode *inode;

    inode = iget5_locked(blockdev_superblock, hash(dev),
            bdev_test, bdev_set, &dev); /* 这里调用了alloc_inode
函数 并最后调用inode = sb->s_op->alloc_inode(sb); 其实经过这一步就已经被“注册”到伪文件系统中了*/
    ------------ex-start-----------
    struct bdev_inode *ei = kmem_cache_alloc(bdev_cachep, GFP_KERNEL);
    if (!ei) /* 从bdev_cachep申请bdev_inode  */
        return NULL;
    return &ei->vfs_inode;
------------ex-end-------------
初始化过程略
    return bdev;
}

struct block_device *bdget_disk(struct gendisk *disk, int partno)
{
    struct hd_struct *part;
    struct block_device *bdev = NULL;

    part = disk_get_part(disk, partno); /* 获取分区 */
    if (part)
        bdev = bdget(part_devt(part)); /* 获取块设备信息 */
    disk_put_part(part); /* 将分区和块设备信息绑定实际是 调用了kobject_put(&dev->kobj); */
    return bdev;
}

然后在kernel/ block/genhd.c中的 register_disk 函数中调用了bdget_disk,函数device_add_disk又调用了register_disk。最后mtd层会由add_mtd_blktrans_dev调用device_add_disk,进行块设备每个分区的注册

void device_add_disk(struct device *parent, struct gendisk *disk)
{
    struct backing_dev_info *bdi;
    dev_t devt;
    int retval;

    retval = blk_alloc_devt(&disk->part0, &devt);/* 获取设备号的过程 */
    disk_to_dev(disk)->devt = devt; /* 记录gendisk的设备号 */
    disk->major = MAJOR(devt); /* 获取主设备号 */
    disk->first_minor = MINOR(devt); /* 获取从设备号 */
    disk_alloc_events(disk);
    bdi = &disk->queue->backing_dev_info; 
    bdi_register_owner(bdi, disk_to_dev(disk));
    blk_register_region(disk_devt(disk), disk->minors, NULL,
                exact_match, exact_lock, disk); /* 同字符设备,将gendisk添加到kobj_map中 */
    register_disk(parent, disk); /* 注册gendisk到通用块层 */
    blk_register_queue(disk);
    retval = sysfs_create_link(&disk_to_dev(disk)->kobj, &bdi->dev->kobj, "bdi");
    disk_add_events(disk);
    blk_integrity_add(disk);
}

3.块设备的打开与其他操作

块设备打开实际上是经由mount进行的,这里以ext4文件系统为例。

static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags,const char *dev_name, void *data)
{
return mount_bdev(fs_type, flags, dev_name, data, ext4_fill_super);
/* 其中mount_bdev的核心为
bdev = blkdev_get_by_path(dev_name, mode, fs_type);
 */
}

struct block_device *blkdev_get_by_path(const char *path, fmode_t mode,
                    void *holder)
{
    struct block_device *bdev;
    int err;
	……
    bdev = lookup_bdev(path);
	……
    err = blkdev_get(bdev, mode, holder);
	……
    return bdev;
}

struct block_device *lookup_bdev(const char *pathname)
{
    struct block_device *bdev;
    struct inode *inode;
    struct path path;
int error;
……
    error = kern_path(pathname, LOOKUP_FOLLOW, &path); /* 这里根据路径名找到对应的path,获取dentry */
	……
    inode = d_backing_inode(path.dentry); /* 通过目录项获取对应的inode */
	……
bdev = bd_acquire(inode); /* 实际上执行的就是bdget,根据inode获取到块设备信息 */
……
    return bdev;
}

经过上述步骤已经找到了块设备信息,通过函数blkdev_get打开对应的块设备,该函数执行主体为__blkdev_get,该函数源码过长,这里不在贴出,大致过程如下

调用 get_gendisk(),根据 block_device 获取 gendisk,根据获取到的partno有以下两种情况:
 如果partno为0,则说明打开的是整个设备而不是分区,那就调用 disk_get_part()获取 gendisk 中的分区数组,然后调用 block_device_operations 里面的 open() 函数打开设备。
 如果 partno 不为 0,也就是说打开的是分区,那我们就调用bdget_disk()获取整个设备的 block_device,赋值给变量 struct block_device *whole,然后调用递归 __blkdev_get(),打开 whole 代表的整个设备,将 bd_contains 设置为变量 whole。

也就是说,在没有出错的情况下,最终一定会调用disk->fops->open(bdev, mode),比如,ext4上的存储介质为emmc,最终会找到mmc_blk_open,对其进行打开,如果是一个单纯的mtd设备,则会找到blktrans_open,等等。

static const struct block_device_operations mmc_bdops = {
    .open           = mmc_blk_open,
    .release        = mmc_blk_release,
    .getgeo         = mmc_blk_getgeo,
    .owner          = THIS_MODULE,
    .ioctl          = mmc_blk_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl       = mmc_blk_compat_ioctl,
#endif
};

static const struct block_device_operations mtd_block_ops = {
    .owner      = THIS_MODULE,
    .open       = blktrans_open,
    .release    = blktrans_release,
    .ioctl      = blktrans_ioctl,
    .getgeo     = blktrans_getgeo,
};

至于块设备的比如读写操作,有以下通用操作,或者是对应文件系统进行提供,该部分比较复杂,这里暂且不进行深入研究

static const struct address_space_operations def_blk_aops = {
    .readpage   = blkdev_readpage,
    .readpages  = blkdev_readpages,
    .writepage  = blkdev_writepage,
    .write_begin    = blkdev_write_begin,
    .write_end  = blkdev_write_end,
    .writepages = blkdev_writepages,
    .releasepage    = blkdev_releasepage,
    .direct_IO  = blkdev_direct_IO,
    .is_dirty_writeback = buffer_check_dirty_writeback,
};

六、文件系统管理

在fs/filesystems.c中维护了表struct file_system_type *file_systems,该项维护了全部注册的文件系统,以及文件系统的挂载方式。
不同类型的文件系统通过next字段形成一个链表,同一种文件系统类型的超级块通过s_instances字段连接在一起,并挂入fs_supers链表中。所有的vfsmount通过mnt_list字段形成一个链表

struct file_system_type {
    const char *name; /* 文件系统名 */
    int fs_flags;
#define FS_REQUIRES_DEV     1 
#define FS_BINARY_MOUNTDATA 2
#define FS_HAS_SUBTYPE      4
#define FS_USERNS_MOUNT     8   /* Can be mounted by userns root */
#define FS_RENAME_DOES_D_MOVE   32768   /* FS will handle d_move() during rename() internally. */
    struct dentry *(*mount) (struct file_system_type *, int,
               const char *, void *); /* 挂载方法 */
    void (*kill_sb) (struct super_block *); /* 释放超级块 */
    struct module *owner;
    struct file_system_type * next;/* 形成链表 */
    struct hlist_head fs_supers; /* 同一种文件类型的超级块形成一个链表,fs_supers是这个链表的头 */

    struct lock_class_key s_lock_key;
    struct lock_class_key s_umount_key;
    struct lock_class_key s_vfs_rename_key;
    struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];
    struct lock_class_key i_lock_key;
    struct lock_class_key i_mutex_key;
    struct lock_class_key i_mutex_dir_key;
};

七、最初的文件系统加载

在start_kernel中调用了vfs_caches_init其源码如下

void __init vfs_caches_init(void)
{
    names_cachep = kmem_cache_create("names_cache", PATH_MAX, 0,
            SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);

    dcache_init();  /* 上面有分析 */
    inode_init();  /* 上面有分析 */
    files_init(); /* 初始化flie的缓存,每次申请文件时都会从filp_cachep中获取 */
    files_maxfiles_init();
    mnt_init(); /* 在这里加载最初的文件系统 */
    bdev_cache_init();
    chrdev_init(); /* 上面有分析 */
}

void __init mnt_init(void)
{
    unsigned u;
    int err;

	/* 略部分参数 */
    mnt_cache = kmem_cache_create("mnt_cache");
    mount_hashtable = alloc_large_system_hash("Mount-cache");
    mountpoint_hashtable = alloc_large_system_hash("Mountpoint-cache");
    /* 略表初始化过程 */

    kernfs_init();
    err = sysfs_init(); /* sysfs先于rootfs(实质是ramfs)进行注册,但是此时没有挂载 */
    init_rootfs(); /* ramfs进行注册 */
    init_mount_tree();  /* ramfs挂载 */
}

static void __init init_mount_tree(void)
{
    struct vfsmount *mnt;
    struct mnt_namespace *ns;
    struct path root;
    struct file_system_type *type;

    type = get_fs_type("rootfs");
	mnt = vfs_kern_mount(type, 0, "rootfs", NULL); /* 进行挂载 */
	---------------ex-start--------------
	ramfs_mount –> 
	ramfs_fill_super ->
	           ramfs_get_inode
	           d_make_root ->
	               __d_alloc ->
	                  static const struct qstr anon = QSTR_INIT("/", 1);
	     由上诉过程获取到根目录
	---------------ex-end----------------

    root.mnt = mnt;
    root.dentry = mnt->mnt_root;
    mnt->mnt_flags |= MNT_LOCKED;
    set_fs_pwd(current->fs, &root); /* 设置进程的当前路径 */
	set_fs_root(current->fs, &root); /* 设置进程的根路径为根目录,
										也就是说ramfs的挂载的根目录就是启动函数的根目录,
										也就是整个系统的根目录 */
}

八、多文件系统挂载关系

假设系统中有xfs, ext2和minix等若干文件系统模块

  1. 现有/dev/sda1和/dev/sdb1上存在xfs文件系统,/dev/sda2上为ext2文件系统,/dev/sdc1上为minix文件系统
  2. 将/dev/sda1挂载到/mnt/a上,将/dev/sdb1挂载到/mnt/b上,将/dev/sdc1不挂载
  3. 之后,将/dev/sda2也挂载到/mnt/a上。再将/dev/sda1同时挂载到/mnt/x上
    file_system_type + super block + mount的关系大致如下
    在这里插入图片描述
    在上面描述中存在三个文件系统,也就有三种file_system_type被注册。sda1和sdb1都是xfs文件系统,所以xfs的file_system_type的fs_supers把这两个同为xfs文件系统的super_block串连在自己下面。sda2是ext2文件系统,所以它挂在ext2的file_system_type下。sdc1是minix文件系统,在sdc1的设备上存在着super_block信息,但是我们这里说的super_block是指内存中的,由于sdc1没有被挂载使用,所以没有它的super_block信息被读入内存。

从挂载实例上看,sda1和sda2都挂载到/mnt/a上,但是从上述关系中很难表述它们的区别,需要借助mount和dentry的关系来说明,下面再具体说明。sda1同时挂在了/mnt/a和/mnt/x上,所以它有两个挂载实例对应同一个super_block。sdc1没有被挂载,所以没有挂载实例和它对应。

mount_hashtable是一个全局挂载实例的哈希表,系统中除了根挂载点以外所有的挂载实例都记录在它下面,搜索一个mount实例时需要借助这个mount的父mount实例和dentry实例来计算的出。比如说/mnt上挂载着一个文件系统,/mnt/a和b上分别又挂载着文件系统,此时要想检索/mnt/a(或b),需要以/mnt上的挂载实例和/mnt/a(或者b)的dentry结构为依据计算hash数值从mount_hashtable上得到一个头指针,这个头指针下就是所有父文件系统是在/mnt上且挂载点是/mnt/a(或b)的挂载到/mnt/a或b下的mount实例。

1.父子挂载点的挂载关系

假设在/mnt上挂载着一个文件系统,根据上面条件我们以/dev/sda1挂载到/mnt/a上为例,来解释一下/mnt/b上这个挂载实例和/mnt的挂载实例的关系,如下图所示:
在这里插入图片描述
mnt_root都指向这个文件系统的根dentry。

根dentry就是一个文件系统的路径的起始,也就是"/“。比如一个路径名/mnt/a/dir/file。在/mnt/b这个文件系统下看这个文件是/dir/file,这个起始的”/“代表/mnt/a下挂载的文件系统的根,也就是如上图红色所示的dentry,它是这一文件系统的起始dentry。当发现到了一个文件系统的根后,如果想继续探寻完整路径应该根据/mnt/b的挂载实例向上找到其父文件系统,也就是/mnt下挂载的文件系统。/dev/sda1挂载在了/mnt/a上,这里的/mnt/a代表/mnt下文件系统的一个子dentry,如图绿色部分所示。注意红色和绿色是两个文件系统下的两个不同的dentry,虽然不是很恰当的说它们从全局来看是一个路径名。那么从/mnt所在的文件系统看/mnt/a就是/a。最后再往上就到了rootfs文件系统,也就是最上层的根”/"。所以我们之前说过,表示一个文件的路径需要<mount, dentry>二元组来共同确定。

子文件系统的mnt_mountpoint就指向了父文件系统的一个dentry,这个dentry也就是子文件系统的真正挂载点。可以说子文件系统在挂载后会新创建一个dentry,并在此构建这个文件系统下的路径结构。

宗上所述,/mnt/b上这个新挂载的文件系统创建了一个新的mount, super_block, 根inode和根dentry

2.多文件系统单挂载点的挂载关系

就像上面所叙述的/dev/sda1挂载到了/mnt/a上,之后/dev/sda2也挂载到/mnt/a上的情况,当两个以上的文件系统先后挂载到同一个路径名下时会是怎样一种情况呢?如下图所示:
在这里插入图片描述
子文件系统A代表/dev/sda1,子文件系统B代表/dev/sda2。父文件系统和子文件系统A的挂载在之前一节已经说明过了,当/dev/sda2在sda1之后也挂载到/mnt/a上时,其关系就像在上一节基础上又添加了子文件系统B的关系。实际上子文件系统A就是子文件系统B的父文件系统,而唯一不同的是子文件系统B的mnt_mountpoint指向了子文件系统A的根dentry。而新的子文件系统B还是有自己的mount, super_block, 根dentry和根inode。

两个文件系统挂载在同一路径名下会造成之前挂载的文件系统被隐藏。

3.单文件系统多挂载点的挂载关系

同一个文件系统被挂载到不同的路经下,就像上面例子中/dev/sda1被挂载到/mnt/a和/mnt/x两个位置一样,如下图所示:
在这里插入图片描述
一个文件系统对应一个super_block,所以同一个文件系统当然只有一个super_block。但是因为挂载了两次,所有每一次挂载对应一个挂载实例struct mount,也就是有两个mount实例。此外同一个文件系统只有一个根,也就是两个挂载实例公用一个根dentry。但是因为挂载在两个不同的路经下,所以每个挂载实例的mnt_mountpoint指向不同的dentry。由于/mnt/a和/mnt/x都属于同一文件系统的下的两个子目录,所以两个子mount才指向同一个父mount(这个不是必须的)。

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜暝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值