咱们都知道,linux对驱动程序提供静态编译进内核和动态加载两种方式,当咱们试图将一个驱动程序编译进内核时,开发者一般提供一个xxx_init()函数接口以启动这个驱动程序同时提供某些服务。html
那么,根据常识来讲,这个xxx_init()函数确定是要在系统启动的某个时候被调用,才能启动这个驱动程序。linux
最简单直观地作法就是:开发者试图添加一个驱动程序时,在内核启动init程序的某个地方直接添加调用本身驱动程序的xxx_init()函数,在内核启动时天然会调用到这个程序。数组
可是,回头一想,这种作法在单人开发的小系统中或许能够,可是在linux中,若是驱动程序是这么个添加法,那就是一场灾难,这个道理我想不用我多说。函数
不难想到另外一种方式,就是集中提供一个地方,若是你要添加你的驱动程序,你就将你的初始化函数在这个地方进行添加,在内核启动的时候统一扫描这个地方,再执行这一部分的全部被添加的驱动程序。post
那到底怎么添加呢?直接在C文件中做一个列表,在里面添加初始化函数?我想随着驱动程序数量的增长,这个列表会让人头昏眼花。优化
固然,对于linus大神而言,这些都不是事,linux的作法是:ui
底层实现上,在内核镜像文件中,自定义一个段,这个段里面专门用来存放这些初始化函数的地址,内核启动时,只须要在这个段地址处取出函数指针,一个个执行便可。操作系统
对上层而言,linux内核提供xxx_init(init_func)宏定义接口,驱动开发者只须要将驱动程序的init_func使用xxx_init()来修饰,这个函数就被自动添加到了上述的段中,开发者彻底不须要关心实现细节。
对于各类各样的驱动而言,可能存在必定的依赖关系,须要遵循前后顺序来进行初始化,考虑到这个,linux也对这一部分作了分级处理。debug
在平台对应的init.h文件中,能够找到xxx_initcall的定义:指针
/*Only for built-in code, not modules.*/ #define early_initcall(fn) __define_initcall(fn, early) #define pure_initcall(fn) __define_initcall(fn, 0) #define core_initcall(fn) __define_initcall(fn, 1) #define core_initcall_sync(fn) __define_initcall(fn, 1s) #define postcore_initcall(fn) __define_initcall(fn, 2) #define postcore_initcall_sync(fn) __define_initcall(fn, 2s) #define arch_initcall(fn) __define_initcall(fn, 3) #define arch_initcall_sync(fn) __define_initcall(fn, 3s) #define subsys_initcall(fn) __define_initcall(fn, 4) #define subsys_initcall_sync(fn) __define_initcall(fn, 4s) #define fs_initcall(fn) __define_initcall(fn, 5) #define fs_initcall_sync(fn) __define_initcall(fn, 5s) #define rootfs_initcall(fn) __define_initcall(fn, rootfs) #define device_initcall(fn) __define_initcall(fn, 6) #define device_initcall_sync(fn) __define_initcall(fn, 6s) #define late_initcall(fn) __define_initcall(fn, 7) #define late_initcall_sync(fn) __define_initcall(fn, 7s)
xxx_init_call(fn)的原型实际上是__define_initcall(fn, n),n是一个数字或者是数字+s,这个数字表明这个fn执行的优先级,数字越小,优先级越高,带s的fn优先级低于不带s的fn优先级。
继续跟踪代码,看看__define_initcall(fn,n):
#define __define_initcall(fn, id) \ static initcall_t __initcall_##fn##id __used \ __attribute__((__section__(".initcall" #id ".init"))) = fn;
值得注意的是,__attribute__()是gnu C中的扩展语法,它能够用来实现不少灵活的定义行为,这里不细究。
__attribute__((__section__(".initcall" #id ".init")))表示编译时将目标符号放置在括号指定的段中。
而#在宏定义中的做用是将目标字符串化,##在宏定义中的做用是符号链接,将多个符号链接成一个符号,并不将其字符串化。
__used是一个宏定义,
#define __used __attribute__((__used__))
使用前提是在编译器编译过程当中,若是定义的符号没有被引用,编译器就会对其进行优化,不保留这个符号,而__attribute__((__used__))的做用是告诉编译器这个静态符号在编译的时候即便没有使用到也要保留这个符号。
为了更方便地理解,咱们拿举个例子来讲明,开发者声明了这样一个函数:pure_initcall(test_init);
因此pure_initcall(test_init)的解读就是:
首先宏展开成:__define_initcall(test_init, 0) 而后接着展开:static initcall_t __initcall_test_init0 = test_init;这就是一个简单的变量定义。 同时声明__initcall_test_init0这个变量即便没被引用也保留符号,且将其放置在内核镜像的.initcall0.init段处。
须要注意的是,根据官方注释能够看到early_initcall(fn)只针对内置的核心代码,不能描述模块。
既然咱们知道了xxx_initcall是怎么定义并且目标函数的放置位置,那么使用xxx_initcall()修饰的函数是怎么被调用的呢?
咱们就从内核C函数起始部分也就是start_kernel开始往下挖,这里的调用顺序为:
start_kernel -> rest_init(); -> kernel_thread(kernel_init, NULL, CLONE_FS); -> kernel_init() -> kernel_init_freeable(); -> do_basic_setup(); -> do_initcalls();
这个do_initcalls()就是咱们须要寻找的函数了,在这个函数中执行全部使用xxx_initcall()声明的函数,接下来咱们再来看看它是怎么执行的:
static initcall_t *initcall_levels[] __initdata = { __initcall0_start, __initcall1_start, __initcall2_start, __initcall3_start, __initcall4_start, __initcall5_start, __initcall6_start, __initcall7_start, __initcall_end, }; int __init_or_module do_one_initcall(initcall_t fn) { ... if (initcall_debug) ret = do_one_initcall_debug(fn); else ret = fn(); ... return ret; } static void __init do_initcall_level(int level) { initcall_t *fn; ... for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn); } static void __init do_initcalls(void) { int level; for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) do_initcall_level(level); }
在上述代码中,定义了一个静态的initcall_levels数组,这是一个指针数组,数组的每一个元素都是一个指针.
do_initcalls()循环调用do_initcall_level(level),level就是initcall的优先级数字,由for循环的终止条件ARRAY_SIZE(initcall_levels) - 1可知,总共会调用do_initcall_level(0)~do_initcall_level(7),一共七次。
而do_initcall_level(level)中则会遍历initcall_levels[level]中的每一个函数指针,initcall_levels[level]其实是对应的__initcall##level##_start指针变量,而后依次取出__initcall##level##_start指向地址存储的每一个函数指针,并调用do_one_initcall(*fn),实际上就是执行当前函数。
能够猜到的是,这个__initcall##level##start所存储的函数指针就是开发者用xxx_initcall()宏添加的函数,对应".initcall##level##.init"段。
do_one_initcall(*fn)的执行:判断initcall_debug的值,若是为真,则调用do_one_initcall_debug(fn);若是为假,则直接调用fn。事实上,调用do_one_initcall_debug(fn)只是在调用fn的基础上添加一些额外的打印信息,能够直接当作是调用fn。
那么,在initcall源码部分有提到,在开发者添加xxx_initcall(fn)时,事实上是将fn放置到了".initcall##level##.init"的段中,可是在do_initcall()的源码部分,倒是从initcall_levelslevel取出,initcall_levels[level]是怎么关联到".initcall##level##.init"段的呢?
答案在vmlinux.lds.h中:
#define INIT_CALLS_LEVEL(level) \ VMLINUX_SYMBOL(__initcall##level##_start) = .; \ KEEP(*(.initcall##level##.init)) \ KEEP(*(.initcall##level##s.init)) \ #define INIT_CALLS \ VMLINUX_SYMBOL(__initcall_start) = .; \ KEEP(*(.initcallearly.init)) \ INIT_CALLS_LEVEL(0) \ INIT_CALLS_LEVEL(1) \ INIT_CALLS_LEVEL(2) \ INIT_CALLS_LEVEL(3) \ INIT_CALLS_LEVEL(4) \ INIT_CALLS_LEVEL(5) \ INIT_CALLS_LEVEL(rootfs) \ INIT_CALLS_LEVEL(6) \ INIT_CALLS_LEVEL(7) \ VMLINUX_SYMBOL(__initcall_end) = .;
在这里首先定义了__initcall_start,将其关联到".initcallearly.init"段。
而后对每一个level定义了INIT_CALLS_LEVEL(level),将INIT_CALLS_LEVEL(level)展开以后的结果是定义__initcall##level##_start,并将
__initcall##level##_start关联到".initcall##level##.init"段和".initcall##level##s.init"段。
到这里,__initcall##level##_start和".initcall##level##.init"段的对应就比较清晰了,因此,从initcall_levels[level]部分一个个取出函数指针并执行函数就是执行xxx_init_call()定义的函数。
便于理解,咱们须要一个示例来梳理整个流程,假设我是一个驱动开发者,开发一个名为beagle的驱动,在系统启动时须要调用beagle_init()函数来启动启动服务。
我须要先将其添加到系统中:
core_initcall(beagle_init)
core_initcall(beagle_init)宏展开为__define_initcall(beagle_init, 1),因此beagle_init()这个函数被放置在".initcall1.init"段处。
在内核启动时,系统会调用到do_initcall()函数。 根据指针数组initcall_levels[1]找到__initcall1_start指针,在vmlinux.lds.h能够查到:__initcall1_start对应".initcall1.init"段的起始地址,依次取出段中的每一个函数指针,并执行函数。
添加的服务就实现了启动。
可能有些C语言基础不太好的朋友不太理解do_initcall_level()函数中依次取出地址并执行的函数执行逻辑:
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn);
fn为函数指针,fn++至关于函数指针+1,至关于:内存地址+sizeof(fn),sizeof(fn)根据平台不一样而不一样,通常来讲,32位机上是4字节,64位机则是8字节(关于指针在操做系统中的大小能够参考另外一篇博客:不一样平台下指针大小 )。
而initcall_levels[level]指向当前".initcall##level##s.init"段,initcall_levels[level+1]指向".initcall##(level+1)##s.init"段,两个段之间的内存就是存放全部添加的函数指针。
也就是从".initcall##level##s.init"段开始,每次取一个函数出来执行,并累加指针,直到取完。
好了,关于linux中initcall系统的讨论就到此为止啦,若是朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言
原创博客,转载请注明出处!
祝各位早日实现项目丛中过,bug不沾身.