linux文件系统-文件系统的安装与拆卸

在一块设备上按一定的格式建立起文件系统的时候,或者系统引导之初,设备上的文件和节点都还是不可访问的。也就是说,还不能按一定的路径名访问其中特定的节点或文件(虽然设备是可访问的)。只有把它安装到计算机系统的文件系统中的某个节点上,才能使设备上的文件和节点成为可访问的。经过安装以后,设备上的文件系统就成为整个文件系统的一部分,或者说一个子系统。一般而言,文件系统的结构就好像一棵倒立的树,不过由于可能存在着的节点间的链接和符号链接不并不一定是严格的图论意义上的一棵树。最初,整个系统只有一个节点,那就是整个文件系统的根节点"/",这个节点是在内存中的,而不在任何具体的设备上。系统在初始化时将一个根设备安装到节点"/",这个设备上的文件系统就成了整个系统中原始的、基本的文件系统(所以才称为根设备)。此后,就可以由超级用户进程通过系统调用mount把其他的子系统安装到已经存在于文件系统中的空闲节点上,使整个文件系统得以扩展,当不再需要使用某个子系统时,或者在关闭系统之前,则通过系统调用umount把已经安装的设备逐个拆卸下来。

系统调用mount将一个可访问的块设备安装到一个可访问的节点上。所谓可访问是指该节点或文件已经存在于已安装的文件系统中,可以通过路径名寻访。Linux将设备看作一种特殊的文件,并在文件系统中有代表着具体设备的节点。称为设备文件,通常都在目录/dev下面。例如ide硬盘上的第一个分区就是/dev/hda1。每个设备文件实际上只有一个索引节点,节点中提供了设备的设备号,有主设备号和次设备号两部分构成。其中主设备号指明了设备的种类,或者更确切的说指明了应该使用哪一组驱动程序。同一个物理的设备,如果有两组不同的驱动程序,在逻辑上就被看作两种不同的设备而在文件系统中有两个不同的设备文件。次设备号则指明该设备是同种设备中的第几个。所以,只要找到代表着某个设备的索引节点,就知道该怎样读、写这个设备了。既然是一个可访问的块设备,那为什么还要安装呢?答案是在安装之前可访问的只是这个设备,通常是作为一个线性的无结构的字节流来访问的,称为原始设备(raw device),而设备上的文件系统则是不可访问的。经过安装后,设备上的文件系统就成为可访问的了。

读者也许已经想到了一个问题,那就是:系统调用mount要求被安装的块设备在安装之前就是可访问到的,那根设备怎么办?在安装根设备之前,系统中只有一个"/"节点,根本就不存在可访问的块设备呀。是的,根设备不能通过系统调用mount来安装。事实上,根据情况的不同,内核中有多个函数用于设备安装的,那就是sys_mount、mount_root以及kern_mount。我们先来看sys_mount,这就是系统调用mount在内核中的实现:


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"等。flags为安装模式,有关的标志位定义如下:

 * 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

/*
 * Flags that can be altered by MS_REMOUNT
 */
#define MS_RMT_MASK	(MS_RDONLY|MS_NOSUID|MS_NODEV|MS_NOEXEC|\
			MS_SYNCHRONOUS|MS_MANDLOCK|MS_NOATIME|MS_NODIRATIME)

/*
 * Magic mount flag number. Has to be or-ed to the flag values.
 */
#define MS_MGC_VAL 0xC0ED0000	/* magic flag number to indicate "new" flags */
#define MS_MGC_MSK 0xffff0000	/* magic flag number mask */

例如,如果MS_NOSUID标志为1,则整个系统中所有可执行文件的suid标志位就都不起作用了。但是,正如原作者的注释所说,这些标志位并不是对所有文件系统都有效的。所有的标志位都在低16位中,而高16位则用作“magic number”。

最后,指针data指向用于安装的附加信息,由于不同文件系统的驱动程序自行加以解释,所以其类型为void指针。

代码中通过getname和copy_mount_options将字符串形式或结构形式的参数值从用户空间复制到系统空间。这些参数的长度均以一个页面为限,但是getname在复制时遇到字符串结构符'\0'就停止,并返回指向该字符串的指针;而copy_mount_options则拷贝整个页面(确切的说是PAGE_SIZE-1个字符),并且返回页面的起始地址。然后就是这个操作的主体do_mount了。下面分段来看其实现。

sys_mount=>do_mount

/*
 * 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.
 */
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() */
	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);

首先对参数的检查。例如对于安装节点名就要求指针dir_name不为0,并且字符串的第一个字符不为0,即不是空字符串,并且字符串的长度不超过一个页面。这里的memchr在指定长度的缓冲区中寻找指定的字符(这里是0),如果找不到就返回0。对设备名dev_name的检查很有趣:如果dev_name为非0,则字符串的长度不得长于一个页面(实际上copy_mount_options保证了这一点,因为它拷贝PAGE_SIZE-1个字符),可是dev_name为0却是允许的。这似乎不可思议,下面我们会看到,在特殊情况下这确实是允许的。

如果调用参数中的MS_REMOUNT    标志位为1,就表示所要求的只是改变一个原已安装的设备的安装方式。例如,原来是按只读方式来安装的,而现在要改为可写方式;或者原来的MS_NOSUID标志位为0,而现在改成1,等等。所以这种操作称为重安装。函数do_remount的代码也在super.c中,读者可以在阅读了do_mount的主流代码以后再回过头来读这个分支的代码。

另一个分支是对特殊设备如/dev/loopback等回接设备的处理。这种设备是特殊的,其实并不是一种设备,而是一种机制。从系统的角度来看,它似乎是一种设备,但实际上他只是提供了一条loopback到某个可访问普通文件或块设备的手段。

	/* 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... */
	fstype = get_fs_type(type_page);
	if (!fstype)		
		return -ENODEV;

	/* ... and mountpoint. Do the lookup first to force automounting. */
	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 */
	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;

进一步操作需要系统管理员的权限,所以先检查当前进程是否具有此项授权。一般超级用户进程都是有这样的授权的。

系统支持的每一种文件系统都有一个file_system_type数据结构:

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指明了具体文件系统的一些特性,有关的标志位定义如下:

/* 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结构,有关的代码如下:

sys_mount=>do_mount=>get_fs_type

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;
}

sys_mount=>do_mount=>get_fs_type=>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;
}

内核中有一个 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系统不支持所需要的文件系统类型。有关模块的装入其他博客会讲。

回到do_mount的代码中。找到了给定文件系统类型的数据结构以后,就要寻找代表安装点的dentry结构了。通过path_init和path_walk寻找目标节点的过程在linux文件系统--从路径名到目录节点已经讲过,就不重复了。找到了安装点的dentry结构(在nameidata结构nd中有个dentry指针)以后,要把待安装设备的超级块读进来并根据超级块中的信息在内存中建立起相应的super_block数据结构。但是,这里因为具体文件系统的不同而有几种情形要区别对待:

  1. 有些虚拟文件系统(如pipe、共享内存区等),要由内核通过kern_mount安装,而根本不允许由用户进程通过系统调用mount来安装。这样的文件系统类型在其fs_flags中的FS_NOMOUNT标志位为1。虚拟文件系统类型的设备其实没有超级块,所以只是按特定的内容初始化,或者说生成一个super_block结构。对于这种文件系统类型,系统调用mount时应出错返回。
  2. 一般的文件系统类型要求有物理的设备作为其物质基础,在其fs_flags中的FS_REQUIRES_DEV标志位为1,这些就是正常的文件系统类型,如ext2、minix、ufs等等。对于这些文件系统类型,通过get_sb_bdev从待安装设备上读入其超级块。
  3. 有些虚拟文件系统在安装了同类型中第一个设备,从而创建了超级块super_block数据结构以后,再安装同一类型的其他设备时就共享已经存在的super_block的FS_SINGLE标志位为1.表示整个文件系统类型只有一个超级块,而不像一般的文件系统类型那样每个具体的设备上都有一个超级块。
  4. 还有些文件系统类型的fs_flags中的FS_NOMOUNT标志位、FS_REQUIRES_DEV以及FS_SINGLE标志位全部为0,所以不属于上列三种情形中的任何一种。这些所谓文件系统其实也是虚拟的,通过只是用来实现某种机制或者规程,所以根本就没有设备。对于这样的文件系统类型都是通过get_sb_nodev来生成一个super_block结构的。

总之,每种文件系统类型都有个file_system_type结构,而结构中的fs_flags则由各种标志位组成,这些标志位表明了具体文件系统类型的特性,也决定了这种文件系统的安装过程。内核代码中提供了两个用来建立file_system_type数据结构的宏操作:

#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)

一般常规文件系统类型都通过DECLARE_FSTYPE_DEV建立其数据结构,因为它们的FS_REQUIRES_DEV标志位为1,而其他标志位为0,例如:

static DECLARE_FSTYPE_DEV(ext2_fs_type, "ext2", ext2_read_super);

相比之下,特殊的、虚拟的文件系统类型则大多直接通过DECLARE_FSTYPE建立其数据结构,因为它们的fs_flags是特殊的,例如

static DECLARE_FSTYPE(proc_fs_type, "proc", proc_read_super, FS_SINGLE);

后面我们会看到,flags中的FS_SINGLE标志位有着很重要的作用。我们在这里只关心常规文件系统的安装,所以我们先阅读get_sb_bdev的代码,后面的博客例如proc文件系统、进程间通信和设备驱动,再来阅读get_sb_single等函数的代码。顺便提一下,这里的get_sb_single和get_sb_nodev都不使用参数dev_name,所以它可以是NULL。get_sb_bdev函数我们分段来阅读:

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

对于常规的文件系统,参数dev_name必须是一个有效的路径名。同样,这里也是通过path_init和path_walk找到目标节点,即相应设备文件的dentry结构以及inode结构。当然,找到的inode结构必须是代表着一个块设备,其i_moode的中S_IFBLK标志位必须为1,否则就错了。宏操作S_ISBLK定义如下:

#define S_ISBLK(m)	(((m) & S_IFMT) == S_IFBLK)

设备文件的inode结构是path_walk中根据从已经安装的磁盘上(或者其他已安装的文件系统中)读入的索引节点建立的。对于ext2文件系统,我们在前面的linux文件系统--从路径名到目录节点中阅读了path_walk的代码时曾在它所辗转调用的ext2_read_inode中看到这么一段代码

path_walk=>real_lookup=>ext2_lookup=>iget=>get_new_inode=>ext2_read_inode

	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,其代码如下:

path_walk=>real_lookup=>ext2_lookup=>iget=>get_new_inode=>ext2_read_inode=>init_special_inode

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数据结构,就可以发现里面一个专门用于设备号的字段都没有。首先,既然索引节点存储在某个设备上,当然就不需要再在里面说明存储在哪个设备上了。再说,一个索引节点如果代表一个设备,那就不需要记录跟文件的物理信息有关的数据了(i_block),从而可以利用这些空间来记录所代表设备的设备号。事实上,当索引节点代表着设备时,其ext2_inode数据结构中数组i_block空着没用,所以就将i_block[0]用于记录设备号。这个设备号在这里的init_special_inode中经过to_kdev_t加以格式转换就变成inode结构中的i_rdev。此外,对于块设备还要使inode结构中的指针i_bdev指向一个block_device结构,具体的数据结构由bdget根据设备号寻找或创建,详见后面的设备驱动的博客。

回到get_sb_bdev代码中:

	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);
}

在block_device结构中有个指针bd_op,指向一个block_device_operations数据结构,这就是块设备驱动程序的函数跳转表。所以,我们可以把block_device结构比喻成块设备驱动总线,而使其指针bd_op指向某个具体的block_device_operations数据结构,就好像将一块接口卡插入总线的插槽,这跟VFS与具体文件系统的关系是一样的。

目前要进行实质性的工作,就是找到或建立待安装设备的super_block数据结构了。首先还是在内核中寻找,内核中维持着一个super_block数据结构的队列super_blocks,所有的super_block结构,包括空闲的,都通过结构中的一个队列头s_list链入到这个队列中,寻找时就通过get_super从队列中寻找,代码如下:


/**
 *	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是个宏操作,定义如下:

#define sb_entry(list)	list_entry((list), struct super_block, s_list)

我们可能会问,这个是否意味着同一个块设备可以安装多次?答案是可以的,例如我们在前面曾经讲到通过回接设备进行的安装,那就是同一个设备的多次安装,

然而,在大多数情况下get_super会失败,因而得从设备读入其超级块并在内存中建立起设备的super_block数据结构。为了这个目的,先得要打开这个设备文件,这是由blkdev_get完成的,其代码如下:


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的代码如下:

sys_mount=>do_mount=>get_sb_bdev=>blkdev_get=>get_blkfops


/*
	Return the function table of a device.
	Load the driver if needed.
*/
const struct block_device_operations * get_blkfops(unsigned int major)
{
	const struct block_device_operations *ret = NULL;

	/* major 0 is used for non-device mounts */
	if (major && major < MAX_BLKDEV) {
#ifdef CONFIG_KMOD
		if (!blkdevs[major].bdops) {
			char name[20];
			sprintf(name, "block-major-%d", major);
			request_module(name);
		}
#endif
		ret = blkdevs[major].bdops;
	}
	return ret;
}

内核中设置了一个以主设备号为下标的结构数组blkdevs,用来保存指向各种块设备的block_device_operations结构的指针:

static struct {
	const char *name;
	struct block_device_operations *bdops;
} blkdevs[MAX_BLKDEV];

系统初始化时将所支持的各种块设备的block_device_operations结构指针填入该数组中的相应的元素中。以可安装模块实现的设备驱动程序则在装入模块时才设置相应的指针。所以,如果相应表项的bdops为0,则表明该设备可能是以可安装模块实现的,但是尚未装入,因此要调用request_module将其装入。在正常情况下,当从get_blkfops返回时指针bdev->bd->op已经设置好了,就好像接口卡已经插入总线了。

为了打开设备,还需要使用几个临时的数据结构,包括file结构、dentry结构以及inode结构。这里要指出,我们现在要打开的是作为文件的设备本身,而不是这个设备在文件系统中的代表如/dev/hda1等节点,那早已经打开了,要不然就无从知道其主设备号和此设备号了。打开设备的操作是通过由具体设备类型的block_device_operations结构中的函数指针open提供的。就一般的ide磁盘而言,其数据结构为bd_fops,而相应的函数指针则指向bd_open。我们在这里不深入下去。

打开了设备,blkdev_get也就完成了。回到get_sb_bdev中,还要做一些检查。有些设备的介质是活动的,可以由用户替换的(例如软盘),对于这样的设备要检查一下其介质是否已经变动了(如果原来已经安装的话)。我们在这里只关心固定介质磁盘,所以就不深入到check_disk_change的代码中去了。最后,还有一项,那就是如果安装的模式不是只读而所欲安装的设备却已经设置成了只读,那就不能安装了。

打开了具体的设备以后,就要通过read_super从设备上读入超级块并在内存中建立super_block结构了,其代码如下:

sys_mount=>do_mount=>get_sb_bdev=>read_super


static struct super_block * read_super(kdev_t dev, struct block_device *bdev,
				       struct file_system_type *type, int flags,
				       void *data, int silent)
{
	struct super_block * s;
	s = get_empty_super();
	if (!s)
		goto out;
	s->s_dev = dev;
	s->s_bdev = bdev;
	s->s_flags = flags;
	s->s_dirt = 0;
	sema_init(&s->s_vfs_rename_sem,1);
	sema_init(&s->s_nfsd_free_path_sem,1);
	s->s_type = type;
	sema_init(&s->s_dquot.dqio_sem, 1);
	sema_init(&s->s_dquot.dqoff_sem, 1);
	s->s_dquot.flags = 0;
	lock_super(s);
	if (!type->read_super(s, data, silent))
		goto out_fail;
	unlock_super(s);
	/* tell bdcache that we are going to keep this one */
	if (bdev)
		atomic_inc(&bdev->bd_count);
out:
	return s;

out_fail:
	s->s_dev = 0;
	s->s_bdev = 0;
	s->s_type = NULL;
	unlock_super(s);
	return NULL;
}

先从super_block队列中找到一个空闲的super_block结构,进行一些简单的初始化以后就要根据具体设备上的文件系统类型读入超级块。如前所述,在代表着具体文件系统类型的file_system_type数据结构中有个函数指针read_super指向具体的函数。对于ext2文件系统,其数据结构为ext2_fs_type,而相应的函数指针则指向ext2_read_super。函数ext2_read_super的代码相当大,逻辑独立,先放一放。继续往下看。

从设备上读入超级块并设置好super_block结构以后,get_sb_bdev的工作就完成了,只是返回前可能需要递增用来实现此种文件系统类型的可安装模块的使用者计数:

sys_mount=>do_mount=>get_sb_bdev=>get_filesystem

/* WARNING: This can be used only if we _already_ own a reference */
static void get_filesystem(struct file_system_type *fs)
{
	if (fs->owner)
		__MOD_INC_USE_COUNT(fs->owner);
}

此外,还要通过path_release释放在path_walk中占用的dentry结构(代表着待安装设备)和可能的vfsmount结构。

回到do_mount的代码继续往下看:
 

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

待安装设备的super_block结构已经解决了,这一边已经没有什么问题了,现在要回过头来看安装点这一边了。前面,在处理待安装设备的超级块之前,已经通过path_init和path_walk找到了安装点的dentry结构、inode结构以及vfsmount结构,通过局部变量nameidata数据结构nd就可以访问到这些数据结构。但是还有一种情况需要考虑。

首先,前面从设备上读入超级块的过程是个颇为漫长的过程,当前进程在等待从设备上读入的过程中几乎可以肯定要进入睡眠,这样就可能会有另一个进程捷足先登抢先将另一个设备安装到了同一个安装点上。要知道是否发生了这种情况,可以通过d_mountpoint来检测:

sys_mount=>do_mount=>d_mountpoint

static __inline__ int d_mountpoint(struct dentry *dentry)
{
	return !list_empty(&dentry->d_vfsmnt);
}

如果代表着安装点的dentry结构中的d_vfsmnt队列非空,那就说明已经有设备安装在上面了。在这种情况下怎么办呢?我们从代码中看到其对策是调用follow_down前进到已安装设备上的根节点,并且要通过while循环进一步检测新的安装点,直到尽头,即前进到不再有设备安装的某个设备上的根节点为止。已安装设备的根目录下一般都是有内容的。是否可以把一个设备安装到一个非空的目录节点下呢?可以的。这一点可能和我们的直觉和想象不同。将一个设备安装到一个有内容的目录节点下时,该节点就变成了一个纯粹的安装点,原来目录中的内容就变成了不可访问的。当然,我们应该尽量避免这种情况,技术上是可以实现的。

sys_mount=>do_mount=>follow_down

int follow_down(struct vfsmount **mnt, struct dentry **dentry)
{
	return __follow_down(mnt,dentry);
}

这个函数只是将inline的__follow_down封装起来,作为一个普通的函数。类似的情况我们之前也见过。

sys_mount=>do_mount=>follow_down=>__follow_down

static inline int __follow_down(struct vfsmount **mnt, struct dentry **dentry)
{
	struct list_head *p;
	spin_lock(&dcache_lock);
	p = (*dentry)->d_vfsmnt.next;
	while (p != &(*dentry)->d_vfsmnt) {
		struct vfsmount *tmp;
		tmp = list_entry(p, struct vfsmount, mnt_clash);
		if (tmp->mnt_parent == *mnt) {
			*mnt = mntget(tmp);
			spin_unlock(&dcache_lock);
			mntput(tmp->mnt_parent);
			/* tmp holds the mountpoint, so... */
			dput(*dentry);
			*dentry = dget(tmp->mnt_root);
			return 1;
		}
		p = p->next;
	}
	spin_unlock(&dcache_lock);
	return 0;
}

把一个设备安装到一个目录节点时要用到一个vfsmount数据结构作为连接件。定义如下:

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数据结构,而指针mnt_root则指向所安装设备上的根目录的dentry数据结构,在二者之间建立桥梁。
  • 可是,在dentry结构中并没有直接指向vfsmount数据结构的指针,而是有个队列头d_vfsmount,这是因为安装点和设备之间是一对多的关系,在同一个安装点可以安装多个设备。相应的,vfsmount结构中也有个队列mnt_clash,通过它链入到安装点dentry的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链入上一层的mnt_mounts中。这样,就形成一种设备安装的树形结构,从一个vfsmount结构的mnt_mounts队列开始可以找到所有直接或间接安装在这个设备上的(文件系统中)其他设备。
  • 此外,系统中还有个全局的vfsmount的队列vfsmntlist。相应的,vfsmount结构中还有个mnt_list队列头。所有已安装设备vfsmount结构通过mnt_list链入vfsmntlist。

所安装设备的super_block数据结构与作为连接件的vfsmount数据结构之间存在一对多的关系,这很容易理解,因为把同一个设备安装到不同的目录节点上是可以的。可是,安装点的dentry和vfsmount也可以存在一对多的关系,这就有点难理解。很难想象可以把多个设备安装到同一个节点上。其实,这二者是联系的,有了前者就会有后者。

回到do_mount函数中。安装点最终确定以后,剩下的就是把待安装设备的super_block数据结构与安装点的dentry数据结构联系在一起,即安装本身了,这是通过add_vfsmnt函数完成的,代码如下:
sys_mount=>do_mount=>add_vfsmnt


/**
 *	add_vfsmnt - add a new mount node
 *	@nd: location of mountpoint or %NULL if we want a root node
 *	@root: root of (sub)tree to be mounted
 *	@dev_name: device name to show in /proc/mounts or %NULL (for "none").
 *
 *	This is VFS idea of mount. New node is allocated, bound to a tree
 *	we are mounting and optionally (OK, usually) registered as mounted
 *	on a given mountpoint. Returns a pointer to new node or %NULL in
 *	case of failure.
 *
 *	Potential reason for failure (aside of trivial lack of memory) is a
 *	deleted mountpoint. Caller must hold ->i_zombie on mountpoint
 *	dentry (if any).
 *
 *	Node is marked as MNT_VISIBLE (visible in /proc/mounts) unless both
 *	@nd and @devname are %NULL. It works since we pass non-%NULL @devname
 *	when we are mounting root and kern_mount() filesystems are deviceless.
 *	If we will get a kern_mount() filesystem with nontrivial @devname we
 *	will have to pass the visibility flag explicitly, so if we will add
 *	support for such beasts we'll have to change prototype.
 */

static struct vfsmount *add_vfsmnt(struct nameidata *nd,
				struct dentry *root,
				const char *dev_name)
{
	struct vfsmount *mnt;
	struct super_block *sb = root->d_inode->i_sb;
	char *name;

	mnt = kmalloc(sizeof(struct vfsmount), GFP_KERNEL);
	if (!mnt)
		goto out;
	memset(mnt, 0, sizeof(struct vfsmount));

	if (nd || dev_name)
		mnt->mnt_flags = MNT_VISIBLE;

	/* It may be NULL, but who cares? */
	if (dev_name) {
		name = kmalloc(strlen(dev_name)+1, GFP_KERNEL);
		if (name) {
			strcpy(name, dev_name);
			mnt->mnt_devname = name;
		}
	}
	mnt->mnt_owner = current->uid;
	atomic_set(&mnt->mnt_count,1);
	mnt->mnt_sb = sb;

	spin_lock(&dcache_lock);
	if (nd && !IS_ROOT(nd->dentry) && d_unhashed(nd->dentry))
		goto fail;
	mnt->mnt_root = dget(root);
	mnt->mnt_mountpoint = nd ? dget(nd->dentry) : dget(root);
	mnt->mnt_parent = nd ? mntget(nd->mnt) : mnt;

	if (nd) {
		list_add(&mnt->mnt_child, &nd->mnt->mnt_mounts);
		list_add(&mnt->mnt_clash, &nd->dentry->d_vfsmnt);
	} else {
		INIT_LIST_HEAD(&mnt->mnt_child);
		INIT_LIST_HEAD(&mnt->mnt_clash);
	}
	INIT_LIST_HEAD(&mnt->mnt_mounts);
	list_add(&mnt->mnt_instances, &sb->s_mounts);
	list_add(&mnt->mnt_list, vfsmntlist.prev);
	spin_unlock(&dcache_lock);
out:
	return mnt;
fail:
	spin_unlock(&dcache_lock);
	if (mnt->mnt_devname)
		kfree(mnt->mnt_devname);
	kfree(mnt);
	return NULL;
}

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

看完了文件系统的安装,再来看文件系统 的卸载,这是由sys_umount完成的,代码如下:


/*
 * Now umount can handle mount points as well as block devices.
 * This is important for filesystems which use unnamed block devices.
 *
 * We now support a flag for forced unmount like the other 'big iron'
 * unixes. Our API is identical to OSF/1 to avoid making a mess of AMD
 */

asmlinkage long sys_umount(char * name, int flags)
{
	struct nameidata nd;
	char *kname;
	int retval;

	lock_kernel();
	kname = getname(name);
	retval = PTR_ERR(kname);
	if (IS_ERR(kname))
		goto out;
	retval = 0;
	if (path_init(kname, LOOKUP_POSITIVE|LOOKUP_FOLLOW, &nd))
		retval = path_walk(kname, &nd);
	putname(kname);
	if (retval)
		goto out;
	retval = -EINVAL;
	if (nd.dentry != nd.mnt->mnt_root)
		goto dput_and_out;

	retval = -EPERM;
	if (!capable(CAP_SYS_ADMIN) && current->uid!=nd.mnt->mnt_owner)
		goto dput_and_out;

	dput(nd.dentry);
	/* puts nd.mnt */
	down(&mount_sem);
	retval = do_umount(nd.mnt, 0, flags);
	up(&mount_sem);
	goto out;
dput_and_out:
	path_release(&nd);
out:
	unlock_kernel();
	return retval;
}

由于path_init的调用参数中LOOKUP_FOLLOW标志位为1,不论给定的是安装点的路径名或是设备文件的路径名,path_walk的结构都是一样的,nd.dentry总是指向设备文件上根目录的dentry结构,而nd.mnt总是指向用来将该设备安装到安装点上的vfsmount数据结构。在安装设备的时候,总是将设备上的根目录作为给设备的代表安装到另一个设备上的某个节点上,所以如果nd.dentry不等于nd.mnt->mnt_root就说明出现了严重错误,通过了这一层的检验,先把这个dentry结构释放,因为我们不再需要使用这个数据结构了。注意,这里的nameidata数据结构nd是个局部变量,所以并不需要释放它的空间。至于nd.mnt所指向的vfsmount结构则还需要在do_umount中使用,所以释放这个数据结构责任就转给了do_umount。完成文件系统卸载操作的主体do_umount在fs/super.c中:

sys_umount=>do_umount


static int do_umount(struct vfsmount *mnt, int umount_root, int flags)
{
	struct super_block * sb = mnt->mnt_sb;

	/*
	 * No sense to grab the lock for this test, but test itself looks
	 * somewhat bogus. Suggestions for better replacement?
	 * Ho-hum... In principle, we might treat that as umount + switch
	 * to rootfs. GC would eventually take care of the old vfsmount.
	 * The problem being: we have to implement rootfs and GC for that ;-)
	 * Actually it makes sense, especially if rootfs would contain a
	 * /reboot - static binary that would close all descriptors and
	 * call reboot(9). Then init(8) could umount root and exec /reboot.
	 */
	if (mnt == current->fs->rootmnt && !umount_root) {
		int retval = 0;
		/*
		 * Special case for "unmounting" root ...
		 * we just try to remount it readonly.
		 */
		mntput(mnt);
		if (!(sb->s_flags & MS_RDONLY))
			retval = do_remount_sb(sb, MS_RDONLY, 0);
		return retval;
	}

	spin_lock(&dcache_lock);

	if (mnt->mnt_instances.next != mnt->mnt_instances.prev) {
		if (atomic_read(&mnt->mnt_count) > 2) {
			spin_unlock(&dcache_lock);
			mntput(mnt);
			return -EBUSY;
		}
		if (sb->s_type->fs_flags & FS_SINGLE)
			put_filesystem(sb->s_type);
		/* We hold two references, so mntput() is safe */
		mntput(mnt);
		remove_vfsmnt(mnt);
		return 0;
	}
	spin_unlock(&dcache_lock);

	/*
	 * Before checking whether the filesystem is still busy,
	 * make sure the kernel doesn't hold any quota files open
	 * on the device. If the umount fails, too bad -- there
	 * are no quotas running any more. Just turn them on again.
	 */
	DQUOT_OFF(sb);
	acct_auto_close(sb->s_dev);

	/*
	 * If we may have to abort operations to get out of this
	 * mount, and they will themselves hold resources we must
	 * allow the fs to do things. In the Unix tradition of
	 * 'Gee thats tricky lets do it in userspace' the umount_begin
	 * might fail to complete on the first run through as other tasks
	 * must return, and the like. Thats for the mount program to worry
	 * about for the moment.
	 */

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

	/*
	 * Shrink dcache, then fsync. This guarantees that if the
	 * filesystem is quiescent at this point, then (a) only the
	 * root entry should be in use and (b) that root entry is
	 * clean.
	 */
	shrink_dcache_sb(sb);
	fsync_dev(sb->s_dev);

	if (sb->s_root->d_inode->i_state) {
		mntput(mnt);
		return -EBUSY;
	}

	/* Something might grab it again - redo checks */

	spin_lock(&dcache_lock);
	if (atomic_read(&mnt->mnt_count) > 2) {
		spin_unlock(&dcache_lock);
		mntput(mnt);
		return -EBUSY;
	}

	/* OK, that's the point of no return */
	mntput(mnt);
	remove_vfsmnt(mnt);

	kill_super(sb, umount_root);
	return 0;
}

调用参数umount_root表示所需要卸载的是否是根设备,我们在前面的代码中看到从sys_umount中调用这个参数为0,用户进程是不能通过umount系统调用直接卸载根设备的。从用户进程通过umount系统调用卸载根设备只意味着将它重安装成只读模式。

在vfsmount数据结构中也有个使用计数mnt_count,在add_vfsmnt中设置为1。从那以后,每当用使用这个数据结构时就通过mntget递增其使用计数,用完了就通过mntput递减其计数。例如在函数path_init函数中就调用了mntget而在path_release中调用了mntput;又如在follow_up和follow_down既调用mntget又调用了mntput。所以,在do_umount函数中所处理的vfsmount结构中的使用计数应该是2,如果大于这个数值就说明还有其他的操作过程还在使用这个结构,因而不完成卸载而只能出错返回。当然,在出错返回之前也要通过mntput递减这个使用计数。

前面讲过,vfsmount结构在安装文件系统时通过其队列头mnt_instances挂入到super_block结构中的s_mounts队列。通常一个块设备只安装一次,所以其super_block结构中的s_mounts队列中只有一个vfsmount结构,因此该vfsmount结构的队列头的两个指针next和prev相等。但是,在有些情况系下同一个设备是可以安装多次的,此时其super_block结构中的s_mounts队列中含有多个vfsmount结构,而队列中的每个vfsmount结构的mnt_instances中的两个指针就不相等了。所以,此时调用remove_vfsmnt所卸载的并不是相应设备仅存的安装。这种情况的卸载比较简单,因为只是卸载该设备多次安装的一次,而不是最终将设备卸载下来。

remove_vfsmnt的代码如下:


/*
 * Called with spinlock held, releases it.
 */
static void remove_vfsmnt(struct vfsmount *mnt)
{
	/* First of all, remove it from all lists */
	list_del(&mnt->mnt_instances);
	list_del(&mnt->mnt_clash);
	list_del(&mnt->mnt_list);
	list_del(&mnt->mnt_child);
	spin_unlock(&dcache_lock);
	/* Now we can work safely */
	if (mnt->mnt_parent != mnt)
		mntput(mnt->mnt_parent);

	dput(mnt->mnt_mountpoint);
	dput(mnt->mnt_root);
	if (mnt->mnt_devname)
		kfree(mnt->mnt_devname);
	kfree(mnt);
}

对这些代码我们应该不会感到困难。函数dput递减一个dentry结构的使用计数,如果递减后达到了0,就将此数据结构转移到dentry_unused队列中。

回到do_umount函数中。相比之下,如果vfsmount数据结构代表着一个设备的唯一安装,那就比较复杂一点了。我们在这里并不关心磁盘空间配额的问题,所以跳过DQUOT_OFF和acc_auto_close直接往下读。

sys_umount=>do_umount

	/*
	 * Before checking whether the filesystem is still busy,
	 * make sure the kernel doesn't hold any quota files open
	 * on the device. If the umount fails, too bad -- there
	 * are no quotas running any more. Just turn them on again.
	 */
	DQUOT_OFF(sb);
	acct_auto_close(sb->s_dev);

	/*
	 * If we may have to abort operations to get out of this
	 * mount, and they will themselves hold resources we must
	 * allow the fs to do things. In the Unix tradition of
	 * 'Gee thats tricky lets do it in userspace' the umount_begin
	 * might fail to complete on the first run through as other tasks
	 * must return, and the like. Thats for the mount program to worry
	 * about for the moment.
	 */

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

	/*
	 * Shrink dcache, then fsync. This guarantees that if the
	 * filesystem is quiescent at this point, then (a) only the
	 * root entry should be in use and (b) that root entry is
	 * clean.
	 */
	shrink_dcache_sb(sb);
	fsync_dev(sb->s_dev);

	if (sb->s_root->d_inode->i_state) {
		mntput(mnt);
		return -EBUSY;
	}

	/* Something might grab it again - redo checks */

	spin_lock(&dcache_lock);
	if (atomic_read(&mnt->mnt_count) > 2) {
		spin_unlock(&dcache_lock);
		mntput(mnt);
		return -EBUSY;
	}

	/* OK, that's the point of no return */
	mntput(mnt);
	remove_vfsmnt(mnt);

	kill_super(sb, umount_root);
	return 0;
}

有些设备要求在卸载时先调用一个函数处理拆卸的开始,这种设备通过其super_operations函数跳转表内的函数指针umount_begin提供相应的函数。

把一个设备最终从文件系统中拆卸下来,这意味着从此刻以后这个子系统中的所有节点都不可以访问了。以前讲过,每当某个过程开始使用一个节点的dentry结构时都要通过dget递增其使用计数,如果内存中尚无此节点的dentry结构存在就要为之建立并将其使用设成1。与其相应的,每当结束使用一个dentry结构时就要通过dput递减其使用计数,如果达到0就要将这个数据结构转移到dentry_unused队列中。之所以不马上将不再使用的dentry结构释放,是因为说不定马上又要用了。可是现在既然要最终卸载下一个设备,则属于这个设备的所有dentry结构再没有保留的必要。所以,此时要扫描dentry_unused队列,把所有属于这个队列的dentry结构都释放掉,这就是shrink_dcache_sb要做的事情。代码如下:

sys_umount=>do_umount=>shrink_dcache_sb


/*
 * Shrink the dcache for the specified super block.
 * This allows us to unmount a device without disturbing
 * the dcache for the other devices.
 *
 * This implementation makes just two traversals of the
 * unused list.  On the first pass we move the selected
 * dentries to the most recent end, and on the second
 * pass we free them.  The second pass must restart after
 * each dput(), but since the target dentries are all at
 * the end, it's really just a single traversal.
 */

/**
 * shrink_dcache_sb - shrink dcache for a superblock
 * @sb: superblock
 *
 * Shrink the dcache for the specified super block. This
 * is used to free the dcache before unmounting a file
 * system
 */

void shrink_dcache_sb(struct super_block * sb)
{
	struct list_head *tmp, *next;
	struct dentry *dentry;

	/*
	 * Pass one ... move the dentries for the specified
	 * superblock to the most recent end of the unused list.
	 */
	spin_lock(&dcache_lock);
	next = dentry_unused.next;
	while (next != &dentry_unused) {
		tmp = next;
		next = tmp->next;
		dentry = list_entry(tmp, struct dentry, d_lru);
		if (dentry->d_sb != sb)
			continue;
		list_del(tmp);
		list_add(tmp, &dentry_unused);
	}

	/*
	 * Pass two ... free the dentries for this superblock.
	 */
repeat:
	next = dentry_unused.next;
	while (next != &dentry_unused) {
		tmp = next;
		next = tmp->next;
		dentry = list_entry(tmp, struct dentry, d_lru);
		if (dentry->d_sb != sb)
			continue;
		if (atomic_read(&dentry->d_count))
			continue;
		dentry_stat.nr_unused--;
		list_del(tmp);
		INIT_LIST_HEAD(tmp);
		prune_one_dentry(dentry);
		goto repeat;
	}
	spin_unlock(&dcache_lock);
}

这段代码的逻辑比较简单,具体释放一个dentry结构的操作在prune_one_dentry完成的,代码如下:

sys_umount=>do_umount=>shrink_dcache_sb=>prune_one_dentry


/*
 * Throw away a dentry - free the inode, dput the parent.
 * This requires that the LRU list has already been
 * removed.
 * Called with dcache_lock, drops it and then regains.
 */
static inline void prune_one_dentry(struct dentry * dentry)
{
	struct dentry * parent;

	list_del_init(&dentry->d_hash);
	list_del(&dentry->d_child);
	dentry_iput(dentry);
	parent = dentry->d_parent;
	d_free(dentry);
	if (parent != dentry)
		dput(parent);
	spin_lock(&dcache_lock);
}

再回到do_umount函数中,下一件事是fsync_dev。

为了提高效率,块设备的输入输出一般都是有缓冲的,无论是对超级块的改变还是对某个索引节点的改变,或者对某个数据块的改变,都只是对它们在内存中的映像的改变,而不一定是马上就写回设备上,现在设备要卸载下来,当然要先把已经改变了,但是还未写回设备的内容写回去。这称为同步,是由fsync_dev完成的,其代码如下:

sys_umount=>do_umount=>shrink_dcache_sb=>fsync_dev               


int fsync_dev(kdev_t dev)
{
	sync_buffers(dev, 0);

	lock_kernel();
	sync_supers(dev);
	sync_inodes(dev);
	DQUOT_SYNC(dev);
	unlock_kernel();

	return sync_buffers(dev, 1);
}

先看超级块的同步,函数sync_supers的代码如下:

sys_umount=>do_umount=>shrink_dcache_sb=>fsync_dev=>sync_supers


/*
 * Note: check the dirty flag before waiting, so we don't
 * hold up the sync while mounting a device. (The newly
 * mounted device won't need syncing.)
 */
void sync_supers(kdev_t dev)
{
	struct super_block * sb;

	for (sb = sb_entry(super_blocks.next);
	     sb != sb_entry(&super_blocks); 
	     sb = sb_entry(sb->s_list.next)) {
		if (!sb->s_dev)
			continue;
		if (dev && sb->s_dev != dev)
			continue;
		if (!sb->s_dirt)
			continue;
		lock_super(sb);
		if (sb->s_dev && sb->s_dirt && (!dev || dev == sb->s_dev))
			if (sb->s_op && sb->s_op->write_super)
				sb->s_op->write_super(sb);
		unlock_super(sb);
	}
}

每当改变一个super_block结构的内容都要将结构中的s_dirt标志位设为1,表示这个结构的内容已经脏了,也就是与设备上的超级块不一致了;而在将超级块写回设备时则将这个标志清零。所以,如果一个super_block结构的s_dirt标志位非0,就表示应该加以同步。不过,如前所述,有些设备,主要是一些虚拟设备,本来就没什么超级块或者类似的东西,所以还要看super_block结构中的指针s_op是否指向一个super_operations结构,以及这个结构中是否为write_super操作提供了一个函数。就ext2文件系统而言,这个函数是ext2_write_super。等我们看了下面的ext2_read_super之后,有兴趣可以看看ext2_write_super。

再来看索引节点的同步,函数sync_inodes的代码如下:

sys_umount=>do_umount=>shrink_dcache_sb=>fsync_dev=>sync_inodes


/**
 *	sync_inodes
 *	@dev: device to sync the inodes from.
 *
 *	sync_inodes goes through the super block's dirty list, 
 *	writes them out, and puts them back on the normal list.
 */
 
void sync_inodes(kdev_t dev)
{
	struct super_block * sb = sb_entry(super_blocks.next);

	/*
	 * Search the super_blocks array for the device(s) to sync.
	 */
	spin_lock(&inode_lock);
	for (; sb != sb_entry(&super_blocks); sb = sb_entry(sb->s_list.next)) {
		if (!sb->s_dev)
			continue;
		if (dev && sb->s_dev != dev)
			continue;

		sync_list(&sb->s_dirty);

		if (dev)
			break;
	}
	spin_unlock(&inode_lock);
}

在super_block结构中还有个队列s_dirty,凡是已经改变了的inode结构就通过它的i_list队列头挂入其所属super_block结构中的s_dirty队列。所以,要同步整个队列,这是由sync_list完成的,有关代码在同一个文件中,如下:

sys_umount=>do_umount=>shrink_dcache_sb=>fsync_dev=>sync_inodes=>sync_list

static inline void sync_list(struct list_head *head)
{
	struct list_head * tmp;

	while ((tmp = head->prev) != head)
		sync_one(list_entry(tmp, struct inode, i_list), 0);
}

sys_umount=>do_umount=>shrink_dcache_sb=>fsync_dev=>sync_inodes=>sync_list=>sync_one


static inline void sync_one(struct inode *inode, int sync)
{
	if (inode->i_state & I_LOCK) {
		__iget(inode);
		spin_unlock(&inode_lock);
		__wait_on_inode(inode);
		iput(inode);
		spin_lock(&inode_lock);
	} else {
		unsigned dirty;

		list_del(&inode->i_list);
		list_add(&inode->i_list, atomic_read(&inode->i_count)
							? &inode_in_use
							: &inode_unused);
		/* Set I_LOCK, reset I_DIRTY */
		dirty = inode->i_state & I_DIRTY;
		inode->i_state |= I_LOCK;
		inode->i_state &= ~I_DIRTY;
		spin_unlock(&inode_lock);

		filemap_fdatasync(inode->i_mapping);

		/* Don't write the inode if only I_DIRTY_PAGES was set */
		if (dirty & (I_DIRTY_SYNC | I_DIRTY_DATASYNC))
			write_inode(inode, sync);

		filemap_fdatawait(inode->i_mapping);

		spin_lock(&inode_lock);
		inode->i_state &= ~I_LOCK;
		wake_up(&inode->i_wait);
	}
}

sys_umount=>do_umount=>shrink_dcache_sb=>fsync_dev=>sync_inodes=>sync_list=>sync_one=>write_inode

static inline void write_inode(struct inode *inode, int sync)
{
	if (inode->i_sb && inode->i_sb->s_op && inode->i_sb->s_op->write_inode)
		inode->i_sb->s_op->write_inode(inode, sync);
}

ext2文件系统的write_inode操作为ext2_write_inode,由于我们在linux文件系统--从路径名到目录节点讲解了ext2_read_inode的代码,这里就不再深入了。

由于我们对磁盘空间配额不感兴趣,剩下的只是数据块的同步了,那就是sync_buffers,我们将在“文件的读与写”博客中读这个函数的代码。

经过这些代码的阅读,读者对super_block数据结构想必已经有了个大致的印象,现在来看它的定义应该容易理解了。定义如下:

struct super_block {
	struct list_head	s_list;		/* Keep this first */
	kdev_t			s_dev;
	unsigned long		s_blocksize;
	unsigned char		s_blocksize_bits;
	unsigned char		s_lock;
	unsigned char		s_dirt;
	struct file_system_type	*s_type;
	struct super_operations	*s_op;
	struct dquot_operations	*dq_op;
	unsigned long		s_flags;
	unsigned long		s_magic;
	struct dentry		*s_root;
	wait_queue_head_t	s_wait;

	struct list_head	s_dirty;	/* dirty inodes */
	struct list_head	s_files;

	struct block_device	*s_bdev;
	struct list_head	s_mounts;	/* vfsmount(s) of this one */
	struct quota_mount_options s_dquot;	/* Diskquota specific options */

	union {
		struct minix_sb_info	minix_sb;
		struct ext2_sb_info	ext2_sb;
		struct hpfs_sb_info	hpfs_sb;
		struct ntfs_sb_info	ntfs_sb;
		struct msdos_sb_info	msdos_sb;
		struct isofs_sb_info	isofs_sb;
		struct nfs_sb_info	nfs_sb;
		struct sysv_sb_info	sysv_sb;
		struct affs_sb_info	affs_sb;
		struct ufs_sb_info	ufs_sb;
		struct efs_sb_info	efs_sb;
		struct shmem_sb_info	shmem_sb;
		struct romfs_sb_info	romfs_sb;
		struct smb_sb_info	smbfs_sb;
		struct hfs_sb_info	hfs_sb;
		struct adfs_sb_info	adfs_sb;
		struct qnx4_sb_info	qnx4_sb;
		struct bfs_sb_info	bfs_sb;
		struct udf_sb_info	udf_sb;
		struct ncp_sb_info	ncpfs_sb;
		struct usbdev_sb_info   usbdevfs_sb;
		void			*generic_sbp;
	} u;
	/*
	 * The next field is for VFS *only*. No filesystems have any business
	 * even looking at it. You had been warned.
	 */
	struct semaphore s_vfs_rename_sem;	/* Kludge */

	/* The next field is used by knfsd when converting a (inode number based)
	 * file handle into a dentry. As it builds a path in the dcache tree from
	 * the bottom up, there may for a time be a subpath of dentrys which is not
	 * connected to the main tree.  This semaphore ensure that there is only ever
	 * one such free path per filesystem.  Note that unconnected files (or other
	 * non-directories) are allowed, but not unconnected diretories.
	 */
	struct semaphore s_nfsd_free_path_sem;
};

对于ext2文件系统,将super_block结构中的union解释为一个ext2_sb_info结构,定义如下:


/*
 * second extended-fs super-block data in memory
 */
struct ext2_sb_info {
	unsigned long s_frag_size;	/* Size of a fragment in bytes */
	unsigned long s_frags_per_block;/* Number of fragments per block */
	unsigned long s_inodes_per_block;/* Number of inodes per block */
	unsigned long s_frags_per_group;/* Number of fragments in a group */
	unsigned long s_blocks_per_group;/* Number of blocks in a group */
	unsigned long s_inodes_per_group;/* Number of inodes in a group */
	unsigned long s_itb_per_group;	/* Number of inode table blocks per group */
	unsigned long s_gdb_count;	/* Number of group descriptor blocks */
	unsigned long s_desc_per_block;	/* Number of group descriptors per block */
	unsigned long s_groups_count;	/* Number of groups in the fs */
	struct buffer_head * s_sbh;	/* Buffer containing the super block */
	struct ext2_super_block * s_es;	/* Pointer to the super block in the buffer */
	struct buffer_head ** s_group_desc;
	unsigned short s_loaded_inode_bitmaps;
	unsigned short s_loaded_block_bitmaps;
	unsigned long s_inode_bitmap_number[EXT2_MAX_GROUP_LOADED];
	struct buffer_head * s_inode_bitmap[EXT2_MAX_GROUP_LOADED];
	unsigned long s_block_bitmap_number[EXT2_MAX_GROUP_LOADED];
	struct buffer_head * s_block_bitmap[EXT2_MAX_GROUP_LOADED];
	unsigned long  s_mount_opt;
	uid_t s_resuid;
	gid_t s_resgid;
	unsigned short s_mount_state;
	unsigned short s_pad;
	int s_addr_per_block_bits;
	int s_desc_per_block_bits;
	int s_inode_size;
	int s_first_ino;
};

如前所述,super_block是内存中数据结构,其内容通常(但不总是)来自具体设备上的特定文件系统的超级块。就ext2文件系统而言,设备上的超级块为ext2_super_block结构,定义如下:


/*
 * Structure of the super block
 */
struct ext2_super_block {
	__u32	s_inodes_count;		/* Inodes count */
	__u32	s_blocks_count;		/* Blocks count */
	__u32	s_r_blocks_count;	/* Reserved blocks count */
	__u32	s_free_blocks_count;	/* Free blocks count */
	__u32	s_free_inodes_count;	/* Free inodes count */
	__u32	s_first_data_block;	/* First Data Block */
	__u32	s_log_block_size;	/* Block size */
	__s32	s_log_frag_size;	/* Fragment size */
	__u32	s_blocks_per_group;	/* # Blocks per group */
	__u32	s_frags_per_group;	/* # Fragments per group */
	__u32	s_inodes_per_group;	/* # Inodes per group */
	__u32	s_mtime;		/* Mount time */
	__u32	s_wtime;		/* Write time */
	__u16	s_mnt_count;		/* Mount count */
	__s16	s_max_mnt_count;	/* Maximal mount count */
	__u16	s_magic;		/* Magic signature */
	__u16	s_state;		/* File system state */
	__u16	s_errors;		/* Behaviour when detecting errors */
	__u16	s_minor_rev_level; 	/* minor revision level */
	__u32	s_lastcheck;		/* time of last check */
	__u32	s_checkinterval;	/* max. time between checks */
	__u32	s_creator_os;		/* OS */
	__u32	s_rev_level;		/* Revision level */
	__u16	s_def_resuid;		/* Default uid for reserved blocks */
	__u16	s_def_resgid;		/* Default gid for reserved blocks */
	/*
	 * These fields are for EXT2_DYNAMIC_REV superblocks only.
	 *
	 * Note: the difference between the compatible feature set and
	 * the incompatible feature set is that if there is a bit set
	 * in the incompatible feature set that the kernel doesn't
	 * know about, it should refuse to mount the filesystem.
	 * 
	 * e2fsck's requirements are more strict; if it doesn't know
	 * about a feature in either the compatible or incompatible
	 * feature set, it must abort and not try to meddle with
	 * things it doesn't understand...
	 */
	__u32	s_first_ino; 		/* First non-reserved inode */
	__u16   s_inode_size; 		/* size of inode structure */
	__u16	s_block_group_nr; 	/* block group # of this superblock */
	__u32	s_feature_compat; 	/* compatible feature set */
	__u32	s_feature_incompat; 	/* incompatible feature set */
	__u32	s_feature_ro_compat; 	/* readonly-compatible feature set */
	__u8	s_uuid[16];		/* 128-bit uuid for volume */
	char	s_volume_name[16]; 	/* volume name */
	char	s_last_mounted[64]; 	/* directory where last mounted */
	__u32	s_algorithm_usage_bitmap; /* For compression */
	/*
	 * Performance hints.  Directory preallocation should only
	 * happen if the EXT2_COMPAT_PREALLOC flag is on.
	 */
	__u8	s_prealloc_blocks;	/* Nr of blocks to try to preallocate*/
	__u8	s_prealloc_dir_blocks;	/* Nr to preallocate for dirs */
	__u16	s_padding1;
	__u32	s_reserved[204];	/* Padding to the end of the block */
};

这个数据结构的定义于ext2文件系统的格式密切相关,下面还要详述。建议读者将这几个数据结构的内容与下面ext2_read_super的代码相互参照印证,再回顾一下之前读过的代码,以求真正理解。

最后,我们来看下ext2_read_super代码。分段阅读:
sys_mount=>do_mount=>get_sb_bdev=>read_super=>ext2_read_super

struct super_block * ext2_read_super (struct super_block * sb, void * data,
				      int silent)
{
	struct buffer_head * bh;
	struct ext2_super_block * es;
	unsigned long sb_block = 1;
	unsigned short resuid = EXT2_DEF_RESUID;
	unsigned short resgid = EXT2_DEF_RESGID;
	unsigned long logic_sb_block = 1;
	unsigned long offset = 0;
	kdev_t dev = sb->s_dev;
	int blocksize = BLOCK_SIZE;
	int hblock;
	int db_count;
	int i, j;

	/*
	 * See what the current blocksize for the device is, and
	 * use that as the blocksize.  Otherwise (or if the blocksize
	 * is smaller than the default) use the default.
	 * This is important for devices that have a hardware
	 * sectorsize that is larger than the default.
	 */
	blocksize = get_hardblocksize(dev);
	if( blocksize == 0 || blocksize < BLOCK_SIZE )
	  {
	    blocksize = BLOCK_SIZE;
	  }

	sb->u.ext2_sb.s_mount_opt = 0;
	if (!parse_options ((char *) data, &sb_block, &resuid, &resgid,
	    &sb->u.ext2_sb.s_mount_opt)) {
		return NULL;
	}

	set_blocksize (dev, blocksize);

	/*
	 * If the superblock doesn't start on a sector boundary,
	 * calculate the offset.  FIXME(eric) this doesn't make sense
	 * that we would have to do this.
	 */
	if (blocksize != BLOCK_SIZE) {
		logic_sb_block = (sb_block*BLOCK_SIZE) / blocksize;
		offset = (sb_block*BLOCK_SIZE) % blocksize;
	}

	if (!(bh = bread (dev, logic_sb_block, blocksize))) {
		printk ("EXT2-fs: unable to read superblock\n");
		return NULL;
	}

参数sb是指向super_block数据结构的指针,在调用这个函数之前对该结构已经作了部分初始化,例如其s_dev字段已经持有具体设备的设备号。但是,结构中的大部分内容还没有设置,而这里要做的正是从设备上读入超级块并根据其内容设置这个super_block数据结构。另一个指针data的是使用,则因文件系统而异,对于ext2文件系统它是指向一个表示安装可选项的字符串。至于参数silent,则表示在读超级块的过程中是否详细地报告出错信息。

首先是确定设备上记录块的大小。ext2文件系统的记录块大小一般是1K字节,但是为了提高效率也可以采用2K或者4K字节。变量blocksize先设置成常数BLOCK_SIZE,即1K字节。但是,内核中有一个以主设备号为下标的指针数组hardsect_size。如果这个数组中相应的元素指向另一个以次设备号为下标的整数数组,其中提供了该设备的记录块大小,并且这个数值大于BLOCK_SIZE,则以此为准。这样,如果某种设备上的记录块大于BLOCK_SIZE,便只要在系统初始化时设置这个数组中的相应元素就可以了。不过,从hardsect_size中读时应通过为此而设的函数get_hardblocksize获取。此外,在确定了某项设备的记录块大小之后要通过set_blocksize将确定了的记录块大小写回这个数组中去,这样即使开始时数组时空的也会慢慢的得到设置。这些操作的逻辑比较简单,这里就不深入阅读这两个函数的代码了。值得注意的是BLOCK_SIZE实际上是记录块大小的最小值。

另一个函数parse_options是用来分析可选项字符串并根据其内容设置一些变量。每种文件系统都有它自己的parse_options,所以这些函数都是静态的,其作用域只是同一个文件,如ext2的parse_options就在fs/ext2/super.c中。函数parse_options通常既简单又冗长,所以我们不在这里列出其代码了。

超级块通常是设备上的1号记录块(即第2个记录块),所以变量sb_block设置为1,在记录块大小为BLOCK_SIZE的设备上其逻辑块号logic_sb_block也是1。但是在记录块大于BLOCK_SIZE的设备上,由于超级块的大小仍然为BLOCK_SIZE,就要通过计算来确定其所在的记录块,以及在块内的位移。此时虽然仍然称为超级块,但是实际上只是记录块中的一部分了。确定了这两个参数以后,就可以通过bread将超级块所在的记录块读入内存了。函数bread属于设备驱动的范畴。等后面分析这块的时候再看。继续往下看:

sys_mount=>do_mount=>get_sb_bdev=>read_super=>ext2_read_super

	/*
	 * Note: s_es must be initialized s_es as soon as possible because
	 * some ext2 macro-instructions depend on its value
	 */
	es = (struct ext2_super_block *) (((char *)bh->b_data) + offset);
	sb->u.ext2_sb.s_es = es;
	sb->s_magic = le16_to_cpu(es->s_magic);
	if (sb->s_magic != EXT2_SUPER_MAGIC) {
		if (!silent)
			printk ("VFS: Can't find an ext2 filesystem on dev "
				"%s.\n", bdevname(dev));
	failed_mount:
		if (bh)
			brelse(bh);
		return NULL;
	}
	if (le32_to_cpu(es->s_rev_level) == EXT2_GOOD_OLD_REV &&
	    (EXT2_HAS_COMPAT_FEATURE(sb, ~0U) ||
	     EXT2_HAS_RO_COMPAT_FEATURE(sb, ~0U) ||
	     EXT2_HAS_INCOMPAT_FEATURE(sb, ~0U)))
		printk("EXT2-fs warning: feature flags set on rev 0 fs, "
		       "running e2fsck is recommended\n");
	/*
	 * Check feature flags regardless of the revision level, since we
	 * previously didn't change the revision level when setting the flags,
	 * so there is a chance incompat flags are set on a rev 0 filesystem.
	 */
	if ((i = EXT2_HAS_INCOMPAT_FEATURE(sb, ~EXT2_FEATURE_INCOMPAT_SUPP))) {
		printk("EXT2-fs: %s: couldn't mount because of "
		       "unsupported optional features (%x).\n",
		       bdevname(dev), i);
		goto failed_mount;
	}
	if (!(sb->s_flags & MS_RDONLY) &&
	    (i = EXT2_HAS_RO_COMPAT_FEATURE(sb, ~EXT2_FEATURE_RO_COMPAT_SUPP))){
		printk("EXT2-fs: %s: couldn't mount RDWR because of "
		       "unsupported optional features (%x).\n",
		       bdevname(dev), i);
		goto failed_mount;
	}
	sb->s_blocksize_bits =
		le32_to_cpu(EXT2_SB(sb)->s_es->s_log_block_size) + 10;
	sb->s_blocksize = 1 << sb->s_blocksize_bits;
	if (sb->s_blocksize != BLOCK_SIZE &&
	    (sb->s_blocksize == 1024 || sb->s_blocksize == 2048 ||
	     sb->s_blocksize == 4096)) {
		/*
		 * Make sure the blocksize for the filesystem is larger
		 * than the hardware sectorsize for the machine.
		 */
		hblock = get_hardblocksize(dev);
		if(    (hblock != 0)
		    && (sb->s_blocksize < hblock) )
		{
			printk("EXT2-fs: blocksize too small for device.\n");
			goto failed_mount;
		}

		brelse (bh);
		set_blocksize (dev, sb->s_blocksize);
		logic_sb_block = (sb_block*BLOCK_SIZE) / sb->s_blocksize;
		offset = (sb_block*BLOCK_SIZE) % sb->s_blocksize;
		bh = bread (dev, logic_sb_block, sb->s_blocksize);
		if(!bh) {
			printk("EXT2-fs: Couldn't read superblock on "
			       "2nd try.\n");
			goto failed_mount;
		}
		es = (struct ext2_super_block *) (((char *)bh->b_data) + offset);
		sb->u.ext2_sb.s_es = es;
		if (es->s_magic != le16_to_cpu(EXT2_SUPER_MAGIC)) {
			printk ("EXT2-fs: Magic mismatch, very weird !\n");
			goto failed_mount;
		}
	}

函数bread返回一个buffer_head结构指针bh,而bh->data就指向缓冲区,offset则为超级块的起点在缓冲区中的位移。对于ext2文件系统,从设备读入的超级块为一个ext2_super_block数据结构。从439行以后,指针es就指向这个数据结构。另一方面,ext2文件系统采用小端格式,所以一般而言对于超级块中的整数都是通过le32_to_cpu或者le16_to_cpu变换成CPU所采用的的格式。不过,由于i386结构本来就是小端格式,所以这些函数实际上不起作用的。这样,结合前面三个数据结构的定义,这个函数中大部分代码都不难理解了,我们只选择几个问题来讲。

从475行到508行是对记录块的大小修正。前面我们已经确定了设备上的记录块大小,但是那未必是来自设备本身的第一手信息。现在已经有了来自设备的超级块,则超级块中提供的信息可能是更为准确。如果发现超级块中提供的记录块大小与原来认为的不同,(只能大不能小)则一来要更正hardsect_size数组中的内容,二来要把已经读入的buffer_head结构连同缓冲区释放(见487行)而根据新计算的参数再来通过bread读入一次。

也许会感到好奇,既然原来的参数不对,那怎么根据不正确的参数读入的超级块倒是对的呢?既然已经读入的超级块是对的,那何必又重新读一遍呢?原因就在于不管原来的参数是否正确,在sb_block等于1的前提下计算出来的logic_sb_block和offset只有两组结果。当sb->s_blocksize大于BLOCK_SIZE时,logic_sb_block总是0而offset总是BLOCK_SIZE,而与sb->s_blocksize的具体数值无关。当sb->s_blocksize等于BLOCK_SIZE时,则logic_sb_block为1而offset为0。所以,只要在记录块大小等于BLOCK_SIZE时将超级块放在第二块(块号为1),而在记录块大于BLOCK_SIZE时,则除将超级块放在第二块的开头处以外再在第一个块中位移为BLOCK_SIZE处放一个副本,就不会错了。这里重读一遍,只不过是让缓冲区中含有整个记录块,而不只是超级块而已。同时,在super_block结构中也保留着两个指针,一个指向缓冲区中超级块的起点(见440行和504行),另一个则指向缓冲区本身(见538行)。

记录块的大小是个重要的参数。从读写的效率考虑,记录块大一些较好,但是,记录块大了往往会造成空间的浪费,因为记录块是设备上存储空间分配的单位。据统计,在Unix(以及linux)环境下大多数文件都是比较小的,这样,浪费的百分比就更大了。权衡之下,ext2选择1K字节为默认记录块大小,但是也可以在格式化时给定更大的数值,这就是前面有关记录块大小的处理的来历。由于时间上和空间上的效率难于兼顾,有些文件系统进一步把记录块划分成若干片段(fragment),当需要的空间较小时就以片段为单位来分配,ext2也准备采用这项技术,并且在数据结构等方面为此做好了准备(所以超级块中有片段大小等字段),但是从总体来说尚未实现,因此目前片段大小总是等于记录块大小。

超级块的内容反映了按特定格式建立在特定设备上的文件系统多方面的信息,主要是结构和管理两方面的信息。其中结构方面的信息是与具体文件系统的格式密切相关的,所以要了解ext2文件系统的格式才能理解其超级块的内容。以前提到过,ext2文件系统的第一个记录块为引导块,第二个块为超级块,然后是索引节点区,接着是数据区。但是那只是概念上讲,是大大简化了的,实际上要复杂很多。现代的磁盘驱动器都是多片的,所以不同盘面上的相同磁道合在一起就形成了柱面的概念。从磁盘读出多个记录块时,如果是从同一个柱面读出就比较快,因为在这种情况下不需要移动磁头(实际上是磁头组)。互相连续的记录块实际上分布在同一个柱面的各个盘面上,只有在一个柱面用满时才进入下一个柱面。所以,在许多文件系统中都把整个设备划分成若干柱面组,将反映着盘面存储空间的组织与管理的信息分散后就近存储在各个柱面组中。相比之下,早期的文件系统往往将这些信息集中存储在一起,使得磁头在文件访问时来回疲于奔命而降低了性能。

但是,柱面组的划分也带来一些新的、附加的要求。首先是关于这些柱面组本身的结构信息,如此就要用一些记录块来保存所有的柱面组的描述,即所谓组描述符。另一方面,有些信息是对于整个设备的而不只是对一个柱面组的,所以不能把它拆散,而只能重复地存储于每个柱面组上。从另一个角度来讲,将某些重要的信息重复存储于每个柱面组为这些信息提供了备份,从而增加了可靠性。对于文件系统来说,最重要的莫过于其超级块了,所以一些文件系统的设计要求设备上不管哪一个记录块、哪一个盘面、哪一个磁道坏了都仍然可以恢复其超级块(通过运行fsck),ext2也采用了这样的结构。不过不称为柱面组,而是记录块组,并且将超级块和所有的块组描述符结构重复存储于每个块组。此外,ext2通过位图来管理每个块组中的记录块和索引节点,所以在每个块组中有两个位图,一个用于记录块,一个用于索引节点。这样,ext2文件系统的格式就变成了下面的形式:

当整个设备只有一个块组时,就简化成了前面讲的那种结构。ext2的块组描述符结构定义如下:

/*
 * Structure of a blocks group descriptor
 */
struct ext2_group_desc
{
	__u32	bg_block_bitmap;		/* Blocks bitmap block */
	__u32	bg_inode_bitmap;		/* Inodes bitmap block */
	__u32	bg_inode_table;		/* Inodes table block */
	__u16	bg_free_blocks_count;	/* Free blocks count */
	__u16	bg_free_inodes_count;	/* Free inodes count */
	__u16	bg_used_dirs_count;	/* Directories count */
	__u16	bg_pad;
	__u32	bg_reserved[3];
};

超级块的内容可以用"/sbin/tune3fs -l"、"/sbin/dumpe2fs"等命令来显示。

代码中用到的一些宏定义基本上是不言自明的,这里值得一提的是,在ext2_sb_info结构中的数组

s_inode_bitmap_number和s_block_bitmap_number都是固定大小的,具体位图数组也是固定大小的,大小为EXT2_MAX_GROUP_LOADED。常数EXT2_MAX_GROUP_LOADED定义为8,与块组的总数一比占很小的比例,所以运行时并不是将所有块组的位图都装入这些数组中,而是只装入其中很小的一部分,根据具体运行的是需要周转。这里只是把这些数组以及有关的变量都初始化成空白,也并没有为位图数组本身分配空间。

继续看ext2_read_super的代码:

sys_mount=>do_mount=>get_sb_bdev=>read_super=>ext2_read_super

	if (le32_to_cpu(es->s_rev_level) == EXT2_GOOD_OLD_REV) {
		sb->u.ext2_sb.s_inode_size = EXT2_GOOD_OLD_INODE_SIZE;
		sb->u.ext2_sb.s_first_ino = EXT2_GOOD_OLD_FIRST_INO;
	} else {
		sb->u.ext2_sb.s_inode_size = le16_to_cpu(es->s_inode_size);
		sb->u.ext2_sb.s_first_ino = le32_to_cpu(es->s_first_ino);
		if (sb->u.ext2_sb.s_inode_size != EXT2_GOOD_OLD_INODE_SIZE) {
			printk ("EXT2-fs: unsupported inode size: %d\n",
				sb->u.ext2_sb.s_inode_size);
			goto failed_mount;
		}
	}
	sb->u.ext2_sb.s_frag_size = EXT2_MIN_FRAG_SIZE <<
				   le32_to_cpu(es->s_log_frag_size);
	if (sb->u.ext2_sb.s_frag_size)
		sb->u.ext2_sb.s_frags_per_block = sb->s_blocksize /
						  sb->u.ext2_sb.s_frag_size;
	else
		sb->s_magic = 0;
	sb->u.ext2_sb.s_blocks_per_group = le32_to_cpu(es->s_blocks_per_group);
	sb->u.ext2_sb.s_frags_per_group = le32_to_cpu(es->s_frags_per_group);
	sb->u.ext2_sb.s_inodes_per_group = le32_to_cpu(es->s_inodes_per_group);
	sb->u.ext2_sb.s_inodes_per_block = sb->s_blocksize /
					   EXT2_INODE_SIZE(sb);
	sb->u.ext2_sb.s_itb_per_group = sb->u.ext2_sb.s_inodes_per_group /
				        sb->u.ext2_sb.s_inodes_per_block;
	sb->u.ext2_sb.s_desc_per_block = sb->s_blocksize /
					 sizeof (struct ext2_group_desc);
	sb->u.ext2_sb.s_sbh = bh;
	if (resuid != EXT2_DEF_RESUID)
		sb->u.ext2_sb.s_resuid = resuid;
	else
		sb->u.ext2_sb.s_resuid = le16_to_cpu(es->s_def_resuid);
	if (resgid != EXT2_DEF_RESGID)
		sb->u.ext2_sb.s_resgid = resgid;
	else
		sb->u.ext2_sb.s_resgid = le16_to_cpu(es->s_def_resgid);
	sb->u.ext2_sb.s_mount_state = le16_to_cpu(es->s_state);
	sb->u.ext2_sb.s_addr_per_block_bits =
		log2 (EXT2_ADDR_PER_BLOCK(sb));
	sb->u.ext2_sb.s_desc_per_block_bits =
		log2 (EXT2_DESC_PER_BLOCK(sb));
	if (sb->s_magic != EXT2_SUPER_MAGIC) {
		if (!silent)
			printk ("VFS: Can't find an ext2 filesystem on dev "
				"%s.\n",
				bdevname(dev));
		goto failed_mount;
	}
	if (sb->s_blocksize != bh->b_size) {
		if (!silent)
			printk ("VFS: Unsupported blocksize on dev "
				"%s.\n", bdevname(dev));
		goto failed_mount;
	}

	if (sb->s_blocksize != sb->u.ext2_sb.s_frag_size) {
		printk ("EXT2-fs: fragsize %lu != blocksize %lu (not supported yet)\n",
			sb->u.ext2_sb.s_frag_size, sb->s_blocksize);
		goto failed_mount;
	}

	if (sb->u.ext2_sb.s_blocks_per_group > sb->s_blocksize * 8) {
		printk ("EXT2-fs: #blocks per group too big: %lu\n",
			sb->u.ext2_sb.s_blocks_per_group);
		goto failed_mount;
	}
	if (sb->u.ext2_sb.s_frags_per_group > sb->s_blocksize * 8) {
		printk ("EXT2-fs: #fragments per group too big: %lu\n",
			sb->u.ext2_sb.s_frags_per_group);
		goto failed_mount;
	}
	if (sb->u.ext2_sb.s_inodes_per_group > sb->s_blocksize * 8) {
		printk ("EXT2-fs: #inodes per group too big: %lu\n",
			sb->u.ext2_sb.s_inodes_per_group);
		goto failed_mount;
	}

	sb->u.ext2_sb.s_groups_count = (le32_to_cpu(es->s_blocks_count) -
				        le32_to_cpu(es->s_first_data_block) +
				       EXT2_BLOCKS_PER_GROUP(sb) - 1) /
				       EXT2_BLOCKS_PER_GROUP(sb);
	db_count = (sb->u.ext2_sb.s_groups_count + EXT2_DESC_PER_BLOCK(sb) - 1) /
		   EXT2_DESC_PER_BLOCK(sb);
	sb->u.ext2_sb.s_group_desc = kmalloc (db_count * sizeof (struct buffer_head *), GFP_KERNEL);
	if (sb->u.ext2_sb.s_group_desc == NULL) {
		printk ("EXT2-fs: not enough memory\n");
		goto failed_mount;
	}
	for (i = 0; i < db_count; i++) {
		sb->u.ext2_sb.s_group_desc[i] = bread (dev, logic_sb_block + i + 1,
						       sb->s_blocksize);
		if (!sb->u.ext2_sb.s_group_desc[i]) {
			for (j = 0; j < i; j++)
				brelse (sb->u.ext2_sb.s_group_desc[j]);
			kfree(sb->u.ext2_sb.s_group_desc);
			printk ("EXT2-fs: unable to read group descriptors\n");
			goto failed_mount;
		}
	}
	if (!ext2_check_descriptors (sb)) {
		for (j = 0; j < db_count; j++)
			brelse (sb->u.ext2_sb.s_group_desc[j]);
		kfree(sb->u.ext2_sb.s_group_desc);
		printk ("EXT2-fs: group descriptors corrupted !\n");
		goto failed_mount;
	}
	for (i = 0; i < EXT2_MAX_GROUP_LOADED; i++) {
		sb->u.ext2_sb.s_inode_bitmap_number[i] = 0;
		sb->u.ext2_sb.s_inode_bitmap[i] = NULL;
		sb->u.ext2_sb.s_block_bitmap_number[i] = 0;
		sb->u.ext2_sb.s_block_bitmap[i] = NULL;
	}
	sb->u.ext2_sb.s_loaded_inode_bitmaps = 0;
	sb->u.ext2_sb.s_loaded_block_bitmaps = 0;
	sb->u.ext2_sb.s_gdb_count = db_count;
	/*
	 * set up enough so that it can read an inode
	 */
	sb->s_op = &ext2_sops;
	sb->s_root = d_alloc_root(iget(sb, EXT2_ROOT_INO));
	if (!sb->s_root) {
		for (i = 0; i < db_count; i++)
			if (sb->u.ext2_sb.s_group_desc[i])
				brelse (sb->u.ext2_sb.s_group_desc[i]);
		kfree(sb->u.ext2_sb.s_group_desc);
		brelse (bh);
		printk ("EXT2-fs: get root inode failed\n");
		return NULL;
	}
	ext2_setup_super (sb, es, sb->s_flags & MS_RDONLY);
	return sb;
}

这段代码比较长,但不复杂,所以我们就看看尾部的代码。

超级块只是反映具体设备上文件系统的组织和管理的信息,并不涉及该文件系统的内容,设备上根目录的索引节点才是打开这个文件系统的钥匙。文件系统中的每一个文件,包括目录,都有一个索引节点。其索引节点号必然存在于该文件所在的目录项中,唯有根目录的索引节点号是固定的,那就是EXT2_ROOT_INO,即2号索引节点。代码中的第630行先通过iget将这个索引节点读入内存并为之建立inode结构,再通过d_alloc_root在内存中为之分配一个dentry结构,并使super_block结构中的指针s_root指向这个dentry结构。这样,通向这个文件系统的途径就可以建立起来了。函数d_alloc_root的代码如下:

sys_mount=>do_mount=>get_sb_bdev=>read_super=>ext2_read_super=>d_alloc_root


/**
 * d_alloc_root - allocate root dentry
 * @root_inode: inode to allocate the root for
 *
 * Allocate a root ("/") dentry for the inode given. The inode is
 * instantiated and returned. %NULL is returned if there is insufficient
 * memory or the inode passed is %NULL.
 */
 
struct dentry * d_alloc_root(struct inode * root_inode)
{
	struct dentry *res = NULL;

	if (root_inode) {
		res = d_alloc(NULL, &(const struct qstr) { "/", 1, 0 });
		if (res) {
			res->d_sb = root_inode->i_sb;
			res->d_parent = res;
			d_instantiate(res, root_inode);
		}
	}
	return res;
}

如前所述,根目录的索引节点号是固定的,它也不出现在哪个目录中。所以,根目录是无名的,而为根目录建立的dentry结构都以'/'为名。代码中的&(const struct qstr) { "/", 1, 0 }表示一个qstr结构指针,它所指向的qstr结构为{"/",1,0},即节点名为"/",节点长度为1;数据结构的内容为常量,不允许改变。

最后(640行)是调用ext2_setup_super设置一些与管理有关的信息,包括此次安装的时间以及递增安装次数,当安装计数达到某个最大值时,就应该对这个文件系统运行e2fsck加以检查了。另一方面,由于每次安装,超级块的内容就一定有些变化(至少安装次数),所以要将超级块的缓冲区标志记成脏。函数ext2_setup_super的代码如下:

sys_mount=>do_mount=>get_sb_bdev=>read_super=>ext2_read_super=>ext2_setup_super


static int ext2_setup_super (struct super_block * sb,
			      struct ext2_super_block * es,
			      int read_only)
{
	int res = 0;
	if (le32_to_cpu(es->s_rev_level) > EXT2_MAX_SUPP_REV) {
		printk ("EXT2-fs warning: revision level too high, "
			"forcing read-only mode\n");
		res = MS_RDONLY;
	}
	if (read_only)
		return res;
	if (!(sb->u.ext2_sb.s_mount_state & EXT2_VALID_FS))
		printk ("EXT2-fs warning: mounting unchecked fs, "
			"running e2fsck is recommended\n");
	else if ((sb->u.ext2_sb.s_mount_state & EXT2_ERROR_FS))
		printk ("EXT2-fs warning: mounting fs with errors, "
			"running e2fsck is recommended\n");
	else if ((__s16) le16_to_cpu(es->s_max_mnt_count) >= 0 &&
		 le16_to_cpu(es->s_mnt_count) >=
		 (unsigned short) (__s16) le16_to_cpu(es->s_max_mnt_count))
		printk ("EXT2-fs warning: maximal mount count reached, "
			"running e2fsck is recommended\n");
	else if (le32_to_cpu(es->s_checkinterval) &&
		(le32_to_cpu(es->s_lastcheck) + le32_to_cpu(es->s_checkinterval) <= CURRENT_TIME))
		printk ("EXT2-fs warning: checktime reached, "
			"running e2fsck is recommended\n");
	es->s_state = cpu_to_le16(le16_to_cpu(es->s_state) & ~EXT2_VALID_FS);
	if (!(__s16) le16_to_cpu(es->s_max_mnt_count))
		es->s_max_mnt_count = (__s16) cpu_to_le16(EXT2_DFL_MAX_MNT_COUNT);
	es->s_mnt_count=cpu_to_le16(le16_to_cpu(es->s_mnt_count) + 1);
	es->s_mtime = cpu_to_le32(CURRENT_TIME);
	mark_buffer_dirty(sb->u.ext2_sb.s_sbh);
	sb->s_dirt = 1;
	if (test_opt (sb, DEBUG))
		printk ("[EXT II FS %s, %s, bs=%lu, fs=%lu, gc=%lu, "
			"bpg=%lu, ipg=%lu, mo=%04lx]\n",
			EXT2FS_VERSION, EXT2FS_DATE, sb->s_blocksize,
			sb->u.ext2_sb.s_frag_size,
			sb->u.ext2_sb.s_groups_count,
			EXT2_BLOCKS_PER_GROUP(sb),
			EXT2_INODES_PER_GROUP(sb),
			sb->u.ext2_sb.s_mount_opt);
#ifdef CONFIG_EXT2_CHECK
	if (test_opt (sb, CHECK)) {
		ext2_check_blocks_bitmap (sb);
		ext2_check_inodes_bitmap (sb);
	}
#endif
	return res;
}

至此,ext2_read_super的工作就全部完成了,函数返回指向该super_block数据结构的指针。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
国微CMS(原PHP168 S系列)是国内政府、学校、集团平台的领导厂商,也是中国南方PHP领域最大的开源系统提供商。此版本包括了国微学校方案和自助站群系统模块;两者放在了一起。 此次在国微CMS的独立站群模式基础上,国微新增推出自助建站站群系统;作为独立站群的重要补充(独立站群使用需另外下载系统) 国微自助站群功能如下: A:后台可以快速创建N个网站(可以是院系、部门、精品课程网站) B、每个分站可以设置独立的域名;可以使用文件夹地址访问; C、每个分站都可以设置一个或很多个独立管理员 D、创建分站的时候可以指定以某个站点为母站,创建时候可以和母站一模一样的效果,极大的节省时间。 E、每个分站可以自由选择不同模板 F、支持每个分站在后台设置横幅、系统名称、导航、版权等 G、支持分站和主站之间的数据相互推送 H、支持每个分站管理员的操作日志查询 I、支持所有内容前台发布,即看到哪里就可以操作到哪里。 J、支持每个分站的标签体系 K、每个分站的功能模块包括图片模块、视频模块、文章模块、下载模块、办事指南模块、信息公开模块、单独页模块、信箱模块等。 系统体系化 模块化体系:所有功能均已系统化、模块化、插件化,如CMS、问答、广告、标签 用户体系: 不仅区分企业、个人,并可自由添加角色组与角色,使其用户体系与实体一致。 权限体系: 所有功能模块封装并与权限匹配,可以细化至栏目对接角色管理权限。 标签体系: 常规标签、变量标签、标签后缀、标签缓存体系等已全面实施。 模板体系: 从方案模板、会员中心、系统模块模板、栏目、列表页面、内容页完全可独立选择。 积分体系: 积分兑换、积分消费、积分规则等已经开始在系统内实施。 菜单体系: 后台菜单、前台菜单、会员中心菜单均可自由添加和控制。 安全体系: 支持IP黑名单、白名单、支持防CC攻击、支持批量过滤敏感词汇。 通讯体系: 手机模块、邮件模块、短消息模块均已做成接口模式,任意功能均可方便调用。 程序整合体系:将支持UC等系统整合,同时互动百科也将整合。 易用性: A、全部前台可视化操作,可视即可操作。 B、一键化应用:如一键安装、一键缓存、一键静态、一键更换模板。 C、标签样式不断增加,只需用鼠标选择想用的样式即可。 D、自由组装和拆卸模块与插件。 E、掌握时间1小时学会建站。 国微CMS学校站群系统(原PHP168 S系列) 更新日志: 2019年11月23日升级包: 1、新增文件扫描对比校验功能。 2、新增了IP库,一键开启局域网访问功能。 3、新增了微信公众号功能。 4、新增了菜单批量处理功能。 5、新增了全局动静态分离功能。 6、新增了广告功能样式 7、新增了误删栏目一键恢复功能。 2020年04月13升级包: 1、更新了微信公众号助手,实现公众号数据互通 2、系统支持一键设置局域网功能 3、系统后台支持一键校验代码,检查代码是否被修改 4、新增后台自动备份数据设置 5、支持PHP7.X高版本 6、推荐:linux环境推荐用oninstack;windows环境推荐用wamp;其中PHP的环境推荐用PHP7.X 7、如果采用PHP7.X环境,需要在数据库配置文件加行代码,见教程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值