Skip to the content.

File System

文件系统我觉得是三个部分中最难的,可能是因为《奔跑吧 linux 内核》降低了学习成本。但我也觉得这部分非常有用,有意思,在开发过程中,常常不能理解各种挂载,格式化操作,所以系统的学习这部分。

目录

虚拟文件系统

虚拟文件系统(Virtual Filesystem Switch)所隐含的思想是把表示很多不同种类文件系统的共同信息放入内核,其中有一个字段或函数来支持内核所支持的所有实际文件系统所提供的任何操作。对所调用的每个读、写或者其他函数,内核都能将其替换成本地文件系统的实际函数。

VFS 支持的文件系统可以分成三种主要类型:

通用文件模型

VFS 为了支持尽可能多的文件系统,引入了一个通用的文件模型,要实现每个具体的文件系统,必须将其物理组织结构转换为虚拟文件系统的通用文件模型。

应用程序对 read() 的调用引起内核调用相应的 sys_read() 系统调用,而文件在内核中是用 struct file 表示的,file 中包含成员变量 f_op,该成员变量包含指向实际文件系统的函数指针,包括读写文件的函数,sys_read() 找到该函数的指针,并调用它。

通用文件模型由下列对象类型组成:

下面我们看看进程怎样与文件进行交互。三个不同的进程已经打开同一个文件,其中两个进程使用同一个硬链接,这样每个进程都有自己的文件对象,但只需要两个目录项对象,每个硬链接对应一个目录项,这两个目录项指向同一个索引节点,该索引节点表示超级块以及随后的普通磁盘文件。

process-VFS.png

VFS 的数据结构

这节介绍 VFS 相关的数据结构及其关系。这些数据结构都是通过 slub 描述符分配内存空间。我们先看看整体的关系图。

VFS.png

super_block

struct super_block {
	struct list_head	s_list;		/* Keep this first */ // 所有文件系统的 sb 组成以
	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; // 超级块方法
	const struct dquot_operations	*dq_op; // 磁盘限额处理方法
	const struct quotactl_ops	*s_qcop; // 磁盘限额管理方法(?)
	const struct export_operations *s_export_op; // 网络文件系统使用的输出操作
	unsigned long		s_flags; // 安装标志
	unsigned long		s_iflags;	/* internal SB_I_* flags */
	unsigned long		s_magic; // 文件系统的魔数
	struct dentry		*s_root; // 文件系统根目录的目录项对象
	struct rw_semaphore	s_umount; // 卸载使用的信号量
	int			s_count; // 引用计数器
	atomic_t		s_active; // 此即引用计数器

    ...

	/*
	 * Keep s_fs_info, s_time_gran, s_fsnotify_mask, and
	 * s_fsnotify_marks together for cache efficiency. They are frequently
	 * accessed and rarely modified.
	 */
	void			*s_fs_info;	/* Filesystem private info */ // 指向特定文件系统的超级块信息的指针

	...

	char			s_id[32];	/* Informational name */
	uuid_t			s_uuid;		/* UUID */

	...

	const struct dentry_operations *s_d_op; /* default d_op for dentries */

	/*
	 * The list_lru structure is essentially just a pointer to a table
	 * of per-node lru lists, each of which has its own spinlock.
	 * There is no need to put them into separate cachelines.
	 */
	struct list_lru		s_dentry_lru;
	struct list_lru		s_inode_lru;
	struct rcu_head		rcu;
	struct work_struct	destroy_work;

	struct mutex		s_sync_lock;	/* sync serialisation lock */

	/*
	 * Indicates how deep in a filesystem stack this SB is
	 */
	int s_stack_depth;

	/* s_inode_list_lock protects s_inodes */
	spinlock_t		s_inode_list_lock ____cacheline_aligned_in_smp;
	struct list_head	s_inodes;	/* all inodes */ // 所有索引节点的链表

	spinlock_t		s_inode_wblist_lock;
	struct list_head	s_inodes_wb;	/* writeback inodes */
} __randomize_layout;

s_fs_info 成员变量指向特定文件系统的超级块信息,如果使用的实际文件系统是 Ext2,那么 s_fs_info 指向 ext2_sb_info,该结构包含磁盘分配位掩码和其他与 VFS 通用文件模型无关的数据。

通常为了效率,s_fs_info 指向的数据结构会被复制到内存中,任何基于磁盘的文件系统都需要读写自己的磁盘分配位图,VFS 允许这些文件系统直接对 s_fs_info 进行操作,而无需访问磁盘。这样就需要引用 s_dirt 位来表示该超级块是否是脏的,这个之后再分析。

inode

inode 是内核选择用于表示文件内容和相关元数据的方法。应该注意这里分析的 inode 是用于在内存中进行处理的,和物理文件系统的 inode 有些不一样,这里的 inode 包含了一些物理存储介质上所没有的信息,这些信息是由内核在从底层文件系统读入信息时动态建立的。有些文件系统是没有 inode 这个结构的。

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; // address_space 对象指针(?)

	...

	/* Stat data, not accessed from path walking */
	unsigned long		i_ino; // 唯一的标号,索引节点号
	/*
	 * Filesystems may only read i_nlink directly.  They shall use the
	 * following functions for modification:
	 *
	 *    (set|clear|inc|drop)_nlink
	 *    inode_(inc|dec)_link_count
	 */
	union {
		const unsigned int i_nlink; // 硬链接数目
		unsigned int __i_nlink;
	};
	dev_t			i_rdev; // 设备标识符,可以通过其找到 struct block_device
	loff_t			i_size; // 文件的字节数
	struct timespec64	i_atime; // 上次访问文件的时间
	struct timespec64	i_mtime; // 上次写文件的时间
	struct timespec64	i_ctime; // 上次修改索引节点的时间
	spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */
	unsigned short          i_bytes; // 文件中最后一个块的字节数
	u8			i_blkbits; // 块的位数
	u8			i_write_hint;
	blkcnt_t		i_blocks; // 文件的块数

	/* Misc */
	unsigned long		i_state; // 索引节点的状态标志
	struct rw_semaphore	i_rwsem;

	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 */
#ifdef CONFIG_CGROUP_WRITEBACK
	struct bdi_writeback	*i_wb;		/* the associated cgroup wb */

	/* foreign inode detection, see wbc_detach_inode() */
	int			i_wb_frn_winner;
	u16			i_wb_frn_avg_time;
	u16			i_wb_frn_history;
#endif
	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; // 引用索引节点的目录项链表的头
		struct rcu_head		i_rcu;
	};
	atomic64_t		i_version;
	atomic64_t		i_sequence; /* see futex */
	atomic_t		i_count;
	atomic_t		i_dio_count;
	atomic_t		i_writecount;
#if defined(CONFIG_IMA) || defined(CONFIG_FILE_LOCKING)
	atomic_t		i_readcount; /* struct files open RO */
#endif
	union {
		const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
		void (*free_inode)(struct inode *);
	};
	struct file_lock_context	*i_flctx;
	struct address_space	i_data; //
	struct list_head	i_devices; // 用于具体的字符或块设备索引节点链表的指针
	union {
		struct pipe_inode_info	*i_pipe; // 该文件是个管道(?)
		struct cdev		*i_cdev;
		char			*i_link;
		unsigned		i_dir_seq;
	};

	__u32			i_generation; // 索引节点版本号

	...

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

每个索引节点都会赋值磁盘中的索引节点包含的一些数据,比如分配给文件的磁盘块数等。

每个索引节点总是出现在下列某个双向循环链表中:

dentry

VFS 把每个目录看作由若干个子目录和文件组成的一个普通的文件,当从实际的磁盘文件系统中读取目录项到内存时,VFS 会将其转换成基于 dentry 结构的一个目录项对象。对于进程查找的路径名中的每个分量,内核都为其创建一个目录项对象,目录项对象将每个分量与其对应的索引节点相联系。可以这样理解,struct dentry 提供了文件名和 inode 之间的关联

struct dentry {
	/* RCU lookup touched fields */
	unsigned int d_flags;		/* protected by d_lock */ // 目录项高速缓存标志
	seqcount_spinlock_t d_seq;	/* per dentry seqlock */
	struct hlist_bl_node d_hash;	/* lookup hash list */
	struct dentry *d_parent;	/* parent directory */
	struct qstr d_name; // 文件名
	struct inode *d_inode; // 与文件关联的索引节点
	unsigned char d_iname[DNAME_INLINE_LEN];	/* small names */

	/* Ref lookup also touches following */
	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;		/* LRU list */
		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;
} __randomize_layout;

每个目录项可以处于以下四种状态:

但为何要这样设计,直接将文件名放在 struct file 中不就可以了?

fs_struct

该数据结构维护进程当前的工作目录和根目录struct task_struct 中的 fs_struct fs 就指向该结构。

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

该数据结构表示进程当前打开的文件。这个和 struct file 有什么区别?这个数据结构可以理解为打开文件表,而 struct file 则是表中的表项,表示具体的文件信息。

/*
 * Open file table structure
 */
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]; // 位图,保存了所有在 exec 系统调用时将要关闭的文件描述符信息
	unsigned long open_fds_init[1];
	unsigned long full_fds_bits_init[1];
	struct file __rcu * fd_array[NR_OPEN_DEFAULT]; // 文件指针的初始化数组
};

fdtableclose_on_exec, open_fds, full_fds_bits 其实都初始化为指向 files_struct 中对应的值,至于为何要这样,不理解。

struct fdtable {
	unsigned int max_fds; // 文件对象的当前最大数目
	struct file __rcu **fd;      /* current fd array */
	unsigned long *close_on_exec; // 执行 exec() 时需要关闭的文件描述符的指针
	unsigned long *open_fds; // 打开文件描述符的指针
	unsigned long *full_fds_bits;
	struct rcu_head rcu;
};

fdtable->fd 通常指向 files_struct->fd_array,该数组的索引就是文件描述符(哪个数据结构?),通常第一个元素(索引为 0)时进程的标准输入文件,第二个是标准输出文件(索引为 1),第三个是标准错误文件(索引为 2)。

我们打开了一个文件后,操作系统会跟踪进程打开的所有文件,即为每个进程维护一个打开文件表,文件表里的每一项代表「文件描述符」,所以说文件描述符是打开文件的标识。

file

该数据结构描述进程怎样与一个打开的文件进行交互(文件描述符?)。struct file 在磁盘上没有对应的映像,所以没有 dirty 位。

struct file {
	union {
		struct llist_node	fu_llist; // 文件链表指针
		struct rcu_head 	fu_rcuhead;
	} f_u;
	struct path		f_path; // 目录么。并不是,其文件名和 inode 之间的关系和文件所在文件系统的有关信息
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op; // 该文件的所有操作

	/*
	 * Protects f_ep, f_flags.
	 * Must not be taken from IRQ context.
	 */
	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; // 通过信号进行 I/O 时间通知的数据
	const struct cred	*f_cred;
	struct file_ra_state	f_ra; // 文件预读状态

	u64			f_version;
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data; // 指向特定文件系统或设备驱动程序所需的数据

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct hlist_head	*f_ep; // 文件的事件轮询等待者链表的头
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
	errseq_t		f_wb_err;
	errseq_t		f_sb_err; /* for syncfs */
} __randomize_layout

存放在 struct file 中的主要信息是文件指针,即文件当前的位置,下一个操作将在该位置发生。

前面说到 path 封装了文件名和 inode 之间的关联以及文件所在文件系统的信息,其实就是两个数据结构:vfsmountdentry

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

这些复杂的数据结构结合上面的图看会更清晰。然后关于各个 struct 的 operation,下面再分析。

文件系统类型

特殊文件系统

我还以为特殊文件是啥呢,原来这些文件系统在内核中非常重要。

网络和磁盘文件系统能够使用户处理存放在内核之外的信息,特殊文件系统能够为系统程序员和管理员提供一种容易的方式来操作内核的数据结构并实现操作系统的特殊特征。下面列出了 linux 内核中常用的特殊文件系统。

名字 安装点 描述
bdev 块设备
binfmt_misc 任意 其他可执行格式
devpts /dev/pts 伪终端支持
eventpollfs 由有效事件轮询机制使用
futexfs 由 futex (快速用户空间加锁)机制使用
pipefs 管道
proc /proc 对内核数据结构的常规访问点
rootfs 为启动阶段提供一个空的根目录
shm IPC 共享线性区
mqueue 任意 实现 POSIX 消息队列时使用
sockfs 套接字
sysfs /sys 对系统数据的常规访问点
tmpfs 任意 临时文件
usbfs /proc/bus/usb USB 设备

文件系统处理

Linux 使用系统的根文件系统(?),其由内核在引导阶段直接安装,并拥有系统初始化脚本以及最基本的系统程序。其他文件系统要么由初始化脚本安装,要么由用户直接安装在已安装文件系统的目录上。

命名空间

在传统的 Unix 系统中,只有一个已安装文件系统树:从系统的根文件系统开始,每个进程通过指定合适的路径名可以访问已安装文件系统中的任何文件。而从 Linux 2.6 开始,每个进程可以拥有自己的已安装文件系统树——进程的命名空间(namespace)。

通常大多数进程共享一个命名空间,即位于系统的根文件系统且被 init 进程使用的已安装文件系统树。不过如果 clone 系统调用以 CLONE_NEWS 标志创建一个新进程,那么新进程将获取这个新的命名空间。

文件系统安装

大多数类 Unix 系统中,每个文件系统只能安装一次,例如通过如下指令安装:

mount -t ext2 /dev/fd0 /flp // 将存放在 /dev/fd0 软盘上的 ext2 文件系统安装在 /flp 上

在没有 umount 之前,无法重复 mount。然后 Linux 不同,同一个文件系统被安装多次是可能的,也就是说该文件系统有多个安装点来访问,但只有一个 super_block

安装的文件系统形成一个层次:一个文件系统的安装点可能称为第二个文件系统的目录,而第二个文件系统的安装点又安装在第三个文件系统上。

filesystem_structre.png

vfsmount

其记录每个装载的文件系统的信息,在 linux 中,装载是可以嵌套的,如上面的图,所以需要一个数据结构表示这样的关系。

struct vfsmount
{
	struct list_head mnt_hash;
	struct vfsmount *mnt_parent;	/* fs we are mounted on */ // 指向父文件系统
	struct dentry *mnt_mountpoint;	/* dentry of mountpoint */ // 该文件系统安装点目录的 dentry
	struct dentry *mnt_root;	/* root of the mounted tree */ // 该文件系统本身的根 dentry
	struct super_block *mnt_sb;	/* pointer to superblock */ // 建立与超级块的联系
	struct list_head mnt_mounts;	/* list of children, anchored here */
	struct list_head mnt_child;	/* and going through their mnt_child */
	atomic_t mnt_count;
	int mnt_flags;
	int mnt_expiry_mark;		/* true if marked for expiry */ // 该文件系统已过期(什么叫过期?)
	char *mnt_devname;		/* Name of device e.g. /dev/dsk/hda1 */
	struct list_head mnt_list;
	struct list_head mnt_fslink;	/* link in fs-specific expiry list */
	struct namespace *mnt_namespace; /* containing namespace */
};

mnt_mountpointmnt_root 表示的其实是一个 dentry。

安装普通文件系统

这部分分析内核安装一个文件系统需要执行的操作,首先考虑一个文件系统安装在另一个文件系统上的情形。mount 系统调用被用来安装一个普通文件系统,它的执行函数是 sys_mount

// 文件系统所在的设备文件的路径名
// 文件系统被安装在某个目录的路径名(安装点)
// 文件系统的类型,有 MS_RDONLY, MS_NOSUID, MS_NODEV 等等
// 安装标志
// 指向一个与文件系统相关的数据结构
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);

	options = copy_mount_options(data);

	ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options);

	...

	return ret;
}
关键函数 do_mount
long do_mount(const char *dev_name, const char __user *dir_name,
		const char *type_page, unsigned long flags, void *data_page)
{
	struct path path; // 这个数据结构是用来干嘛的?上面补充介绍了
	int ret;

	ret = user_path_at(AT_FDCWD, dir_name, LOOKUP_FOLLOW, &path); // 获取路径名
	if (ret)
		return ret;
	ret = path_mount(dev_name, &path, type_page, flags, data_page); // 检查众多标志位
	path_put(&path);
	return ret;
}

下面我们看看 sysfs 特殊文件系统的挂载过程,

#0  do_mount (dev_name=dev_name@entry=0xffff888100c26070 "sysfs", // 该特殊文件系统为 sysfs
    dir_name=dir_name@entry=0x7fffd702ff8a "/sys", // 挂载点为 /sys
    type_page=type_page@entry=0xffff888100c26608 "sysfs", flags=flags@entry=32782,
    data_page=data_page@entry=0x0 <fixed_percpu_data>) at fs/namespace.c:3328
#1  0xffffffff8135576b in __do_sys_mount (data=<optimized out>, flags=32782,
    type=<optimized out>, dir_name=0x7fffd702ff8a "/sys", dev_name=<optimized out>)
    at fs/namespace.c:3539
#2  __se_sys_mount (data=<optimized out>, flags=32782, type=<optimized out>,
    dir_name=140736800685962, dev_name=<optimized out>) at fs/namespace.c:3516
#3  __x64_sys_mount (regs=<optimized out>) at fs/namespace.c:3516
#4  0xffffffff81c0711b in do_syscall_x64 (nr=<optimized out>, regs=0xffffc90000473f58)
    at arch/x86/entry/common.c:50
#5  do_syscall_64 (regs=0xffffc90000473f58, nr=<optimized out>) at arch/x86/entry/common.c:80
#6  0xffffffff81e0007c in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:113
#7  0x0000000000000000 in ?? ()
int path_mount(const char *dev_name, struct path *path,
		const char *type_page, unsigned long flags, void *data_page)
{
	unsigned int mnt_flags = 0, sb_flags;
	int ret;

	...

	/* Separate the per-mountpoint flags */
	if (flags & MS_NOSUID)
		mnt_flags |= MNT_NOSUID;
	if (flags & MS_NODEV)
		mnt_flags |= MNT_NODEV;
	if (flags & MS_NOEXEC)
		mnt_flags |= MNT_NOEXEC;
	if (flags & MS_NOATIME)
		mnt_flags |= MNT_NOATIME;
	if (flags & MS_NODIRATIME)
		mnt_flags |= MNT_NODIRATIME;
	if (flags & MS_STRICTATIME)
		mnt_flags &= ~(MNT_RELATIME | MNT_NOATIME);
	if (flags & MS_RDONLY)
		mnt_flags |= MNT_READONLY;
	if (flags & MS_NOSYMFOLLOW)
		mnt_flags |= MNT_NOSYMFOLLOW;

	/* The default atime for remount is preservation */
	if ((flags & MS_REMOUNT) &&
	    ((flags & (MS_NOATIME | MS_NODIRATIME | MS_RELATIME |
		       MS_STRICTATIME)) == 0)) {
		mnt_flags &= ~MNT_ATIME_MASK;
		mnt_flags |= path->mnt->mnt_flags & MNT_ATIME_MASK;
	}

	sb_flags = flags & (SB_RDONLY | // 设置 superblock 的标志位
			    SB_SYNCHRONOUS |
			    SB_MANDLOCK |
			    SB_DIRSYNC |
			    SB_SILENT |
			    SB_POSIXACL |
			    SB_LAZYTIME |
			    SB_I_VERSION);

	if ((flags & (MS_REMOUNT | MS_BIND)) == (MS_REMOUNT | MS_BIND))
		return do_reconfigure_mnt(path, mnt_flags);
	if (flags & MS_REMOUNT) // 该标志位表示改变 sb->s_flags 的安装标志一级已安装文件系统 mnt_flags 字段
		return do_remount(path, flags, sb_flags, mnt_flags, data_page);
	if (flags & MS_BIND) // 在系统目录树的另一个安装点上的文件或目录是可见的
		return do_loopback(path, dev_name, flags & MS_REC);
	if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))
		return do_change_type(path, flags); // 改变文件系统的类型
	if (flags & MS_MOVE) // 改变已安装文件系统的安装点
		return do_move_mount_old(path, dev_name);

    // 最普通的情况,安装一个特殊文件系统或者存放在磁盘分区中的普通文件系统
	return do_new_mount(path, type_page, sb_flags, mnt_flags, dev_name,
			    data_page);
}
关键函数 do_new_mount_fc

do_new_mount -> do_new_mount_fc

/*
 * Create a new mount using a superblock configuration and request it
 * be added to the namespace tree.
 */
static int do_new_mount_fc(struct fs_context *fc, struct path *mountpoint,
			   unsigned int mnt_flags)
{
	struct vfsmount *mnt;
	struct mountpoint *mp;
	struct super_block *sb = fc->root->d_sb;
	int error;

	...

	up_write(&sb->s_umount); // 读写信号量

	mnt = vfs_create_mount(fc);
	if (IS_ERR(mnt))
		return PTR_ERR(mnt);

	mnt_warn_timestamp_expiry(mountpoint, mnt);

	mp = lock_mount(mountpoint);
	if (IS_ERR(mp)) {
		mntput(mnt);
		return PTR_ERR(mp);
	}
	error = do_add_mount(real_mount(mnt), mp, mountpoint, mnt_flags);
	unlock_mount(mp);
	if (error < 0)
		mntput(mnt);
	return error;
}
关键函数 vfs_create_mount

这个函数主要是配置 vfsmount,并将其插入到链表中。

/**
 * vfs_create_mount - Create a mount for a configured superblock
 * @fc: The configuration context with the superblock attached
 *
 * Create a mount to an already configured superblock.  If necessary, the
 * caller should invoke vfs_get_tree() before calling this.
 *
 * Note that this does not attach the mount to anything.
 */
struct vfsmount *vfs_create_mount(struct fs_context *fc)
{
	struct mount *mnt;

	...

	atomic_inc(&fc->root->d_sb->s_active);
	mnt->mnt.mnt_sb		= fc->root->d_sb;
	mnt->mnt.mnt_root	= dget(fc->root);
	mnt->mnt_mountpoint	= mnt->mnt.mnt_root;
	mnt->mnt_parent		= mnt;

	lock_mount_hash();
	list_add_tail(&mnt->mnt_instance, &mnt->mnt.mnt_sb->s_mounts);
	unlock_mount_hash();
	return &mnt->mnt;
}
EXPORT_SYMBOL(vfs_create_mount);
关键函数 do_add_mount

这个函数主要是防止同一个文件系统装载到同一个位置。

/*
 * add a mount into a namespace's mount tree
 */
static int do_add_mount(struct mount *newmnt, struct mountpoint *mp,
			struct path *path, int mnt_flags)
{
	struct mount *parent = real_mount(path->mnt);

	mnt_flags &= ~MNT_INTERNAL_FLAGS;

	...

	/* Refuse the same filesystem on the same mount point */
	if (path->mnt->mnt_sb == newmnt->mnt.mnt_sb &&
	    path->mnt->mnt_root == path->dentry)
		return -EBUSY;

	if (d_is_symlink(newmnt->mnt.mnt_root))
		return -EINVAL;

	newmnt->mnt.mnt_flags = mnt_flags;
	return graft_tree(newmnt, parent, mp);
}
关键函数 attach_recursive_mnt

文件系统通过 attach_recursive_mnt 添加到父文件系统的命名空间(对命名空间不了解)。这部分之后再分析。

graft_tree -> attach_recursive_mnt

static int attach_recursive_mnt(struct mount *source_mnt,
			struct mount *dest_mnt,
			struct mountpoint *dest_mp,
			bool moving)
{
	struct user_namespace *user_ns = current->nsproxy->mnt_ns->user_ns;
	HLIST_HEAD(tree_list);
	struct mnt_namespace *ns = dest_mnt->mnt_ns;
	struct mountpoint *smp;
	struct mount *child, *p;
	struct hlist_node *n;
	int err;

	/* Preallocate a mountpoint in case the new mounts need
	 * to be tucked under other mounts.
	 */
	smp = get_mountpoint(source_mnt->mnt.mnt_root);
	if (IS_ERR(smp))
		return PTR_ERR(smp);

	/* Is there space to add these mounts to the mount namespace? */
	if (!moving) {
		err = count_mounts(ns, source_mnt);
		if (err)
			goto out;
	}

	if (IS_MNT_SHARED(dest_mnt)) {
		err = invent_group_ids(source_mnt, true);
		if (err)
			goto out;
		err = propagate_mnt(dest_mnt, dest_mp, source_mnt, &tree_list);
		lock_mount_hash();
		if (err)
			goto out_cleanup_ids;
		for (p = source_mnt; p; p = next_mnt(p, source_mnt))
			set_mnt_shared(p);
	} else {
		lock_mount_hash();
	}
	if (moving) {
		unhash_mnt(source_mnt);
		attach_mnt(source_mnt, dest_mnt, dest_mp);
		touch_mnt_namespace(source_mnt->mnt_ns);
	} else {
		if (source_mnt->mnt_ns) {
			/* move from anon - the caller will destroy */
			list_del_init(&source_mnt->mnt_ns->list);
		}
		mnt_set_mountpoint(dest_mnt, dest_mp, source_mnt);
		commit_tree(source_mnt);
	}

	hlist_for_each_entry_safe(child, n, &tree_list, mnt_hash) {
		struct mount *q;
		hlist_del_init(&child->mnt_hash);
		q = __lookup_mnt(&child->mnt_parent->mnt,
				 child->mnt_mountpoint);
		if (q)
			mnt_change_mountpoint(child, smp, q);
		/* Notice when we are propagating across user namespaces */
		if (child->mnt_parent->mnt_ns->user_ns != user_ns)
			lock_mnt_tree(child);
		child->mnt.mnt_flags &= ~MNT_LOCKED;
		commit_tree(child);
	}
	put_mountpoint(smp);
	unlock_mount_hash();

	return 0;

	...
}

路径名查找

路径名查找也就是根据给定的文件路径名导出相应的索引节点。执行这一任务的标准过程就是分析路径名并把它们拆分成一个文件名序列。根据第一个字符是不是 “/” 决定从 current->fs->root 还是 current->fs->pwd 开始搜索。

整个查找过程是一个循环,内核首先检查与第一个名字匹配的目录项以获取相应的索引节点,然后从磁盘中读取包含哪个索引节点的目录,并检查与第二个名字匹配的目录项,以获取第二个索引节点,如此反复。而反复读取磁盘效率低下,所以有目录项高速缓存,将最近常使用的目录项保存在内存中。

虽然 Linux 使用万物皆文件的思想,但路径名的查找并不像上面描述的那样简单,有很多情况需要考虑:

内核的路径名查找是由 path_lookupat 完成的,

/* Returns 0 and nd will be valid on success; Retuns error, otherwise. */
static int path_lookupat(struct nameidata *nd, unsigned flags, struct path *path)
{ // nameidata 存放了查找操作的结果
	const char *s = path_init(nd, flags); // 初始化 namaidata 的标志位等等
	int err;

	if (unlikely(flags & LOOKUP_DOWN) && !IS_ERR(s)) { // 暂时不清楚 LOOKUP_DOWN 是干啥的
		err = handle_lookup_down(nd);
		if (unlikely(err < 0))
			s = ERR_PTR(err);
	}

	while (!(err = link_path_walk(s, nd)) &&
	       (s = lookup_last(nd)) != NULL)
		;

    ...
}

nameidata

变化很大,和 understanding 上。

struct nameidata {
	struct path	path;
	struct qstr	last; // 路径名的最后一个分量
	struct path	root;
	struct inode	*inode; /* path.dentry.d_inode */
	unsigned int	flags, state;
	unsigned	seq, m_seq, r_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;
	unsigned	root_seq;
	int		dfd;
	kuid_t		dir_uid;
	umode_t		dir_mode;
} __randomize_layout;
/*
 * Name resolution.
 * This is the basic name resolution function, turning a pathname into
 * the final dentry. We expect 'base' to be positive and a directory.
 *
 * Returns 0 and nd will have valid dentry and mnt on success.
 * Returns error and drops reference to input namei data on failure.
 */
static int link_path_walk(const char *name, struct nameidata *nd)
{
	int depth = 0; // depth <= nd->depth
	int err;

	nd->last_type = LAST_ROOT;
	nd->flags |= LOOKUP_PARENT;
	if (IS_ERR(name))
		return PTR_ERR(name);
	while (*name=='/') // 跳过路径名第一个分量前的任何斜杠 '/'
		name++;
	if (!*name) { // 路径为空,直接返回
		nd->dir_mode = 0; // short-circuit the 'hardening' idiocy
		return 0;
	}

	/* At this point we know we have a real path component. */
	for(;;) {
		struct user_namespace *mnt_userns;
		const char *link;
		u64 hash_len;
		int type;

		mnt_userns = mnt_user_ns(nd->path.mnt);
		err = may_lookup(mnt_userns, nd); // 不懂
		if (err)
			return err;

		hash_len = hash_name(nd->path.dentry, name);

		type = LAST_NORM;
		if (name[0] == '.') switch (hashlen_len(hash_len)) {
			case 2:
				if (name[1] == '.') {
					type = LAST_DOTDOT;
					nd->state |= ND_JUMPED;
				}
				break;
			case 1:
				type = LAST_DOT;
		}
		if (likely(type == LAST_NORM)) {
			struct dentry *parent = nd->path.dentry;
			nd->state &= ~ND_JUMPED;
			if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
				struct qstr this = { { .hash_len = hash_len }, .name = name };
				err = parent->d_op->d_hash(parent, &this);
				if (err < 0)
					return err;
				hash_len = this.hash_len;
				name = this.name;
			}
		}

		nd->last.hash_len = hash_len;
		nd->last.name = name;
		nd->last_type = type;

		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 == '/'));
		if (unlikely(!*name)) {
OK:
			/* pathname or trailing symlink, done */
			if (!depth) {
				nd->dir_uid = i_uid_into_mnt(mnt_userns, nd->inode);
				nd->dir_mode = nd->inode->i_mode;
				nd->flags &= ~LOOKUP_PARENT;
				return 0;
			}
			/* last component of nested symlink */
			name = nd->stack[--depth].name;
			link = walk_component(nd, 0);
		} else {
			/* not the last component */
			link = walk_component(nd, WALK_MORE);
		}
		if (unlikely(link)) {
			if (IS_ERR(link))
				return PTR_ERR(link);
			/* a symlink to follow */
			nd->stack[depth++].name = name;
			name = link;
			continue;
		}
		if (unlikely(!d_can_lookup(nd->path.dentry))) {
			if (nd->flags & LOOKUP_RCU) {
				if (!try_to_unlazy(nd))
					return -ECHILD;
			}
			return -ENOTDIR;
		}
	}
}

问题