Linux文件系统分析与实战

概述

引用毛德操在《Linux 内核源代码情景分析》中的一段话:

若要问构成一个“操作系统”的最重要的部件是什么,那就莫过于进程管理和文件系统了。事实上,有些操作系统(如一些“嵌入式”系统)可能有进程管理而没有文件系统;而另一些操作系统(如MSDOS)则有文件系统而没有进程管理。可是,要是二者都没有,那就称不上“操作系统”了。

可见文件系统对于一个操作系统的重要性。

通常意义上说的文件系统是指FAT32、NTFS、ext4这种对存储介质上的数据进行有效组织和管理的一种方法,可以称之为狭义文件系统。

linux中文件系统的作用远不止于此,linux的基本哲学是“万物皆文件”。普通文件、目录、link文件、字符设备、块设备、FIFO、socket都与文件系统有关。

linux还实现了一些特殊文件系统用于系统管理,例如:ramfs、tmpfs、作为初始化根目录的rootfs、用于/proc目录的procfs、用于/dev目录的devtmpfs、用于/sys目录的sysfs、用于debug的debugfs等等。

数据结构

dentry

在linux下目录也是一种文件,文件的内容就是若干个目录项。每个目录项表示一个子目录或者一个文件,目录项包含文件名、文件模式、所有者、文件大小、修改时间等内容。除了这些基本信息,还包含访问该文件内容所需要的索引信息,可以说目录项就是访问一个目录和文件的入口。

linux下使用两个数据结构来表示一个目录项:dentry和inode。dentry中保存了文件名和指向inode的指针,inode中保存了其余信息。为什么要这么做?这是为了支持硬链接文件。当创建一个硬链接文件时,实际上只新建了一个dentry,该dentry指向被链接文件的inode。两个文件可以在不同的目录下,也可以有不同的文件名,但实际上它们指向同一个文件,除了文件名以外的数据相同且只有一份。

struct dentry_operations {
    int (*d_revalidate)(struct dentry *, unsigned int);
    int (*d_weak_revalidate)(struct dentry *, unsigned int);
    int (*d_hash)(const struct dentry *, struct qstr *);
    int (*d_compare)(const struct dentry *,
            unsigned int, const char *, const struct qstr *);
    int (*d_delete)(const struct dentry *);
    int (*d_init)(struct dentry *);
    void (*d_release)(struct dentry *);
    void (*d_prune)(struct dentry *);
    void (*d_iput)(struct dentry *, struct inode *);
    char *(*d_dname)(struct dentry *, char *, int);
    struct vfsmount *(*d_automount)(struct path *);
    int (*d_manage)(const struct path *, bool);
    struct dentry *(*d_real)(struct dentry *, const struct inode *,
                 unsigned int);
} ____cacheline_aligned;

struct dentry {
    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 */
    const struct dentry_operations *d_op;
    struct super_block *d_sb;   /* The root of the dentry tree */
    //省略其它...
};

dentry除了包含文件名和inode,还包含super block类型指针d_sb和dentry_operations类型指针d_op。super block为整个文件系统的入口,dentry_operations是一组与dentry操作相关的函数。

inode

与dentry结构类似,inode除了包含文件基本信息以外,还包含inode_operations类型指针i_op、super_block类型指针i_sb、file_operations类型指针i_fop。

struct inode {
    umode_t         i_mode;
    unsigned short      i_opflags;
    kuid_t          i_uid;
    kgid_t          i_gid;
    unsigned int        i_flags;

    const struct inode_operations   *i_op;
    struct super_block  *i_sb;
    struct address_space    *i_mapping;

    /* Stat data, not accessed from path walking */
    unsigned long       i_ino;

    dev_t           i_rdev;
    loff_t          i_size;
    struct timespec     i_atime;
    struct timespec     i_mtime;
    struct timespec     i_ctime;
    spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
    unsigned short          i_bytes;
    unsigned int        i_blkbits;
    blkcnt_t        i_blocks;

    const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
    //省略其它...
};

inode_operations是一组与inode操作相关的函数,lookup用于在目录中查找子目录和文件,create、link、unlink用于创建和删除文件,mkdir、rmdir用于创建和删除目录,mknod用于创建设备文件,setattr和getattr用于设置和获取属性,等等。

struct inode_operations {
    struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
    const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *);
    int (*permission) (struct inode *, int);
    struct posix_acl * (*get_acl)(struct inode *, int);
    int (*readlink) (struct dentry *, char __user *,int);
    int (*create) (struct inode *,struct dentry *, umode_t, bool);
    int (*link) (struct dentry *,struct inode *,struct dentry *);
    int (*unlink) (struct inode *,struct dentry *);
    int (*symlink) (struct inode *,struct dentry *,const char *);
    int (*mkdir) (struct inode *,struct dentry *,umode_t);
    int (*rmdir) (struct inode *,struct dentry *);
    int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
    int (*rename) (struct inode *, struct dentry *,
            struct inode *, struct dentry *, unsigned int);
    int (*setattr) (struct dentry *, struct iattr *);
    int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
    ssize_t (*listxattr) (struct dentry *, char *, size_t);
    int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
              u64 len);
    int (*update_time)(struct inode *, struct timespec *, int);
    int (*atomic_open)(struct inode *, struct dentry *,
               struct file *, unsigned open_flag,
               umode_t create_mode, int *opened);
    int (*tmpfile) (struct inode *, struct dentry *, umode_t);
    int (*set_acl)(struct inode *, struct posix_acl *, int);
} ____cacheline_aligned;
file_operations

file_operations是一组与文件操作相关的函数,open、release用于文件打开和关闭,read、write用于文件读写,等等。

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    //省略其它...
};
super_block

super block是整个文件系统的总入口。如果要检索文件系统中的文件,必须通过以下路径完成:super block->根目录dentry->子目录dentry->…->最终文件的dentry。super block一般放在文件系统的固定位置,如FAT32使用磁盘0扇区存放super block数据。

super_block包含file_system_type类型指针s_type、super_operations类型指针s_op、指向文件系统根目录的s_root。super_operations是一组与super block操作相关的函数。

struct super_operations {
    struct inode *(*alloc_inode)(struct super_block *sb);
    void (*destroy_inode)(struct inode *);

    void (*dirty_inode) (struct inode *, int flags);
    int (*write_inode) (struct inode *, struct writeback_control *wbc);
    int (*drop_inode) (struct inode *);
    void (*evict_inode) (struct inode *);
    void (*put_super) (struct super_block *);
    int (*sync_fs)(struct super_block *sb, int wait);
    int (*freeze_super) (struct super_block *);
    int (*freeze_fs) (struct super_block *);
    int (*thaw_super) (struct super_block *);
    int (*unfreeze_fs) (struct super_block *);
    int (*statfs) (struct dentry *, struct kstatfs *);
    int (*remount_fs) (struct super_block *, int *, char *);
    void (*umount_begin) (struct super_block *);

    int (*show_options)(struct seq_file *, struct dentry *);
    int (*show_devname)(struct seq_file *, struct dentry *);
    int (*show_path)(struct seq_file *, struct dentry *);
    int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
    ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
    ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
    struct dquot **(*get_dquots)(struct inode *);
#endif
    int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
    long (*nr_cached_objects)(struct super_block *,
                  struct shrink_control *);
    long (*free_cached_objects)(struct super_block *,
                    struct shrink_control *);
};

struct super_block {
    struct list_head    s_list;     /* Keep this first */
    dev_t           s_dev;      /* search index; _not_ kdev_t */
    unsigned char       s_blocksize_bits;
    unsigned long       s_blocksize;
    loff_t          s_maxbytes; /* Max file size */
    struct file_system_type *s_type;
    const struct super_operations   *s_op;
    struct dentry       *s_root;
    struct block_device *s_bdev;
    const struct dentry_operations *s_d_op; /* default d_op for dentries */
    struct list_head    s_inodes;   /* all inodes */
    //省略其它...
};
file_system_type

file_system_type用于表示一种具体的文件系统类型,将它传递给register_filesystem函数注册一种文件系统到内核。

mount函数指针在挂载该类型的文件系统时调用,它返回文件系统根目录的dentry。kill_sb函数在卸载时调用。

struct file_system_type {
    const char *name;
    int fs_flags;
    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;
    //省略其它...
};
mount

每次mount一个文件系统时,都会创建一个struct mount结构体,假设执行以下mount命令,则会创建两个struct mount。

# mount -t ext4 /dev/sda1 /
# mount -t ext4 /dev/sdb1 /mnt

对于第二个struct mount来说,mnt_parent指向第一个mount、mnt_mountpoint指向挂载点/dev/sda1 /mnt目录的dentry,、mnt_devname保存mount时的设备名("/dev/sdb1")、mnt_ns指向命名空间、mnt.mnt_root指向/dev/sdb1根目录的dentry,mnt.mnt_sb指向/dev/sdb1的super block。

既然vfsmount已经包含了super_block指针,而super_block又包含了指向根目录dentry的指针,为啥vfsmount还需要mnt_root来指向根目录?使用mnt_sb->s_root来访问不就行了吗?

struct mountpoint {
    struct hlist_node m_hash;
    struct dentry *m_dentry;
    struct hlist_head m_list;
    int m_count;
};

struct vfsmount {
    struct dentry *mnt_root;    /* root of the mounted tree */
    struct super_block *mnt_sb; /* pointer to superblock */
    int mnt_flags;
};

struct mount {
    struct hlist_node mnt_hash;
    struct mount *mnt_parent;
    struct dentry *mnt_mountpoint;
    struct vfsmount mnt;
    const char *mnt_devname;    /* Name of device e.g. /dev/dsk/hda1 */
    struct mnt_namespace *mnt_ns;   /* containing namespace */
    struct mountpoint *mnt_mp;  /* where is it mounted */
    struct hlist_node mnt_mp_list;  /* list mounts with the same mountpoint */
    //省略其它...
};
path

每个以绝对路径或相对路径字符串表示的一个文件或者目录,在内核里最终都会转换为一个struct path结构体。很明显path指向一个文件或者目录,所以需要包含一个dentry,还需要一个vfsmount来指向该文件所在文件系统的挂载信息。

struct path {
    struct vfsmount *mnt;
    struct dentry *dentry;
};

常见流程

注册文件系统

register_filesystem函数注册一种文件系统到内核,该函数实现比较简单,在file_systems链表中查找一个空位,并将新的文件系统插入。

static struct file_system_type **find_filesystem(const char *name, unsigned len)
{
    struct file_system_type **p;
    for (p = &file_systems; *p; p = &(*p)->next)
        if (strncmp((*p)->name, name, len) == 0 &&
            !(*p)->name[len])
            break;
    return p;
}

int register_filesystem(struct file_system_type * fs)
{
    int res = 0;
    struct file_system_type ** p;

    BUG_ON(strchr(fs->name, '.'));
    if (fs->next)
        return -EBUSY;
    write_lock(&file_systems_lock);
    p = find_filesystem(fs->name, strlen(fs->name));
    if (*p)
        res = -EBUSY;
    else
        *p = fs;
    write_unlock(&file_systems_lock);
    return res;
}
mount文件系统

do_new_mount函数mount文件系统到一个目录,path参数为挂载目录,fstype参数为文件系统类型,name参数为要mount的设备名称。

该函数先将字符串类型文件系统名转换为file_system_type,然后调用vfs_kern_mount分配一个新的mount并初始化,最后将新的mount关联到挂载点对应的path。

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;

    if (!fstype)
        return -EINVAL;

    type = get_fs_type(fstype);
    if (!type)
        return -ENODEV;

    mnt = vfs_kern_mount(type, flags, name, data);
    if (!IS_ERR(mnt) && (type->fs_flags & FS_HAS_SUBTYPE) &&
        !mnt->mnt_sb->s_subtype)
        mnt = fs_set_subtype(mnt, fstype);

    put_filesystem(type);
    if (IS_ERR(mnt))
        return PTR_ERR(mnt);

    if (mount_too_revealing(mnt, &mnt_flags)) {
        mntput(mnt);
        return -EPERM;
    }

    err = do_add_mount(real_mount(mnt), path, mnt_flags);
    if (err)
        mntput(mnt);
    return err;
}

vfs_kern_mount函数分配一个新的mount,调用文件系统类型mount函数(file_system_type->mount)创建文件系统对应的super block和root dentry。

struct vfsmount *
vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
    struct mount *mnt;
    struct dentry *root;

    if (!type)
        return ERR_PTR(-ENODEV);

    mnt = alloc_vfsmnt(name);
    if (!mnt)
        return ERR_PTR(-ENOMEM);

    if (flags & MS_KERNMOUNT)
        mnt->mnt.mnt_flags = MNT_INTERNAL;

    root = mount_fs(type, flags, name, data);
    if (IS_ERR(root)) {
        mnt_free_id(mnt);
        free_vfsmnt(mnt);
        return ERR_CAST(root);
    }

    mnt->mnt.mnt_root = root;
    mnt->mnt.mnt_sb = root->d_sb;
    mnt->mnt_mountpoint = mnt->mnt.mnt_root;
    mnt->mnt_parent = mnt;
    lock_mount_hash();
    list_add_tail(&mnt->mnt_instance, &root->d_sb->s_mounts);
    unlock_mount_hash();
    return &mnt->mnt;
}
路径查找

路径查找用于将字符串路径转换为对应的path结构,所有与路径相关的系统调用都会涉及该操作。如:打开一个文件、进入一个目录等等。

filename_lookup函数用于路径查找。dfd参数指示查找起点,可以是当前目录(AT_FDCWD),也可以是一个已打开的句柄;name参数为要查找的全路径名,如:"/usr/local/bin";path参数用于返回结果;root参数用于指定起始目录。

nameidata类型用于保存查找的中间结果,path成员保存当前所在路径;last保存当前需要在path中查找的节点,全路径被路径分隔符"/"拆分为若干个节点;inode保存path对应dentry的inode指针;last_type保存当前节点的类型。

filename_lookup先调用set_nameidata对nd进行简单初始化,再调用path_lookupat完成路径查找。

#define EMBEDDED_LEVELS 2
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;
    int     total_link_count;
    struct saved {
        struct path link;
        struct delayed_call done;
        const char *name;
        unsigned seq;
    } *stack, internal[EMBEDDED_LEVELS];
    struct filename *name;
    struct nameidata *saved;
    struct inode    *link_inode;
    unsigned    root_seq;
    int     dfd;
};

static void set_nameidata(struct nameidata *p, int dfd, struct filename *name)
{
    struct nameidata *old = current->nameidata;
    p->stack = p->internal;
    p->dfd = dfd;
    p->name = name;
    p->total_link_count = old ? old->total_link_count : 0;
    p->saved = old;
    current->nameidata = p;
}

static int filename_lookup(int dfd, struct filename *name, unsigned flags,
               struct path *path, struct path *root)
{
    int retval;
    struct nameidata nd;
    if (IS_ERR(name))
        return PTR_ERR(name);
    if (unlikely(root)) {
        nd.root = *root;
        flags |= LOOKUP_ROOT;
    }
    set_nameidata(&nd, dfd, name);
    retval = path_lookupat(&nd, flags | LOOKUP_RCU, path);
    if (unlikely(retval == -ECHILD))
        retval = path_lookupat(&nd, flags, path);
    if (unlikely(retval == -ESTALE))
        retval = path_lookupat(&nd, flags | LOOKUP_REVAL, path);

    if (likely(!retval))
        audit_inode(name, path->dentry, flags & LOOKUP_PARENT);
    restore_nameidata();
    putname(name);
    return retval;
}

path_lookupat函数先调用path_init初始化起始目录nd->path,规则如下:

  1. 如果指定了nd->root,则以它做为起点;
  2. 如果路径名以"/"开头,则以当前进程的根目录current->fs->root做为起点;
  3. 如果nd->dfd等于AT_FDCWD,则以当前进程的当前目录current->fs->pwd做为起点;
  4. 否则以nd->dfd的dentry做为起点;

最后调用link_path_walk函数完成中间路径节点的查找,调用lookup_last函数完成最后一个节点的查找。

static int path_lookupat(struct nameidata *nd, unsigned flags, struct path *path)
{
    const char *s = path_init(nd, flags);
    int err;

    if (IS_ERR(s))
        return PTR_ERR(s);
    while (!(err = link_path_walk(s, nd))
        && ((err = lookup_last(nd)) > 0)) {
        s = trailing_symlink(nd);
        if (IS_ERR(s)) {
            err = PTR_ERR(s);
            break;
        }
    }
    if (!err)
        err = complete_walk(nd);

    if (!err && nd->flags & LOOKUP_DIRECTORY)
        if (!d_can_lookup(nd->path.dentry))
            err = -ENOTDIR;
    if (!err) {
        *path = nd->path;
        nd->path.mnt = NULL;
        nd->path.dentry = NULL;
    }
    terminate_walk(nd);
    return err;
}

路径节点查找的主体循环在link_path_walk函数中实现,针对当前节点名的不同种类,需要做不同的处理:

  • “.”:停留在当前目录
  • “..”:跳转到上级目录
  • 软链接文件:根据flags决定是否跳转
  • mount点:跳转到挂载文件系统的root目录

开始查找前先过滤掉所有前置路径分隔符"/",使name指向第一个需要查找的路径节点名。

static int link_path_walk(const char *name, struct nameidata *nd)
{
    int err;

    while (*name=='/')
        name++;
    if (!*name)
        return 0;

然后进入主体循环,先对当前目录dentry和当前节点名做一个哈希运算,然后将当前节点名和哈希结果存放到nd->last变量。接着让name指向下一个节点名,并跳过两个节点名之间的路径分隔符"/"。最后调用walk_component在nd->path中查找nd->last,找到之后将path结果写入nd->path,使它成为当前查找目录。这样就可以进入下一次循环,直到处理完所有节点。

name += hashlen_len(hash_len);
if (!*name)
    goto OK;
/*
 * If it wasn't NUL, we know it was '/'. Skip that
 * slash, and continue until no more slashes.
 */
do {
    name++;
} while (unlikely(*name == '/'));

rootfs的初始化

内核初始化时将rootfs文件系统挂载为最初始的根目录"/",它以ramfs或tmpfs做为自己的后端。ramfs和tmpfs都是一种内存文件系统,不同之处在于:ramfs无大小限制,里面的数据不能被交换出去,只有超级用户才有权限写入;tmpfs限制了文件系统的大小,数据可以被交换出去,普通用户也可以写入。

注册rootfs

init_rootfs函数用于注册rootfs,调用register_filesystem注册到文件系统,并初始化ramfs或tmpfs后端。

static struct file_system_type rootfs_fs_type = {
    .name       = "rootfs",
    .mount      = rootfs_mount,
    .kill_sb    = kill_litter_super,
};

int __init init_rootfs(void)
{
    int err = register_filesystem(&rootfs_fs_type);

    if (err)
        return err;

    if (IS_ENABLED(CONFIG_TMPFS) && !saved_root_name[0] &&
        (!root_fs_names || strstr(root_fs_names, "tmpfs"))) {
        err = shmem_init();
        is_tmpfs = true;
    } else {
        err = init_ramfs_fs();
    }

    if (err)
        unregister_filesystem(&rootfs_fs_type);

    return err;
}
mount rootfs

init_mount_tree函数用于mount rootfs,调用vfs_kern_mount创建一个mount,调用create_mnt_ns创建一个命名空间,并初始化init_task命名空间,最后将当前进程的根目录和当前目录都指向rootfs的root。

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");
    if (!type)
        panic("Can't find rootfs type");
    mnt = vfs_kern_mount(type, 0, "rootfs", NULL);
    put_filesystem(type);
    if (IS_ERR(mnt))
        panic("Can't create rootfs");

    ns = create_mnt_ns(mnt);
    if (IS_ERR(ns))
        panic("Can't allocate initial namespace");

    init_task.nsproxy->mnt_ns = ns;
    get_mnt_ns(ns);

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

实战

重新挂载rootfs

initramfs详解中说道,内核初始化时会将initramfs中的文件提取到根目录中,那个根目录就是rootfs。/init将真实根文件系统挂载到/root目录,并将根目录切换到/root完成初始化。

进入系统之后rootfs不再使用,那还可以将它mount到系统中查看里面的文件吗?不管那么多,先mount试一试:

root@debian:~# mount -t rootfs rootfs /rootfs
mount: unknown filesystem type ‘rootfs’

结果跟想像的差距有点大,检查代码发现,在rootfs_mount函数中对mount次数做了限制,只允许mount一次。删除该限制后再试一次:

root@debian:~# mount -t rootfs rootfs /rootfs
root@debian:~# cd /rootfs
root@debian:/rootfs# ls
root@debian:/rootfs#

这次成功mount上了,但里面空空如也,应该能看到initramfs中的文件才对啊。再次检查rootfs_mount函数,它使用的是mount_nodev来挂载,这样每次mount都会产生一个新的文件系统实例,我们需要的效果是重复mount都指向第一个实例。所以将mount_nodev改为mount_single,修改完成的代码如下:

static struct dentry *rootfs_mount(struct file_system_type *fs_type,
    int flags, const char *dev_name, void *data)
{
    static unsigned long once;
    void *fill = ramfs_fill_super;

    //if (test_and_set_bit(0, &once))
        //return ERR_PTR(-ENODEV);

    if (IS_ENABLED(CONFIG_TMPFS) && is_tmpfs)
        fill = shmem_fill_super;

    //return mount_nodev(fs_type, flags, data, fill);
    return mount_single(fs_type, flags, data, fill);
}

编译内核后再试一次:

root@debian:~# mount -t rootfs rootfs /rootfs
root@debian:~# cd /rootfs
root@debian:/rootfs# ls
root
root@debian:/rootfs#

在rootfs目录中只能看到一个root目录,其它文件去哪儿了?检查initramfs中的/init脚本发现,挂载真实文件系统到/root目录后,会调用switch_root命令切换根目录。该命令在切换之前会删除/root目录以外的所有文件,于是将switch_root命令改为chroot命令。再试一次看看:

root@debian:~# mount -t rootfs rootfs /rootfs
root@debian:~# cd /rootfs
root@debian:/rootfs# ls
bin conf dev etc init lib proc root run sbin scripts sys tmp var
root@debian:/rootfs#

这次可以看到initramfs中的所有文件和目录了。

使能rootfs/root

前面说到系统启动时会将真实根文件系统挂载到/root目录,进入系统后又成功将rootfs挂载到真实根文件系统的/rootfs目录,那这时候进入/rootfs/root目录会发生什么?是否能看到和根目录"/"一样的文件?试了一下,/rootfs/root目录里面是空的,为啥?

mount点的查找是通过__lookup_mnt函数完成的,该函数对当前目录的dentry地址和vfsmount地址进行哈希运算得到一个表头,然后在表中查找对应的mount点。由于每次mount都会分配一个新的vfsmount,虽然它们指向同一个文件系统实例,但自身的地址确不一样,所以进入/rootfs/root目录不能成功跳转。

于是对__lookup_mnt函数进行修改:

static inline struct hlist_head *m_hash(struct vfsmount *mnt, struct dentry *dentry)
{
    //unsigned long tmp = ((unsigned long)mnt / L1_CACHE_BYTES);
    unsigned long tmp = ((unsigned long)mnt->mnt_root / L1_CACHE_BYTES);
    tmp += ((unsigned long)dentry / L1_CACHE_BYTES);
    tmp = tmp + (tmp >> m_hash_shift);
    return &mount_hashtable[tmp & m_hash_mask];
}

struct mount *__lookup_mnt(struct vfsmount *mnt, struct dentry *dentry)
{
    struct hlist_head *head = m_hash(mnt, dentry);
    struct mount *p;

    hlist_for_each_entry_rcu(p, head, mnt_hash)
        //if (&p->mnt_parent->mnt == mnt && p->mnt_mountpoint == dentry)
        if (p->mnt_parent->mnt.mnt_sb == mnt->mnt_sb && p->mnt_mountpoint == dentry) {
            return p;
    return NULL;
}

将mnt改为mnt->mnt_sb,mnt指针虽然不一样,但它们指向的super block却是一样的。改完再试一次:

root@debian:~# cd /rootfs
root@debian:/rootfs# ls
全是/dev目录下的文件…

为啥进入/rootfs目录会跳转到/dev目录了?在devtmpfs分析中说到,devtmpfs初始化时会创建一个内核线程devtmpfsd,该线程会将devtmpfs文件系统mount到rootfs根目录。那devtmpfs的mount不会影响其它模块的初始化吗?答案是一个线程的mount操作只会影响同一个命名空间的其它线程,devtmpfsd函数在mount之前调用sys_unshare(CLONE_NEWNS)创建了自己的命名空间,所以不会影响其它模块。

修改__lookup_mnt函数之后,执行"cd /rootfs"时,实际上先跳转到rootfs的根目录,由于devtmpfs挂载到rootfs根目录,所以最终跳转到了devtmpfs的根目录下。为了达到想要的效果,需要将不在同一个命名空间的mount点过滤掉,于是在__lookup_mnt函数if条件中增加一项check_mnt(p)的检查。

最终效果如下:

root@debian:~# cd /rootfs/root
root@debian:/rootfs/root# ls
bin boot dev etc home initrd.img lib lost+found media mnt opt proc root rootfs run sbin srv sys tmp usr var vmlinuz
root@debian:/rootfs/root# cd /rootfs/root/rootfs/root
root@debian:/rootfs/root/rootfs/root# ls
bin boot dev etc home initrd.img lib lost+found media mnt opt proc root rootfs run sbin srv sys tmp usr var vmlinuz
root@debian:/rootfs/root/rootfs/root#

"/rootfs/root/…"可以无限循环下去了,有点意思。


公众号二维码

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值