在Linux移植之内核启动过程start_kernel函数简析中已经指出了start_kernel函数的调用层次,这篇主要是对具体的tag参数列表进行解析。
2、命令行参数ATAG_CMDLINE解析,以传入的命令参数bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0为列:
start_kernel setup_arch //解析UBOOT传入的启动参数 setup_command_line //解析UBOOT传入的启动参数 do_early_param //解析early参数,uboot中没传这个参数 unknown_bootoption//解析到了命令行参数,saved_root_name在这块初始化 console_init();//控制台初始化 rest_init kernel_thread kernel_init prepare_namespace //解析命令行参数解析成功挂接在哪个分区 mount_root//挂接根文件系统 init_post //执行应用程序
看到arch\arm\kernel\Setup.c文件,在setup_arch函数里看到如下几行,首先根据内核启动时第一阶段得到的machine_arch_type,取得mdesc结构体,这个结构体在Linux移植之内核启动过程引导阶段分析已经介绍过,这里主要关心的是boot_params参数,里面存放的是tag参数列表的存放地址,然后将取得的物理地址转换为虚拟地址供后面使用tag。
776 console_init();//控制台初始化 777 arch\arm\kernel\Setup.c 778 779 setup_processor();//设置处理器相关的一些设置 780 mdesc = setup_machine(machine_arch_type);//获得开发板的machine_desc结构 781 machine_name = mdesc->name;//取得开发板的名称 782 783 if (mdesc->soft_reboot) 784 reboot_setup("s"); 785 786 if (mdesc->boot_params)//确定uboot传入的启动参数的地址 787 tags = phys_to_virt(mdesc->boot_params);//将启动参数的物理地址转换为虚拟地址
setup_arch函数继续往下看
109 static struct meminfo meminfo __initdata = { 0, }; 798 if (tags->hdr.tag == ATAG_CORE) {//ATAG_CORE为tag标记列表的开始 799 if (meminfo.nr_banks != 0)//如果已经在内核中定义了meminfo结构 780 squash_mem_tags(tags);//则忽略内存tag 781 parse_tags(tags);//解释每个tag 782 }
其中meminfo就是处理完ATAG_MEN参数后,将里面的内容放去meninfo中,它的结构定义在include\asm-arm\Setup.h 中
207 struct meminfo { 208 int nr_banks; 209 struct membank bank[NR_BANKS]; 210 };
接着继续看parse_tags函数,它也位于arch\arm\kernel\Setup.c中
733 static void __init parse_tags(const struct tag *t) 734 { 735 for (; t->hdr.size; t = tag_next(t))//循环取出tag列表,然后处理 736 if (!parse_tag(t)) //处理取出的tag列表 737 printk(KERN_WARNING 738 "Ignoring unrecognised tag 0x%08x\n", 739 t->hdr.tag); 740 }
接着分析parse_tag函数,它同样位于arch\arm\kernel\Setup.c中
715 static int __init parse_tag(const struct tag *tag) 716 { 717 extern struct tagtable __tagtable_begin, __tagtable_end; 718 struct tagtable *t; 719 720 for (t = &__tagtable_begin; t < &__tagtable_end; t++)//从.taglist.init段找出符合的处理tag列表的结构 721 if (tag->hdr.tag == t->tag) {//找到符合的tag 722 t->parse(tag);//调用相应的处理tag的函数处理 723 break; 724 } 725 726 return t < &__tagtable_end;//t<&__tagtable_end说明找到了tag 727 }
parse_tag会从.taglist.init段找出符合的tag,然后调用相应的处理函数处理。tagtable 的结构如下,它位于include\asm-arm\Setup.h 中
171 struct tagtable { 172 __u32 tag;//处理的tag值 173 int (*parse)(const struct tag *);//处理函数 174 };
我们需要的是处理ATAG_MEN参数的函数,搜搜ATAG_MEN,在arch\arm\kernel\Setup.c中找到了parse_tag_mem32处理ATAG_MEN参数的函数。它的功能就是取出内存的开始地址与大小信息后存放在meminfo结构中
614 static int __init parse_tag_mem32(const struct tag *tag) 615 { 616 if (meminfo.nr_banks >= NR_BANKS) { 617 printk(KERN_WARNING 618 "Ignoring memory bank 0x%08x size %dKB\n", 619 tag->u.mem.start, tag->u.mem.size / 1024); 620 return -EINVAL; 621 } 622 arm_add_memory(tag->u.mem.start, tag->u.mem.size);//取出内存的开始地址与大小信息后存放在meminfo结构中 623 return 0; 624 } 625 626 __tagtable(ATAG_MEM, parse_tag_mem32);//解析ATAG_MEM列表,函数为parse_tag_mem32
再看到__tagtable,同样位于include\asm-arm\Setup.h中。主要就是将tagtable 这个结构体放在.taglist.init段
188 #define __tag __used __attribute__((__section__(".taglist.init"))) 189 #define __tagtable(tag, fn) \ 190 static struct tagtable __tagtable_##fn __tag = { tag, fn }
到这里就分析完了tag列表中ATAG_MEM参数的处理,接下去分析ATAG_CMDLINE参数的处理。
找到与ATAG_CMDLINE参数的过程与前面ATAG_MEM参数一样的流程就不分析了,直接找到处理ATAG_CMDLINE参数的函数,它位于arch\arm\kernel\Setup.c中。它只是简单的将tag->u.cmdline.cmdline的内容复制到default_command_line中。
702 static int __init parse_tag_cmdline(const struct tag *tag) 703 { 704 strlcpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);//简单的将tag的内容复制到字符串default_command_line中 705 return 0; 706 } 707 708 __tagtable(ATAG_CMDLINE, parse_tag_cmdline);
接着看到default_command_line,它定义在arch\arm\kernel\Setup.c中,它的大小为1024字节
114 static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;
它初始化为CONFIG_CMDLINE,位于include\linux\Autoconf.h中
374 #define CONFIG_CMDLINE "root=/dev/hda1 ro init=/bin/bash console=ttySAC0"
所以拷贝之后
default_command_line[] = "noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0"
继续往下看default_command_line,在arch\arm\kernel\Setup.c下的setup_arch函数中:其中parse_cmdline是对位于.early_param.init段的内容进行前期的初始化。相应的命令有:cachepolicy=、nocache、nowb、ecc=、initrd=、mem=等等,我们的参数没有涉及到这类命令,所以不去细细的分析这个函数了。
809 memcpy(boot_command_line, from, COMMAND_LINE_SIZE);//form指向default_command_line,将default_command_line中的内容拷贝到boot_command_line中 810 boot_command_line[COMMAND_LINE_SIZE-1] = '\0';//以'\0'结束字符串 811 parse_cmdline(cmdline_p, from);//对位于.early_param.init段命令进行一些先期的处理 812 paging_init(&meminfo, mdesc);//重新初始化页表 813 request_standard_resources(&meminfo, mdesc);//资源的初始化
接着看到paging_init这个函数,这个函数调用了meminfo这个从ATAG_MEM取得的参数以及mdesc我们按照以下调用层次分析
paging_init devicemaps_init //设备maps初始化 mdesc->map_io //调用map_io函数初始化
在arch\arm\mach-s3c2440\Mach-smdk2440.c中找到mdesc这个结构
339 MACHINE_START(S3C2440, "SMDK2440") 340 /* Maintainer: Ben Dooks <ben@fluff.org> */ 341 .phys_io = S3C2410_PA_UART, 342 .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc, 343 .boot_params = S3C2410_SDRAM_PA + 0x100, 344 345 .init_irq = s3c24xx_init_irq, 346 .map_io = smdk2440_map_io, 347 .init_machine = smdk2440_machine_init, 348 .timer = &s3c24xx_timer, 349 MACHINE_END
其中smdk2440_map_io就等要调用的函数,它同样位于arch\arm\mach-s3c2440\Mach-smdk2440.c下,可以看到这里修改过晶振的值。
324 static void __init smdk2440_map_io(void) 325 { 326 s3c24xx_init_io(smdk2440_iodesc, ARRAY_SIZE(smdk2440_iodesc)); 327 s3c24xx_init_clocks(12000000);//根据开发板合适的晶振配置 328 s3c24xx_init_uarts(smdk2440_uartcfgs, ARRAY_SIZE(smdk2440_uartcfgs)); 329 }
继续分析UBOOT传入的需要解析的参数:
当没有使用ramdisk启动系统的时候,你需要使用noinitrd这个参数,但是如果使用了的话,就需要指定initrd=r_addr,size, r_addr表示initrd在内存中的位置,size表示initrd的大小。看到代码里面,位于init\Do_mounts_initrd.c下,可以看到处理函数只是简单的将mount_initrd 置为0,说明不支持ramdisk启动。
19 static int __init no_initrd(char *str) 20 { 21 mount_initrd = 0; 22 return 1; 23 } 24 25 __setup("noinitrd", no_initrd);
接着分析一下__setup的定义,看到include\linux\Init.h里面有它的定义。
160 #define __setup_param(str, unique_id, fn, early) \ 161 static char __setup_str_##unique_id[] __initdata = str; \ 162 static struct obs_kernel_param __setup_##unique_id \ 163 __attribute_used__ \ 164 __attribute__((__section__(".init.setup"))) \ 165 __attribute__((aligned((sizeof(long))))) \ 166 = { __setup_str_##unique_id, fn, early } 167 168 #define __setup_null_param(str, unique_id) \ 169 __setup_param(str, unique_id, NULL, 0) 170 171 #define __setup(str, fn) \ 172 __setup_param(str, fn, fn, 0)
先看__setup_param。它定义了两个参数,一个是char型的字符串__setup_str_##unique_id,另外一个为obs_kernel_param 结构体,它位于include\linux\Init.h。obs_kernel_param 结构体位于
.init.setup段,它的str参数即为__setup_str_##unique_id。__setup宏调用__setup_param传入两个参数str与fn,代表命令行名字与处理函数。
148 struct obs_kernel_param { 149 const char *str; 150 int (*setup_func)(char *); 151 int early; 152 };
回到init\Main.c 中的start_kernel函数继续分析
525 setup_arch(&command_line);//返回的command_line是还未处理的命令行参数存放的首地址 526 setup_command_line(command_line);//static_command_line存放未处理的命令行参数,saved_command_line存放所有的命令行参数 544 printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);//打印命令行参数 545 parse_early_param();//一些前期代码的初始化 546 parse_args("Booting kernel", static_command_line, __start___param, 547 __stop___param - __start___param, 548 &unknown_bootoption);//后续的命令处理
其中parse_early_param函数是对一些early属性的命令后做解析,它位于.early_param.init段,包括:cachepolicy=、nocache、nowb、ecc=、initrd=、mem=等等,我们的参数没有涉及到这类命令,所以不去细细的分析这个函数了。
重点关注parse_args函数,先分析函数的参数:
static_command_line :存放未处理的命令行参数首地址
__start___param : 内核参数的存放地址,它处于__param段
__stop___param - __start___param : 内核参数大小
unknown_bootoption : 处理函数
接着看到parse_args函数内部阶段,它位于kernel\Params.c 下,可以看到在这里会将所有命令行处理完成。
144 while (*args) {//循环处理剩余的命令行,直到全部处理完成 145 int ret; 146 int irq_was_disabled; 147 148 args = next_arg(args, ¶m, &val);//找出下一个命令行参数*param为命令名称,*val为参数值 149 irq_was_disabled = irqs_disabled(); 150 ret = parse_one(param, val, params, num, unknown);//处理
接着看到处理函数parse_one,它位于kernel\Params.c 下。这里面还判断了一个内核的参数,我们传入的参数没有内核参数,内核参数存在于__param段,有:nousb、block2mtd_setup等等,我们传入的命令行参数没有内核参数,所以不关心
49 static int parse_one(char *param, 50 char *val, 51 struct kernel_param *params, 52 unsigned num_params, 53 int (*handle_unknown)(char *param, char *val)) 54 { 55 unsigned int i; 56 57 /* Find parameter */ 58 for (i = 0; i < num_params; i++) {//从__param段找出与命令行参数相同的名字 59 if (parameq(param, params[i].name)) { 60 DEBUGP("They are equal! Calling %p\n", 61 params[i].set); 62 return params[i].set(val, ¶ms[i]);//如果是内核的参数,那么直接传给内核参数,然后返回。 63 } 64 } 65 66 if (handle_unknown) {//如果不是内核的参数,并且处理函数存在 67 DEBUGP("Unknown argument: calling %p\n", handle_unknown); 68 return handle_unknown(param, val);//调用处理函数处理 69 } 70 71 DEBUGP("Unknown argument `%s'\n", param); 72 return
接着看到handle_unknown函数,即unknown_bootoption函数,它位于init\Main.c中,截取其中的一段程序
260 /* Change NUL term back to "=", to make "param" the whole string. */ 261 if (val) {//如果val不为空,做一些处理 262 /* param=val or param="val"? */ 263 if (val == param+strlen(param)+1) 264 val[-1] = '='; 265 else if (val == param+strlen(param)+2) { 266 val[-2] = '='; 267 memmove(val-1, val, strlen(val)+1); 268 val--; 269 } else 270 BUG(); 271 } 272 273 /* Handle obsolete-style parameters */ 274 if (obsolete_checksetup(param)) 275 return 0;
接着看到obsolete_checksetup函数,它同样位于init\Main.c中,这个函数大致的意思就是在.init.setup中找到符合的命令行参数,如果不是前期已经处理的参数(即early值为0的参数,那么调用处理函数处理它。它由__setup宏定义或者__setup_null_param宏定义(这两个宏定义前面已经介绍过了),搜索一下这两个宏定义,发现了__setup("root=", root_dev_setup);、__setup("init=", init_setup);、__setup("console=", console_setup);都在这里面被处理。
190 static int __init obsolete_checksetup(char *line) 191 { 192 struct obs_kernel_param *p; 193 int had_early_param = 0; 194 195 p = __setup_start;//.init.setup的首地址 196 do { 197 int n = strlen(p->str); 198 if (!strncmp(line, p->str, n)) {//在.init.setup中寻找相符的命令行参数 199 if (p->early) {//如果early大于0,那么这个参数在前面已经处理过了 200 /* Already done in parse_early_param? 201 * (Needs exact match on param part). 202 * Keep iterating, as we can have early 203 * params and __setups of same names 8( */ 204 if (line[n] == '\0' || line[n] == '=') 205 had_early_param = 1; 206 } else if (!p->setup_func) {//如果处理函数不存在,则报错 207 printk(KERN_WARNING "Parameter %s is obsolete," 208 " ignored\n", p->str); 209 return 1; 210 } else if (p->setup_func(line + n))//调用处理函数处理 211 return 1; 212 } 213 p++; 214 } while (p < __setup_end); 215 216 return had_early_param; 217 }
接着分析__setup("root=", root_dev_setup)宏,它位于kernel\Printk.c下,可以看到它调用的是root_dev_setup函数来处理root=参数,接着看root_dev_setup函数
211 static int __init root_dev_setup(char *line) 212 { 213 strlcpy(saved_root_name, line, sizeof(saved_root_name)); 214 return 1; 215 } 216 217 __setup("root=", root_dev_setup);
可以看到它的处理函数直接将root命令行参数拷贝到saved_root_name里,接着搜索一下在哪里调用的saved_root_name。找到了在init\Do_mounts.c 中的prepare_namespace函数用到了它,这个函数的作用是挂接根文件系统的。列出部分代码:如何挂接根文件系统后面说明
430 if (saved_root_name[0]) { 431 root_device_name = saved_root_name;//将saved_root_name赋给root_device_name 432 if (!strncmp(root_device_name, "mtd", 3)) { 433 mount_block_root(root_device_name, root_mountflags); 434 goto out; 435 } 436 ROOT_DEV = name_to_dev_t(root_device_name); 437 if (strncmp(root_device_name, "/dev/", 5) == 0) 438 root_device_name += 5; 439 }
前面已经分析了命令行参数的提取过程,这里直接看到宏定义__setup("init=", init_setup)。处理init=参数的是init_setup函数,来到Init_setup函数,它位于
init\Main.c 中
315 static int __init init_setup(char *str) 316 { 317 unsigned int i; 318 319 execute_command = str; 320 /* 321 * In case LILO is going to boot us with default command line, 322 * it prepends "auto" before the whole cmdline which makes 323 * the shell think it should execute a script with such name. 324 * So we ignore all arguments entered _before_ init=... [MJ] 325 */ 326 for (i = 1; i < MAX_INIT_ARGS; i++) 327 argv_init[i] = NULL; 328 return 1; 329 } 330 __setup("init=", init_setup);
可以看到init_setup函数直接将init=的命令行参数拷贝到execute_command 中,搜索execute_command ,在init\Main.c函数下找到了init_post函数,这是start_kernel函数最后调用的一个函数rest_init建立的一个进程函数。取出部分内容,可以看到execute_command是内核运行的根文件系统上的第一个进程
774 if (execute_command) {//如果存在execute_command进程 775 run_init_process(execute_command);运行execute_command进程 776 printk(KERN_WARNING "Failed to execute %s. Attempting " 777 "defaults...\n", execute_command); 778 }
看到宏定义__setup("console=", console_setup);它位于kernel\Printk.c 中,console=参数调用的是console_setup处理它
655 static int __init console_setup(char *str) 656 { 657 char name[sizeof(console_cmdline[0].name)]; 658 char *s, *options; 659 int idx; 660 661 /* 662 * Decode str into name, index, options. 663 */ 664 if (str[0] >= '0' && str[0] <= '9') {//如果以数字0-9开头 665 strcpy(name, "ttyS"); 666 strncpy(name + 4, str, sizeof(name) - 5); 667 } else { 668 strncpy(name, str, sizeof(name) - 1);//将str拷贝到name中,去除结束符 669 } 670 name[sizeof(name) - 1] = 0; 671 if ((options = strchr(str, ',')) != NULL)//如果参数中存在,的话。说明带波特率参数 672 *(options++) = 0; 673 #ifdef __sparc__ 674 if (!strcmp(str, "ttya")) 675 strcpy(name, "ttyS0"); 676 if (!strcmp(str, "ttyb")) 677 strcpy(name, "ttyS1"); 678 #endif 679 for (s = name; *s; s++) 680 if ((*s >= '0' && *s <= '9') || *s == ',') 681 break; 682 idx = simple_strtoul(s, NULL, 10);//取出波特率参数,转换成整形 683 *s = 0; 684 685 add_preferred_console(name, idx, options);//将参数保存在console_cmdline中 686 return 1; 687 } 688 __setup("console=", console_setup);
struct console_cmdline { char name[8]; /* Name of the driver *///设备名称 int index; /* Minor dev. to use *///设备编号 char *options; /* Options for the driver *///设备选项 };
可以看到console_setup函数也只是将console=的参数解析后保存在console_cmdline而已,接着搜索console_cmdline。在start_kernel中有一个console_init函数,找到它的原型,在drivers\char\Tty_io.c 中找到了它
void __init console_init(void) { initcall_t *call; /* Setup the default TTY line discipline. */ (void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);//设置默认控制台 /* * set up the console device so that later boot sequences can * inform about problems etc.. */ call = __con_initcall_start; while (call < __con_initcall_end) {//在.con_initcall.init段,寻找存在的控制台 (*call)(); call++; } }
可以看到这个函数的作用是调用.con_initcall.init中的所有存在函数,来注册控制台。在include\linux\Init.h 中找到了定义.con_initcall.init段的宏。
140 #define console_initcall(fn) \ 141 static initcall_t __initcall_##fn \ 142 __attribute_used__ __attribute__((__section__(".con_initcall.init")))=fn
搜索console_initcall宏。在drivers\serial\S3c2410.c 中找到了这个宏定义的函数
1950 console_initcall(s3c24xx_serial_initconsole);
截取s3c24xx_serial_initconsole函数内容
1994 s3c24xx_serial_init_ports(info);//控制台端口初始化 1995 1996 register_console(&s3c24xx_serial_console);//注册控制台
可以看到register_console函数中,s3c24xx_serial_console参数结构体的信息为
1901 static struct console s3c24xx_serial_console = 1902 { 1903 .name = S3C24XX_SERIAL_NAME,//控制台名称ttySAC 1904 .device = uart_console_device,//以后使用/dev/console时,用来构造设备节点 1905 .flags = CON_PRINTBUFFER,//控制台可以之前,printk已经在缓冲区打印了,CON_PRINTBUFFER表示可以打印以前的信息了 1906 .index = -1, //表示可以匹配任意序列号 1907 .write = s3c24xx_serial_console_write,//打印函数 1908 .setup = s3c24xx_serial_console_setup.//设置函数 1909 };
在看到register_console函数,它位于kernel\Printk.c 中,截取函数部分内容。
964 for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; 965 i++) { 966 if (strcmp(console_cmdline[i].name, console->name) != 0)//新注册的控制台与console_cmdline是否匹配ttySAC0 967 continue; 968 if (console->index >= 0 && 969 console->index != console_cmdline[i].index) 970 continue; 971 if (console->index < 0)//可以匹配任意的编号,比如是ttySAC0/1/2 972 console->index = console_cmdline[i].index; 973 if (console->setup && 974 console->setup(console, console_cmdline[i].options) != 0) 975 break; 976 console->flags |= CON_ENABLED; 977 console->index = console_cmdline[i].index; 978 if (i == selected_console) { 979 console->flags |= CON_CONSDEV; 980 preferred_console = selected_console; 981 } 982 break; 983 } 1007 console->next = console_drivers->next;//将控制台放入console_drivers链表 1008 console_drivers->next = console;
以上内容概括为将新注册的控制台s3c24xx_serial_console与console_cmdline比较。如果比较成功,则继续向下运行。到了后面1007行、1008行则是将
s3c24xx_serial_console控制台放入console_drivers链表中。以后的prink信息就会从这个控制台输出。
到这里uboot传入的tag列表参数全部解析完成。