一起分析Linux系统设计思想——03内核启动流程分析(九,uboot命令行参数解析和flash分区)

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

3 命令行参数解析(接上篇)

剩余命令行参数的解析会分两个阶段进行。第一个阶段解析和处理器架构相关的命令行参数;第二个阶段处理通用的命令行参数。

为什么要分两个阶段来进行解析呢?是为了将通用的和特殊的流程进行 分离 ,将 变与不变 分离,这样代码才能 结构化 ,结构化才能 搭建平台兼容性 才更好。

我们在自己的项目中也要借鉴和学习这种思想,最直接的就是可以尝试着将初始化代码划分为不同的阶段。划分阶段的依据是抓住项目初始化中的变与不变或流程上的差异化,这就是代码流程设计最核心的工作。

我们来看一下 start_kernel 函数中与参数解析和使用相关的代码:


asmlinkage void __init start_kernel(void)
{
	char * command_line;
	extern struct kernel_param __start___param[], __stop___param[];
	...
	printk(KERN_NOTICE);
	printk(linux_banner); /*打印内核版本等信息*/
	setup_arch(&command_line); /*获取command line的相关信息存储到command_line*/
	/* 该函数只是简单拷贝:
	   strcpy (saved_command_line, boot_command_line);拷贝原始命令行参数
	   strcpy (static_command_line, command_line);拷贝首次解析之后的命令行参数*/
	setup_command_line(command_line); 
	...
	/* 打印原始命令行参数(在串口上可以看到这行打印) */
	printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
	parse_early_param(); /*第一阶段解析:arch相关的参数解析*/
        /* 第二阶段解析:通用的参数解析 */
	parse_args("Booting kernel", static_command_line, __start___param,
		   __stop___param - __start___param,
		   &unknown_bootoption); /*真正用来干活儿的是unknown_bootoption函数*/
	...

	/* Do the rest non-__init'ed, we're now alive */
	rest_init(); /*参数的使用需要进入这个函数内部*/
}

3.1 第一阶段解析

第一阶段的解析在 parse_early_param() 函数中完成。内核是怎样区分第一阶段和第二阶段呢?其实是靠一个 early 参数,如果该参数配置成非0值就是第一阶段,如果配置成0就是第二阶段。

这种实现的细节我们也可以在我们的代码中进行模仿,而且可以扩展。比如定义一个 phase 参数,如果参数配置成1,相关的处理就放在第一阶段;如果参数配置成2,相关的处理就放在第二阶段……

下面我们看一下parse_early_param()函数。我们重点关注代码是如何实现区分第一阶段和第二阶段的,其他的细节我们放在下一节分析,因为两个阶段的解析思路是一致的。

/* /init/main.c */

/* Arch code calls this early on, or if not, just before other parsing. */
void __init parse_early_param(void)
{
	static __initdata int done = 0;
	static __initdata char tmp_cmdline[COMMAND_LINE_SIZE];

	if (done)
		return;

	/* All fall through to do_early_param. */
	strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
        /* 真正用来干活儿的是do_early_param函数 */
	parse_args("early options", tmp_cmdline, NULL, 0, do_early_param); 
	done = 1;
}

/* Check for early params. */
static int __init do_early_param(char *param, char *val)
{
	struct obs_kernel_param *p;

	for (p = __setup_start; p < __setup_end; p++) {
         /* 在这个函数中我们主要关注代码是如何区分是否为第一阶段的。
            在这个条件中我们发现p->early为非零值代码才会执行 */
		if (p->early && strcmp(param, p->str) == 0) {
			if (p->setup_func(val) != 0)
				printk(KERN_WARNING
				       "Malformed early option '%s'\n", param);
		}
	}
	/* We accept everything at this stage. */
	return 0;
}

3.2 第二阶段解析

第二阶段的解析主要处理和处理器架构不相关的参数。下面以 root=/dev/mtdblock3 为例来说明参数是如何被解析和使用的。

3.2.1.参数的解析

我们知道第二阶段的参数解析,真正干活儿的是 unknown_bootoption() 函数。接下来我们就从该函数入手进行分析。由于代码量比较多,而且跳转比较频繁,这里先总体上说一下解析的思路,防止看完代码还云里雾里。

首先,搞清除参数解析的本质。我们再来看一下命令行参数:noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 。其实,命令行参数是按照 键值对 来设计的,其中的 rootinitconsole 就是 ,等号后边的就是 。注意,这句话很重要: 键是确定的,值是不确定的 。依照这个特点,内核在指定的段( .init.setup)内存放对应的 解析键值对 (以结构体struct obs_kernel_param的方式实现),这里的 也是 rootinitconsole ,而 却有所不同,是函数指针。

数据结构搞清楚了,我们再来看看解析的流程。整个流程就是遍历自定义段中结构体,取出结构体的键(例如root)与命令行参数中的键进行匹配,如果找到匹配的键,则调用该键对应的函数指针指向的函数(例如root_dev_setup)将命令行参数中的值 截取 出来放到全局变量(例如saved_root_name)中进行存储备用。这个过程就叫做参数的解析。

理清楚了思路,看代码就好比摧枯拉朽一般。

下面先看一下解析过程的代码:

/* /init/main.c */

/*
 * Unknown boot options get handed to init, unless they look like
 * failed parameters
 */
/* 此函数是第二阶段的入口解析函数 */
static int __init unknown_bootoption(char *param, char *val)
{
	...
	/* Handle obsolete-style parameters */
	if (obsolete_checksetup(param))
		return 0;

	...
	return 0;
}

extern struct obs_kernel_param __setup_start[], __setup_end[];
/* 该函数是真正的解析函数 */
static int __init obsolete_checksetup(char *line)
{
	struct obs_kernel_param *p;
	int had_early_param = 0;

	p = __setup_start;
	do {
		int n = strlen(p->str);
		if (!strncmp(line, p->str, n)) {
			if (p->early) { /*这一步处理可以先跳过,属于冗余处理*/
				/* Already done in parse_early_param?
				 * (Needs exact match on param part).
				 * Keep iterating, as we can have early
				 * params and __setups of same names 8( */
				if (line[n] == '\0' || line[n] == '=')
					had_early_param = 1;
                        /* 下面这两个分支是第二阶段解析的代码 */
			} else if (!p->setup_func) {
				printk(KERN_WARNING "Parameter %s is obsolete,"
				       " ignored\n", p->str);
				return 1;
			} else if (p->setup_func(line + n))
				return 1;
		}
		p++;
	} while (p < __setup_end);

	return had_early_param;
}

看完解析过程的代码,再来看一下内核是如何存放 解析键值对 到自定义段中的。这种解析键值对背后的设计思想其实是 事件驱动 就是事件,值(函数指针) 就是响应,是非常值得我们学习的一种设计方式。

这里的技巧点(也是初学内核的TX的难点)有两个。

一个是使用了自定义段,这样做使得 定义调用 分离了,优点是可以做到 形散神聚 ,缺点是阅读代码容易被打断,不能持续跳转,而是要靠全局搜素。看这种代码的技巧是记得 中转站 是链接脚本。

另外一个是使用了函数指针,使用函数指针就意味着不是直接调用,优点是可以保证对外调用接口不变,缺点依然是阅读代码容易被打断。看这种代码的技巧是——习惯就好,哈哈~~ C语言的精髓就是在指针上,看到后应该兴奋哈。

/* /init/do_mounts.c */

/* 解析,后处理函数 */
static int __init root_dev_setup(char *line)
{
	strlcpy(saved_root_name, line, sizeof(saved_root_name));
	return 1;
}
/* 注册 解析键值对 到指定的段中 */
__setup("root=", root_dev_setup);

下面的代码是两个技巧实现的核心:

/* /include/linux/init.h */

#define __setup(str, fn)					\
	__setup_param(str, fn, fn, 0)

/* 解析键值对 结构体定义*/
struct obs_kernel_param {
	const char *str;		  /*键:事件标记*/
	int (*setup_func)(char *); /*值:响应函数*/
	int early; /*该参数用来控制第一阶段还是第二阶段运行*/
};
/*
 * Only for really core code.  See moduleparam.h for the normal way.
 *
 * Force the alignment so the compiler doesn't space elements of the
 * obs_kernel_param "array" too far apart in .init.setup.
 */
#define __setup_param(str, unique_id, fn, early)			\
	static char __setup_str_##unique_id[] __initdata = str;	\
	static struct obs_kernel_param __setup_##unique_id	\
		__attribute_used__				\
		__attribute__((__section__(".init.setup")))	\ /*将该结构体强制放入.init.setup段*/
		__attribute__((aligned((sizeof(long)))))	\
		= { __setup_str_##unique_id, fn, early }

阅读使用了强制段的代码时,链接脚本是桥梁,桥的一端是段的起止边界,另一端是段的名称。段名用来注册,段的起止边界用来遍历解析。

/* /arch/arm/kernel/vmlinux.lds */
  __setup_start = .;
   *(.init.setup)
  __setup_end = .;

3.2.2.参数的使用

此处依然使用root参数为例进行说明。root参数被解析出来存储到了saved_root_name中。该参数是用来告诉内核根文件系统存放的位置的,因此,最终会被挂载根文件系统的阶段使用。挂载完根文件系统,就开始启动第一个应用程序,启动阶段到此为止。

上代码:

/* /init/do_mounts.c */

/*
 * Prepare the namespace - decide what/where to mount, load ramdisks, etc.
 */
void __init prepare_namespace(void)
{
	int is_floppy;

	...

    /* 此时saved_root_name数组是有内容的,在参数的解析阶段填充的 */
	if (saved_root_name[0]) { 
		root_device_name = saved_root_name;
		if (!strncmp(root_device_name, "mtd", 3)) {
			mount_block_root(root_device_name, root_mountflags);
			goto out;
		}
         /* 将字符串转换为数字赋值给全局变量ROOT_DEV,
            后面在挂接根文件系统时使用*/
		ROOT_DEV = name_to_dev_t(root_device_name);
		if (strncmp(root_device_name, "/dev/", 5) == 0)
			root_device_name += 5;
	}
	...
	if (is_floppy && rd_doload && rd_load_disk(0))
		ROOT_DEV = Root_RAM0;

	mount_root(); /*ROOT_DEV在该函数中被使用*/
out:
	sys_mount(".", "/", NULL, MS_MOVE, NULL);
	sys_chroot(".");
	security_sb_post_mountroot();
}

挂载根文件系统在 mount_root() 函数中完成,该函数中使用了 ROOT_DEV 来创建根设备和挂载根文件系统。

/* /init/do_mounts.c */
void __init mount_root(void)
{
#ifdef CONFIG_ROOT_NFS /*配置了该宏,但是我们先不讨论网络文件系统*/
	if (MAJOR(ROOT_DEV) == UNNAMED_MAJOR) {
		if (mount_nfs_root())
			return;

		printk(KERN_ERR "VFS: Unable to mount root fs via NFS, trying floppy.\n");
		ROOT_DEV = Root_FD0;
	}
#endif
...
#ifdef CONFIG_BLOCK 
    /*设备真正挂接根文件系统在这里完成;
      关于根文件系统和创建设备节点等知识后续分析*/
	create_dev("/dev/root", ROOT_DEV);
	mount_block_root("/dev/root", root_mountflags);
#endif
}

4 flash分区

由于篇幅问题,和本着循序渐进的思路,这里仅仅说明flash分区是在哪里定义的,详细内容会在文件系统系列文章中再进一步分析。

内核在启动时会打印下述分区信息:

/* Linux内核打印的磁盘分区信息(注意这里的地址不要和内存地址混淆) */
Creating 4 MTD partitions on "NAND 256MiB 3,3V 8-bit":
0x00000000-0x00040000 : "bootloader"
0x00040000-0x00060000 : "params"
0x00060000-0x00260000 : "kernel"
0x00260000-0x10000000 : "root"

决定这些分区的代码并不是存储在flash芯片中,而是在内核代码中写死的。

/* /arch/arm/plat-s3c24xx/common-smdk.c */

/* NAND parititon from 2.4.18-swl5 */
static struct mtd_partition smdk_default_nand_part[] = {
	[0] = { /*mtdblock0*/
        .name   = "bootloader",
        .size   = 0x00040000,
		.offset	= 0,
	},
	[1] = { /*mtdblock1*/
        .name   = "params",
        /* 紧接上一个分区,即0x00040000 */
        .offset = MTDPART_OFS_APPEND, 
        .size   = 0x00020000,
	},
	[2] = { /*mtdblock2*/
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00200000,
	},
	[3] = { /*mtdblock3*/
        .name   = "root",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,
	}
};

恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿越临界点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值