4.4 内核选项解析
各个子系统的初始化是内核整个初始化过程必然要完成的基本任务,这些任务按照固定的模式来处理,可以归纳为两个部分:内核选项的解析以及子系统初始化函数的调用。
本节讲解内核选项的注册及解析机制,下一节将会讲解各个子系统的初始化函数如何被调用。
4.4.1 内核选项
Linux允许用户传递内核配置选项给内核,内核在初始化过程中调用parse_args函数对这些选项进行解析,并调用相应的处理函数。
parse_args函数能够解析形如"变量名=值"的字符串,在模块加载时,它也会被调用来解析模块参数。
内核选项的使用格式同样为"变量名=值",打开系统的grub文件,然后找到kernel行,比如:
- kernel /boot/vmlinuz-2.6.18 root=/dev/sda1 ro splash=silent vga=0x314 pci=noacpi
其中的"pci=noacpi"等都表示内核选项。
内核选项不同于模块参数,模块参数通常在模块加载时通过"变量名=值"的形式指定,而不是内核启动时。如果希望在内核启动时使用模块参数,则必须添加模块名作为前缀,使用"模块名.参数=值"的形式,比如,使用下面的命令在加载usbcore时指定模块参数autosuspend的值为2。
- $ modprobe usbcore autosuspend=2
若是在内核启动时指定,则必须使用下面的形式:
- usbcore.autosuspend=2
从Documentation/kernel-parameters.txt文件里可以查询到某个子系统已经注册的内核选项,比如PCI子系统注册的内核选项为:
- pci=option[,option...] [PCI] various PCI subsystem options:
- off [X86-32] don't probe for the PCI bus
- bios [X86-32] force use of PCI BIOS, don't access
- the hardware directly. Use this if your machine
- has a non-standard PCI host bridge.
- nobios [X86-32] disallow use of PCI BIOS, only direct
- hardware access methods are allowed. Use this
- if you experience crashes upon bootup and you
- suspect they are caused by the BIOS.
- conf1 [X86-32] Force use of PCI Configuration
- Mechanism 1.
- conf2 [X86-32] Force use of PCI Configuration
- Mechanism 2.
- nommconf [X86-32,X86_64] Disable use of MMCONFIG for PCI
- Configuration
- nomsi [MSI] If the PCI_MSI kernel config parameter is
- enabled, this kernel boot option can be used to
- disable the use of MSI interrupts system-wide.
- nosort [X86-32] Don't sort PCI devices according to
- order given by the PCI BIOS. This sorting is
- done to get a device order compatible with
- older kernels.
- biosirq [X86-32] Use PCI BIOS calls to get the interrupt
- routing table. These calls are known to be buggy
- on several machines and they hang the machine
- when used, but on other computers it's the only
- way to get the interrupt routing table. Try
- this option if the kernel is unable to allocate
- IRQs or discover secondary PCI buses on your
- motherboard.
- rom [X86-32] Assign address space to expansion ROMs.
- Use with caution as certain devices share
- address decoders between ROMs and other
- resources.
- irqmask=0xMMMM [X86-32] Set a bit mask of IRQs allowed to be
- assigned automatically to PCI devices. You can
- make the kernel exclude IRQs of your ISA cards
- this way.
- pirqaddr=0xAAAAA [X86-32] Specify the physical address
- of the PIRQ table (normally generated
- by the BIOS) if it is outside the
- F0000h-100000h range.
- lastbus=N [X86-32] Scan all buses thru bus #N. Can be
- useful if the kernel is unable to find your
- secondary buses and you want to tell it
- explicitly which ones they are.
- assign-busses [X86-32] Always assign all PCI bus
- numbers ourselves, overriding
- whatever the firmware may have done.
- usepirqmask [X86-32] Honor the possible IRQ mask stored
- in the BIOS $PIR table. This is needed on
- some systems with broken BIOSes, notably
- some HP Pavilion N5400 and Omnibook XE3
- notebooks. This will have no effect if ACPI
- IRQ routing is enabled.
- noacpi [X86-32] Do not use ACPI for IRQ routing
- or for PCI scanning.
- routeirq Do IRQ routing for all PCI devices.
- This is normally done in pci_enable_device(),
- so this option is a temporary workaround
- for broken drivers that don't call it.
- firmware [ARM] Do not re-enumerate the bus but instead
- just use the configuration from the
- bootloader. This is currently used on
- IXP2000 systems where the bus has to be
- configured a certain way for adjunct CPUs.
- noearly [X86] Don't do any early type 1 scanning.
- This might help on some broken boards which
- machine check when some devices' config space
- is read. But various workarounds are disabled
- and some IOMMU drivers will not work.
- bfsort Sort PCI devices into breadth-first order.
- This sorting is done to get a device
- order compatible with older (<= 2.4) kernels.
- nobfsort Don't sort PCI devices into breadth-first order.
- cbiosize=nn[KMG] The fixed amount of bus space which is
- reserved for the CardBus bridge's IO window.
- The default value is 256 bytes.
- cbmemsize=nn[KMG] The fixed amount of bus space which is
- reserved for the CardBus bridge's memory
- window. The default value is 64 megabytes.
4.4.2 注册内核选项
我们不必理解parse_args函数的实现细节,但必须知道如何注册内核选项。模块参数使用module_param系列的宏注册,内核选项则使用__setup宏来注册。
__setup宏在include/linux/init.h文件中定义。
- 171 #define __setup(str, fn) \
- 172 __setup_param(str, fn, fn, 0)
__setup需要两个参数,其中str是内核选项的名字,fn是该内核选项关联的处理函数。__setup宏告诉内核,在启动时如果检测到内核选项str,则执行函数fn。str除了包括内核选项名字之外,必须以"="字符结束。
不同的内核选项可以关联相同的处理函数,比如内核选项netdev和ether都关联了netdev_boot_setup函数。
除了__setup宏之外,还可以使用early_param宏注册内核选项。它们的使用方式相同,不同的是,early_param宏注册的内核选项必须要在其他内核选项之前被处理。
如图4.7所示,__setup宏和early_param宏都由__setup_param宏实现。__setup_param宏将__setup宏和early_param宏注册的内核选项所关联的函数存放到.init.setup节。
图4.7 setup_param宏及其封装 |
4.4.3 两次解析
相应于__setup宏和early_param宏两种注册形式,内核在初始化时,调用了两次parse_args函数进行解析。
如图4.8所示,parse_args函数第一次被调用是在parse_early_param函数中,用于处理early_param宏注册的高优先级的内核选项。parse_early_param函数执行结束之后,parse_args函数被第二次调用,处理其他的选项。
图4.8 内核选项的两次解析 |
- __setup_start = .;
- .init.setup : AT(ADDR(.init.setup) - 0xC0000000) { *(.init.setup) }
- __setup_end = .;
其中的__setup_start指向.init.setup节的开始,__setup_end指向.init.setup节的结尾。解析时,会在__setup_start和__setup_end之间查找内核选项,当识别有内核选项时,即会调用相应的处理函数。
在内核启动之后,.init.setup节会被释放,其中存放的内存选项不再需要,用户不能够在系统运行时查看或修改它们。
4.5 子系统的初始化
内核选项的解析完成之后,各个子系统的初始化即进入第二部分-初始化函数的调用。这由4.2节提到的,内核初始化过程主线上的第三个函数kernel_init,通过do_basic_setup函数再去调用do_initcalls函数来完成。
4.5.1 do_initcalls()函数
do_initcall函数通过for循环,由__initcall_start开始,直到__initcall_end结束,依次调用识别到的初始化函数。而位于__initcall_start和__initcall_end之间的区域组成了.initcall.init节,其中保存了由xxx_initcall形式的宏标记的函数地址,do_initcall函数可以很轻松地取得函数地址并执行其指向的函数。
.initcall.init节所保存的函数地址有一定的优先级,越前面的函数优先级越高,也会比位于后面的函数先被调用。
由do_initcalls函数调用的函数不应该改变其优先级状态和禁止中断。因此,每个函数执行后,do_initcalls会检查该函数是否做了任何变化,如果有必要,它会校正优先级和中断状态。
另外,这些被执行的函数又可以完成一些需要异步执行的任务,flush_scheduled_work函数则用于确保do_initcalls函数在返回前等待这些异步任务结束。
代码清单4.5 do_initcalls函数
- 666 static void __init do_initcalls(void)
- 667 {
- 668 initcall_t *call;
- 669 int count = preempt_count();
- 670
- 671 for (call = __initcall_start; call < __initcall_end; call++) {
- 672 ktime_t t0, t1, delta;
- 673 char *msg = NULL;
- 674 char msgbuf[40];
- 675 int result;
- 676
- 677 if (initcall_debug) {
- 678 printk("Calling initcall 0x%p", *call);
- 679 print_fn_descriptor_symbol(": %s()",
- 680 (unsigned long) *call);
- 681 printk("\n");
- 682 t0 = ktime_get();
- 683 }
- 684
- 685 result = (*call)();
- 686
- 687 if (initcall_debug) {
- 688 t1 = ktime_get();
- 689 delta = ktime_sub(t1, t0);
- 690
- 691 printk("initcall 0x%p", *call);
- 692 print_fn_descriptor_symbol(": %s()",
- 693 (unsigned long) *call);
- 694 printk(" returned %d.\n", result);
- 695
- 696 printk("initcall 0x%p ran for %Ld msecs: ",
- 697 *call, (unsigned long long)delta.tv64 >> 20);
- 698 print_fn_descriptor_symbol("%s()\n",
- 699 (unsigned long) *call);
- 700 }
- 701
- 702 if (result && result != -ENODEV && initcall_debug) {
- 703 sprintf(msgbuf, "error code %d", result);
- 704 msg = msgbuf;
- 705 }
- 706 if (preempt_count() != count) {
- 707 msg = "preemption imbalance";
- 708 preempt_count() = count;
- 709 }
- 710 if (irqs_disabled()) {
- 711 msg = "disabled interrupts";
- 712 local_irq_enable();
- 713 }
- 714 if (msg) {
- 715 printk(KERN_WARNING "initcall at 0x%p", *call);
- 716 print_fn_descriptor_symbol(": %s()",
- 717 (unsigned long) *call);
- 718 printk(": returned with %s\n", msg);
- 719 }
- 720 }
- 721
- 722 /* Make sure there is no pending stuff from the initcall sequence */
- 723 flush_scheduled_work();
- 724 }
4.5.2 .initcall.init节
如图4.8所示描述了内核初始化代码的内存分布。内核使用了各式各样的宏来标识函数或结构所具有的初始化属性,这些宏定义位于include/linux/init.h文件,用于通知连接器将具有这些属性的函数或结构存放到专用的内存节。通过这种方式,我们能够很便利地访问某一类具有同样属性的对象。
图4.9的左边一列为每个内存节的开始和结束的指针名,右边一列则表示用于将函数或结构存放到相关内存节的宏。
图4.9 初始化代码的内存分布 __init宏我们已不再陌生,它所修饰的函数所占用内存在初始化结束后便会释放。__initdata用于启动时已经初始化的结构。__setup_param宏正是我们上节所讲述的内容之一。
余下即是位于__initcall_start和__initcall_end之间.initcall.init节。.initcall.init节又分为7个子节,每个子节都对应一个形如xxx_initcall的宏,core_initcall宏将函数指针放在.initcall1.init子节,postcore_initcall宏将函数指针放在了.initcall2.init子节,依此类推。
各个子节的顺序是固定的,位于前面的子节具有更高的优先级,即do_initcalls函数执行时,会先调用.initcall1.init中的函数指针,再调用.initcall2.init中的函数指针。
内核使用各式各样的宏来标识函数或结构所具有的初始化属性,除了使我们能够方便地访问某一类具有同样属性的对象之外,同样还为了优化对内存的使用。
不同于用户空间的代码和数据,内核的代码和数据会一直保留在内存之中,因此在内核运行时尽可能减少对内存的浪费变得非常必要。而图4.9中.initcall.init节的内容在初始化结束后,即使用free_initmem函数释放。xxx_initcall宏和__init宏等,一起协作完成了对内存的优化使用。
4.5.3 分析示例
这里以PCI子系统为例,分析一下它的初始化都使用了上述的哪些宏标记。
与很多子系统不同,PCI子系统的实现代码分布在内核代码树的两个地方,除去drivers/pci存放了体系结构无关部分的代码之外,还有arch/i386/pci存放了体系结构相关部分的代码,因此我们需要在这两个地方分别查找它的初始化代码。
如表4.1所示为PCI子系统的初始化代码分布情况。
PCI子系统的初始化几乎使用了本节所描述的所有xxx_initcall宏,它的初始化也严格按照表4.1所描述的内存存放顺序执行。
表4.1 PCI子系统初始化代码分布情况
文 件
初始化函数
宏 标 记
内 存 位 置
arch/i386/pci/acpi.c
pci_acpi_init
subsys_initcall
.initcall4.init
arch/i386/pci/common.c
pcibios_init
subsys_initcall
.initcall4.init
arch/i386/pci/i386.c
pcibios_assign_resources
fs_initcall
.initcall5.init
arch/i386/pci/legacy.c
pci_legacy_init
subsys_initcall
.initcall4.init
drivers/pci/pci-acpi.c
acpi_pci_init
arch_initcall
.initcall3.init
drivers/pci/pci- driver.c
pci_driver_init
postcore_initcall
.initcall2.init
drivers/pci/pci- sysfs.c
pci_sysfs_init
late_initcall
.initcall7.init
drivers/pci/pci.c
pci_init
device_initcall
.initcall6.init
drivers/pci/proc.c
pci_proc_init
__initcall
.initcall6.init
arch/i386/pci/init.c
pci_access_init
arch_initcall
.initcall3.init
但是,我们可以从表4.1中发现,PCI子系统的一些初始化函数位于同一子节,前面只是讲述了不同子节之间的函数按照子节的优先级顺序执行,并没有讲述同一子节函数之间的调用顺序。
当然,我们可以指出一个事实,同一子节之间,地址位于最前面的函数会首先被调用。但是我们并不知道哪个函数位于前边、哪个函数位于后边,比如同样位于.initcall2.init子节的pcibus_class_init函数和pci_driver_init函数,有没有一个简单的方法来进行判断?
下面是GCC手册中的一段话。
- the linker searches and processes libraries and object files in the order they are specified. Thus, 'foo.o -lz bar.o' searches library 'z' after file 'foo.o' but before 'bar.o'.
即是说,连接器按照库文件和目标文件被指定的顺序进行处理,打开pcibus_class_init函数和pci_driver_init函数所在目录drivers/pci/下的Makefile文件,可以看到:
- 5 obj-y += access.o bus.o probe.o remove.o pci.o quirks.o \
- 6 pci-driver.o search.o pci-sysfs.o rom.o setup-res.o
probe.o在pci- driver.o之前被指定,因此probe.c文件中的pcibus_class_init函数将在pci- driver.c文件中的pci_driver_init函数之前被调用。
对于pcibus_class_init函数和pci_driver_init函数这样位于同一目录位置的可以通过该目录Makefile文件指定的链接顺序来判断,而对于.initcall3.init子节中的acpi_pci_init函数和pci_access_init函数则不能使用这个方法。
acpi_pci_init函数位于drivers/pci/pci-acpi.c文件,而pci_access_init 函数位于arch/i386/pci/init.c文件,它们位于不同的目录,此时问题即转化为arch/i386/pci下的Makefile和drivers/pci下的Makefile谁先谁后的问题,这就涉及kbuild构建内核的运行机制。
内核中的Makefile主要有如下3种。
内核源码树根目录里的Makefile。虽说只有一个,但地位远远高于其他Makefile,其中定义了所有与体系结构无关的变量和目标。
arch/*/Makefile。与特定体系结构相关,它会被根目录下的Makefile包含,为kbuild提供体系结构的特定信息。而它又包含了arch/*/目录下面各级子目录下的那些Makefile。
drivers/等各个子目录下的那些Makefile。
kbuild构建内核时,首先从根目录Makefile开始执行,从中获得与体系结构无关的变量和依赖关系,并同时从arch/*/Makefile中获得体系结构特定的变量等信息,用来扩展根目录Makefile所提供的变量。
此时,kbuild已经拥有了构建内核需要的所有变量和目标。然后,kbuild进入各个子目录,把部分变量传递给子目录里的Makefile,子目录Makefile根据配置信息决定编译哪些源文件,从而构建出一个需要编译的文件列表。
之后,即是内核编译的漫长过程。现在,很明显,arch/i386/pci下的Makefile是在drivers/pci下的Makefile之前被执行,即是说,pci_access_init函数在acpi_pci_init函数之前被执行。
掌握这些规则,我们在研究某个子系统时,即可获得初始化函数的执行顺序,并按照该顺序进行深入的分析。