在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。
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 。其实,命令行参数是按照 键值对 来设计的,其中的 root 、init 、console 就是 键 ,等号后边的就是 值 。注意,这句话很重要: 键是确定的,值是不确定的 。依照这个特点,内核在指定的段( .init.setup)内存放对应的 解析键值对 (以结构体struct obs_kernel_param的方式实现),这里的 键 也是 root 、init 、console ,而 值 却有所不同,是函数指针。
数据结构搞清楚了,我们再来看看解析的流程。整个流程就是遍历自定义段中结构体,取出结构体的键(例如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,
}
};
恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~