linux从ubi文件系统启动分析

本文深入剖析Linux系统中UBI文件系统的加载流程,从解析启动参数到mtd设备的初始化,再到UBI卷的加载及根文件系统的挂载,详细解读了Linux如何将真实的根文件系统加载至系统中。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在前面linux块设备原理文章中,已经分析过linux如何加载mtd设备。那么linux是如何把真实的根文件系统加载到系统中的呢,这边以ubi文件系统为例,分析linux真实根文件系统的加载。

沿着函数调用顺序一步步来看相关代码。

1 ubi卷的加载

kernel_init

    --------------->kernel_init_freeable

             ---------------------->do_basic_setup

                              ------------------->do_initcalls

在do_initcalls中,有一个ubi相关的初始化函数,它是如下定义的:

static int __init ubi_init(void)
{
	int err, i, k;

	。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
	/* Create base sysfs directory and sysfs files */
	ubi_class = class_create(THIS_MODULE, UBI_NAME_STR);
	if (IS_ERR(ubi_class)) {
		err = PTR_ERR(ubi_class);
		ubi_err("cannot create UBI class");
		goto out;
	}

	err = class_create_file(ubi_class, &ubi_version);
	if (err) {
		ubi_err("cannot create sysfs file");
		goto out_class;
	}

	err = misc_register(&ubi_ctrl_cdev);
	if (err) {
		ubi_err("cannot register device");
		goto out_version;
	}

	ubi_wl_entry_slab = kmem_cache_create("ubi_wl_entry_slab",
					      sizeof(struct ubi_wl_entry),
					      0, 0, NULL);
	if (!ubi_wl_entry_slab)
		goto out_dev_unreg;

	err = ubi_debugfs_init();
	if (err)
		goto out_slab;


	/* Attach MTD devices */
------------------------------------------------------------(1)
	for (i = 0; i < mtd_devs; i++) {
		struct mtd_dev_param *p = &mtd_dev_param[i];
		struct mtd_info *mtd;

		cond_resched();
-------------------------------------------------------------(2)
		mtd = open_mtd_device(p->name);
		if (IS_ERR(mtd)) {
			err = PTR_ERR(mtd);
			goto out_detach;
		}

		mutex_lock(&ubi_devices_mutex);
--------------------------------------------------------------(3)
		err = ubi_attach_mtd_dev(mtd, UBI_DEV_NUM_AUTO,
					 p->vid_hdr_offs, p->max_beb_per1024);
		mutex_unlock(&ubi_devices_mutex);
	。。。。。。。。。。。。。。。。。。。。。。。。。。
	}

	return 0;

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
}
late_initcall(ubi_init);

该函数的主要作用是,根据前面已经注册的mtd块设备以及uboot的启动参数中的参数,找到对应的mtd设备,从mtd分区中读取ubi卷信息,在系统中创建相应的ubi设备,完成mtd驱动和ubi卷的结合。

(1)获取mtd设备的编号。mtd_dev_param是从哪里来的呢。具体的初始化是在ubi_mtd_param_parse函数,该函数用module_param_call宏进行修饰:

module_param_call(mtd, ubi_mtd_param_parse, NULL, NULL, 000);

用上述宏定义的结构,最终会被放入__param段中。在系统初始化,解析uboot参数的时候:

start_kernel

    ------------>parse_args

parse_args("Booting kernel", static_command_line, __start___param,
		   __stop___param - __start___param,
		   -1, -1, &unknown_bootoption);

parse_args会依次取出uboot传过来的参数,和定义在__param段中的结构进行对比,如果名字上面匹配,则会调用__param中的函数进行解析。这边使用的uboot的启动参数为:

console=ttySAC0,115200 ubi.mtd=3 root=ubi0:rootfs rootfstype=ubifs

相应的,会匹配到这个字段ubi.mtd,所以ubi_mtd_param_parse函数会被调用,用来解析该字段后面的参数。uboot参数的本意是使用mtd3设备来加载ubi卷。看一下解析参数的这个过程:

static int __init ubi_mtd_param_parse(const char *val, struct kernel_param *kp)
{
	int i, len;
	struct mtd_dev_param *p;
	char buf[MTD_PARAM_LEN_MAX];
	char *pbuf = &buf[0];
	char *tokens[MTD_PARAM_MAX_COUNT];

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

	strcpy(buf, val);  //这边传入的val,就是字符3

	/* Get rid of the final newline */
	if (buf[len - 1] == '\n')
		buf[len - 1] = '\0';

	for (i = 0; i < MTD_PARAM_MAX_COUNT; i++)
		tokens[i] = strsep(&pbuf, ",");

	if (pbuf) {
		ubi_err("UBI error: too many arguments at \"%s\"\n", val);
		return -EINVAL;
	}

	p = &mtd_dev_param[mtd_devs];  //mtd_devs为0
	strcpy(&p->name[0], tokens[0]);   //把参数3放入mtd_dev_param[0]的name中
。。。。。。。。。。。。。。。。。。。。

	mtd_devs += 1;
	return 0;
}

可以看到,最终会解析到mtd分区3,然后存在mtd_dev_param的name中。

(2)获取到了第一个mtd分区,利用该分区索引,能找到对应的分区描述符:

static struct mtd_info * __init open_mtd_device(const char *mtd_dev)
{
	struct mtd_info *mtd;
	int mtd_num;
	char *endp;

	mtd_num = simple_strtoul(mtd_dev, &endp, 0);//将字符转化为数字,以这边为例,mtd_num为3
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
	mtd = get_mtd_device(NULL, mtd_num);//根据分区号获取分区描述符
	return mtd;
}

进去看一下get_mtd_device函数:

struct mtd_info *get_mtd_device(struct mtd_info *mtd, int num)
{
	struct mtd_info *ret = NULL, *other;
	int err = -ENODEV;

	mutex_lock(&mtd_table_mutex);
。。。。。。。。。。。。。。。。。
     else if (num >= 0) {
		ret = idr_find(&mtd_idr, num);//根据mtd分区号,获取mtd分区描述符
		if (mtd && mtd != ret)
			ret = NULL;
	}

。。。。。。。。。。。。。。。。。
	return ret;
}

在注册mtd设备的时候:

int add_mtd_device(struct mtd_info *mtd)
{
	struct mtd_notifier *not;
	int i, error;
	。。。。。。。。。。。。。。。
	i = idr_alloc(&mtd_idr, mtd, 0, 0, GFP_KERNEL);
    。。。。。。。。。。。。。。。
}

会为要注册的mtd描述符分配一个索引号,所以上面通过mtd设备索引号,就能快速的找到mtd设备描述符

(3)利用获取到的mtd分区描述符,读取分区flash中信息,完成ubi卷的加载,具体加载过程这边不分析。加载完以后,ubi device已经就绪了,随时可以挂载ubi文件系统。

2 ubi根文件系统挂载

在分析具体的挂载代码之前,大概把该路径上的代码都撸一遍。做完do_initcalls以后,回到kernel_init_freeable函数中:

static noinline void __init kernel_init_freeable(void)
{
	.。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

	do_basic_setup();

	/* Open the /dev/console on the rootfs, this should never fail */
----------------------------------------------------------------------(1)
	if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
		pr_err("Warning: unable to open an initial console.\n");

	(void) sys_dup(0);
	(void) sys_dup(0);
	/*
	 * check if there is an early userspace init.  If yes, let it do all
	 * the work
	 */
------------------------------------------------------------------------(2)
	if (!ramdisk_execute_command){
		ramdisk_execute_command = "/init";
	}

	if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
		ramdisk_execute_command = NULL;
--------------------------------------------------------------------------(3)
		prepare_namespace();
	}

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
}

(1)打开/dev/console这个文件,如果打开失败,则系统引导失败。网上有很多文章,在制作根文件系统的时候,在根文件系统下面需要手动建立/dev/console这个文件,不然会引导失败(我也是这么做的),其实不然,这边真实的根文件系统都还没有挂载。linux默认都是会编译initramfs的,编译的时候,即使没有指定initramfs需要压缩的rootfs路径,initramfs自己也会生成一个很小的文件系统,可以参考gen_inittamfs_list.sh脚本,initramfs不指定rootfs路径的情况下,会默认创建这些文件:

default_initramfs() {
	cat <<-EOF >> ${output}
		# This is a very simple, default initramfs

		dir /dev 0755 0 0
		nod /dev/console 0600 0 0 c 5 1
		dir /root 0700 0 0
		# file /kinit usr/kinit/kinit 0755 0 0
		# slink /init kinit 0755 0 0
	EOF
}

所以这边的/dev/console来自initramfs,initramfs在之前已经解压过,可以参考这篇文章:

https://blog.csdn.net/oqqYuJi12345678/article/details/103218438

(2)ramdisk_execute_command默认为空,所以这边初始化为/init,从initramfs中启动,会需要该文件,所以制作initramfs启动方式的文件系统时,该文件系统中必须包含/init文件,不然会启动失败

(3)由于这里选择从ubi文件系统启动,所以initramfs解压出来不包含/init文件,ramdisk_execute_command重新置为null,prepare_namespace用来挂载真实的ubi文件系统。

void __init prepare_namespace(void)
{
	int is_floppy;

	。。。。。。。。。。。。。。。。。。。。。。。。。。
	wait_for_device_probe();//等待driver的probe函数都执行完,执行probe函数之前有个统计量会加1,执行完则减1,所以该统计量为0的时候必然已经执行完

	md_run_setup();

	if (saved_root_name[0]) {
		root_device_name = saved_root_name;
--------------------------------------------------------------------(3.1)
		if (!strncmp(root_device_name, "mtd", 3) ||
		    !strncmp(root_device_name, "ubi", 3)) {
---------------------------------------------------------------------(3.2)
			mount_block_root(root_device_name, root_mountflags);
			goto out;
		}
		。。。。。。。。。。。。。。。。。。。。。。。。
	}

	。。。。。。。。。。。。。。。。。。。。。。。。。。
out:
------------------------------------------------------------------------(3.3)
	devtmpfs_mount("dev");
--------------------------------------------------------------------------(3.4)
	sys_mount(".", "/", NULL, MS_MOVE, NULL);//把当前真实的文件系统挂载到根目录下面
	sys_chroot("."); //设置根目录路径为刚挂载的真实文件系统
}

(3.1)saved_root_name在哪里赋值呢,和前面解析mtd.ubi参数一样,parse_args中解析root=参数:

static int __init root_dev_setup(char *line)
{
	strlcpy(saved_root_name, line, sizeof(saved_root_name));
	return 1;
}

__setup("root=", root_dev_setup);

saved_root_name被设置为ubi0:rootfs,该函数为设备ubi0名叫rootfs的卷

(3.2)匹配ubi名字成功,调用mount_block_root函数:

void __init mount_block_root(char *name, int flags)
{
	struct page *page = alloc_page(GFP_KERNEL |
					__GFP_NOTRACK_FALSE_POSITIVE);
	char *fs_names = page_address(page);
	char *p;
#ifdef CONFIG_BLOCK
	char b[BDEVNAME_SIZE];
#else
	const char *b = name;
#endif

	get_fs_names(fs_names);//这里获取rootfs的类型,又是通过解析uboot启动参数得到的,
                           //匹配rootfstype参数,设置root_fs_names变量为ubifs,fs_names变为ubifs
retry:
	for (p = fs_names; *p; p += strlen(p)+1) {
//name为ubi0:rootfs,文件系统类型为ubifs,root_mount_data不设置默认就以只读方式挂载
		int err = do_mount_root(name, p, flags, root_mount_data);
		switch (err) {
			case 0:
				goto out;
			case -EACCES:
				flags |= MS_RDONLY;
				goto retry;
			case -EINVAL:
				continue;
		}
         。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
	}

	。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
out:
	put_page(page);
}

do_mount_root用于挂载真实的ubi文件系统:

static int __init do_mount_root(char *name, char *fs, int flags, void *data)
{
	struct super_block *s;
	int err = sys_mount(name, "/root", fs, flags, data);//把文件系统挂载在/root路径下
	if (err)
		return err;

	sys_chdir("/root");//设置当前路径为/root,即默认的当前路径为新挂载的真实的文件系统
	s = current->fs->pwd.dentry->d_sb;
	ROOT_DEV = s->s_dev;
	printk(KERN_INFO
	       "VFS: Mounted root (%s filesystem)%s on device %u:%u.\n",
	       s->s_type->name,
	       s->s_flags & MS_RDONLY ?  " readonly" : "",
	       MAJOR(ROOT_DEV), MINOR(ROOT_DEV));
	return 0;
}

/root目录在前面建立默认的initramfs中可以看到,由initramfs创建,该目录应该就是为了这边挂载真实的文件系统所用的。最终真实的文件系统被挂载在/root目录下面

(3.3)把devtmpfs文件系统挂载到/root/dev/目录下面

devtmpfs目录是个什么呢,前面驱动初始化的时候,注册了很多设备,由于真实的文件系统还没有挂载,也就不可能有udev和mdev程序来负责在/dev目录下面创建设备节点。linux采用了另一个方法,在系统初始化的时候,先利用devtmpfs文件系统,把设备节点都创建在该文件系统下面,最后把该文件系统挂载在/root/dev下面。

看一下devtmpfs创建设备节点的基本原理。

系统初始化的时候:

int __init devtmpfs_init(void)
{
	int err = register_filesystem(&dev_fs_type);//注册devtmpfs文件系统
	。。。。。。。。。。。。。。。。。。。。。。。

	thread = kthread_run(devtmpfsd, &err, "kdevtmpfs");//创建devtmpfsd线程,来处理创建设备节点请求
	。。。。。。。。。。。。。。。。。。。。。。。
	return 0;
}

看一下devtmpfsd线程:

static int devtmpfsd(void *p)
{
	char options[] = "mode=0755";
	int *err = p;
	*err = sys_unshare(CLONE_NEWNS);
	if (*err)
		goto out;
//挂载devtmpfs文件系统到根目录
	*err = sys_mount("devtmpfs", "/", "devtmpfs", MS_SILENT, options);
	if (*err)
		goto out;
	sys_chdir("/.."); /* will traverse into overmounted root */
	sys_chroot(".");//
	complete(&setup_done);
	while (1) {
		spin_lock(&req_lock);
		while (requests) {
			struct req *req = requests;
			requests = NULL;
			spin_unlock(&req_lock);
			while (req) {
				struct req *next = req->next;
				req->err = handle(req->name, req->mode,
						  req->uid, req->gid, req->dev);
				complete(&req->done);
				req = next;
			}
			spin_lock(&req_lock);
		}
		__set_current_state(TASK_INTERRUPTIBLE);
		spin_unlock(&req_lock);
		schedule();//没有任务执行则睡眠
	}
	return 0;
out:
	complete(&setup_done);
	return *err;
}

上面函数先把devtmpfs文件系统挂载到了根目录,当device注册的时候

device_add

     -------------->devtmpfs_create_node

int devtmpfs_create_node(struct device *dev)
{
	const char *tmp = NULL;
	struct req req;

	if (!thread)
		return 0;

	req.mode = 0;
	req.uid = GLOBAL_ROOT_UID;
	req.gid = GLOBAL_ROOT_GID;
//获取设备名
	req.name = device_get_devnode(dev, &req.mode, &req.uid, &req.gid, &tmp);
	if (!req.name)
		return -ENOMEM;

	if (req.mode == 0)
		req.mode = 0600;
	if (is_blockdev(dev))
		req.mode |= S_IFBLK; //根据字符设备还是块设备,设置标记,最后创建的操作集函数会不一样
	else
		req.mode |= S_IFCHR;

	req.dev = dev;

	init_completion(&req.done);

	spin_lock(&req_lock);
	req.next = requests;
	requests = &req;
	spin_unlock(&req_lock);
//新建request,唤醒线程来处理新建设备节点的请求
	wake_up_process(thread);
	wait_for_completion(&req.done);

	kfree(tmp);

	return req.err;
}

创建设备节点函数为:

handle

    ------------>handle_create

static int handle_create(const char *nodename, umode_t mode, kuid_t uid,
			 kgid_t gid, struct device *dev)
{
	struct dentry *dentry;
	struct path path;
	int err;

	dentry = kern_path_create(AT_FDCWD, nodename, &path, 0);//找到父目录的dentry
	if (dentry == ERR_PTR(-ENOENT)) {
		create_path(nodename);
		dentry = kern_path_create(AT_FDCWD, nodename, &path, 0);
	}
	if (IS_ERR(dentry))
		return PTR_ERR(dentry);
//创建设备节点
	err = vfs_mknod(path.dentry->d_inode, dentry, mode, dev->devt);
	if (!err) {
		struct iattr newattrs;

		newattrs.ia_mode = mode;
		newattrs.ia_uid = uid;
		newattrs.ia_gid = gid;
		newattrs.ia_valid = ATTR_MODE|ATTR_UID|ATTR_GID;
		mutex_lock(&dentry->d_inode->i_mutex);
		notify_change(dentry, &newattrs);
		mutex_unlock(&dentry->d_inode->i_mutex);

		/* mark as kernel-created inode */
		dentry->d_inode->i_private = &thread;
	}
	done_path_create(&path, dentry);
	return err;
}

最终devtmpfs_mount把devtmpfs文件系统又挂载到/root/dev目录下

(3.4)然后把当前目录,也就是root目录,重新挂载到根文件夹/下面,也就是说真实的文件系统被放到了根目录/下,然后设置绝对路径为/,至此程序的当前路径和绝对路径都被设置为/根目录,其下面挂载的就是真实的根文件系统了

最终真实的根文件系统挂载完成:

static int __ref kernel_init(void *unused)
{
	。。。。。。。。。。。。。。。。。。。。。。。。。。。。
	if (!run_init_process("/sbin/init") ||
	    !run_init_process("/etc/init") ||
	    !run_init_process("/bin/init") ||
	    !run_init_process("/bin/sh"))
		return 0;
	panic("No init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/init.txt for guidance.");
}

最终会运行真实根文件系统的某个初始化程序,kernel_init线程不会再返回。系统启动完成

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值