8.4.2  文件系统的安装

要使用一个文件系统,仅仅注册是不行的,还必须安装这个文件系统。在安装Linux时,硬盘上已经有一个分区安装了Ext2文件系统,它是作为根文件系统的,根文件系统在启动时自动安装。其实,在系统启动后你所看到的文件系统,都是在启动时安装的。如果你需要自己(一般是超级用户)安装文件系统,则需要指定三种信息:文件系统的名称、包含文件系统的物理块设备、文件系统在已有文件系统中的安装点。例如:

$ mount -t iso9660 /dev/hdc /mnt/cdrom

其中,iso9660就是文件系统的名称,/dev/hdc是包含文件系统的物理块设备,/mnt/cdrom就是将要安装到的目录,即安装点。从这个例子可以看出,安装一个文件系统实际上是安装一个物理设备。

    把一个文件系统(或设备)安装到一个目录点时要用到的主要数据结构为vfsmount,定义于include/linux/mount.h中:

   struct vfsmount

{

        struct list_head mnt_hash;

        struct vfsmount *mnt_parent;    /* fs we are mounted on */

        struct dentry *mnt_mountpoint;  /* dentry of mountpoint */

        struct dentry *mnt_root;        /* root of the mounted tree */

         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;

         char *mnt_devname;              /* Name of device e.g. /dev/dsk/hda1 */

         struct list_head mnt_list;

};

   下面对结构中的主要域给予进一步说明:

·      为了对系统中的所有安装点进行快速查找,内核把它们按哈希表来组织,mnt_hash就是形成哈希表的队列指针。

·      mnt_mountpoint是指向安装点dentry结构的指针。而dentry指针指向安装点所在目录树中根目录的dentry结构。

·      mnt_parent是指向上一层安装点的指针。如果当前的安装点没有上一层安装点(如根设备),则这个指针为NULL。同时,vfsmount结构中还有mnt_mountsmnt_child两个队列头,只要上一层vfsmount结构存在,就把当前vfsmount结构中mnt_child链入上一层vfsmount结构的mnt_mounts队列中。这样就形成一颗设备安装的树结构,从一个vfsmount结构的mnt_mounts队列开始,可以找到所有直接或间接安装在这个安装点上的其他设备。

·      mnt_sb指向所安装设备的超级块结构super_blaock

·      mnt_list是指向vfsmount结构所形成链表的头指针。

另外,系统还定义了vfsmntlist变量,指向mnt_list队列。对这个数据结构的进一步理解请看后面文件系统安装的具体实现过程。

   文件系统的安装选项,也就是vfsmount结构中的安装标志mnt_flagslinux/fs.h中定义如下:

/*

* These are the fs-independent mount-flags: up to 32 flags are supported

*/

 #define MS_RDONLY        1      /* Mount read-only */

 #define MS_NOSUID        2      /* Ignore suid and sgid bits */

 #define MS_NODEV         4     /* Disallow access to device special files */

 #define MS_NOEXEC        8      /* Disallow program execution */

 #define MS_SYNCHRONOUS  16      /* Writes are synced at once */

 #define MS_REMOUNT      32      /* Alter flags of a mounted FS */

 #define MS_MANDLOCK     64      /* Allow mandatory locks on an FS */

 #define MS_NOATIME      1024    /* Do not update access times. */

 #define MS_NODIRATIME   2048    /* Do not update directory access times */

 #define MS_BIND         4096

 #define MS_MOVE         8192

 #define MS_REC          16384

#define MS_VERBOSE      32768

#define MS_ACTIVE       (1<<30)

#define MS_NOUSER       (1<<31)

   从定义可以看出,每个标志对应32位中的一位。安装标志是针对整个文件系统中的所有文件的。例如,如果MS_NOSUID标志为1,则整个文件系统中所有可执行文件的suid标志位都不起作用了。其他安装标志的具体含义在后面介绍do_mount()函数代码时再进一步介绍。

1.安装根文件系统

每个文件系统都有它自己的根目录,如果某个文件系统(如Ext2)的根目录是系统目录树的根目录,那么该文件系统称为根文件系统。而其他文件系统可以安装在系统的目录树上,把这些文件系统要插入的那些目录就称为安装点

当系统启动时,就要在变量ROOT_DEV中寻找包含根文件系统的磁盘主码。当编译内核或向最初的启动装入程序传递一个合适的选项时,根文件系统可以被指定为/dev目录下的一个设备文件。类似地,根文件系统的安装标志存放在root_mountflags变量中。用户可以指定这些标志,这是通过对已编译的内核映像执行/sbin/rdev外部程序,或者向最初的启动装入程序传递一个合适的选项来达到的。根文件系统的安装函数为mount_root(  )

2.安装一个常规文件系统

  一旦在系统中安装了根文件系统,就可以安装其他的文件系统。每个文件系统都可以安装在系统目录树中的一个目录上。

前面我们介绍了以命令方式来安装文件系统,在用户程序中要安装一个文件系统则可以调用mount()系统调用。Mount()系统调用在内核的实现函数为sys_mount(),其代码在fs/namespace.c中。

asmlinkage long sys_mount(char * dev_name, char * dir_name, char * type,

                          unsigned long flags, void * data)

{

         int retval;

         unsigned long data_page;

         unsigned long type_page;

         unsigned long dev_page;

         char *dir_page;

 

         retval = copy_mount_options (type, &type_page);

         if (retval < 0)

                 return retval;

 

         dir_page = getname(dir_name);

         retval = PTR_ERR(dir_page);

         if (IS_ERR(dir_page))

                 goto out1;

 

        retval = copy_mount_options (dev_name, &dev_page);

        if (retval < 0)

                 goto out2;

 

         retval = copy_mount_options (data, &data_page);

         if (retval < 0)

                 goto out3;

 

         lock_kernel();

         retval = do_mount((char*)dev_page, dir_page, (char*)type_page,

                           flags, (void*)data_page);

         unlock_kernel();

         free_page(data_page);

 

out3:

         free_page(dev_page);

out2:

         putname(dir_page);

out1:

         free_page(type_page);

         return retval;

}

下面给出进一步的解释:

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

·      copy_mount_options()和getname()函数将结构形式或字符串形式的参数值从用户空间拷贝到内核空间;这些参数值的长度均以一个页面为限,但是getname()在复制时遇到字符串结尾符“\0就停止,并返回指向该字符串的指针;而copy_mount_options()则拷贝整个页面,并返回该页面的起始地址。

 该函数调用的主要函数为do_mount()do_mount()执行期间要加内核锁,不过这个锁是针对SMP,我们暂不考虑。do_mount()的实现代码在fs/namespace.c中:

long do_mount(char * dev_name, char * dir_name, char *type_page,

                   unsigned long flags, void *data_page)

{

        struct nameidata nd;

        int retval = 0;

        int mnt_flags = 0;

 

        /* Discard magic */

         if ((flags & MS_MGC_MSK) == MS_MGC_VAL)

                flags &= ~MS_MGC_MSK;

 

         /* Basic sanity checks */

 

         if (!dir_name || !*dir_name || !memchr(dir_name, 0, PAGE_SIZE))

                 return -EINVAL;

         if (dev_name && !memchr(dev_name, 0, PAGE_SIZE))

                 return -EINVAL;

 

        /* 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;

        flags &= ~(MS_NOSUID|MS_NOEXEC|MS_NODEV);

 

        /* ... and get the mountpoint */

         if (path_init(dir_name, LOOKUP_FOLLOW|LOOKUP_POSITIVE, &nd))

                retval = path_walk(dir_name, &nd);

         if (retval)

                 return retval;

 

        if (flags & MS_REMOUNT)

                retval = do_remount(&nd, flags & ~MS_REMOUNT, mnt_flags,

                                     data_page);

         else if (flags & MS_BIND)

                 retval = do_loopback(&nd, dev_name, flags & MS_REC);

         else if (flags & MS_MOVE)

                 retval = do_move_mount(&nd, dev_name);

         else

                 retval = do_add_mount(&nd, type_page, flags, mnt_flags,

                                      dev_name, data_page);

        path_release(&nd);

         return retval;

}

下面对函数中的主要代码给予解释:

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

·      对参数dir_namedev_name进行基本检查,注意“!dir_name ” 和“!*dir_name”之不同,前者指向字符串的指针为不为空,而后者指字符串不为空。Memchr()函数在指定长度的字符串中寻找指定的字符,如果字符串中没有结尾符“\0,也是一种错误。前面以说过,对于基于网络的文件系统dev_name可以为空。

·      把安装标志为MS_NOSUIDMS_NOEXECMS_NODEV的三个标志位从flags分离出来,放在局部安装标志变量mnt_flags中。

·      函数path_init()和path_walk()寻找安装点的dentry数据结构,找到的dentry结构存放在局部变量nddentry域中。

·      如果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_add_mount()函数实现的,而do_add_mount()首先调用do_kern_mount()函数形成一个安装点,该函数的代码在fs/super.c中:

    struct vfsmount *do_kern_mount(char *type, int flags, char *name, void *data)

{

         struct file_system_type * fstype;

         struct vfsmount *mnt = NULL;

         struct super_block *sb;

 

         if (!type || !memchr(type, 0, PAGE_SIZE))

                 return ERR_PTR(-EINVAL);

     

         /* we need capabilities... */

         if (!capable(CAP_SYS_ADMIN))

                 return ERR_PTR(-EPERM);

 

         /* ... filesystem driver... */

         fstype = get_fs_type(type);

        if (!fstype)           

                return ERR_PTR(-ENODEV);

 

         /* ... allocated vfsmount... */

        mnt = alloc_vfsmnt();

         if (!mnt) {

                 mnt = ERR_PTR(-ENOMEM);

                goto fs_out;

         }

         set_devname(mnt, name);

         /* get locked superblock */

         if (fstype->fs_flags & FS_REQUIRES_DEV)

                 sb = get_sb_bdev(fstype, name, flags, data);

         else if (fstype->fs_flags & FS_SINGLE)

                 sb = get_sb_single(fstype, flags, data);

         else

                 sb = get_sb_nodev(fstype, flags, data);

 

         if (IS_ERR(sb)) {

                 free_vfsmnt(mnt);

                 mnt = (struct vfsmount *)sb;

                 goto fs_out;

        }

         if (fstype->fs_flags & FS_NOMOUNT)

                 sb->s_flags |= MS_NOUSER;

 

         mnt->mnt_sb = sb;

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

         mnt->mnt_mountpoint = mnt->mnt_root;

         mnt->mnt_parent = mnt;

         up_write(&sb->s_umount);

 fs_out:

         put_filesystem(fstype);

         return mnt;

 }

   对该函数的解释如下:

·      只有系统管理员才具有安装一个设备的权力,因此首先要检查当前进程是否具有这种权限。

·      get_fs_type()函数根据具体文件系统的类型名在file_system_file链表中找到相应的结构。

·      alloc_vfsmnt()函数调用Slab分配器给类型vfsmount结构的局部变量mnt分配空间,并进行相应的初始化。

·      set_devname()函数设置设备名。

·      一般的文件系统类型要求有物理的设备作为其物质基础,如果fs_flags中的FS_REQUIRES_DEV标志位为1,说明这就是正常的文件系统类型,如Ext2mnix等。对于这种文件系统类型,通过调用get_sb_bdev()从待安装设备上读其超级块。

·      如果fs_flags中的FS_SINGLE标志位为1,说明整个文件系统只有一个类型,也就是说,这是一种虚拟的文件系统类型。这种文件类型在安装了同类型的第一个“设备” ,通过调用get_sb_single()创建了超级块super_block结构后,再安装的同类型设备就共享这个数据结构。但是像Ext2这样的文件系统类型在每个具体设备上都有一个超级块。

·      还有些文件系统类型的fs_flags中的FS_NOMOUNTFS_REUIRE_DEV以及FS_SINGLE标志位全都为0,那么这些所谓的文件系统其实是“虚拟的”,通常只是用来实现某种机制或者规程,所以根本就没有对应的物理设备。对于这样的文件系统类型都是通过get_sb_nodev()来生成一个super_block结构的。

·      如果文件类型fs_flagsFS_NOMOUNT标志位为1,说明根本就没有用户进行安装,因此,把超级块中的MS_NOUSER标志位置1

·      mnt->mnt_sb指向所安装设备的超级块sbmnt->mnt_root指向其超级块的根b->s_rootdget()函数把dentry的引用计数count1mnt->mnt_mountpoint也指向超级块的根,而mnt->mnt_parent指向自己。到此为止,仅仅形成了一个安装点,但还没有把这个安装点挂接在目录树上。

 下面我们来看do_add_mount()的代码:

   static int do_add_mount(struct nameidata *nd, char *type, int flags,

                         int mnt_flags, char *name, void *data)

{

         struct vfsmount *mnt = do_kern_mount(type, flags, name, data);

         int err = PTR_ERR(mnt);

 

         if (IS_ERR(mnt))

                goto out;

 

         down(&mount_sem);

         /* Something was mounted here while we slept */

         while(d_mountpoint(nd->dentry) && follow_down(&nd->mnt, &nd->dentry))

                 ;

         err = -EINVAL;

         if (!check_mnt(nd->mnt))

                goto unlock;

 

        /* Refuse the same filesystem on the same mount point */

        err = -EBUSY;

         if (nd->mnt->mnt_sb == mnt->mnt_sb && nd->mnt->mnt_root == nd->dentry)

                 goto unlock;

 

        mnt->mnt_flags = mnt_flags;

        err = graft_tree(mnt, nd);

unlock:

         up(&mount_sem);

         mntput(mnt);

 out:

         return err;

   }

 下面是对以上代码的解释:

·      首先检查do_kern_mount()所形成的安装点是否有效。

·      do_mount()函数中,path_init()path_walk()函数已经找到了安装点的dentry结构、inode结构以及vfsmount结构,并存放在类型为nameidata的局部变量nd 中,在do_add_mount()中通过参数传递了过来。

·      但是,在do_kern_mount()函数中从设备上读入超级块的过程是个较为漫长的过程,当前进程在等待从设备上读入超级块的过程中几乎可肯定要睡眠,这样就有可能另一个进程捷足先登抢先将另一个设备安装到了同一个安装点上。d_mountpoint()函数就是检查是否发生了这种情况。如果确实发生了这种情况,其对策就是调用follow_down()前进到已安装设备的根节点,并且通过while循环进一步检测新的安装点,直到找到一个空安装点为止。 

·      如果在同一个安装点上要安装两个同样的文件系统,则出错。

·      调用graft_tree()把mnt与安装树挂接起来,完成最终的安装。

至此,设备的安装就完成了。