本文转载自微信公众号《Linux内核航海者》,作者Linux内核航海者。转载本文请联系Linux内核航海者公众号。1.开启环境:处理器架构:arm64内核源码:linux-5.11ubuntu版本:20.04.1代码阅读工具:vim+ctags+cscope我们知道,在Linux系统中,我们经常会把文件系统挂载到块设备上,以这个文件系统下的文件只能在某个目录下访问,但是你有没有想过:为什么挂载块设备后就可以访问文件了?文件系统挂载后Linux内核为我们做了什么?是不是可以不使用文件系统挂载到特定目录也可以访问?下面,本文将详细解说Linxu系统挂载文件系统的奥秘。注:本文主要讲解文件系统挂载的核心逻辑,不涉及挂载命名空间和绑定挂载(后面可能会涉及到),并以ext2磁盘文件系统为例进行说明挂载。这篇专题文章分为两部分。第一部分主要介绍挂载的全貌和挂载具体文件系统的方法。第二部分介绍如何通过挂载实例关联挂载点和超级块。2、vfs的几个重要对象这里不介绍整个IO栈,只说明vfs和文件系统相关的具体文件系统层。我们知道,在Linux中,通过虚拟文件系统层VFS统一了所有特定的文件系统,提取了所有特定文件系统的共性,屏蔽了特定文件系统的差异性。VFS既是一个向下的接口(所有的文件系统都必须实现这个接口),也是一个向上的接口(用户进程最终可以通过系统调用来访问文件系统的功能)。我们来看看vfs中几个重要的结构对象:2.1file_system_type结构描述了一个文件系统类型。一般特定的文件系统都会定义这个结构体,然后注册到系统中;它定义了一个特定的文件系统。挂载和卸载方法,当文件系统被挂载时,调用其挂载方法构造superblock、dentry等实例。文件系统分为以下几种:1)磁盘文件系统文件存储在非易失性存储介质(如硬盘、闪存)上,断电后文件不会丢失。如ext2、ext4、xfs2)内存文件系统文件存放在内存中,断电后会丢失。如tmpfs3)伪文件系统是一种假文件系统,它使用虚拟文件系统的接口(可以对用户可见如proc和sysfs,也可以对用户不可见的内核不可见如套接字系统、bdev)。如proc、sysfs、sockfs、bdev4)网络文件系统这种文件系统允许访问另一台计算机上的数据,该计算机通过网络连接到本地计算机。例如nfs文件系统结构定义源码路径:include/linux/fs.h+22262.2super_block超级块,用于描述一个文件系统在块设备上的整体信息(如文件块大小,最大文件大小、文件系统幻数等),块设备上的文件系统可以多次挂载,但内存中只能有一个super_block来描述它(至少对于磁盘文件系统而言)。该结构体定义了源码路径:include/linux/fs.h+14142.3mount挂载描述符,用于建立超级块与挂载点等的连接,描述文件系统的挂载,a块设备上的文件系统可以多次挂载,每次挂载在内存中都有一个挂载对象描述。该结构体定义了源代码路径:fs/mount.h+392.4inodeindex节点对象,它描述了磁盘上的一个文件元数据(文件属性、位置等)。有些文件系统需要从块设备中读取磁盘上的索引节点。然后在内存中创建vfs索引节点对象,一般是第一次打开文件的时候。该结构定义了源代码路径:include/linux/fs.h+6102.5dentry目录项对象,用于描述文件的层次结构,从而构建文件系统的目录树。文件系统把目录看成一个文件,目录的数据由目录项组成,每个目录项存储目录或文件的名称、inode号等内容。每当进程访问一个目录项时,就会在内存中创建一个目录项对象(如ext2路径名查找,通过查找父目录数据块的目录项,找到对应的文件/目录的名称,并获得inode号来查找对应的inode)。该结构体定义了源码路径:include/linux/dcache.h+902.6file文件对象,描述了进程打开的文件。当进程打开文件时,会创建一个文件对象并添加到进程的文件打开表中,并通过文件描述符对文件对象进行索引,后续的读写等操作都是通过文件描述符(一个文件可以被多个进程打开,每个进程的文件打开表中都会加入多个文件对象,但只有一个inode)。该结构定义了源代码路径:include/linux/fs.h+9153。挂载总体流程3.1系统调用处理用户通过系统调用路径执行挂载进入内核进行处理,拷贝用户空间并传递参数给内核,挂载委托do_mount。//fs/namespace.cSYSCALL_DEFINE5(mountparameter:dev_name待挂载的块设备dir_name挂载点目录类型文件系统类型名flags挂载标志数据挂载选项->kernel_type=copy_mount_string(type);//复制文件系统类型名到内核space->kernel_dev=copy_mount_string(dev_name)//将块设备路径名复制到内核空间->options=copy_mount_options(data)//将挂载选项复制到内核空间->do_mount(kernel_dev,dir_name,kernel_type,flags,options)//mountentrustmentdo_mount3.2mountpointpathlookupmountpointpathlookup,mountentrustpath_mountdo_mount->user_path_at(AT_FDCWD,dir_name,LOOKUP_FOLLOW,&path)//挂载点路径lookuplookupmountpoint目录的vfsmount和dentry存放在path中->path_mount(dev_name,&path,type_page,flags,data_page)//挂载委托path_mount3.3参数合法性检查参数合法性检查,新挂载委托do_new_mountpath_mount->参数合法性检查->调用不同根据挂载标志进行函数处理。这里的解释是do_new_mount3.4默认调用具体的文件系统挂载方法do_new_mount->type=get_fs_type(fstype)//根据传入的文件系统名找到注册的文件系统类型->fc=fs_context_for_mount(type,sb_flags)//分配挂载文件系统上下文structfs_context->alloc_fs_context->allocatefs_contextfc=kzalloc(sizeof(structfs_context),GFP_KERNEL)->set...->fc->fs_type=get_filesystem(fs_type);//赋值对应的文件系统type->init_fs_context=**fc->fs_type->init_fs_context**;//新内核使用fs_type->init_fs_context接口初始化文件系统上下文if(!init_fs_context)//init_fs_context回调主要用于初始化init_fs_context=**legacy_init_fs_context**;//Nofs_type->init_fs_contextinterface->init_fs_context(fc)//初始化文件系统上下文(初始化一些回调函数供后续使用)查看文件系统类型没有实现init_fs_context接口的情况://fs/fs_context.cinit_fs_context=legacy_init_fs_context->fc->ops=&legacy_fs_context_ops//设置上下层操作的文件系统->.get_tree=legacy_get_tree//操作方法的get_tree用于读取磁盘超级块并在内存中创建超级块,创建根inode,按照dentry->root=fc->fs_type->mount(fc->fs_type,fc->sb_flags,|fc->source,ctx->legacy_data)//调用themountmethodofthefilesystemtypetoreadandcreateasuperblock->fc->root=root//给创建的文件系统分配dentry使用原来的接口(fs_type.mount=xxx_mount):一些文件系统如ext2和ext4使用了一个新的接口(fs_type.init_fs_context=xxx_init_fs_context):无论xfs、proc、sys使用哪一个,fc->ops=&xxx_context_ops接口都会在xxx_init_fs_contex中实现,后面会看到调用fc->ops.get_tree读取并创建超级块实例,继续往下走:do_new_mount->...->fc=fs_context_for_mount(type,sb_flags)//分配文件系统上下文->parse_monolithic_mount_data(fc,data)//调用fc->ops->parse_monolithic解析挂载选项->mount_capable(fc)//检查是否有mount权限->vfs_get_tree(fc)//fs/super.cMountkey调用fc->ops->get_tree(fc)读取并创建superblock实例...3.5Mount实例添加进入全局文件系统树do_new_mount...->do_new_mount_fc(fc,path,mnt_flags)//创建一个挂载实例关联一个挂载点和一个超级块,并将其添加到命名空间的挂载树中。下面主要看vfs_get_tree和do_new_mount_fc:4.具体文件系统挂载方法1)vfs_get_tree//以ext2文件系统为例vfs_get_tree//fs/namespace.c->fc->ops->get_tree(fc)->legacy_get_tree//上面分析的fs_type->init_fs_context==NULL使用老接口(ext2为NULL)->fc->fs_type->mount->ext2_mount//fs/ext2/super.c调用的mount方法具体文件系统看ext2是如何处理挂载的:启动时初始化->//fs/ext2/super.cmodule_init(init_ext2_fs)init_ext2_fs->init_inodecache//创建ext2_inode_cache对象缓存->register_filesystem(&ext2_fs_type)//注册ext2文件系统typestaticstructfile_system_typeext2_fs_type={.owner=THIS_MODULE,.name="ext2",.mount=ext2_mount,//挂载时调用读取创建超级块instance.kill_sb=kill_block_super,//卸载时调用释放超级块。fs_flags=FS_REQUIRES_DEV,//文件系统标志为请求块设备,文件系统在块设备上};挂载时调用->//fs/ext2/super.cstaticstructdentry*ext2_mount(structfile_system_type*fs_type,intflags,constchar*dev_name,void*data){returnmount_bdev(fs_type,flags,dev_name,data,ext2_fill_super);}ext2_mount执行实际文件系统挂载工作通过调用mount_bdev,ext2_fill_super函数指针作为参数传递给get_sb_bdev。此函数用于填充超级块对象。如果内存中没有合适的超级块对象,就必须从硬盘中读取数据。mount_bdev是一个常用函数,一般的磁盘文件系统都会用它来读取磁盘上的超级块,根据具体文件系统的fill_super方法创建内存超级块。下面看一下mount_bdev的实现(**执行后会创建vfs、super_block、rootinode和rootdentry这三大数据结构**):2)mount_bdev源码分析//fs/super.cmount_bdev->bdev=blkdev_get_by_path(dev_name,mode,fs_type)//通过要挂载的块设备的路径名获取其块设备描述符block_device(会涉及到路径名查找,通过bdev文件系统找到block_devicedevicenumber,block_device是将块设备添加到系统中)->s=sget(fs_type,test_bdev_super,set_bdev_super,flags|sb_NOSEC,|bdev);//查找或创建vfs的superblock(会先检查是否有在文件系统类型的fs_supers链表中读取到指定的超级块会比较每个超级块的s_bdev块设备描述符,没有一个被创建)->if(s->s_root){//是rootdentry分配的超级块?...}else{//没有赋值描述时新创建的sb...->sb_set_blocksize(s,block_size(bdev))//根据块设备描述符设置文件系统块大小->fill_super(s,data,flags&sb_SILENT?1:0)//调用具体文件系统填充超级块方法读取填充超级块等,如ext2_fill_super->bdev->bd_super=s//块设备bd_super指向sb}->returndget(s->s_root)//返回文件系统的rootdentry,可以看到mount_bdev主要是:1.根据要挂载的块设备的文件名找到对应的块设备描述符(内核后面的块设备使用块设备描述符);2.首先检查该文件系统类型的fs_supers链表中是否读取了指定的vfs超级块,比较每个超级块的s_bdev块设备描述符,没有创建vfs超级块;3、新创建的vfssuperblock块,需要调用具体文件系统的fill_super方法读取并填充superblock。那么下面主要关注具体文件系统的fill_super方法,这里是ext2_fill_super:分析关键代码如下:3)ext2_fill_super源码分析//fs/ext2/super.cstaticintext2_fill_super(structsuper_block*sb,void*data,intsilent){structbuffer_head*bh;//bufferheader记录了读取的磁盘超级块structtext2_sb_info*sbi;//内存的ext2超级块信息structex2_super_block*es;//磁盘上的超级块信息...sbi=kzalloc(sizeof(*sbi),GFP_KERNEL);//分配内存的ext2超级块信息结构if(!sbi)gotofailed;...sb->s_fs_info=sbi;//vfs超??级块的s_fs_info指向theext2superblockinformationstructureofmemorysbi->s_sb_block=sb_block;if(!(bh=sb_bread(sb,logic_sb_block))){//读取磁盘上的超级块到内存中使用buffer_head关联内存缓冲区磁盘扇区ext2_msg(sb,KERN_ERR,"error:unabletoreadsuperblock");gotofailed_sbi;}es=(structext2_super_block*)(((char*)bh->b_data)+offset);//转换成structext2_super_block结构体sbi->s_es=es;//内存的ext2超级块信息结构体s_es指向真正的ext2磁盘超级块信息structuresb->s_magic=le16_to_cpu(es->s_magic);//获取文件系统幻数ext2为0xEF53if(sb->s_magic!=EXT2_SUPER_MAGIC)//验证幻数是否正确gotocantfind_ext2;blocksize=BLOCK_SIZE<
