Ext2文件系统—安装和卸载

本文详细解析了Ext2文件系统的安装和卸载过程,包括安装的定义、vfsmount概念,以及安装步骤中的关键函数分析。文章还介绍了卸载过程,包括umount系统调用的工作原理,如何处理设备的卸载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


1、文件系统安装含义

1.1 定义

       对于文件系统安装,在Ext2文件系统—路径名查找—1 中已有准确的解释,这里做进一步补充说明。所谓安装,就是从一个存储设备上读入超级块,在内存中建立起一个super_block结构,再进而将此设备上的根目录与文件系统中已经存在的一个空白目录挂上钩安装一个文件系统实际上是安装一个物理设备(文件系统在此物理设备上)。

系统调用mount()将一个可访问的块设备安装到一个可访问的节点上。所谓“可访问”是指该节点或文件已经存在于已安装的文件系统中,可以通过路径名寻访。也就是说,设备安装前就要是“可访问”的,那既然可访问为什么还要安装呢?原因是在安装之前可访问的只是这个设备,如/dev/sda1。这通常是作为一个线性的无结构的字节流来访问的,成为原始设备(rawdevice)。而设备上的文件系统是不可访问的。经过安装以后,设备上的文件系统就成为可访问的了。

1.2 vfsmount

对于vfsmount,在Ext2文件系统—路径名查找—1也有详细解释,这里进一步补充。vfsmount之间的关系如下图

转载自:http://blog.youkuaiyun.com/mishifangxiangdefeng/article/details/7566575

 

(1)vfsmount结构中有mnt_mounts和mnt_child两个队列头,只要上一层vfsmount结构存在,就把当前vfsmount结构中mnt_child链入上一层vfsmount结构的mnt_mounts队列中。这样就形成一颗设备安装的树结构,从一个vfsmount结构的mnt_mounts队列开始,可以找到所有直接或间接安装在这个安装点上的其他设备。

(2)一个安装点可以安装多个设备,一个设备可以安装到多个安装点上。

(3)vfsmount->root指向所安装设备的根目录的dentry,vfsmount->mnt_sb指向所安装设备的超级块的super_block,super_block->s_mounts指向安 装同一设备的vfsmount的队列头。

 

2、安装

2.1代码分析

-→sys_mount()

参数dev_name为待安装文件系统所在设备的路径名,如果不需要的话就为空(例如,当待安装的是基于网络的文件系统时);dir_name则是安装点(空闲目录)的路径名;type是文件系统的类型,必须是已注册文件系统的字符串名(如“Ext2”,“MSDOS”等);flags是安装模式,如前面所述。Data指向一个与文件系统相关的数据结构(可以为NULL)。

-→-→copy_mount_options()

将字符串形式的或结构形式的参数值从用户空间复制到系统空间。它拷贝整个页面(确切的说是PAGE_SIZE-1个字节),并返回页面的起始地址。

if (!(page = __get_free_page(GFP_KERNEL)))

                   return-ENOMEM;

         /* copy_from_usercannot cross TASK_SIZE ! */

size = TASK_SIZE - (unsigned long)data;

if (size > PAGE_SIZE)

size = PAGE_SIZE;

这里要做的事情是,准备从data开始,拷贝一个page的数据。如果data+PAGE_SIZE超出TASK_SIZE(3GB)的范围(也就是到了内核的空间),那么超出的部分不能拷贝。一般情况下,size > PAGE_SIZE,最终size = PAGE_SIZE,也就是拷贝一个page。用TASK_SIZE来减,是为了避免超出TASK_SIZE。

-→-→-→exact_copy_from_user ()

    while (n) {

        if (__get_user(c, f)) {

            memset(t, 0, n);

            break;

        }

        *t++ = c;

        f++;

        n--;

    }

    return n;

一般copy_from_user()函数不会返回出错误时剩下待copy的字节数,但是copy_mount_options()需要这个功能,于是便由exact_copy_from_user实现。exact_copy_from_user是遇到不能访问的内存地址时才返回,返回值是尚未copy的字节数。

返回到copy_mount_options

if (!i) {

                   free_page(page);

                   return-EFAULT;

         }

         if (i != PAGE_SIZE)

                   memset((char*)page + i, 0, PAGE_SIZE - i);

i表示已copy的字节数。如果一个字节都没被拷贝,i==0,则返回错误。如果拷贝的字节数不足一个page,则把余下的部分填0。

-→-→getname ()

也是将字符串形式的或结构形式的参数值从用户空间复制到系统空间,正常结束时返回内核分配的空间首地址,出错时返回错误代码。但是getname()在复制时遇到字符串结尾符“\0”就停止

-→-→-→do_getname()

{

         int retval;

         unsigned long len =PATH_MAX;  //内核允许的最大路径长度

//如果进程的地址限制和KERNEL_DS相等,则检查文件名是否小于用户进程空间。

         if(!segment_eq(get_fs(), KERNEL_DS)) {

//文件名大于用户进程空间则返回错误

                   if((unsigned long) filename >= TASK_SIZE)

                            return-EFAULT;

                   if(TASK_SIZE - (unsigned long) filename < PATH_MAX)

                            len= TASK_SIZE - (unsigned long) filename;

         }

//将filename拷贝len长度到page中,返回实际拷贝长度。注意这里是用的字符串拷贝,//因此到字符串结尾符“\0”就会停止。

         retval =strncpy_from_user(page, filename, len);

-→-→do_mount()

sys_mount的操作主体就是do_mount。

MS_MGC_VAL 和 MS_MGC_MSK是在以前的版本中定义的安装标志和掩码,现在的安装标志中已经不使用这些魔数了,因此,当还有这个魔数时,则丢弃它。

对参数dir_name和dev_name进行基本检查,注意“!dir_name ” 和“!*dir_name”之不同,前者指指向字符串的指针为不为空,而后者指字符串不为空。

-→-→-→memchr()

当第一次遇到字符ch时停止查找。如果成功,返回指向字符ch的指针;否则返回NULL。Memchr()函数在指定长度的字符串中寻找指定的字符,如果字符串中没有结尾符“\0”,也是一种错误。

返回到do_mount()。

将安装标志MS_NOSUID、MS_NOEXEC、MS_NODEV、MS_NOATIME、MS_NODIRATIME、MS_STRICTATIME、MS_RDONLY从flags标志中分离出来,并相应的设置挂载标志变量mnt_flags。

-→-→-→path_lookup ()

这是路径查找函数,已经分析过,不再赘述。这里就是找到挂载点(dir_name)的dentry 结构,并把找到的dentry结构存放在局部变量nd的dentry域中,为后面的挂载所用。

-→-→-→security_sb_mount ()

调用security_sb_mount()函数,来进行安全性检查。security_sb_mount()函数调用security_ops结构的sb_mount成员函数,security_ops是一个静态变量(security/security.c, static struct security_operations*security_ops;),它是一个security_operations 类型的指针。而security_operations类型是一个hook函数表,这些hook函数应用管理与内核对象相关的安全信息以及执行内核每个操作的访问控制。

返回到do_mount()。

它据调用参数 flags 来决定调用以下四个函数之一:do_remount()、 do_loopback()、do_move_mount()、do_new_mount()。  

如果flags中的MS_REMOUNT标志位为1,就表示所要求的只是改变一个原已安装设备的安装方式,例如从“只读“安装方式改为“可写”安装方式,这是通过调用do_remount()函数完成的。

如果flags中的MS_BIND标志位为1,就表示把一个“回接”设备捆绑到另一个对象上。回接设备是一种特殊的设备(虚拟设备),而实际上并不是一种真正设备,而是一种机制,这种机制提供了把回接设备回接到某个可访问的常规文件或块设备的手段。通常在/dev目录中有/dev/loop0和/dev/loop1两个回接设备文件。调用do_loopback()来实现回接设备的安装。

如果flags中的MS_MOVE标志位为1,就表示把一个已安装的设备可以移到另一个安装点,这是通过调用do_move_mount()函数来实现的。

而一般的主流情况下,是调用do_new_mount来实现的。

-→-→-→do_new_mount ()

do_new_mount完成新设备的挂载操作。思路很清晰,分两步做,先调用do_kern_mount(传入的参数是dev_name)得到待挂载设备的vfsmount结构,包含了设备的根目录dentry以及设备的super block。然后调用do_add_mount 待挂载设备的vfsmount 与之前已找好的挂载点dentry (存放在nd中)链接在一起。

-→-→-→-→capable ()//安全性权限检查,需要系统管理员权限。

-→-→-→-→do_kern_mount ()

-→-→-→-→-→get_fs_type ()

根据具体文件系统的类型名在内核中找到相应的file_system_type结构。这里要详细介绍下file_system_type,即文件系统的注册。

VFS必须对那些代码已经在内核中的所有文件系统的类型进行跟踪。这就是通过进行文件系统类型注册来实现的。每个注册的文件系统类型为file_system_type的对象来描述。

内核中有一个file_system_type结构队列,叫做file_systems,队列中的每个数据结构都代表着一种文件系统。系统初始化时将内核支持的各种文件系统的file_system_type数据结构通过一个函数register_filesystem()挂入这个队列,这个过程称之为文件系统的注册。除此之外,若是通过可安装模块装入文件系统,在装入这些模块时也会调用register_filesystem()函数注册文件系统。定义如下:

struct file_system_type {

         const char *name;

         int fs_flags;

         int (*get_sb)(struct file_system_type *, int,

                          const char *, void *, struct vfsmount*);

         void (*kill_sb)(struct super_block *);

         struct module*owner;

         structfile_system_type * next;

         struct list_headfs_supers;

    … …

};

name: 文件系统类型的名字,比如"ext2","iso9660","msdos"等

fs_flags:各种标志(i.e. FS_REQUIRES_DEV, FS_NO_DCACHE, etc.)

get_sb: 当有一个该文件系统的新的实例被挂载的时候调用的方法,该字段依赖于文件系统类型,该函数分配并初始化一个超级快对象并初始化它。

kill_sb: 当该文件系统的一个实例被卸载的时候调用的方法。

owner: 给VFS内部使用: 在大多数情况下你应该将它初始化为THIS_MODULE。

next: 给VFS内部使用: 你应该将它初始化为NULL 

fs_supers:表示给定类型已安装文件系统所对应的超级快链表的头,链表元素的向后和向前链接存放在超级块对象的s_instances字段中。

所以总结得到的关系是这样的:

 

图2

关键是理解super_block、设备、文件系统、file_system_type之间的关系。首先核心就是super_block和 设备 始终是联系在一起的。可以是一个super_block对应一个设备,也可以是一个super_block对应几个设备(当FS_SINGLE标志为1),但无论如何 只要有设备,就要想到肯定有个super_block对应它。然后每个设备上有一个文件系统,这个文件系统对应某个file_system_type。

图3

所以说只要是把一个文件系统(设备)加入到内核中,就要先从file_systems去找,得到这个文件系统的file_system_type。如果没有,就要填充新文件系统的file_system_type,并通过注册把它加入到file_systems链中去。

-→-→-→-→-→-→find_filesystem ()

根据文件系统类型的名字,在文件系统类型链表file_systems中查找。如果找到,则返回连接该file_system_type的file_system_type的next字段的地址,否则返回链表尾file_system_type的next字段的地址

-→-→-→-→-→-→try_module_get ()

递增相应module结构中的共享计数。

-→-→-→-→-→-→request_module ()

如果没找到,则通过request_module装入所需文件系统的可安装模块,然后再扫描队列。注意上面这句话的意思。只要没找到file_system_type,那么就要通过request_module来注册新的文件系统到file_systems链中去。

关于具体request_module,这进入到块驱动层了,它是通过call_usermodehelper 函数,透过外部呼叫,执行“modprobe”这个程序来把所要求的module载入系统中。

举个例子:在soundcore_open打开/dev/dsp节点函数中会调用到下面的:request_module("sound-slot-%i",unit>>4);函数,这表示,让linux系统的用户空间调用/sbin/modprobe函数加载名为sound-slot-0.ko模块luther@gliethttp:~$ cat/proc/sys/kernel/modprobe 

/sbin/modprobe
我们也可以向/proc/sys/kernel/modprobe添加新的modprobe应用程序路径,这里的/sbin/modprobe是内核默认路径。

详细可参考:http://www.360doc.com/content/08/0703/14/36491_1393274.shtml

-→-→-→-→-→vfs_kern_mount ()

接收get_fs_type找到的file_system_type,以及dev_name等参数,完成创建返回待挂载设备vfsmount结构的任务。

-→-→-→-→-→-→alloc_vfsmnt ()

会在cache中动态分配并并初始化一个vfsmount,alloc_vfsmnt()函数执行如下操作:

a.从vfsmount对象内存池中分配一个该对象,并初始化为0。

b.调用mnt_alloc_id()为该vfsmount对象分配一个标识符,并设置vfsmount对象的标识符字段mnt_id。

c.初始化vfsmount对象的mnt_devname字段。

d.将vfsmount对象的引用计数设置为1。

e.初始化连接各种链表的list_head。

返回到vfs_kern_mount做一些和security相关的安全性检查后就调用具体文件系统的get_sb方法,读取设备的超级块

-→-→-→-→-→-→get_sb () // ext2_get_sb

-→-→-→-→-→-→-→get_sb_bdev ()

它接收的参数包括文件系统类型file_system_type、设备名dev_name,以及通过驱动从块设备上读super_block的方法ext2_fill_super。

-→-→-→-→-→-→-→-→open_bdev_excl ()

-→-→-→-→-→-→-→-→-→lookup_bdev ()

通过给定的设备路径名dev_name,得到一个block_device结构。

-→-→-→-→-→-→-→-→-→-→path_lookup ()

首先通过设备路径名dev_name 得到它的dentry,存放在临时变量nd中。path_lookup本身不用多讲,而是要注意这里得到的就是,设备作为设备文件的那个节点的dentry,这也就印证了文中一开始讲的设备必须是“可访问”的。然而得到这个原始设备文件节点的dentry远远不够,我们要的是能访问它的文件系统,所以就要得到它的super_block结构,于是就要先打开这个设备,得到block_devick结构。

-→-→-→-→-→-→-→-→-→-→bd_acquire ()

它所接收的参数就是之前通过path_lookup得到的原始设备节点的inode(inode = nd.path.dentry->d_inode;)。bd_acquire的作用就是通过原始设备节点的inode得到此设备的block_device结构。

要分析bd_acquire,就要分析一个新的东西,叫bdev文件系统(bdevfs)。

块设备的管理是以一个块设备伪文件系统组织的,每个块设备是这个文件系统上的一个块设备节点。其节点结构如下:

struct bdev_inode {

         struct block_device bdev;

         struct inode vfs_inode;

};

static struct vfsmount *bd_mnt;//块设备文件系统在VFS中的挂载点结构体
struct super_block *blockdev_superblock;//块设备文件系统超级块结构体,用于操作块设备文件系统

这个块设备管理文件系统在内核构建的过程大体如下:

void __init bdev_cache_init(void)
{
       int err;
       bdev_cachep =kmem_cache_create("bdev_cache", sizeof(struct bdev_inode),
             0,SLAB_HWCACHE_ALIGN|SLAB_RECLAIM_ACCOUNT|SLAB_PANIC,
                  init_once, NULL);
//为struct bdev_inode建立slab缓存,用于分配对象给具体的块设备,用于分配节点
       err = register_filesystem(&bd_type);//注册块设备文件系统
       if (err)
           panic("Cannot register bdev pseudo-fs");
       bd_mnt = kern_mount(&bd_type);//将块设备文件系统挂载到vfs文件系统链表
       err = PTR_ERR(bd_mnt);
       if (IS_ERR(bd_mnt))
       panic("Cannot create bdevpseudo-fs");
       blockdev_superblock = bd_mnt->mnt_sb;//得到块设备文件系统的超级块

其为块文件系统的结构类型结构体:
static struct file_system_type bd_type = {
      .name = "bdev",//名称

      .get_sb = bd_get_sb,//得到超级块函数

      .kill_sb = kill_anon_super,
};

static struct inode *bdev_alloc_inode(struct super_block *sb)
{
      struct bdev_inode *ei =kmem_cache_alloc(bdev_cachep, SLAB_KERNEL);
      //为某个块设备分配一个块设备节点
      if (!ei)
      return NULL;
      return &ei->vfs_inode;//还回节点结构
}

static void bdev_destroy_inode(struct inode *inode)
{
      struct bdev_inode *bdi = BDEV_I(inode);//根据节点获取块节点地址

      bdi->bdev.bd_inode_backing_dev_info = NULL;
      kmem_cache_free(bdev_cachep, bdi);//释放slab对象,即节点缓存
}

有了这个预备知识,下面来详细介绍bdevfs。这个伪文件系统,它表征节点的结构是bdev_inode(可以类比为ext2_inode),而它与VFS层的inode转换可以由很重要的一个宏BDEV_I完成。代码如下:

static inline struct bdev_inode *BDEV_I(struct inode *inode)

{

         returncontainer_of(inode, struct bdev_inode, vfs_inode);

}

即bdev_inode包含VFS的inode, BDEV_I宏可以根据传给它的VFS inode得到相应块设备伪文件系统bdevfs中的节点bdev_inode。

打开一个块设备时,实际上涉及两个VFS inode。为什么?因为首先这个块设备会在根文件系统中以一个设备文件的形式存在(如/dev/sda1,它所存在的大环境是ext2),那么它在根文件系统中就有一个本地inode节点(如ext2_inode),这个本地inode节点(ext2_inode)会对应一个VFS的inode,这是第一个inode。另外一个就是这个块设备还会在块设备伪文件系统vdevfs中存在一个节点bdev_inode,它也会对应一个VFS的inode,这是第二个inode。     

比如,可以在一个ext2文件系统上建一个块设备结点,指定它的设备号;在proc文件系统下上建一个块设备结点,指定同样的设备号。只要设备号相同,那么它在bdevfs中的节点bdev_inode就是同一个,自然bdev_inode对应的inode也是同一个。这样的情况下,bdev_inode除了一对一的对应自己的VFS inode外,还映射了两个VFS inode,分别是ext2_inode对应的VFS inode 和proc_inode对应的VFS inode。

总结。首先明确同一个块设备可能有不同的节点表示。注意这里是指作为原始设备文件(即/dev/sda1这样),而不是挂载点(因为这里还是在打开原始设备,都还没挂载呢)。即便是作为原始设备也可以有不同的节点表示,如上述在ext2中打开和在proc中打开。然后在这种情况下,设备在它的bdevfs中始终只有一个bdev_inode,对应一个inode,也只有一个block_device,而这些都是由设备号决定的,一个设备号,一个bdev_inode。最后设备从bdevfs映射到其它的具体文件系统中(ext2、proc等),会在每个具体文件系统中都对应一个本地设备文件inode(进而VFS inode)。

因此在bdevfs的世界里,设备号就相当于它这个文件系统映射到VFS的inode号。因为在bdevfs到VFS的映射中,一个inode号就可以确定bdevfs在VFS的inode,进而确定bdevfs中的bdev_inode,进而找到block_device的

下面来边分析bd_aquire边理解bdevfs。

bdev = inode->i_bdev;

         if (bdev) {

                   atomic_inc(&bdev->bd_inode->i_count);

                   spin_unlock(&bdev_lock);

                   returnbdev;

         }

通过前面分析知道,作为bd_acquire参数的inode(VFS)并非通过bdevfs分配的,而是某种其它文件系统返回的,如ext2,proc。多个使用者打开同一个结点时,得到的自然是同一个inode,如果第一个使用者通过一系列程序找到了inode所表示的block_device,就会把它缓存起来,放到inode里,第二个使用者得到同样的inode后就可能直接用的。这就是这段代码的逻辑。

-→-→-→-→-→-→-→-→-→-→bdget ()

由前面分析知道,决定一个block_device身份的东西不是某个文件系统的inode,而是设备号。就如上面的例子,同一个块设备可能有不同的结点表示。如果一个人打开了一个块设备结点,第二个人通过另一个结节打开这个块设备,那么第二个人在bd_acquire上看到的inode的block_device是空的,即便第一个人已经找到对应的block_device了。这时,显然不能再生成一个block_device,block_device需要通过其它的方式缓存。而获得block_device的key就是块设备的设备号,这才是最终决定一个块设备身份的东西。bdget正是做这事的,它根据设备号建立块设备结构和对应的节点。

-→-→-→-→-→-→-→-→-→-→-→iget5_locked ()

bdevfs会对打开过的block_device进行缓存,一个设备号对应一个block_device。我们知道在bdevfs里,设备号就相当于它这个文件系统映射到VFS的inode号。因为在bdevfs到VFS的映射中,一个inode号就可以确定bdevfs在VFS的inode,进而确定bdevfs中的bdev_inode,进而找到block_device的。iget5_locked接收的参数就是bdevfs的超级块super_block,以及bdevfs世界中的inode号——设备号。只不过它这里的设备号做了哈希。

iget5_locked的作用就是在bdevfs的世界里,通过这个伪文件系统的super_block和设备号(bdevfs世界中的inode号)在缓存中寻找对应的inode。

-→-→-→-→-→-→-→-→-→-→-→-→ifind ()

如果在缓存中找到,则返回。由于传进来的设备号做了哈希,这里会调用之前一起传进来的test函数逐个验证具有相同哈希值的inode,它的设备号是不是和传进来的相等。

-→-→-→-→-→-→-→-→-→-→-→-→get_new_inode ()

如果没有在缓存里找到inode,就会调用文件系统的方法生成一个inode。bdevfs对应的方法实际上生成了一个VFS inode+block_device。注意这里,在bdevfs中,生成一个VFS的inode,就会在本地(bdevfs中)生成一个对应节点bdev_inode,它就包含了block_device。block_device就在这里生成。得到bdevfs生成的inode,就得到了block_device。

返回到bdget,通过BDEV_I,从bdevfs的VFSinode得到block_device。然后如果bdget返回的inode是新生成的,就要进行一些初始化操作。

然后再返回到bd_acquire。

bdev = bdget(inode->i_rdev);

         if (bdev) {

                   spin_lock(&bdev_lock);

                   if(!inode->i_bdev) {

                            atomic_inc(&bdev->bd_inode->i_count);

                            inode->i_bdev= bdev; //将新建的块设备赋给节点,此时的节点与块设备中的节

//点不同故而赋值一次

                            inode->i_mapping= bdev->bd_inode->i_mapping; //实质将新建节点的

//i_mapping给老节点i_mapping

                            list_add(&inode->i_devices,&bdev->bd_inodes); //添加到块设备节点链表

                   }

                   spin_unlock(&bdev_lock);

         }

这里还要注意一点。bd_acquire所接收的参数inode*是其它具体文件系统在VFS中的inode,而iget5_locked里面的inode*都是bdevfs在VFS中的inode,因为传给iget5_locked的是bdevfs的super_block。因此它返回给bdget的也是bdevs在VFS中的inode。这也就是上面代码中加亮部分的含义。

最后,还在bd_acquire中,无论如何,最终得到了block_device,将其缓存在inode里,好像所有的任务都做完了。但是,这个生成的block_device毫无意义---bdevfs有求必应,不管给我什么设备号,我总会生成一个block_device。并非每个设备号下面都有驱动支持!bdget返回的(从inode中推导出来的)block_device只是个空壳,与下层的驱动没有任何联系。所以,还要回到open方法,它最后调用的 blkdev_get才真正让block_device与驱动扯上了关系。它会通过设备号找到gendisk,于是就找到了queue,于是就找到了驱动提供的各种方法。

对于bdevfs的分析可以参考以下:

http://bbs.chinaunix.net/thread-3714392-1-1.html

http://blog.tianya.cn/blogger/post_show.asp?BlogID=862226&PostID=21295327

-→-→-→-→-→-→-→-→-→blkdev_get ()

接收前面得到的block_device作为参数,然后通过设备驱动打开这个设备。

-→-→-→-→-→-→-→-→-→-→__blkdev_get ()

-→-→-→-→-→-→-→-→-→-→-→do_open ()

深入到打开设备的过程就进入到设备驱动了,这里略过。

回到get_sb_bdev,已经得到待挂载设备的block_device,并且已经打开了这个设备。接着就先调用sget从缓存中找这个设备的super_block是否存在。

-→-→-→-→-→-→-→-→sget ()

它接收的参数包括最开始找到的file_system_type、前面返回的block_device、测试函数等。

if (test) {

                   list_for_each_entry(old,&type->fs_supers, s_instances) {

                            if(!test(old, data))

                                     continue;

                            if(!grab_super(old))

                                     gotoretry;

                            if(s)

                                     destroy_super(s);

                            returnold;

                   }

         }

         if (!s) {

                   spin_unlock(&sb_lock);

                   s = alloc_super(type);

                   if (!s)

                            returnERR_PTR(-ENOMEM);

                   gotoretry;

         }

         err = set(s, data);

通过上述代码,可以清晰看到sget会先根据file_system_type,从它的fs_supers链中依个寻找super_block(参考图2),并测试super_block所代表的设备是不是目标设备,若找到则缓存命中,直接返回缓存中super_block,若没找到,则新建一个super_block,并做适当初始化后返回。

再次回到get_sb_bdev,如果从sget得到的super_block是新创建的,则通过fill_super,即ext2_fill_super函数来从打开的设备中读取超级块。

-→-→-→-→-→-→-→-→fill_super () //ext2_fill_super

fill_super函数较长,关键是要理解ext2的磁盘结构,然后读取super_block。参考Ext2文件系统—路径名查找—3--ext2_lookup一节可以帮助理解。

-→-→-→-→-→-→-→-→simple_set_mnt ()

通过这个函数把最开始为待挂载设备创建的vfsmount结构与现在得到的设备super_block挂上钩,完成对mnt的填充。代码如下:

int simple_set_mnt(struct vfsmount *mnt, struct super_block *sb)

{

         mnt->mnt_sb = sb;

         mnt->mnt_root =dget(sb->s_root);

         return 0;

}

注意加亮的代码是挂钩的关键。这里就是把为设备所创建的vfsmount结构mnt 和设备的根目录(索引节点号为2的目录)挂上钩的地方。设备根目录的索引节点号为2,但是没有目录名,它就是通过让mnt->mnt_root指向它,通过mnt来访问这个根目录,进而访问这个设备上的各个子目录。

从get_sb_bdev返回到ext2_get_sb,再返回到vfs_kern_mount。经过一些安全性检查和对vfsmount 的补充性赋值。注意这里的补充性赋值是把mnt->mnt_parent设置成自己mnt,把mnt->mnt_mountpoint 设置成设备本身的根目录mnt->mnt_root(因为到这里为止,为设备所创建的vfsmount结构mnt只是和它本身的super_block挂上钩了,还没有和真正的挂载点挂钩,所以就先把设备设置成独立,相当于挂载在自己的根目录上)。返回到do_kern_mount。这样do_kern_mount完成了它的使命,返回得到的vfsmount结构mnt。回到do_new_mount,最后调用do_add_mount

-→-→-→-→do_add_mount ()

待安装设备的super_block结构已经解决了,这一边已经没有什么问题,现在要回过头看安装点这一边了。前面已经通过path_lookup找到了安装点的dentry结构、inode结构以及vfsmount结构,通过局部变量nameidata数据结构nd就可以访问到这些数据结构。

首先,前面从设备上读入超级块的过程是个颇为漫长的过程,这样就有可能会有另一个进程捷足先登抢先将另一个设备安装到了同一个安装点上。于是要通过d_mountpoint来检测是否发生了这种情况。

-→-→-→-→-→d_mountpoint ()

-→-→-→-→-→follow_down ()

如果是发生了这种情况,则调用follow_down(它会进一步调用lookup_mnt),前进到下一层已安装设备的根节点,得到它的vfsmount。通过一个while循环进一步检测d_mountpoint,直到最底下一层,即不再有设备安装的某个设备上的根节点为止。

返回到do_add_mount,安装点确定以后,剩下的就是把待安装设备的super_block数据结构与安装点的dentry结构联系在一起,即“安装”本身了。

-→-→-→-→-→graft_tree ()

graft_tree接收的参数就是安装点的(通过d_mountpoint循环之后的)dentry和待安装设备的vfsmount结构mnt。在经过一些安全性检查之后,调用attach_recursive_mnt,完成两者的挂接。它还会把 do_kern_mount()函数返回的struct vfsmount 类型的变量加入到安装系统链表中

-→-→-→-→-→-→attach_recursive_mnt ()

这个函数的注释很长- -。

-→-→-→-→-→-→-→propagate_mnt ()

这里貌似是关于linux的一个共享子树的机制,详细可看文档Linux文档sharedsubtree.txt。

-→-→-→-→-→-→-→mnt_set_mountpoint ()

{

         child_mnt->mnt_parent= mntget(mnt);

         child_mnt->mnt_mountpoint= dget(dentry);

         dentry->d_mounted++;

}

mnt_set_mountpoint这个函数把待挂载设备的vfsmount(child_mnt)与安装点的vfsmount(mnt)和安装点的dentry(dentry)挂接在一起。

-→-→-→-→-→-→-→commit_tree ()

Commit_tree是将新的文件系统vfsmount加入到mount_hashtable这个hash表中。也就是前面说的把 do_kern_mount() 函数返回的struct vfsmount 类型的变量加入到安装系统链表中。

mount实际上把待安装设备的vfsmount对象链接到一个全局hash表,同时也链接到父vfsmount对象的链表头。而目的dentry,就是根安装点目录的dentry对象的d_mounted要加一,这是判断这个目录是否有mount点的依据。

从graft_tree返回,do_add_mount也结束了。整个do_new_mount结束。

2.2、总结

最后给两幅图总结下设备挂载的系统结构。关于文件系统及挂载的参考可见如下网址:

http://www.ibm.com/developerworks/cn/linux/l-vfs/

http://blog.youkuaiyun.com/easyblue99/article/details/7186048


3、卸载

-→sys_umount()

retval = user_path(name, &path);

if (retval)

         goto out;

retval = -EINVAL;

if (path.dentry != path.mnt->mnt_root)

         goto dput_and_out;

首先,它调用user_path()找到设备根目录的dentry。回顾前面就知道,设备根目录的索引节点号为2,但是没有目录名,它就是通过让mnt->mnt_root指向它,通过mnt来访问的。

情景分析中说“不论给定的是安装点的路径名或是设备文件的路径名,path_walk()的结果都是一样的,nd.dentry总是指向设备文件上的根目录的dentry。”这个说法是不对的。传给user_path的路径名name,必须是安装点的路径名,因为只用通过这个路径才能经过__follow_mountàd_mountpointàlookup_mnt一步一步顺利的得到设备根目录的dentry。如果是给设备文件的路径名(如/dev/sda1)是完全没用的,因为这只是设备所在的上层文件系统的一个普通索引节点。

然而实际操作中的确既可以通过安装点路径名(如umount /mnt/dir1)来卸载,也可以通过设备文件路径名(如 umount /dev/sda1)来卸载,这是为什么。原因就是我们这里所调用的umount并不是系统调用“umount”,而是bash命令的umount。通过straceumount 可以看到,无论通过哪种方法(umount /mnt/dir1或umount /dev/sda1)来卸载,bash命令的umount 最终调用系统调用umount的形式都是 umount(“/mnt/dir1”),这才是真正系统调用传给sys_umount的参数。

其实通过umount系统调用的接口说明也可以得出这个结论

#include <sys/mount.h>

int mount(const char *source, const char *target,
const char *filesystemtype, unsigned long mountflags, const void *data);

int umount(const char *target);

int umount2(const char *target, int flags);

参数: 
source:将要挂上的文件系统,通常是一个设备名。
target:文件系统所要挂在的目标目录。
filesystemtype:文件系统的类型,可以是"ext2","msdos","proc","nfs","iso9660" 。。。
mountflags:指定文件系统的读写访问标志

-→-→do_umount ()

         if (flags &MNT_EXPIRE) {

                   if (mnt ==current->fs->root.mnt ||

                       flags & (MNT_FORCE | MNT_DETACH))

                            return-EINVAL;

                   if(atomic_read(&mnt->mnt_count) != 2)

                            return-EBUSY;

         }

在vfsmount数据结构中,有个使用计数mnt_count,在add_vfsmnt中设置为1.从那以后,每当要使用这个数据结构时就通过mntget()递增其使用计数,用完了就通过mntput()递减其计数(如path_init中调用了mntget(),path_release()中调用了mnt_put(),在follow_up()和follow_down()中既调用了mnt_get(),有调用了mnt_put())。所以在do_umount()中所处理的vfsmount结构中的使用计数应该是2。否则说明还有其它的操作还在使用这个数据结构,因此不能完成拆卸只能返回错误。

         if (flags &MNT_FORCE && sb->s_op->umount_begin) {

                   lock_kernel();

                   sb->s_op->umount_begin(sb);

                   unlock_kernel();

         }

vfsmount结构在安装文件系统是通过其队列头mnt_instances挂入一个super_block结构的s_mounts队列。通常一个块设备只安装一次,所以其super_block结构中的队列s_mounts只含有一个vfsmount结构,但是有些情况下,同一个设备是可以安装多次的,此时其super_block结构中的s_mounts队列含有多个vfsmounts。(这是2.4内核分析中插入的,在2.6内核中并没有看到相关代码如remove_vfsmnt())

有些设备要求在拆卸时先调用一个函数处理拆卸的开始,这种设备通过其super_operations函数跳转表内的函数指针umount_begin提供相应的函数。ext2并没有填充这个函数,因此它的这一项为NULL。

         if (mnt ==current->fs->root.mnt && !(flags & MNT_DETACH)) {

                    /* Special case for "unmounting"root ...

                    * we just try to remount it readonly.*/

                   if(!(sb->s_flags & MS_RDONLY)) {

                            lock_kernel();

                            retval= do_remount_sb(sb, MS_RDONLY, NULL, 0);

                            unlock_kernel();

                   }

         }

这里表示所要拆卸的是根设备。用户进程是不能他通过umount()直接拆卸根设备的。从用户进程通过umount()系统调用拆卸根设备,只意味着将它重安装成“只读”模式。

-→-→-→do_remount_sb ()

-→-→-→-→shrink_dcache_sb ()

-→-→-→-→-→__shrink_dcache_sb ()

把一个设备最终从文件系统中卸载下来,这意味着从此以后这个子系统中的所有节点都不再是可访问的了。我们知道dentry_unused队列中存放的dput完后使用计数为0的dentry,它们暂时不使用,因此存放在dentry_unused缓存中以防止后面马上又会用到。然而要最终卸载一个设备,则属于这个设备的所有dentry结构再也没有保留的必要,所以要扫描dentry_unused队列,把所有属于这个队列的dentry结构都释放掉。

代码注释是这样说的:为一个特定的super_block缩减dcache,用于在卸载一个文件系统时释放dcache。

/*

 * Shrink the dentry LRU ona given superblock.

 * @sb   : superblock to shrink dentry LRU.

 * @count: If count is NULL,we prune all dentries on superblock.

 * @flags: If flags isnon-zero, we need to do special processing based on

 * which flags are set. Thismeans we don't need to maintain multiple

 * similar copies of thisloop.

 */

shrink_dcache_sb调用__shrink_dcache_sb时,count为NULL,flags为0。

if (count == NULL)

                   list_splice_init(&sb->s_dentry_lru,&tmp);

         else {

                   while(!list_empty(&sb->s_dentry_lru)) {

                            dentry= list_entry(sb->s_dentry_lru.prev,

                                               structdentry, d_lru);

count是为NULL的,因此就把super_block的s_dentry_lru队列中的所有dentry移到一个暂时的tmp队列中,并逐一释放。

while (!list_empty(&tmp)) {

                   dentry =list_entry(tmp.prev, struct dentry, d_lru);

                   dentry_lru_del_init(dentry);

                   spin_lock(&dentry->d_lock);

                   /*

                    * We found an inuse dentry which was notremoved from

                    * the LRU because of laziness duringlookup.  Do not free

                    * it - just keep it off the LRU list.

                    */

                   if(atomic_read(&dentry->d_count)) {

                            spin_unlock(&dentry->d_lock);

                            continue;

                   }

                   prune_one_dentry(dentry);

                   /*dentry->d_lock was dropped in prune_one_dentry() */

                   cond_resched_lock(&dcache_lock);

         }

在一个while循环中逐一取出每一个dentry,然后调用prune_one_dentry释放之。

-→-→-→-→-→-→prune_one_dentry ()

-→-→-→-→-→-→-→__d_drop ()

-→-→-→-→-→-→-→d_kill ()

通过 __d_drop()将dentry移出哈希队列,通过d_kill()将dentry释放,返回父dentry。检查父dentry,若已无后代,则将父dentry也删除,并一直循环。关于dcache删除机制,参照《Linux dentry cache学习》。

-→-→-→-→fsync_super ()

-→-→-→-→-→__fsync_super ()

块设备的输入输出一般是有缓冲的,无论是对超级块的改变还是对某个索引节点的改变,或者对某个数据块的改变,都只是对他们在内存中映像的改变,而不一定马上就写回设备。现在设备要卸载,就要先把已经改变了,但是尚未写回的设备内容写回去,这称为“同步”。

后面就是同步机制的内容,包括同步超级块(包含writesuper)、同步inodes(包含write inode)。

-→-→-→-→-→-→sync_inodes_sb ()

-→-→-→-→-→-→ext2_write_super ()

-→-→-→-→-→-→sync_blockdev ()

这些函数的分析需要透彻了解linux同步机制,从sys_sync()系统调用,一整套机制。再此仅仅简单介绍略过,以后会回过头系统学习这个机制。

多个节点同步回写操作函数sync_inodes调用图如下:

参考链接:

http://www.2cto.com/os/201204/126687.html

 

 

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值