内核命令行处理(1) 在启动代码main.c执行完早期的一些内核初始化任务之后,就会显示内核的命令行信息。为方便起见,这里重新列出代码清单5-3中的第10行内容: Kernel command line: console=ttyS0,115200 ip=bootp root=/dev/nfs 在这个简单的例子中,引导中的内核在串行设备ttyS0(通常是第一个串口)上打开一个控制台,通信波特率设定为115Kbit/s。此外,它还通过一个BOOTP服务器获得自身的初始化IP地址,并且通过NFS协议挂载根文件系统。(我们将在第12章讲到BOOTP、在第9章和第12章中讲到NFS。现在我们只是讨论Linux内核的命令行机制。) 引导装入程序或第二阶段引导装入程序通过一系列被称为内核命令行的参数实现对Linux的引导。尽管在实际中并不是通过shell命令提示来调用内核,但是许多版本的引导装入程序常常采用将参数传递给Linux内核这种非常流行的模式。某些平台上的引导装入程序不能很好地识别Linux,那么就在内核编译时定义内核命令行参数,并且将其作为Linux内核二进制映像固件代码的一部分。而在另一些平台(例如运行Red Hat Linux的桌面PC)中,命令行参数可以由用户修改而不用重新编译内核。第二阶段引导装入程序(在PC中是Grub或Lilo)通过一个配置文件建立内核命令行并且在内核引导过程中传递给内核。这些命令行参数是一种引导机制,用来在给定硬件平台上设置为正确引导所需的初始化配置。 Linux在整个内核中定义了大量的命令行参数。在Linux源码中的.../Documentation子目录中有一个名为kernel-parameters.txt文件,该文件包含了Linux内核命令行参数列表,它们按字母顺序依次列出。前面提到关于内核文档的警告信息:内核的变化要快于内核文档的变化,因此,可以以该文件为向导,但它并不是一个最权威的参考。在kernel-parameters.txt文件中有超过400多个内核命令行参数,但这并不是所有的内核命令行参数,所以必须直接查阅源代码。 Linux内核命令行参数的基本语法比较简单,大部分从代码清单5-3第10行里很容易看到。内核命令行参数的形式可以是单个单词、key=value对或key= value1, value2, …等复合形式。通过使用这些信息进行数据传递,所有的命令行都是可用的并且可以由许多的模块来处理。前面提到的main.c中的setup_arch()函数就是通过内核命令行参数调用的。通过这种调用,可以向体系结构级或硬件平台级相关代码中传递参数和配置指令。 设备驱动程序编写者和内核开发者都可以为他们特定的需要而增加相应的命令行参数。我们来看一下这种方式的实现机制。遗憾的是,在处理这些内核命令行参数的时候会涉及一些复杂的因素,首先就是原先的机制将受到抑制以便实现更为健壮的系统。第二个难点是我们需要掌握复杂的链接脚本以全面理解这种实现机制 。 __setup宏 可以考虑将控制台设备作为使用内核命令行参数的一个例子。我们希望该设备在内核引导的早期阶段就初始化,这样在引导过程中控制台信息就可以通过该设备输出,该初始化过程创建在名为printk.o的内核目标文件中,其C源代码位于.../kernel/printk.c。执行控制台初始化的函数是console_setup(),该函数将内核命令行的参数作为其唯一的参数。 配置程序和设备驱动程序与在内核命令行中所指定控制台参数进行通信的难点,在于要求该参数是标准通用的模式。该情形更复杂的情况是,命令行参数在那些模块调用它们之前(或就在此时)就要用到。在文件main.c中的启动代码里,在内核命令行进行主要处理的位置,如果没有每一个参数的使用信息,就不可能知道这几百个内核命令行参数中每一个参数的目标函数,所以需要用一种灵活通用的方法将内核命令行参数传递给其使用者。 对于Linux 2.4或更早的版本,开发者通过使用一个简单的宏来解决上述问题。尽管没有得到重视,但是__setup宏仍然在整个Linux内核中得到了广泛使用。在后续内容中,我们会使用代码清单5-3中的内核命令行来演示__setup是如何工作的。 从代码清单5-3的第10行可知,下面的内容即是第一个传递给内核的完整的命令行参数: console=ttyS0,115200 引用该例子的真正目的并不在于命令行参数的实际含义,而在于说明其工作机制,所以如果你没有理解该参数或参数值并不要紧。 代码清单5-4的内容是.../kernel/printk.c中的一部分代码,其中去掉了函数的主体部分,因为它与这里讨论的内容无关,我们关心的只是在代码清单5-4中列出的内容,即对__setup的宏调用。__setup宏在这里有两个参数:一个字符串参数和一个函数指针。传递给__setup宏的字符串与第一个与内核命令行相关的8字符的参数console=一致是绝非偶然的。 代码清单5-4 控制台设置部分代码 /* * Setup a list of consoles. Called from init/main.c */ static int __init console_setup(char *str) { char name[sizeof(console_cmdline[0].name)]; char*s, *options; int idx; /* * Decode str into name, index, options. */ return 1; } __setup("console=", console_setup); 你可以将__setup宏看作是内核命令行控制台参数在内核中的注册函数。当字符串信息console=出现在内核命令行时,就通过__setup宏的第2个参数调用函数console_setup()。但是在并不知道控制台功能的情况下,这个模块之外的配置代码是如何获取该信息呢?事实上,其实现机制巧妙而复杂,并且依赖于目标链接器所创建的列表。 真正的细节隐藏于一系列的宏当中,这些宏通过在一部分目标代码中增加段属性(或其他属性)用来隐藏。目标文件会联合函数指针(function pointer)依字母顺序建立一个静态列表,该列表会由最终vmlinux ELF映像中一个独立ELF段的编译器发出。理解上述技术细节非常重要,它在内核中许多进行特殊处理的地方都要用到。 我们来看看对于__setup宏这是如何实现的。代码清单5-5是定义了__setup宏系列的头文件.../include/linux/init.h下的部分内容。 代码清单5-5 init.h下的_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"))) / __attribute__((aligned((sizeof(long))))) / = { __setup_str_##unique_id, fn, early } #define __setup_null_param(str, unique_id) / __setup_param(str, unique_id, NULL, 0) #define __setup(str, fn) / __setup_param(str, fn, fn, 0) ...
内核命令行处理(2)
代码清单5-5是语法乏味的定义。回想代码清单5-4,我们最初所调用的__setup宏的形式如下:
- __setup("console=", console_setup);
经过稍稍简化,编译器在宏扩展后,其预处理器产生如下结果:
- static char __setup_str_console_setup[] __initdata = "console=";
- static struct obs_kernel_param __setup_console_setup /
- __attribute__((__section__(".init.setup")))=
- {__setup_str_console_setup, console_setup, 0};
为了增加可读性,将上述结果的第2行和第3行采用UNIX的行续符"/"分隔开来。
我们故意略去了两个和本次讨论内容无关的编译器属性。简要地说,__attribute_ used__(本身就是一个隐藏了很多语法细节的宏)会告诉编译器发出一个函数或变量,即使在编译过程中并没有用到任何优化参数 。__attribute__(aligned)会告诉编译器按照特定的边界来对齐结构,在本例中是sizeof(long)。
简化处理后剩下的就是这种机制的核心部分。首先,编译器会产生名为__setup_str_ console_setup[]的初始化后字符数组,该数组包含console=字符串信息;其次,编译器会产生一个包含三个成员的结构:指向内核命令行字符串(在字符数组中声明)的指针、指向配置函数本身的指针和一个简单的标识。这里的关键在于依附于结构的段属性,该属性会通知编译器将该结构送到ELF目标模块内名为.init.setup的特殊段中。在这个链接阶段,所有由__setup宏定义的结构一起被放置到这个.init.setup段中,实际结果就是创建了一个包含这些结构的数组。代码清单5-6是.../init/main.c中的一部分内容,它们说明了这个数据是如何获取和使用的。
代码清单5-6 内核命令行处理
- 1 extern struct obs_kernel_param __setup_start[], __setup_end[];
- 2
- 3 static int __init obsolete_checksetup(char *line)
- 4 {
- 5 struct obs_kernel_param *p;
- 6
- 7 p = __setup_start;
- 8 do {
- 9 int n = strlen(p->str);
- 10 if (!strncmp(line, p->str, n)) {
- 11 if (p->early) {
- 12 /* Already done in parse_early_param? (Needs
- 13 * exact match on param part) */
- 14 if (line[n] == '/0' || line[n] == '=')
- 15 return 1;
- 16 } else if (!p->setup_func) {
- 17 printk(KERN_WARNING "Parameter %s is obsolete,"
- 18 " ignored/n", p->str);
- 19 return 1;
- 20 } else if (p->setup_func(line + n))
- 21 return 1;
- 22 }
- 23 p++;
- 24 } while (p < __setup_end);
- 25 return 0;
- 26 }
对该段代码解释还算简单。函数由一个在main.c文件中其他地方解析的单命令行参数调用。在这个例子中,我们要讨论的指针line指向字符串console=ttyS0,115200,它是内核命令行的一个组成部分。两个外部结构指针__setup_start和__setup_end是在一个链接脚本文本文件中定义的,而不是定义在C文件或头文件中。对于obs_kernel_param结构数组用来标记该数组起始和结束的标签则存在于目标文件的.init.setup段中。
在代码清单5-6中,通过指针p对这个特殊的内核命令行参数寻找匹配信息的过程,对整个结构都进行了扫描。具体在本例中,代码要为字符串信息console=寻找匹配信息,在这个相关的结构中,函数返回一个指向console_setup()函数的指针,它会以该参数(字符串ttyS0,115200)作为其唯一的函数参数,这一处理过程会在内核命令行处理完毕之前不停地重复。
采用所描述的这种机制将目标对象存放到ELF段的列表中,这种机制在内核中的许多地方都用到了。另一个采用这种机制的例子是,使用__init宏系列将初始化程序放到目标文件中一个普通的段中。与其很相近的__initdata被__setup宏用来标记为只在初始化过程中用到的数据。使用这些宏标记的初始化函数和数据被集中放到ELF段中,接下来,当使用了这些用来初始化的函数和数据之后,内核会释放之前它们所占用的内存空间。你也许在引导过程的最后阶段看到过类似的内核信息:"Freeing init memory: 296K."。不同用户对这些函数和数据的使用可能不尽相同,但是如果有三分之一兆,就值得使用__init宏系列,这也恰恰就是使用前面声明的__setup_str_console_setup[]数组里的__initdata宏的目的所在。
你也许会对代码清单5-6中的obsolete_符号感到迷惑,这是因为内核开发者正在用一种更通用的机制来代替内核命令行处理机制,以实现对引导时间和可加载模块参数的注册。在当前情况下,__setup宏声明了几百个参数,然而在新的开发中希望使用内核头文件.../include/ linux/moduleparam.h中定义的一系列函数来实现,更值得注意的是使用module_param*宏系列。这些内容将在第8章中介绍设备驱动程序的时候详细介绍。
上面所说的这种新机制通过在解析程序中包含一个未知的函数指针参数进而保持了向后兼容性,因此,对于module_param*结构来说,是未知的参数就会被视为未知参数,并且对命令行的处理过程就在开发者的控制下重新回到了原有的机制。在仔细研究../kernel/params.c中的代码和.../init/main.c中的parse_args()调用后就可以对这一过程有很好的理解。
对于由__setup宏所创建的结构obs_kernel_param,其中标志(flag)成员的用途是最后要注意的内容。仔细研究代码清单5-6就会明白。该结构中称为early的标志用来指示这个特定的内核命令行参数是否会在引导过程中预先使用,一些命令行参数就是特意要在引导过程中提前用到,那么在这种情况下的标志就会为提前解析命令行参数提供一种实现机制。你会在main.c代码中看到一个名为do_early_param()的函数,该函数会遍历数组,该数组是__setup宏结构由目标链接器产生的,同时该函数会处理每一个被标记为预先使用的内核命令行参数,在引导过程执行这一处理操作时给开发者一些控制权。
子系统初始化
许多Linux子系统的初始化代码都可在main.c中找到。一些子系统的初始化代码在main.c中显而易见,如对init_timers()和console_init()的调用,它们在初始化过程之初就要调用。另外一些子系统所采用的初始化机制与前面所提到的__setup宏非常类似,简单地讲,目标代码链接器会为不同的初始化程序创建函数指针列表,同时采用简单的循环机制依次执行。代码清单5-7显示了这一过程。
代码清单5-7 初始化程序示例
- static int __init customize_machine(void)
- {
- /* customizes platform devices, or adds new ones */
- if (init_machine)
- init_machine();
- return 0;
- }
- arch_initcall(customize_machine);
这部分代码来源于.../arch/arm/kernel/setup.c,它是为一个特殊开发板提供用户定制的简单程序。
*_initcall宏
对于代码清单5-7中的初始化程序,有两个要点需要注意。首先,程序中的函数是由__init宏定义的,就像在前面看到的。__init宏将该函数放到了vmlinux ELF文件中一个称为.init.text的段中,我们可以想到将一个函数放到目标文件中一个特殊段中的目的,这是为了当函数不再使用后可以将函数所占用的内存空间释放。
第二个需要注意的事情是在函数定义之后的宏,即arch_initcall(customize_machine),该宏是在.../include/linux/init.h中所定义的一系列宏中的一个。这些宏如代码清单5-8所示。
代码清单5-8 initcall宏系列
- #define __define_initcall(level,fn) /
- static initcall_t __initcall_##fn __attribute_used__ /
- __attribute__((__section__(".initcall" level ".init"))) = fn
- #define core_initcall(fn) __define_initcall("1",fn)
- #define postcore_initcall(fn) __define_initcall("2",fn)
- #define arch_initcall(fn) __define_initcall("3",fn)
- #define subsys_initcall(fn) __define_initcall("4",fn)
- #define fs_initcall(fn) __define_initcall("5",fn)
- #define device_initcall(fn) __define_initcall("6",fn)
- #define late_initcall(fn) __define_initcall("7",fn)
__initcall宏与前面介绍的__setup宏在形式上非常相似,这些宏基于函数名声明了一个数据列表,并且使用段属性将这些数据内容放到vmlinux ELF文件中被唯一命名的段中。这样做的好处是,main.c可以任意调用其并不知道的子系统初始化程序,如果不这样做,那么唯一的方法就是采用前面描述的方法,即只能改写main.c中的相关内容,让内核了解每一个子系统。
如代码清单5-8所示,这些段的名称为.initcallN.init,这里的N表示的是数量1~7,数据被分配到由宏命名的函数地址处。在代码清单5-7和代码清单5-8所示的例子中,数据的分配形式如下(为了简化起见,省去了段属性):
- static initcall_t __initcall_customize_machine = customize_machine;
该数据被放到内核目标文件中的一个名为.initcall1.init的段中。
这里的N用来提供初始化调用的顺序关系,比如使用core_initcall()宏声明的函数在其他所有函数之前被调用,使用postcore_initcall()宏声明的函数在其后被调用,依次类推,使用late_initcall()宏声明的初始化函数在最后被调用。
和__setup宏系列非常类似,*_initcall宏系列可以看作是内核子系统初始化程序的注册函数,而且这些初始化程序也是在内核启动后就要执行,且执行后不再使用。这些宏提供了一种机制,以实现在系统启动过程中可以执行初始化程序,并且在程序执行之后将程序丢弃同时回收内存。在执行初始化程序的时候也为开发者提供了7种不同的级别,因此,如果一个子系统依赖于另一个子系统可用,那么就可以使用这些级别来提高它的执行顺序。如果使用grep命令查找内核中的[a-z]*_initcall字符串信息,就会发现这些系列的宏在内核中使用非常广泛。
对于*_initcall系列的宏,最后要注意的是:多级别的用法在Linux 2.6内核的开发过程中引入,早期版本的内核是用__initcall()宏来实现的,目前__initcall()宏仍然在广泛使用中,尤其是在设备驱动程序中。为了保持向后兼容性,已经将__initcall()宏定义为device_initcall(),这是一个级别为6的initcall。
5.5 init线程
.../init/main.c中的内容主要用来实现内核的运转。在start_kernel()函数通过调用一些初始化函数执行一些基本的内核初始化任务之后,就产生了第一个内核线程。该线程最终成为内核的init()线程,其线程ID号(PID)为1。可以知道,init()就成为用户空间中所有Linux进程的父进程。在引导过程中运行着两个截然不同的线程:一个是前面提到的start_kernel();另一个就是现在的init()。前者在完成自身的任务之后最终成为idle进程,而后者称为init进程,如代码清单5-9所示。
代码清单5-9 内核init线程的创建
- static void noinline rest_init(void)
- __releases(kernel_lock)
- {
- kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
- numa_default_policy();
- unlock_kernel();
- preempt_enable_no_resched();
- /*
- * The boot idle thread must execute schedule()
- * at least one to get things moving:
- */
- schedule();
- cpu_idle();
- }
从代码清单5-9可以看出,start_kernel()函数调用了rest_init(),通过调用kernel_thread().init来产生内核的init进程,以继续完成内核其余的初始化任务,而由start_kernel()开始的线程在调用cpu_idle()的过程中不停地重复执行。
这样的结果非常有趣。你或许也注意到这个相当庞大的start_kernel()函数被__init宏所标记,这意味着它所占用的内存空间将在内核初始化的最后阶段被释放。在释放内存之前需要退出该函数和它所占用的地址空间,这是通过start_kernel()调用rest_init()来实现的,如代码清单5-9所示,一段非常小的内存空间处在了空闲状态。
5.5.1 通过initcall初始化
当创建init()之后,它会调用do_initcalls()函数,而do_initcalls()函数是用来调用所有被*_initcall宏系列所注册的初始化函数的,其实现代码如代码清单5-10所示。
代码清单5-10 使用initcalls初始化
- static void __init do_initcalls(void)
- {
- initcall_t *call;
- for( call = &__initcall_start; call < &__initcall_end; call++) {
- if (initcall_debug) {
- printk(KERN_DEBUG "Calling initcall 0x%p", *call);
- print_symbol(":%s()", (unsigned long) *call);
- printk("/n");
- }
- (*call)();
- }
除了两个用于指示循环范围的标签__initcall_start和__initcall_end之外,该段代码很好理解。在C源代码和头文件中不会看到这样的标签,它们是在vmlinux链接阶段所用的链接脚本文件中定义的,用来表示使用*_initcall宏系列所生成的初始化函数列表的起始和结束位置。你可以在Linux内核顶层目录下的System.map文件中看到每一个这样的标签,这些标签以字符串__initcall开始,就像代码清单5-8中所表示的那样。
你如果对do_initcalls()函数中的调试打印信息感到疑惑的话,可以看一下由在引导过程中设置的内核命令行参数initcall_debug所执行的系统调用,该命令行参数允许打印如代码清单5-10所示的调试信息。内核只需简单地以内核命令行参数initcall_debug开始就可以实现这些调试信息的输出 。
下面是一个启用了这些调试语句时的输出的例子:
- ...
- Calling initcall 0xc00168f4: tty_class_init+0x0/0x3c()
- Calling initcall 0xc000c32c: customize_machine+0x0/0x2c()
- Calling initcall 0xc000c4f0: topology_init+0x0/0x24()
- Calling initcall 0xc000e8f4: coyote_pci_init+0x0/0x20()
- PCI: IXP4xx is host
- PCI: IXP4xx Using direct access for memory space
- ...
注意在代码清单5-7中对customize_machine()的调用,调试信息的输出包括了函数的虚拟内核地址(在该例中是0xc000c32c)和函数大小(在这里是0x2c)。这是了解内核初始化的一个有效方法,特别是对不同子系统和模块的调用次序的理解。即使是在一个具有相当配置的嵌入式系统之上,也有几十个这样的初始化函数通过这种方式调用。在这个以嵌入式ARM XScale为平台的例子中,共有92个这样不同的内核初始化程序。
5.5.1 通过initcall初始化
当创建init()之后,它会调用do_initcalls()函数,而do_initcalls()函数是用来调用所有被*_initcall宏系列所注册的初始化函数的,其实现代码如代码清单5-10所示。
代码清单5-10 使用initcalls初始化
- static void __init do_initcalls(void)
- {
- initcall_t *call;
- for( call = &__initcall_start; call < &__initcall_end; call++) {
- if (initcall_debug) {
- printk(KERN_DEBUG "Calling initcall 0x%p", *call);
- print_symbol(":%s()", (unsigned long) *call);
- printk("/n");
- }
- (*call)();
- }
除了两个用于指示循环范围的标签__initcall_start和__initcall_end之外,该段代码很好理解。在C源代码和头文件中不会看到这样的标签,它们是在vmlinux链接阶段所用的链接脚本文件中定义的,用来表示使用*_initcall宏系列所生成的初始化函数列表的起始和结束位置。你可以在Linux内核顶层目录下的System.map文件中看到每一个这样的标签,这些标签以字符串__initcall开始,就像代码清单5-8中所表示的那样。
你如果对do_initcalls()函数中的调试打印信息感到疑惑的话,可以看一下由在引导过程中设置的内核命令行参数initcall_debug所执行的系统调用,该命令行参数允许打印如代码清单5-10所示的调试信息。内核只需简单地以内核命令行参数initcall_debug开始就可以实现这些调试信息的输出 。
下面是一个启用了这些调试语句时的输出的例子:
- ...
- Calling initcall 0xc00168f4: tty_class_init+0x0/0x3c()
- Calling initcall 0xc000c32c: customize_machine+0x0/0x2c()
- Calling initcall 0xc000c4f0: topology_init+0x0/0x24()
- Calling initcall 0xc000e8f4: coyote_pci_init+0x0/0x20()
- PCI: IXP4xx is host
- PCI: IXP4xx Using direct access for memory space
- ...
注意在代码清单5-7中对customize_machine()的调用,调试信息的输出包括了函数的虚拟内核地址(在该例中是0xc000c32c)和函数大小(在这里是0x2c)。这是了解内核初始化的一个有效方法,特别是对不同子系统和模块的调用次序的理解。即使是在一个具有相当配置的嵌入式系统之上,也有几十个这样的初始化函数通过这种方式调用。在这个以嵌入式ARM XScale为平台的例子中,共有92个这样不同的内核初始化程序。