转载:http://www.eefocus.com/article/09-06/75215s.html(深入分析linux内核源码)
图8.7 仅示意性地说明系统中已安装的三个文件系统Ext2、proc、iso9660其file_system_type结构所形成的链表。当然,系统中实际安装的文件系统要更多。
file_system_type的数据结构在fs.h中定义如下:
struct file_system_type { const char *name; int fs_flags; struct super_block *(*read_super) (struct super_block *, void *, int); struct module *owner; struct file_system_type * next; struct list_head fs_supers; }; |
对其中几个域的说明如下:
· name:文件系统的类型名,以字符串的形式出现。
· fs_flags:指明具体文件系统的一些特性,有关标志定义于fs.h中:
/* public flags for file_system_type */ #define FS_REQUIRES_DEV 1 #define FS_NO_DCACHE 2 /* Only dcache the necessary things. */ #define FS_NO_PRELIM 4 /* prevent preloading of dentries, even if * FS_NO_DCACHE is not set. */ #define FS_SINGLE 8 /* Filesystem that can have only one superblock */ #define FS_NOMOUNT 16 /* Never mount from userland */ #define FS_LITTER 32 /* Keeps the tree in dcache */ #define FS_ODD_RENAME 32768 /* Temporary stuff; will go away as soon * as nfs_rename() will be cleaned up */ |
对某些常用标志的说明如下:
(1) 有些虚拟的文件系统,如pipe、共享内存等,根本不允许由用户进程通过系统调用mount()来安装。这样的文件系统其fs_flags中的FS_NOMOUNT标志位为1。
(2) 一般的文件系统类型要求有物理的设备作为其物质基础,其fs_flags中的FS_REQUIRES_DEV标志位为1,这些文件系统如Ext2、Minix、ufs等。
(3) 有些虚拟文件系统在安装了同类型中的第一个“设备”,从而创建了其超级块的super_block数据结构,在安装同一类型中的其他设备时就共享已存在的super_block结构,而不再有自己的超级块结构。此时fs_flags中的FS_SINGLE标志位为1,表示整个文件系统只有一个超级块,而不像一般的文件系统类型那样,每个具体的设备上都有一个超级块。
· read_super:这是各种文件系统读入其超级块的函数指针。因为不同的文件系统其超级块不同,因此其读入函数也不同。
· owner:如果file_system_type所代表的文件系统是通过可安装模块实现的,则该指针指向代表着具体模块的module结构。如果文件系统是静态地链接到内核,则这个域为NULL。实际上,你只需要把这个域置为THIS_MODLUE (这是个一个宏),它就能自动地完成上述工作。
· next:把所有的file_system_type结构链接成单项链表的链接指针,变量file_systems指向这个链表。这个链表是一个临界资源,受file_systems_lock自旋读写锁的保护。
· fs_supers:这个域是Linux2.4.10以后的内核版本中新增加的,这是一个双向链表。链表中的元素是超级块结构。如前说述,每个文件系统都有一个超级块,但有些文件系统可能被安装在不同的设备上,而且每个具体的设备都有一个超级块,这些超级块就形成一个双向链表。
搞清楚这个数据结构的各个域以后,就很容易理解下面的注册函数register_filesystem(),该函数定义于fs/super.c:
/** * register_filesystem - register a new filesystem * @fs: the file system structure * * Adds the file system passed to the list of file systems the kernel * is aware of for mount and other syscalls. Returns 0 on success, * or a negative errno code on an error. * * The &struct file_system_type that is passed is linked into the kernel * structures and must not be freed until the file system has been * unregistered. */ int register_filesystem(struct file_system_type * fs) { int res = 0; struct file_system_type ** p; if (!fs) return -EINVAL; if (fs->next) return -EBUSY; INIT_LIST_HEAD(&fs->fs_supers); write_lock(&file_systems_lock); p = find_filesystem(fs->name); if (*p) res = -EBUSY; else *p = fs; write_unlock(&file_systems_lock); return res; } find_filesystem()函数在同一个文件中定义如下: static struct file_system_type **find_filesystem(const char *name) { struct file_system_type **p; for (p=&file_systems; *p; p=&(*p)->next) if (strcmp((*p)->name,name) == 0) break; return p; } |
注意,对注册链表的操作必须互斥地进行,因此,对该链表的查找加了写锁write_lock。
文件系统注册后,还可以撤消这个注册,即从注册链表中删除一个file_system_type 结构,此后系统不再支持该种文件系统。fs/super.c中的unregister_filesystem()函数就是起这个作用的,它在执行成功后返回0,如果注册链表中本来就没有指定的要删除的结构,则返回-1,其代码如下:
/** * unregister_filesystem - unregister a file system * @fs: filesystem to unregister * * Remove a file system that was previously successfully registered * with the kernel. An error is returned if the file system is not found. * Zero is returned on a success. * * Once this function has returned the &struct file_system_type structure * may be freed or reused. */ it unregister_filesystem(struct file_system_type * fs) { struct file_system_type ** tmp; write_lock(&file_systems_lock); tmp = &file_systems; while (*tmp) { if (fs == *tmp) { *tmp = fs->next; fs->next = NULL; write_unlock(&file_systems_lock); return 0; } tmp = &(*tmp)->next; } write_unlock(&file_systems_lock); return -EINVAL; } |
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_mounts和mnt_child两个队列头,只要上一层vfsmount结构存在,就把当前vfsmount结构中mnt_child链入上一层vfsmount结构的mnt_mounts队列中。这样就形成一颗设备安装的树结构,从一个vfsmount结构的mnt_mounts队列开始,可以找到所有直接或间接安装在这个安装点上的其他设备。
· mnt_sb指向所安装设备的超级块结构super_blaock。
· mnt_list是指向vfsmount结构所形成链表的头指针。
另外,系统还定义了vfsmntlist变量,指向mnt_list队列。对这个数据结构的进一步理解请看后面文件系统安装的具体实现过程。
文件系统的安装选项,也就是vfsmount结构中的安装标志mnt_flags在linux/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_name和dev_name进行基本检查,注意“!dir_name ” 和“!*dir_name”之不同,前者指指向字符串的指针为不为空,而后者指字符串不为空。Memchr()函数在指定长度的字符串中寻找指定的字符,如果字符串中没有结尾符“\0”,也是一种错误。前面以说过,对于基于网络的文件系统dev_name可以为空。
· 把安装标志为MS_NOSUID、MS_NOEXEC和MS_NODEV的三个标志位从flags分离出来,放在局部安装标志变量mnt_flags中。
· 函数path_init()和path_walk()寻找安装点的dentry数据结构,找到的dentry结构存放在局部变量nd的dentry域中。
· 如果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,说明这就是正常的文件系统类型,如Ext2、mnix等。对于这种文件系统类型,通过调用get_sb_bdev()从待安装设备上读其超级块。
· 如果fs_flags中的FS_SINGLE标志位为1,说明整个文件系统只有一个类型,也就是说,这是一种虚拟的文件系统类型。这种文件类型在安装了同类型的第一个“设备” ,通过调用get_sb_single()创建了超级块super_block结构后,再安装的同类型设备就共享这个数据结构。但是像Ext2这样的文件系统类型在每个具体设备上都有一个超级块。
· 还有些文件系统类型的fs_flags中的FS_NOMOUNT、FS_REUIRE_DEV以及FS_SINGLE标志位全都为0,那么这些所谓的文件系统其实是“虚拟的”,通常只是用来实现某种机制或者规程,所以根本就没有对应的物理设备。对于这样的文件系统类型都是通过get_sb_nodev()来生成一个super_block结构的。
· 如果文件类型fs_flags的FS_NOMOUNT标志位为1,说明根本就没有用户进行安装,因此,把超级块中的MS_NOUSER标志位置1。
· mnt->mnt_sb指向所安装设备的超级块sb;mnt->mnt_root指向其超级块的根b->s_root,dget()函数把dentry的引用计数count加1;mnt->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)) goo 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与安装树挂接起来,完成最终的安装。
至此,设备的安装就完成了。
如果文件系统中的文件当前正在使用,该文件系统是不能被卸载的。如果文件系统中的文件或目录正在使用,则 VFS 索引节点高速缓存中可能包含相应的 VFS 索引节点。根据文件系统所在设备的标识符,检查在索引节点高速缓存中否有来自该文件系统的 VFS 索引节点,如果有且使用计数大于0,则说明该文件系统正在被使用,因此,该文件系统不能被卸载。否则,查看对应的 VFS 超级块,如果该文件系统的 VFS 超级块标志为“脏”,则必须将超级块信息写回磁盘。上述过程结束之后,对应的 VFS 超级块被释放,vfsmount 数据结构将从vfsmntlist 链表中断开并被释放。具体的实现代码为fs/super.c中的sysy_umount()函数,在此不再进行详细的讨论。