5.4 文件系统的安装和拆卸
在一个块设备(见本书下册 “设备驱动” 一章)上按一定的格式建立起文件系统的时候,或者系统引导之初,设备上的文件和节点都还是不可访问的。也就是说,还不能按一定的路径名访问其中特定的节点或文件(虽然作为 “设备” 是可访问的)。只有把它 “安装” 到计算机系统的文件系统中某个节点上,才能使设备上的文件和节点成为可访问的。经过安装以后,设备上的 “文件系统” 就成为整个文件系统的一部分,或者说一个子系统。一般而言,文件系统的结构就好像一棵倒立的树,不过由于可能存在着的节点间的 “连接” 和 “符号连接” 而并不一定是严格的图论意义上的 “树” 。最初时, 整个系统中只有一个节点,那就是整个文件系统的 “根” 节点 “/” ,这个节点存在于内存中,而不在任何具体的设备上。系统在初始化时将一个 “根设备” 安装到节点 “/” 上,这个设备上的文件系统就成了整个系统中原始的、基本的文件系统(所以才称为根设备)。此后,就可以由超级用户进程通过系统调用 mount() 把其他的子系统安装到已经存在于文件系统中的空闲节点上,使整个文件系统得以扩展,当不再需要使用某个子系统时,或者在关闭系统之前,则通过系统调用 umount() 把已经安装的设备逐个 “拆卸” 下来。
系统调用 mount() 将一个可访问的块设备安装到一个可访问的节点上。所谓 “可访问” 是指该节点或文件已经存在于已安装的文件系统中,可以通过路径名寻访。Unix(以及Linux)将设备看作一种特殊的文件,并在文件系统中有代表着具体设备的节点,称为 “设备文件” ,通常都在目录 “/dev” 中。例如 IDE 硬盘上的第一个分区就是 /dev/hda1 。 每个设备文件实际上只是一个索引节点,节点中提供了设备的 “设备号” ,由 “主设备号” 和 “次设备号” 两部分构成。其中主设备号指明了设备的种类,或者更确切地说是指明了应该使用哪一组驱动程序。同一个物理的设备,如果有两组不同的驱动程序,在逻辑上就被视作两种不同的设备而在文件系统中有两个不同的 “设备文件”。次设备号则指明该设备是同种设备中的第几个。所以,只要找到代表着某个设备的索引节点,就知道该怎样读/写这个设备了。 既然是一个 “可访问” 的块设备,那为什么还要安装呢?答案是在安装之前可访问的只是这个设备, 通常是作为一个线性的无结构的字节流来访问的,称为 “原始设备” (raw device);而设备上的文件系统则是不可访问的。经过安装以后,设备上的文件系统就成为可访问的了。
读者也许已经想到了一个问题,那就是:系统调用 mount() 要求被安装的块设备在安装之前就是可访问的,那根设备怎么办?在安装根设备之前,系统中只有一个节点,根本就不存在可访问的块设备啊。是的,根设备不能通过系统调用 mount() 来安装。事实上,根据情况的不同,内核中有三个函数是用于设备安装的,那就是 sys_mount() 、mount_root() 以及 kem_mount() 。我们先来看 sys_mount(),这就是系统调用 mount() 在内核中的实现,其代码在 fs/super.c 中。
1、sys_mount
// fs/super.c // 参数dev_name为待安装设备的路径名;dir_name则是安装点(空闲目录节点)的路径名;type是 // 表示文件系统类型(即格式)的字符串,如 “ext2"、 “iso9660” 等。此外,flags为安装模式,有关的 // 标志位定义于 include/linux/fs.h // // 最后,指针 data 指向用于安装的附加信息,由不同文件系统的驱动程序自行加以解释,所以其类 // 型为 void 指针 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; // 代码中通过 getname() 和 copy_mount_options() 将字符串形式或结构形式的参数值从用户空间复制 // 到系统空间。这些参数值的长度均以一个页面为限,但是 getname() 在复制时遇到字符串结尾符 “\0” // 就停止,并返回指向该字符串的指针;而 copy_mount_options() 则拷贝整个页面(确切地说是 // PAGE_SIZE - 1 个字节),并且返回页面的起始地址。然后,就是这个操作的主体 do_mount() 了 // 我们分段来看 (super.c) 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; }
(1)mount flags
// include/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
例如,如果 MS_NOSUID 标志为 1,则整个系统中所有可执行文件的 suid 标志位就都不起作用了。 但是,正如原作者的注释所说,这些标志位并不是对所有文件系统都有效的。所有的标志位都在低 16 位中,而高 16 位则用作 “magic number” 。
(2)do_mount
// fs/super.c /* * Flags is a 16-bit value that allows up to 16 non-fs dependent flags to * be given to the mount() call (ie: read-only, no-dev, no-suid etc). * * data is a (void *) that can point to any structure up to * PAGE_SIZE-1 bytes, which can contain arbitrary fs-dependent * information (or be NULL). * * NOTE! As pre-0.97 versions of mount() didn't use this setup, the * flags used to have a special 16-bit magic number in the high word: * 0xC0ED. If this magic number is present, the high word is discarded. */ // 首先是对参数的检验。例如对于安装节点名就要求指针dir_name不为0,并且字符串的第一个字 // 符不为0,即不是空字符串,并且字符串的长度不超过一个页面。这里的 memchr() 在指定长度的缓冲 // 区中寻找指定的字符(这里是0),如果找不到就返回 0。对设备名dev_name的检验很有趣:如果 // dev_name 为非 0,则字符串的长度不得长于一个页面(实际上 copy_mount_options() 保证了这一点,因 // 为它拷贝 PAGE_SIZE—1 个字节),可是 dev_name 为0却是允许的。这似乎不可思议,下面读者将会 // 看到,在特殊情况下这确实是允许的。 long do_mount(char * dev_name, char * dir_name, char *type_page, unsigned long flags, void *data_page) { struct file_system_type * fstype; struct nameidata nd; struct vfsmount *mnt = NULL; struct super_block *sb; int retval = 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; /* OK, looks good, now let's see what do they want */ /* just change the flags? - capabilities are checked in do_remount() */ // 如果调用参数中的 MS_REMOUNT 标志位为1,就表示所要求的只是改变一个原已安装的设备的 // 安装方式。例如,原来是按 “只读” 方式来安装的,而现在要改为 “可写” 方式;或者原来的MS_NOSUID // 标志位为0,而现在要改变成1,等等。所以这种操作称为 “重安装”。函数 do_remount() 的代码也在 // super.c 中,读者可以在阅读了 do_mount() 的 “主流” 以后回过来自己读一下这个 “支流” 的代码。 // 另一个分支是对特殊设备如 /dev/loopback 等 “回接” 设备的处理。这种设备是特殊的,其实并不 // 是一种设备,而是一种机制。从系统的角度来看,它似乎是一种设备,但实际上它只是提供了一条 // “loopback” (回接)到某个可访问普通文件或块设备的手段。举例来说,系统的管理人员可以通过实 // 用程序 losetup,实际上是系统调用 ioctl(),建立起 /dev/loop0 与一个普通文件 /blkfile // 之间的联系,或者说将 /dev/loop0 “回接” 到 /blkfile,从而将这个文件当作一个块设备来使用: // losetup -e des /dev/loop0 /blkfile // 这里的可选项 -e des 表示在通过 /dev/loop0 读写作为虚拟块设备的 /blkfile 时要对内容加密,而加 // 密的算法则为DES (一种加密/解密标准)。也可以使用比较简单的加密算法XOR,此时可选项即为 // "-e xor"。如果不加密就不用 -e 可选项。回接以后,通过 /dev/loop0 访问的文件 /blkfile 就作为 // 一个 “块设备” 来使用了,所以也要加以格式化: // mkfs -t ext2 /dev/loop0 100 // 参数 -t ext2 表示按 Ext2 格式化,也可以改用其他文件系统的格式。参数100表示该设备的大小为 // 100个记录块。当然,文件 /blkfile 原来的大小要是够,并且其原来的内容就丢失了,所以一般可以先 // 建立起一个是够大的空文件: // dd if=/dev/zero of=/blkfile bs=lk count=100 // 回接的对象并不非得是一个普通文件,也可以是一个常规的块设备文件如/dev/hda2等。但是,以 // 普通文件为回接对象给我们提供了将它格式化成一个文件系统并加以安装的手段。我们在回接时采用 // 了加密,所以格式化以后的文件系统映象是加了密的,然后,就可以把这个虚拟的块设备安装到文件 // 系统中了: // mount -t ext2 /dev/loop0 /mnt // 从此以后,就跟一般己安装的子系统一样了,只是在我们这个例子中对这个子系统的读/写都加了密。 // 回接的对象还可以是一个已经安装的块设备。例如,/dev/hdal 已经安装在根节点/上,我们仍可 // 以把它作为回接的对象。此时当然不能再加密,也不能再格式化了,但是还可以通过 /dev/loop0 再安装 // 一次(在另外一个节点上),例如把它安装成 “只读” 方式。如果回忆一下,一个进程(例如某种网络 // 服务进程)可以通过系统调用设置自己的 “根” 目录,就不难想像这种 “回接” 设备对子系统安全性 // 可能有用处了。通常在/dev目录中有 /dev/loop0 和 /dev/loopl 两个回接设备文件,需要的话可以 // 通过 mknod 再创建,其上设备号为7。 // 对通过回接设备的安装,以前在mount命令行中有个 “-o loop” 可选项,现在则改成将命令行中 // 的文件类型加上一种 "bind",即 "-t bind",表示所安装的设备是个 “捆绑” 到另一个对象上的回接设 // 备。所以,如果flags中的 MS_BIND 标志位为 1 (见代码中的第1341行,),就调用 do_loopback() // 来完成回接设备的安装。我们暂且跳过它继续往下读(super.c)。 if (flags & MS_REMOUNT) return do_remount(dir_name, flags & ~MS_REMOUNT, (char *) data_page); /* "mount --bind"? Equivalent to older "mount -t bind" */ /* No capabilities? What if users do thousands of these? */ if (flags & MS_BIND) return do_loopback(dev_name, dir_name); /* For the rest we need the type */ if (!type_page || !memchr(type_page, 0, PAGE_SIZE)) return -EINVAL; #if 0 /* Can be deleted again. Introduced in patch-2.3.99-pre6 */ /* loopback mount? This is special - requires fewer capabilities */ if (strcmp(type_page, "bind")==0) return do_loopback(dev_name, dir_name); #endif /* for the rest we _really_ need capabilities... */ // 进一步的操作需要系统管理员的权限,所以先检查当前进程是否具有此项授权。一般超级用户进 // 程都是有这种授权的。 if (!capable(CAP_SYS_ADMIN)) return -EPERM; /* ... filesystem driver... */ // 系统支持的每一种文件系统都有一个file_system_type数据结构,定义于include/linux/fs.h: fstype = get_fs_type(type_page); if (!fstype) return -ENODEV; /* ... and mountpoint. Do the lookup first to force automounting. */ // 回到 do_mount() 的代码中。找到了给定文件系统类型的数据结构以后,就要寻找代表安装点的 // dentry 数据结构了。通过path_init() 和path_walk() 寻找目标节点的过程以前已经讲过,就不重复了。 // 找到了安装点的dentry结构(在nameidata结构nd中有个dentry指针)以后,要把待安装设备的 “超级 // 块” 读进来并根据超级块中的信息在内存中建立起相应的super_block数据结构。但是,这里因具体文 // 件系统的不同而有几种情形要区别对待: // (1)有些虚拟的文件系统(如pipe、共享内存区等),要由内核通过 kern_mount() 安装,而根本 // 不允许由用户进程通过系统调用 mount() 来安装。这样的文件系统类型在其 fs_flag 中的 // FS_NOMOUNT 标志位为1。虚拟文件系统类型的 “设备” 其实没有超级块,所以只是按特 // 定的内容初始化,或者说生成一个 super_block 结构。对于这种文件系统类型,系统调用 mount() // 时应出错返回. // (2) 一般的文件系统类型要求有物理的设备作为其物质基础,在其 fs_flag 中的 // FS_REQUIRES_DEV 标志位为 1,这些就是 “正常” 的文件系统类型,如ext2、minix、ufs // 等等。对于这些文件系统类型,通过 get_sb_bdev() 从待安装设备上读入其超级块。 // (3)有些虚拟文件系统在安装了同类型中的第一个 “设备” ,从而创建了超级块的 super_block 数 // 据结构以后,再安装同一类型中的其他设备时就共享已经存在的 super_block 结构,而不再有 // 其自己的超级块结构。此时相应 file_system_type 结构的 fs_flags 中的 FS_SINGLE 标志位为 1, // 表示整个文件系统类型只有一个超级块,而不像一般的文件系统类型那样每个具体的设备上 // 都有一个超级块。 //(4)还有些文件系统类型的 fs_flags 中的 FS_NOMOUNT 标志位、FS_REQUIRE_DEV 标志位以 // 及 FS_SINGLE 标志位全都为0,所以不属于上述三种情形中的任何一种。这些所谓 “文件系 // 统” 其实也是虚拟的,通常只是用来实现某种机制或者规程,所以根本就没有 “设备” 。对 // 于这样的 “文件系统类型” 都是通过 get_sb_nodev() 来生成一个 super_block 结构的。 // 总之,每种文件系统类型都有个 file_system_type 结构,而结构中的 fs_flags 则由各种标志位组成, // 这些标志位表明了具体文件系统类型的特性,也决定着这种文件系统的安装过程。内核代码中提供了 // 两个用来建立 file_system_type 数据结构的宏操作,其定义在 fs.h 中 if (path_init(dir_name, LOOKUP_FOLLOW|LOOKUP_POSITIVE|LOOKUP_DIRECTORY, &nd)) retval = path_walk(dir_name, &nd); if (retval) goto fs_out; /* get superblock, locks mount_sem on success */ // 以后读者会看一到,flags 中的 FS_STNGLE 标志位有着很重要的作用。我们在这里只关心常规文件 // 系统的安装,所以只阅读 get_sb_bdev() 的代码,以后我们会结合其他章节,如进程间通信和设备驱动, // 再来阅读 get_sb_single() 等函数的代码。顺便提一下,这里 get_sb_single() 和 get_sb_nodev() // 都不使用参数 dev_name,所以它可以是 NULL。这个函数的代码也在 fs/super.c中,我们分段阅读。 if (fstype->fs_flags & FS_NOMOUNT) sb = ERR_PTR(-EINVAL); else if (fstype->fs_flags & FS_REQUIRES_DEV) sb = get_sb_bdev(fstype, dev_name, flags, data_page); else if (fstype->fs_flags & FS_SINGLE) sb = get_sb_single(fstype, flags, data_page); else sb = get_sb_nodev(fstype, flags, data_page); retval = PTR_ERR(sb); if (IS_ERR(sb)) goto dput_out; /* Something was mounted here while we slept */ while(d_mountpoint(nd.dentry) && follow_down(&nd.mnt, &nd.dentry)) ; /* Refuse the same filesystem on the same mount point */ retval = -EBUSY; if (nd.mnt && nd.mnt->mnt_sb == sb && nd.mnt->mnt_root == nd.dentry) goto fail; retval = -ENOENT; if (!nd.dentry->d_inode) goto fail; down(&nd.dentry->d_inode->i_zombie); if (!IS_DEADDIR(nd.dentry->d_inode)) { retval = -ENOMEM; mnt = add_vfsmnt(&nd, sb->s_root, dev_name); } up(&nd.dentry->d_inode->i_zombie); if (!mnt) goto fail; retval = 0; unlock_out: up(&mount_sem); dput_out: path_release(&nd); fs_out: put_filesystem(fstype); return retval; fail: if (list_empty(&sb->s_mounts)) kill_super(sb, 0); goto unlock_out; }
① file_system_type
// include/linux/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 vfsmount *kern_mnt; /* For kernel mount, if it's FS_SINGLE fs */ struct file_system_type * next; };
结构中的 fs_flags 指明了具体文件系统的一些特性,有关的标志位定义见文件fs.h:
// include/linux/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; * kernel-wide vfsmnt is placed in ->kern_mnt by * kern_mount() which must be called _after_ * register_filesystem(). */ #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 */
对这些标志他的意义和作用我们将随着代码解释的进展加以说明。
结构中有个函数指针 read_super,各种文件系统通过这个指针提供用来读入其超级块的函数,因为不同文件系统的超级块也是不同的。显然,这个数据结构也是从虚拟文件系统 VFS 进入具体文件系统的一个转接点。同时,每种文件系统还有个字符串形式的文件系统类型名。
安装文件系统时要说明文件系统的类型,例如系统命令 mount 就有个可选项 “-t” 用于类型名。 文件系统的类型名以字符串的形式复制到 type_page 中,现在就用来比对、寻找其 file_system_type 数据结构。
函数 get_fs_type() 根据具体文件系统的类型名在内核中找到相应的 file_system_type 结构,有关的代码在 super.c 中。
② get_fs_type
// fs/super.c struct file_system_type *get_fs_type(const char *name) { struct file_system_type *fs; read_lock(&file_systems_lock); fs = *(find_filesystem(name)); if (fs && !try_inc_mod_count(fs->owner)) fs = NULL; read_unlock(&file_systems_lock); if (!fs && (request_module(name) == 0)) { read_lock(&file_systems_lock); fs = *(find_filesystem(name)); if (fs && !try_inc_mod_count(fs->owner)) fs = NULL; read_unlock(&file_systems_lock); } return fs; } // ======================================================================= 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; }
内核中有一个 file_system_type 结构队列,叫做 file_systems,队列中的每个数据结构都代表着一种文件系统。系统初始化时将内核支持的各种文件系统的 file_system_type 数据结构通过一个函数 register_filesystem() 挂入这个队列,这个过程称为文件系统的注册。除此之外,对有些文件系统的支持可以通过 “可安装模块” 的方式来实现。在装入这些模块时,也会将相应的数据结构注册挂入该队列中。
函数 find_filesystem() 则扫描 file_systems 队列,找到所需文件系统类型的数据结构。在 file_system_type 结构中有一个指针 owner,如果结构所代表的文件系统类型是通过可安装模块实现的, 则该指针指向代表着具体模块的 module 结构。找到了 file_system_type结构以后,要调用 try_inc_mod_count() 看看该文件系统是否由可安装模块实现,是的话就要递增相应 module 结构中的共享计数,因为现在这个模块多了一个使用者。
要是在 file_systems 队列中找不到所需的文件系统类型怎么办呢?那就通过request_module() 试试能否(在已安装的文件系统中)找到用来实现所需文件系统类型的可安装模块,并将其装入内核;如果成功的话就再去 file_systems 队列中找一遍。如果装入所需的可安装模块失败,或者装入以后还是找不到相应的 file_system_type 结构,那就说明 Linux 系统不支持所要求的文件系统类型。有关模块的装入可参考 “设备驱动” 一章。
③ 文件系统定义
// include/linux/fs.h #define DECLARE_FSTYPE(var,type,read,flags) \ struct file_system_type var = { \ name: type, \ read_super: read, \ fs_flags: flags, \ owner: THIS_MODULE, \ } #define DECLARE_FSTYPE_DEV(var,type,read) \ DECLARE_FSTYPE(var,type,read,FS_REQUIRES_DEV) // ===================================================================== // fs/ext2/super.c static DECLARE_FSTYPE_DEV(ext2_fs_type, "ext2", ext2_read_super); // ===================================================================== // fs/umsdos/inode.c static DECLARE_FSTYPE_DEV(umsdos_fs_type, "umsdos", UMSDOS_read_super); // ===================================================================== // fs/pipe.c static DECLARE_FSTYPE(pipe_fs_type, "pipefs", pipefs_read_super, FS_NOMOUNT|FS_SINGLE);
④ get_sb_bdev
// fs/super.c static struct super_block *get_sb_bdev(struct file_system_type *fs_type, char *dev_name, int flags, void * data) { struct inode *inode; struct block_device *bdev; struct block_device_operations *bdops; struct super_block * sb; struct nameidata nd; kdev_t dev; int error = 0; /* What device it is? */ // 对于常规的文件系统,参数 dev_name 必须是一个有效的路径名。同样,这里也是通过 path_init() // 和 path_walk() 找到目标节点,即相应设备文件的 dentry 结构以及 inode 结构。当然,找到的 // inode 结构必须是代表着一个块设备,其 i_mode 中的 S_IFBLK 标志位必须为 1,否则就错了。 // 宏操作 S_ISBLK() 定义于 include/linux/stat.h: // // 设备文件的 inode 结构是在 path_walk() 中根据从已经安装的磁盘上(或其他已安装的文件系统中) // 读入的索引节点建立的。对于Ext2文件系统,我们在 “从路径名到目标节点” 一节中阅读 path_walk() // 的代码时曾在它所辗转调用的 ext2_read_inode() 中看到这么一段代码: if (!dev_name || !*dev_name) return ERR_PTR(-EINVAL); if (path_init(dev_name, LOOKUP_FOLLOW|LOOKUP_POSITIVE, &nd)) error = path_walk(dev_name, &nd); if (error) return ERR_PTR(error); inode = nd.dentry->d_inode; error = -ENOTBLK; if (!S_ISBLK(inode->i_mode)) goto out; error = -EACCES; if (IS_NODEV(inode)) goto out; // 在block_device结构中有个指针bd_op,指向一个block_device_operations数据结构,这就是块设 // 备驱动程序的函数跳转表。所以,我们可以把block_device结构比喻为块设备驱动 “总线” ,而使其指 // 针bd_op指向某个具体的block_device_operations数据结构,就好像是将一块"接口卡"插入了总线的 // 插槽,这跟VFS与具体文件系统的关系是一样的。 // 那么,这里要把什么样的 “接口卡” 插到总线上去呢?原来,在Linux的设备驱动方面正在进行 // 着一项称为 “devfs” 的改革。传统的/dev目录是一种 “平面” 结构而不像其他目录那样是树状结构。 // 每一个设备都有个 “主设备号” 和一个 “次设备号” ,每当要在/dev中建立一个节点(即设备文件)时 // 就要将主、次设备号合成一个单一的 “设备号” ,再通过系统调用mknod()来建立,传统的主、次设备 // 号都是8位的,所以每种设备最多只能有255个。随着技术的发展,这个限制开始成为问题了。所以 // Linux内核已经开始使用16位的主、次设备号。可是,另有一派意见认为,/dev的这种平面结构和主、 // 次设备号的使用根本就应该改革。也就是说,把/dev改成树状结构,这样一来路径名就可以惟一地确 // 定一个设备的类型和序号,例如/dev/hda/1,这样就可以把主、次设备号隐藏在路径名的背后,不需要 // 在用户界面上用什么主设备号、次设备号了。目前这项改革正在进行中,对有些设备(如软盘、磁带 // 等)的支持已开始使用这种新的方案。但是,内核必须同时支持新、旧两种方案,这里对 devfs() 和 // devfs_get_handle_form_inode() 就是出于对devfs的考虑。目前(以及在未来相当一段时期内),对多 // 数块设备的支持还会沿用传统的模式,如果尚不支持devfs则这两个函数都返回NULL而不起作用, // 相当于让插槽暂时空着。我们在 “设备驱动” 一章中还要回到devfs这个话题上。另一方面,由于在内 // 核中已经开始使用16位的主、次设备号,而在大多数文件系统中都还是8位的,所以要通过to_kdev_t() // 加以转换。 // 完成了上面的这些准备以后,现在要进行实质性的工作,就是找到或建立待安装设备的 super_block // 数据结构了。首先还是在内核中寻找,内核中维持着一个super_block数据结构的队列super_blocks, // 所有的super_block结构,包括空闲的,都通过结构中的一个队列头s_list链入到这个队列中。寻找时 // 就通过 get_super() 从队列中寻找,其代码在 fs/super.c 中。 bdev = inode->i_bdev; bdops = devfs_get_ops ( devfs_get_handle_from_inode (inode) ); if (bdops) bdev->bd_op = bdops; /* Done with lookups, semaphore down */ down(&mount_sem); dev = to_kdev_t(bdev->bd_dev); sb = get_super(dev); if (sb) { if (fs_type == sb->s_type && ((flags ^ sb->s_flags) & MS_RDONLY) == 0) { path_release(&nd); return sb; } } else { mode_t mode = FMODE_READ; /* we always need it ;-) */ if (!(flags & MS_RDONLY)) mode |= FMODE_WRITE; error = blkdev_get(bdev, mode, 0, BDEV_FS); if (error) goto out; check_disk_change(dev); error = -EACCES; if (!(flags & MS_RDONLY) && is_read_only(dev)) goto out1; error = -EINVAL; sb = read_super(dev, bdev, fs_type, flags, data, 0); if (sb) { get_filesystem(fs_type); path_release(&nd); return sb; } out1: blkdev_put(bdev, BDEV_FS); } out: path_release(&nd); up(&mount_sem); return ERR_PTR(error); }
⑴ ext2_read_inode
// fs/ext2/inode.c if (inode->i_ino == EXT2_ACL_IDX_INO || inode->i_ino == EXT2_ACL_DATA_INO) /* Nothing to do */ ; else if (S_ISREG(inode->i_mode)) { inode->i_op = &ext2_file_inode_operations; inode->i_fop = &ext2_file_operations; inode->i_mapping->a_ops = &ext2_aops; } else if (S_ISDIR(inode->i_mode)) { inode->i_op = &ext2_dir_inode_operations; inode->i_fop = &ext2_dir_operations; } else if (S_ISLNK(inode->i_mode)) { if (!inode->i_blocks) inode->i_op = &ext2_fast_symlink_inode_operations; else { inode->i_op = &page_symlink_inode_operations; inode->i_mapping->a_ops = &ext2_aops; } } else init_special_inode(inode, inode->i_mode, le32_to_cpu(raw_inode->i_block[0]));
由于设备文件既不是常规文件,也不是目录,更不是符号连接,所以必然会调用 init_special_inode(), 其代码在 fs/devices.c 中
⑵ init_special_inode
// fs/devices.c void init_special_inode(struct inode *inode, umode_t mode, int rdev) { inode->i_mode = mode; if (S_ISCHR(mode)) { inode->i_fop = &def_chr_fops; inode->i_rdev = to_kdev_t(rdev); } else if (S_ISBLK(mode)) { inode->i_fop = &def_blk_fops; inode->i_rdev = to_kdev_t(rdev); inode->i_bdev = bdget(rdev); } else if (S_ISFIFO(mode)) inode->i_fop = &def_fifo_fops; else if (S_ISSOCK(mode)) inode->i_fop = &bad_sock_fops; else printk(KERN_DEBUG "init_special_inode: bogus imode (%o)\n", mode); }
以前说过,在 inode 数据结构中有两个设备号。一个是索引节点所在设备的号码i_dev,另一个是索引节点所代表的设备的号码 i_rdev 。可是,如果看一下存储在设备上的索引节点 ext2_inode 数据结构, 就可以发现里面一个专门用于设备号的字段也没有。首先,既然索引节点存储在某个设备上,当然就不需要再在里面说明存储在哪个设备上了。再说,一个索引节点如果代表着一个设备,那就不需要记录跟文件的物理信息有关的数据了,从而可以利用这些空间来记录所代表设备的设备号。事实上,当索引节点代表着设备时,其 ext2_inode 数据结构中的数组 i_block[] 空着没用,所以就将 i_block[0] 用于设备号。这个设备号在这里的 init_special_node() 中经过 to_kdev_t() 加以格式转换以后就变成 inode 结构中的 i_rdev。此外,对于块设备还要使 inode 结构中的指针 i_bdev 指向一个 block_device 结构。具体的数据结构由 bdget() 根据设备号寻找或创建,详见 “设备驱动” 一章中有关的内容。
⑶ get_super
// fs/super.c /** * get_super - get the superblock of a device * @dev: device to get the superblock for * * Scans the superblock list and finds the superblock of the file system * mounted on the device given. %NULL is returned if no match is found. */ struct super_block * get_super(kdev_t dev) { struct super_block * s; if (!dev) return NULL; restart: s = sb_entry(super_blocks.next); while (s != sb_entry(&super_blocks)) if (s->s_dev == dev) { wait_on_super(s); if (s->s_dev == dev) return s; goto restart; } else s = sb_entry(s->s_list.next); return NULL; }
这里的 sb_entry() 是个宏操作,定义于 include/linux/fs.h:
#define sb_entry(list) list_entry((list), struct super_block, s_list)
读者也许会问,这是否意味着同一个块设备可以安装多次?答案是可以的,例如我们在前面曾经讲到通过 “回接设备” 进行的安装,那就是同一设备的多次安装。
然而,在大多数情况下 get_super() 实际上都会失败,因而得从设备读入其超级块并在内存中建立起该设备的 super_block 数据结构。为了这个目的,先得要 “打开” 这个设备文件,这是由 blkdev_get() 完成的,其代码在fs/block_dev,c中
⑷ blkdev_get
// fs/block_dev.c int blkdev_get(struct block_device *bdev, mode_t mode, unsigned flags, int kind) { int ret = -ENODEV; kdev_t rdev = to_kdev_t(bdev->bd_dev); /* this should become bdev */ down(&bdev->bd_sem); if (!bdev->bd_op) bdev->bd_op = get_blkfops(MAJOR(rdev)); if (bdev->bd_op) { /* * This crockload is due to bad choice of ->open() type. * It will go away. * For now, block device ->open() routine must _not_ * examine anything in 'inode' argument except ->i_rdev. */ struct file fake_file = {}; struct dentry fake_dentry = {}; struct inode *fake_inode = get_empty_inode(); ret = -ENOMEM; if (fake_inode) { fake_file.f_mode = mode; fake_file.f_flags = flags; fake_file.f_dentry = &fake_dentry; fake_dentry.d_inode = fake_inode; fake_inode->i_rdev = rdev; ret = 0; if (bdev->bd_op->open) ret = bdev->bd_op->open(fake_inode, &fake_file); if (!ret) atomic_inc(&bdev->bd_openers); else if (!atomic_read(&bdev->bd_openers)) bdev->bd_op = NULL; iput(fake_inode); } } up(&bdev->bd_sem); return ret; }
由于 block_device 结构中的 bd_dev 有可能还在使用 8 位的主、次设备号,或者说 16 位的设备号, 这里先通过 to_kdev_t() 将它们换成 16 位(或者说 32 位的设备号)。前面讲过,block_device 结构中的指针 bd_op 指向一个 block_device_operations 数据结构。对于 devfs 的设备这个指针已经在前面设置好了,而对于传统的块设备则这个指针尚未设置,暂时还空着,所以要通过 get_blkfops() 根据设备的主设备号来设置这个指针。函数 get_blkfops() 的代码也在 fs/block_dev.c 中。
⑤ vfsmount
// include/linux/mount.h struct vfsmount { struct dentry *mnt_mountpoint; /* dentry of mountpoint */ struct dentry *mnt_root; /* root of the mounted tree */ struct vfsmount *mnt_parent; /* fs we are mounted on */ struct list_head mnt_instances; /* other vfsmounts of the same fs */ struct list_head mnt_clash; /* those who are mounted on (other */ /* instances) of the same 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; char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */ struct list_head mnt_list; uid_t mnt_owner; };
结构中主要成分的作用为:
指针 mnt_mountpoint 指向安装点的 dentry 数据结构,而指针 mount_root 则指向所安装设备上根目录的 dentry 数据结构,在二者之间搭起一座桥梁。
可是,在 dentry 结构中却没有直接指向 vfsmount 数据结构的指针,而是有个队列头 d_vfsmount, 这是因为安装点和设备之间是一对多的关系,在同一个安装点上可以安装多个设备。相应地, vfsmount 结构中也有个队列头 mnt_clash,通过它链入到安装点的 d_vfsmount 队列中。不过, 从所安装设备上根目录的 dentry 数据结构出发却不能直接找到其 vfsmount 结构,而得要通过其 super_block 数据结构中转。
指针 mnt_sb 指向所安装设备的超级块的 super_block 数据结构。反之,在所安装设备的 super_block 数据结构中却并没有直接指向 vfsmount 数据结构的指针,而是有个队列头 s_mounts,因为设备与安装点之间也是一对多的关系,同一个设备可以安装到多个安装点上。 相应地,vfsmount 结构中也有个队列头 mnt_instances,通过它链入到设备的 s_mounts 队列中。
指针 mnt_parent 指向安装点所在设备当初安装时的 vfsmount 数据结构,就是上一层的 vfsmount 数据结构。不过,在根设备或其他不存在上一层 vfsmount 数据结构的情况下,这个指针指向该数据结构本身。同时,vfsmount 数据结构中还有 mnt_child 和 mnt_mounts 两个队列头,只要上一层的 vfsmount 数据结构存在,就通过 mnt_child 链入上一层 vfsmount 结构的 mnt_mounts 队列中。这样,就形成一种设备安装的树形结构,从一个 vfsmount 结构的 mnt_mounts 队列开始可以找到所有直接或间接安装在这个设备上(的文件系统中)的其他设备。
此外,系统中还有个总的 vfsmount 结构队列 vfsmntlist,相应地 vfsmount 数据结构中还有个队列头 mnt_list。所有已安装设备的 vfsmount 结构都通过 mnt_list 链入 vfsmntlist 队列中。
⑥ 导图
⑦ 导图
kern_mount
Linux 内核源代码情景分析(四)(中):https://developer.aliyun.com/article/1598003