注册内核选项

来源:互联网 发布:php获取js变量 编辑:程序博客网 时间:2024/05/18 00:23

4.4 内核选项解析

各个子系统的初始化是内核整个初始化过程必然要完成的基本任务,这些任务按照固定的模式来处理,可以归纳为两个部分:内核选项的解析以及子系统初始化函数的调用。

本节讲解内核选项的注册及解析机制,下一节将会讲解各个子系统的初始化函数如何被调用。

4.4.1  内核选项

Linux允许用户传递内核配置选项给内核,内核在初始化过程中调用parse_args函数对这些选项进行解析,并调用相应的处理函数。

parse_args函数能够解析形如"变量名=值"的字符串,在模块加载时,它也会被调用来解析模块参数。

内核选项的使用格式同样为"变量名=值",打开系统的grub文件,然后找到kernel行,比如:

  1. kernel      /boot/vmlinuz-2.6.18 root=/dev/sda1 ro splash=silent vga=0x314 pci=noacpi 

其中的"pci=noacpi"等都表示内核选项。

内核选项不同于模块参数,模块参数通常在模块加载时通过"变量名=值"的形式指定,而不是内核启动时。如果希望在内核启动时使用模块参数,则必须添加模块名作为前缀,使用"模块名.参数=值"的形式,比如,使用下面的命令在加载usbcore时指定模块参数autosuspend的值为2。

  1. $ modprobe usbcore autosuspend=2 

若是在内核启动时指定,则必须使用下面的形式:

  1. usbcore.autosuspend=2 

从Documentation/kernel-parameters.txt文件里可以查询到某个子系统已经注册的内核选项,比如PCI子系统注册的内核选项为:

  1. pci=option[,option...]  [PCI] various PCI subsystem options:  
  2.     off     [X86-32] don't probe for the PCI bus  
  3.     bios        [X86-32] force use of PCI BIOS, don't access  
  4.             the hardware directly. Use this if your machine  
  5.             has a non-standard PCI host bridge.  
  6.     nobios      [X86-32] disallow use of PCI BIOS, only direct  
  7.             hardware access methods are allowed. Use this  
  8.             if you experience crashes upon bootup and you  
  9.             suspect they are caused by the BIOS.  
  10.     conf1       [X86-32] Force use of PCI Configuration  
  11.             Mechanism 1.  
  12.     conf2       [X86-32] Force use of PCI Configuration  
  13.             Mechanism 2.  
  14.     nommconf    [X86-32,X86_64] Disable use of MMCONFIG for PCI  
  15.             Configuration  
  16.     nomsi       [MSI] If the PCI_MSI kernel config parameter is  
  17.             enabled, this kernel boot option can be used to  
  18.             disable the use of MSI interrupts system-wide.  
  19.     nosort      [X86-32] Don't sort PCI devices according to  
  20.             order given by the PCI BIOS. This sorting is  
  21.             done to get a device order compatible with  
  22.             older kernels.  
  23.     biosirq     [X86-32] Use PCI BIOS calls to get the interrupt  
  24.             routing table. These calls are known to be buggy  
  25.             on several machines and they hang the machine  
  26.             when used, but on other computers it's the only  
  27.             way to get the interrupt routing table. Try  
  28.             this option if the kernel is unable to allocate  
  29.             IRQs or discover secondary PCI buses on your  
  30.             motherboard.  
  31.     rom     [X86-32] Assign address space to expansion ROMs.  
  32.             Use with caution as certain devices share  
  33.             address decoders between ROMs and other  
  34.             resources.  
  35.     irqmask=0xMMMM  [X86-32] Set a bit mask of IRQs allowed to be  
  36.             assigned automatically to PCI devices. You can  
  37.             make the kernel exclude IRQs of your ISA cards  
  38.             this way.  
  39.     pirqaddr=0xAAAAA    [X86-32] Specify the physical address  
  40.             of the PIRQ table (normally generated  
  41.             by the BIOS) if it is outside the  
  42.             F0000h-100000h range.  
  43.     lastbus=N   [X86-32] Scan all buses thru bus #N. Can be  
  44.             useful if the kernel is unable to find your  
  45.             secondary buses and you want to tell it  
  46.             explicitly which ones they are.  
  47.     assign-busses   [X86-32] Always assign all PCI bus  
  48.             numbers ourselves, overriding  
  49.             whatever the firmware may have done.  
  50.     usepirqmask [X86-32] Honor the possible IRQ mask stored  
  51.             in the BIOS $PIR table. This is needed on  
  52.             some systems with broken BIOSes, notably  
  53.             some HP Pavilion N5400 and Omnibook XE3  
  54.             notebooks. This will have no effect if ACPI  
  55.             IRQ routing is enabled.  
  56.     noacpi      [X86-32] Do not use ACPI for IRQ routing  
  57.             or for PCI scanning.  
  58.     routeirq    Do IRQ routing for all PCI devices.  
  59.             This is normally done in pci_enable_device(),  
  60.             so this option is a temporary workaround  
  61.             for broken drivers that don't call it.  
  62.     firmware    [ARM] Do not re-enumerate the bus but instead  
  63.             just use the configuration from the  
  64.             bootloader. This is currently used on  
  65.             IXP2000 systems where the bus has to be  
  66.             configured a certain way for adjunct CPUs.  
  67.     noearly     [X86] Don't do any early type 1 scanning.  
  68.             This might help on some broken boards which  
  69.             machine check when some devices' config space  
  70.             is read. But various workarounds are disabled  
  71.             and some IOMMU drivers will not work.  
  72.     bfsort      Sort PCI devices into breadth-first order.  
  73.             This sorting is done to get a device  
  74.             order compatible with older (<= 2.4) kernels.  
  75.     nobfsort    Don't sort PCI devices into breadth-first order.  
  76.     cbiosize=nn[KMG]    The fixed amount of bus space which is  
  77.             reserved for the CardBus bridge's IO window.  
  78.             The default value is 256 bytes.  
  79.     cbmemsize=nn[KMG]   The fixed amount of bus space which is  
  80.             reserved for the CardBus bridge's memory  
  81.             window. The default value is 64 megabytes. 
4.4.2  注册内核选项
我们不必理解parse_args函数的实现细节,但必须知道如何注册内核选项。模块参数使用module_param系列的宏注册,内核选项则使用__setup宏来注册。
__setup宏在include/linux/init.h文件中定义。
  1. 171 #define __setup(str, fn)                    \  
  2. 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宏和early_param宏注册的内核选项所关联的处理函数存放在.init.setup节。打开内核连接器脚本文件arch/i386/kernel/vmlinux.lds,查找.init.setup,然后便会看到:
  1. __setup_start = .;  
  2. .init.setup : AT(ADDR(.init.setup) - 0xC0000000) { *(.init.setup) }  
  3. __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函数

  1. 666 static void __init do_initcalls(void)  
  2. 667 {  
  3. 668     initcall_t *call;  
  4. 669     int count = preempt_count();  
  5. 670   
  6. 671     for (call = __initcall_start; call < __initcall_end; call++) {  
  7. 672         ktime_t t0, t1, delta;  
  8. 673         char *msg = NULL;  
  9. 674         char msgbuf[40];  
  10. 675         int result;  
  11. 676   
  12. 677         if (initcall_debug) {  
  13. 678             printk("Calling initcall 0x%p", *call);  
  14. 679             print_fn_descriptor_symbol(": %s()",  
  15. 680                     (unsigned long) *call);  
  16. 681             printk("\n");  
  17. 682             t0 = ktime_get();  
  18. 683         }  
  19. 684   
  20. 685         result = (*call)();  
  21. 686   
  22. 687         if (initcall_debug) {  
  23. 688             t1 = ktime_get();  
  24. 689             delta = ktime_sub(t1, t0);  
  25. 690   
  26. 691             printk("initcall 0x%p", *call);  
  27. 692             print_fn_descriptor_symbol(": %s()",  
  28. 693                     (unsigned long) *call);  
  29. 694             printk(" returned %d.\n", result);  
  30. 695   
  31. 696             printk("initcall 0x%p ran for %Ld msecs: ",  
  32. 697                 *call, (unsigned long long)delta.tv64 >> 20);  
  33. 698             print_fn_descriptor_symbol("%s()\n",  
  34. 699                 (unsigned long) *call);  
  35. 700         }  
  36. 701   
  37. 702         if (result && result != -ENODEV && initcall_debug) {  
  38. 703             sprintf(msgbuf, "error code %d", result);  
  39. 704             msg = msgbuf;  
  40. 705         }  
  41. 706         if (preempt_count() != count) {  
  42. 707             msg = "preemption imbalance";  
  43. 708             preempt_count() = count;  
  44. 709         }  
  45. 710         if (irqs_disabled()) {  
  46. 711             msg = "disabled interrupts";  
  47. 712             local_irq_enable();  
  48. 713         }  
  49. 714         if (msg) {  
  50. 715             printk(KERN_WARNING "initcall at 0x%p", *call);  
  51. 716             print_fn_descriptor_symbol(": %s()",  
  52. 717                     (unsigned long) *call);  
  53. 718             printk(": returned with %s\n", msg);  
  54. 719         }  
  55. 720     }  
  56. 721   
  57. 722     /* Make sure there is no pending stuff from the initcall sequence */  
  58. 723     flush_scheduled_work();  
  59. 724 } 
  60. 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手册中的一段话。

    1. the linker searches and processes libraries andobject 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文件,可以看到:

    1. 5 obj-y           += access.o bus.o probe.o remove.o pci.o quirks.o \  
    2. 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函数之前被执行。

    掌握这些规则,我们在研究某个子系统时,即可获得初始化函数的执行顺序,并按照该顺序进行深入的分析。

原创粉丝点击