1. 如何安装
安装根文件系统是系统初始化的关键部分。Linux内核允许根文件系统存放在很多不同的地方,比如硬盘分区、软盘、通过NFS共享的远程文件系统,设置保存在ramdisk中(RAM中的虚拟块设备)。
接下来讨论的是假定根文件系统存放在硬盘分区。安装根文件系统分两个阶段:
- 内核安装特殊rootfs文件系统,该文件系统仅提供一个作为初始安装点的空目录。
- 内核在空目录上安装实际根文件系统。
rootfs文件系统的存在,允许内核容易地改变实际根文件系统。事实上,在某些情况下,内核逐个地安装和卸载几个根文件系统。
2. 阶段一:安装 rootfs 文件系统
2.1 初始化流程
第一阶段主要由 init_rootfs() 和 init_mount_tree() 函数完成。
start_kernel
vfs_caches_init(num_physpages);
mnt_init(mempages);
init_rootfs();
init_mount_tree();
2.2 init_rootfs()
init_rootfs() 函数完成了 rootfs 的注册。
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.get_sb = rootfs_get_sb,
.kill_sb = kill_litter_super,
};
int __init init_rootfs(void)
register_filesystem(&rootfs_fs_type);
2.3 init_mount_tree()
static void __init init_mount_tree(void)
struct vfsmount *mnt;
struct mnt_namespace *ns;
/* 安装操作,返回一个已安装文件系统描述符,参考2.3.1 */
mnt = do_kern_mount("rootfs", 0, "rootfs", NULL);
/* 为进程0的命名空间分配一个命名空间并初始化 */
ns = kmalloc(sizeof(*ns), GFP_KERNEL);
atomic_set(&ns->count, 1);
INIT_LIST_HEAD(&ns->list);
list_add(&mnt->mnt_list, &ns->list);
/* 命名空间根目录的已安装文件系统描述符 */
ns->root = mnt;
mnt->mnt_ns = ns;
init_task.nsproxy->mnt_ns = ns;
/* 将进程0的当前工作目录目录设置为根文件系统 */
set_fs_pwd(current->fs, ns->root, ns->root->mnt_root);
fs->pwdmnt = mntget(mnt);
fs->pwd = dget(dentry);
/* 将进程0的根目录设置为根文件系统 */
set_fs_root(current->fs, ns->root, ns->root->mnt_root);
fs->rootmnt = mntget(mnt);
fs->root = dget(dentry);
2.3.1 do_kern_mount()
处理安装操作并返回一个新安装文件系统描述符的地址,安装操作的核心函数
/**
* @fstype: 要安装的文件系统的类型名
* @flags: 安装标志
* @name: 存放文件系统的块设备的路径名(或特殊文件系统的类型名)
* @data:
*/
struct vfsmount *
do_kern_mount(const char *fstype, int flags, const char *name, void *data)
/* 在文件系统类型链表中搜索并确定存放在 fstype 参数中的名字的位置 */
struct file_system_type *type = get_fs_type(fstype);
struct vfsmount *mnt;
mnt = vfs_kern_mount(type, flags, name, data);
struct vfsmount *mnt;
/* 分配一个新的已安装文件系统描述符 */
mnt = alloc_vfsmnt(name);
struct vfsmount *mnt = kmem_cache_zalloc(mnt_cache, GFP_KERNEL);
mnt->mnt_devname = newname; //rootfs
/* 1. 分配并初始化一个超级块对象 */
type->get_sb(type, flags, name, data, mnt);
mnt->mnt_mountpoint = mnt->mnt_root;
mnt->mnt_parent = mnt;
- rootfs 的 get_sb 方法
static int rootfs_get_sb(struct file_system_type *fs_type, int flags, const char *dev_name, void *data, struct vfsmount *mnt) { /* 参考2.3.2 */ return get_sb_nodev(fs_type, flags|MS_NOUSER, data, ramfs_fill_super, mnt); }
2.3.2 get_sb_nodev()
该函数对于可以安装多次的特殊文件系统。
/**
* @fs_type: 文件系统类型
* @flags: 安装标志
* @data:
* @fill_super: 分配并填充超级块
* @mnt: 已安装文件系统描述符
*/
int get_sb_nodev(struct file_system_type *fs_type,
int flags, void *data,
int (*fill_super)(struct super_block *, void *, int),
struct vfsmount *mnt)
/* 分配新的超级块,参考2.3.3 */
struct super_block *s = sget(fs_type, NULL, set_anon_super, NULL);
s->s_flags = flags;
/* 填充超级块对象,参考2.3.4 */
fill_super(s, data, flags & MS_SILENT ? 1 : 0);
s->s_flags |= MS_ACTIVE;
simple_set_mnt(mnt, s);
mnt->mnt_sb = sb;
mnt->mnt_root = dget(sb->s_root);
2.3.3 sget()
/**
* @type: 文件系统类型
* @test: 指向一个用于比较的函数,这里传入NULL
* @set: 指向一个用于设置的函数
* @data:
*/
struct super_block *sget(struct file_system_type *type,
int (*test)(struct super_block *,void *),
int (*set)(struct super_block *,void *),
void *data)
struct super_block *s = NULL;
/* 分配并初始化一个超级块对象 */
s = alloc_super(type);
/* 1. 对超块对象某些字段进行设置 */
set(s, data);
s->s_type = type;
strlcpy(s->s_id, type->name, sizeof(s->s_id));
list_add_tail(&s->s_list, &super_blocks);
list_add(&s->s_instances, &type->fs_supers);
- set 实际指向 set_anon_super() 函数,对超级块对象进行设置。rootfs为特殊文件系统,主设备号为0,次设备号不同于其他特殊文件系统次设备号。
int set_anon_super(struct super_block *s, void *data) s->s_dev = MKDEV(0, dev & MINORMASK);
2.3.4 fill_super
fill_super 实际指向 ramfs_fill_super() 函数。由于rootfs是特殊文件系统,没有对应的磁盘信息,所以对于超级块的初始化不需要读取磁盘。
static int ramfs_fill_super(struct super_block * sb, void * data, int silent)
struct inode * inode;
struct dentry * root;
sb->s_magic = RAMFS_MAGIC;
sb->s_op = &ramfs_ops;
/* 分配根索引节点,参考2.3.5 */
inode = ramfs_get_inode(sb, S_IFDIR | 0755, 0);
/* 分配根目录项, 参考2.3.6 */
root = d_alloc_root(inode);
/* 设置超级块的根目录项 */
sb->s_root = root;
2.3.5 根索引节点
struct inode *ramfs_get_inode(struct super_block *sb, int mode, dev_t dev)
struct inode * inode = new_inode(sb);
static unsigned int last_ino;
struct inode * inode;
inode = alloc_inode(sb);
list_add(&inode->i_list, &inode_in_use);
list_add(&inode->i_sb_list, &sb->s_inodes);
inode->i_ino = ++last_ino;
inode->i_mode = mode;
/* 不同的文件类型拥有不同的操作集合 */
switch (mode & S_IFMT) {
default:
break;
case S_IFREG:
inode->i_op = &ramfs_file_inode_operations;
inode->i_fop = &ramfs_file_operations;
break;
case S_IFDIR:
inode->i_op = &ramfs_dir_inode_operations;
inode->i_fop = &simple_dir_operations;
break;
case S_IFLNK:
inode->i_op = &page_symlink_inode_operations;
break;
}
2.3.6 根目录项
struct dentry * d_alloc_root(struct inode * root_inode)
struct dentry *res = NULL;
static const struct qstr name = { .name = "/", .len = 1 };
/* 分配目录项 */
res = d_alloc(NULL, &name);
struct dentry *dentry;
dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL);
dentry->d_name.name = dname;
memcpy(dname, name->name, name->len);
dentry->d_flags = DCACHE_UNHASHED;
dentry->d_mounted = 0;
res->d_sb = root_inode->i_sb;
/* 根目录项的d_parent字段指向自己 */
res->d_parent = res;
d_instantiate(res, root_inode);
list_add(&entry->d_alias, &inode->i_dentry);
/* 将根目录项和根索引节点关联起来 */
entry->d_inode = inode;
3. 阶段二:安装实际根文件系统
根文件系统安装操作的第二阶段是由内核在系统初始化即将结束时进行的。根据内核被编译时所选择的选项,和内核装入程序所传递的启动选项,可以有几种方法安装实际的根文件系统。下面我们只考虑磁盘文件系统的情况,它的设备文件名已通过“root”启动参数传递给内核。
3.1 prepare_namespace()
void __init prepare_namespace(void)
if (saved_root_name[0]) {
/* 把 root_device_name 设置为从启动参数root中获取的设备文件名 */
root_device_name = saved_root_name;
/* 把 ROOT_DEV 变量设置为同一设备文件的设备标识符,参考3.1.1 */
ROOT_DEV = name_to_dev_t(root_device_name);
if (strncmp(root_device_name, "/dev/", 5) == 0)
root_device_name += 5;
}
/* 参考3.2 */
mount_root();
/* 把已安装文件系统移动到rootfs的根目录"/" */
sys_mount(".", "/", NULL, MS_MOVE, NULL);
/* 改变进程根目录 */
sys_chroot(".");
3.1.1 name_to_dev_t()
把设备路径名解析为设备标识符。刚开始还没有挂载实际的根文件系统,所以无法通过文件系统访问一个设备文件。内核是通过将设备的名子转换成设备标识符,有了设备标识符就可以通过主次设备号找到驱动程序从而访问磁盘。所以这个设备的名字并不是真的存在一个设备文件,我们的目的是从这个名字找到对应的设备标识符。
而这种转换的本质实际上是借用了sysfs文件系统,这里有已知的磁盘设备,我们读取相应的文件可以获取设备主次设备号。
dev_t name_to_dev_t(char *name)
{
char s[32];
char *p;
dev_t res = 0;
int part;
/* 1. 创建一个sys目录,将sysfs挂载到/sys目录 */
int mkdir_err = sys_mkdir("/sys", 0700);
if (sys_mount("sysfs", "/sys", "sysfs", 0, NULL) < 0)
goto out;
/* 2. 如果设备文件名不以/dev/开头 */
if (strncmp(name, "/dev/", 5) != 0) {
unsigned maj, min;
if (sscanf(name, "%u:%u", &maj, &min) == 2) {
res = MKDEV(maj, min);
if (maj != MAJOR(res) || min != MINOR(res))
goto fail;
} else {
res = new_decode_dev(simple_strtoul(name, &p, 16));
if (*p)
goto fail;
}
goto done;
}
/* 右移去掉/dev/ */
name += 5;
/* 如果根文件系统是网络文件系统,返回网络文件系统设备文件标识符 */
res = Root_NFS;
if (strcmp(name, "nfs") == 0)
goto done;
/* 如果根文件系统是内存文件系统,返回内存文件系统设备文件标识符 */
res = Root_RAM0;
if (strcmp(name, "ram") == 0)
goto done;
/* 把设备文件名中的'/'替换为'!' */
strcpy(s, name);
for (p = s; *p; p++)
if (*p == '/')
*p = '!';
/* 以设备文件名为参数获取设备标识符,参考3.1.2 */
res = try_name(s, 0);
if (res)
goto done;
/* 解析分区号 */
while (p > s && isdigit(p[-1]))
p--;
part = simple_strtoul(p, NULL, 10);
*p = '\0';
/* 以设备文件名和分区号为参数获取设备标识符,参考3.1.2 */
res = try_name(s, part);
if (res)
goto done;
if (p < s + 2 || !isdigit(p[-2]) || p[-1] != 'p')
goto fail;
p[-1] = '\0';
/* 分区格式还可能是<disk_name>p<decimal> */
res = try_name(s, part);
done:
/* 卸载sysfs文件系统 */
sys_umount("/sys", 0);
out:
/* 删除sys目录 */
sys_rmdir("/sys");
return res;
}
- 内核在编译时设置了sysfs文件系统,在根目录下创建sys目录,并安装sysfs文件系统。
- 如果设备文件名不以/dev/开头,则直接从设备路径名中取主设备号和次设备号,组成设备文件标识符。
3.1.2 try_name()
static dev_t try_name(char *name, int part)
{
char path[64];
char buf[32];
int range;
dev_t res;
char *s;
int len;
int fd;
unsigned int maj, min;
/* /sys/block/设备文件/dev读取主次设备号 */
sprintf(path, "/sys/block/%s/dev", name);
fd = sys_open(path, 0, 0);
len = sys_read(fd, buf, 32);
sys_close(fd);
if (sscanf(buf, "%u:%u", &maj, &min) == 2) {
/* Try the %u:%u format */
res = MKDEV(maj, min);
} else {
/* Try old-style "0321" */
res = new_decode_dev(simple_strtoul(buf, &s, 16));
}
/* 如果分区为0,则返回主设备号+次设备号组成的设备标识符 */
if (!part)
return res;
/* 读取次设备号范围大小 */
sprintf(path, "/sys/block/%s/range", name);
fd = sys_open(path, 0, 0);
len = sys_read(fd, buf, 32);
sys_close(fd);
range = simple_strtoul(buf, &s, 10);
/* 如果分区大于0,则返回主设备号+次设备号(次设备号+分区号)组成的设备标识符 */
if (part < range)
return res + part;
}
3.2 mount_root()
void __init mount_root(void)
/* 在/dev目录中创建root设备文件,设备标识符为ROOT_DEV */
create_dev("/dev/root", ROOT_DEV);
sys_mknod(name, S_IFBLK|0600, new_encode_dev(dev));
/* 安装实际根文件系统 */
mount_block_root("/dev/root", root_mountflags);
/* 分配一个缓冲区 */
char *fs_names = __getname();
/* 获取文件系统类型的表,参考3.2.1 */
get_fs_names(fs_names);
for (p = fs_names; *p; p += strlen(p)+1) {
do_mount_root(name, p, flags, root_mount_data);
/* 1. 将/dev/root挂载到/root目录 */
sys_mount(name, "/root", fs, flags, data);
/* 改变进程的当前目录 */
sys_chdir("/root");
ROOT_DEV = current->fs->pwdmnt->mnt_sb->s_dev;
}
- 扫描建立好的文件系统类型名表。对每个名字,调用 sys_mount() 试图安装给定的文件系统类型。由于每个特定于文件系统的方法使用不同的魔数 s_magic,因此对 get_sb() 的调用大都会失败,但有一个例外,那就是用根设备上实际使用的文件系统的函数来填充超级块的那个调用,该文件系统被安装到rootfs文件系统的/root目录上。
3.2.1 get_fs_names()
获取文件系统类型名表。该表要么通过启动参数“rootfstype”传递给内核,要么通过扫描文件系统类型链表中的元素建立。
static void __init get_fs_names(char *page)
{
char *s = page;
if (root_fs_names) {
/* 通过传入参数rootfstype获取文件系统类型名表 */
strcpy(page, root_fs_names);
while (*s++) {
if (s[-1] == ',')
s[-1] = '\0';
}
} else {
/* 通过扫描文件系统类型链表元素建立文件系统名表 */
int len = get_filesystem_list(page);
char *p, *next;
page[len] = '\0';
for (p = page-1; p; p = next) {
next = strchr(++p, '\n');
if (*p++ != '\t')
continue;
while ((*s++ = *p++) != '\n')
;
s[-1] = '\0';
}
}
*s = '\0';
}
4. 补充说明
4.1 rootfs默认的目录和文件
上面分析安装过程中提到了两个目录:/dev目录和/root目录,那么这两个目录是什么时候创建的?创建完rootfs后,安装实际根文件系统前,系统都创建了哪些目录和文件?
static int __init default_rootfs(void)
/* 创建了dev目录 */
sys_mkdir("/dev", 0755);
/* 创建了console设备节点 */
sys_mknod((const char __user *) "/dev/console",
S_IFCHR | S_IRUSR | S_IWUSR,
new_encode_dev(MKDEV(5, 1)));
/* 创建了root目录 */
sys_mkdir("/root", 0700);
rootfs_initcall(default_rootfs);
#define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)
/* 宏定义设置了段属性 */
#define __define_initcall(level,fn,id) \
static initcall_t __initcall_##fn##id __attribute_used__ \
__attribute__((__section__(".initcall" level ".init"))) = fn
4.2 如何调用default_rootfs()
start_kernel
/* 这里创建了rootfs */
vfs_caches_init(num_physpages);
mnt_init(mempages);
init_rootfs();
init_mount_tree();
rest_init
kernel_init
do_basic_setup();
/* 1. */
do_initcalls();
for (call = __initcall_start; call < __initcall_end; call++) {
(*call)();
}
- 这里调用了.initcall段内的函数,从而调用default_rootfs创建rootfs内默认的目录和文件。