linux initcall机制

2019-07-13 03:44发布


(2015-11-26 10:45:58) 转载 标签:

linux

c

内核

嵌入式

驱动

分类:嵌入式Linux杂记 Linux系统启动过程很复杂,因为它既需要支持模块静态加载机制也要支持动态加载机制。模块动态加载机制给系统提供了极大的灵活性,驱动程序既可支持静态编译进内核,也可以支持动态加载机制。Linux系统中对设备和子系统的初始化在最后进行,主要过程可以用下图表示。

图1 进入子系统初始化时,在内核init进程中进行设备初始化,最为复杂、诡异的机制莫过于do_initcalls()函数调用,该函数完成了所有需要静态加载模块的初始化,需要进行静态加载的内核模块,需要使用一些特定的宏进行处理,下面详细来说明一些linux内核中initcalls机制。 先来看看do_initcalls()函数原型:
图2 核心部分是639~671之间,该部分是一个函数指针调用,遍历_initcall_start~_initcall_end范围,逐个调用函数指针。 那_initcall_start~_initcall_end之间存放的是什么呢,可以以下面一幅示意图来说明。
图3 图左边是地址指针,右边是相关宏,使用相关宏处理函数指针,可以将被处理的函数指针放在特定的指针范围内。例如,网络设备层初始化函数是net_dev_init(),定义在net/core/dev.c中,在该函数下方有条宏处理subsys_initcall(net_dev_init),该宏完成将net_dev_init函数指针放在上图中.initcall4.init段中,在do_initcalls()函数调用时,其处于_initcall_start~_initcall_end直接,所以net_dev_init()就这样被调用了。 这种机制真是比较巧妙,也比较难以理解,设计初衷就是为了实现一个通用的启动流程,使移植或扩展时,只需要对需要启动加载的模块进行宏处理即可。 下面来详细了解这种机制的实现方法。 先说一说gcc对手动定位代码段位置的支持,_attribute_是gcc的关键字,指示编译器给符号设置特定属性。编译完成后输入到链接器的是各个带有符号表的文件,链接器对各个文件中符号进行重定位,_attribute_在该阶段进行处理,将指定符号放在链接生成文件段中特定位置,不单只指代码段,也包括数据段,如系统初始化中经常见到的_initdata,即将指定符号放到数据段特定位置。 当然,具体这些段是如何生成的,也是有文件进行配置,即在链接配置文件arch/xxx/vmlinux.ds.S.中,如下

inux中初始化程序是分等级的,在include/linux/init.h中我们可以找到 #define early_initcall(fn)__define_initcall("early",fn,early) #define pure_initcall(fn)__define_initcall("0",fn,0) #define core_initcall(fn)__define_initcall("1",fn,1) #define core_initcall_sync(fn)__define_initcall("1s",fn,1s) #define postcore_initcall(fn)__define_initcall("2",fn,2) #define postcore_initcall_sync(fn)__define_initcall("2s",fn,2s) #define arch_initcall(fn)__define_initcall("3",fn,3) #define arch_initcall_sync(fn)__define_initcall("3s",fn,3s) #define subsys_initcall(fn)__define_initcall("4",fn,4) #define subsys_initcall_sync(fn)__define_initcall("4s",fn,4s) #define fs_initcall(fn)__define_initcall("5",fn,5) #define fs_initcall_sync(fn)__define_initcall("5s",fn,5s) #define rootfs_initcall(fn)__define_initcall("rootfs",fn,rootfs) #define device_initcall(fn)__define_initcall("6",fn,6) #define device_initcall_sync(fn)__define_initcall("6s",fn,6s) #define late_initcall(fn)__define_initcall("7",fn,7) #define late_initcall_sync(fn)__define_initcall("7s",fn,7s) 一共17个等级(实际上后接_sync的7个等级是几乎没有用到的)。优先级从上往下越来越低,early > 0 > 1....> 7s(从下面的INITCALLS的链接顺序可以看出) start_kernel->rest_init 系统在启动后在rest_init中会创建init内核线程 init->do_basic_setup->do_initcalls do_initcalls中会把.initcall.init.中的函数依次执行一遍:  for (call = __initcall_start; call < __initcall_end; call++) { .    ..... result = (*call)(); .    ........ }   这个__initcall_start是在文件定义的:  .initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) {    __initcall_start = .;    INITCALLS    __initcall_end = .;   } 而在Vmlinux.lds.h中我们可以找到 #define INITCALLS *(.initcallearly.init) VMLINUX_SYMBOL(__early_initcall_end) = .;    *(.initcall0.init)    *(.initcall0s.init)    *(.initcall1.init)    *(.initcall1s.init)    *(.initcall2.init)    *(.initcall2s.init)    *(.initcall3.init)    *(.initcall3s.init)    *(.initcall4.init)    *(.initcall4s.init)    *(.initcall5.init)    *(.initcall5s.init)    *(.initcallrootfs.init)    *(.initcall6.init)    *(.initcall6s.init)    *(.initcall7.init)    *(.initcall7s.init) 可以看到优先级越来越低。 好吧,这么多的等级,怎么让driver早些出现呢,可不可以随便指定一个呢,比如直接用pure_initcall。大哥,虽然没人说你这样做伤天害理,但大家都是文明人,买车票上厕所都是要排队的。不过孕妇是可以不排队的,好吧,我们就让指定的driver享享孕妇的待遇吧。 但是查队插到什么地方好呢,是不是越靠前越好呢?当然不是,我们先来理理通常的初始化函数的顺序设定。通常设备的加载是arch_initcall,从init/main.c看起: -->Start_kernel (kernel启动的第一个函数) ... -->setup_arch (arch/arm/kernel/setup.c) ... -->init_machine = mdesc->init_machine;//一般的设备都是在                                      //mdesc->init_machine中定义加载 另外我们可以在arch/arm/kernel/setup.c中看到: static int __init customize_machine(void) { if (init_machine) init_machine(); return 0; } arch_initcall(customize_machine); 这里对customize_machine定义的初始化级别为arch_initcall,也就是说一般device加载的优先级是arch_initcall,那么我们就没必要将driver提到arch_initcall之前吧,不然driver妹妹也是再那空等,咱们device还是要讲点绅士风度的,约会不迟到。 好吧我们就可以给开后门的这个驱动优先级设定为arch_initcall、arch_initcall_sync、subsys_initcall、ubsys_initcall_sync、fs_initcall、fs_initcall_sync、rootfs_initcall等几个等级了(有的人要问了,不还有device_initcall、device_initcall_sync、late_initcall、late_initcall_sync几个等级呢?额,正常我们设备驱动都是用device_initcall这个等级的,咱们没必要帮倒忙吧)。至于具体给他那个优先级呢,这个就要考虑些其他因素了,比如这个驱动的初始化是否依赖其他设备初始化的完成,如果要依赖于某个设备的初始化,那么就优先级就必须比另外那个的低。 好吧,下面我们给出一般设备和驱动加载的优先级列表: 初始化等级              对应的初始化设备或驱动的函数      备注 early_initcall migration_init和spawn_ksoftirqd等 主要是register_cpu_notifier pure_initcall init_cpufreq_transition_notifier_list core_initcall netlink_proto_init、cpuidle_init、xxx_gpio_init、filelock_init、pm_init、sock_init、wakelocks_init等 主要是一些关键部分的初始化,像gpio、通信、电源管理等部分 core_initcall_sync postcore_initcall backlight_class_init、dma_sysclass_init、i2c_init、kobject_uevent_init、pci_driver_init、spi_init、tty_class_init等 主要是一些总线的节点的创建和链表初始化等(总线驱动的加载在后面) postcore_initcall_sync arch_initcall xxx_init_device、xxx_devices_setup、customize_machine、platform_init等 主要是板级设备的加载(i2c、spi、usb、串口等以及一些外设的加载) arch_initcall_sync subsys_initcall blk_ioc_ini、xxx_dma_init、xxx_i2c_init_driver、usb_init、xxx_spi_init等 块设备驱动、以及主要的bus总线驱动 subsys_initcall_sync fs_initcall inet_init、alignment_init、chr_dev_init、tracer_alloc_buffers等 fs_initcall_sync rootfs_initcall populate_rootfs、default_rootfs Rootfs先关初始化 device_initcall 一般的外设驱动的加载函数 module_init = device_initcall,外设驱动 device_initcall_sync late_initcall late_resume_init 等 late_initcall_sync 另外一般的外设驱动加载函数都是device_initcall级别的,如果外设很多,必然导致这一等级的加载函数很多。如何在这同一等级中调整driver加载的顺序呢? 不同的芯片厂家给的BSP包中具体的初始化顺序可能会有些差异,但是大概都是和这差不多。 对于同一等级(device_initcall)中的driverA和driverB,driverA说我比driverB大点,我要排到前面点去,driverB也没有意见,那好吧,我们可以来改改Makefile试试。 下面贴我自己写的几个驱动看看,这几个驱动都是放在/drivers/i2c/chip目录下的,先看我最初的Makefile: 好,接着可以从System.map中看看各个驱动的链接的顺序: 看到了吧,链接中的顺序和Makefile中的先后顺序是一模一样的。接下来我们调整一下Makefile中编译的顺序,看看链接的顺序是否也会跟着变过来。 这是修改后的顺序,把bma023放到中间了,重新编译来看看System.map里面的顺序 看到了吧,真的也跑到中间去了,这样是可以修改设备驱动的加载顺序的,说明了linux kernel下链接的顺序和编译的顺序是一样的(顶层的Makefile中规则指定的吧,具体我没弄明白)。看链接的符号表我想大家应该想起了前面定义在INITCALLS里面的*(.initcall6.init)了吧,是的上面这个函数就是按顺序放在*(.initcall6.init)这个段中的。 对于不同目录的driver如果需要调整顺序,当然也可以调整/drivers目录下Makefile下各种驱动类型的编译顺序(不过这个必须谨慎,不能随便乱改)。 总结:linux设备驱动有bus、device、driver三个重要的组成部分,先有bus,然后在bus上挂device和driver,有匹配的才调驱动的probe函数真正的初始化设备并注册API; 修改设备加载的顺序可以有两种方法,一种是修改设备加载的初始化等级,另外就是修改Makefile中的顺序。 备注:在Linux中,usb、sd、i2c、spi等总线上可以挂设备,但是它们自己相对于系统来说也是设备,也要有自己的驱动,在linux中,所有的这些总线都挂在platform这个虚拟总线上,在customize_machine(arch_initcall级)的时候,我们先会加载这些总线的platform_device结构,而这些总线的驱动(platform _driver)一般是定义为subsys_initcall级。 转自:http://blog.chinaunix.net/uid-26772535-id-3262471.html http://blog.csdn.net/ericghw/article/details/8302689