Linux内核很吊之 module_init解析 (下)
Linux内核很吊之 module_init解析 (下)
转自: http://blog.****.net/richard_liujh/article/details/46758073
忙了一段时间,终于有时间把inux内核很吊之 module_init解析 (下)整理完毕。
从上一篇博文http://blog.****.net/richard_liujh/article/details/45669207介绍了module_init宏函数,简单来说上篇博文介绍module_init如何注册驱动的init函数,这篇博文将详细分析kernel启动过程又是如何执行我们注册的init函数。
如果了解过linux操作系统启动流程,那么当bootloader加载完kernel并解压并放置与内存中准备开始运行,首先被调用的函数是start_kernel。start_kernel函数顾名思义,内核从此准备开启了,但是start_kernel做的事情非常多,简单来说为内核启动做准备工作,复杂来说也是非常之多(包含了自旋锁检查、初始化栈、CPU中断、立即数、初始化页地址、内存管理等等等…)。所以这篇博文我们还是主要分析和module_init注册函数的执行过程。
start_kernel函数在 init/main.c文件中,由于start_kernel本身功能也比较多,所以为了简介分析过程我把函数从start_kernel到do_initcalls的调用过程按照如下方式展现出来
- start_kernel -> reset_init -> kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
- |
- |->static int __ref kernel_init(void *unused)
- |
- |-> kernel_init_freeable( )
- |
- |-> do_basic_setup();
- |
- |——> do_initcalls();
- /*
- * Create a kernel thread.
- */
- pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
- {
- return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
- (unsigned long)arg, NULL, NULL);
- }
do_initcalls函数如下
- static void __init do_initcalls(void)
- {
- int level;
- for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
- do_initcall_level(level);
- }
这个函数看起来就非常简单了,里面有for循环,每循环一次就调用一次do_initcall_level(level);其实可以发现在我们分析kernel源码时,大部分函数都能从函数名猜到函数的功能,这也是一名优秀程序猿的体现,大道至简,悟在天成。
接下来我们就开始具体分析do_initcalls函数啦~~
这句for循环很简单,循环执行条件是 level < ARRAY_SIZE(initcall_levels)。ARRAY_SIZE是一个宏,用于求数组元素的个数,在文件include\linux\kernel.h文件中
当然ARRAY_SIZE宏里面还多了一个__must_be_array(),这个主要是确保我们传过来的arr是一个数组,防止ARRAY_SIZE的误用。所以在我们写kernel驱动程序时,遇到需要求一个数组的大小请记得使用ARRAY_SIZE。有安全感又高大上…哈哈
那么,initcall_levels是不是数组呢?如果是,里面有什么内容?
还是在文件main.c中有数组initcall_levels的定义
- static initcall_t *initcall_levels[] __initdata = {
- __initcall0_start,
- __initcall1_start,
- __initcall2_start,
- __initcall3_start,
- __initcall4_start,
- __initcall5_start,
- __initcall6_start,
- __initcall7_start,
- __initcall_end,
- };
谈到数组,我们知道是元素的集合,那么initcall_levels数组中得元素是什么???(看下面的分析前,请先弄清楚数组指针 和指针数组的区别,不然容易走火入魔…)
1. 数组的名字,根据数组标志性的‘[ ]’,我们应该很容易知道数组名字是initcall_levels
2.数组的元素类型,由于定义中出现了指针的符号‘ * ’,也很容知道initcall_levels原来是一个指针数组啦。
所以现在我们知道了initcall_levels数组里面保存的是指针啦,也就是指针的一个集合而已。掰掰脚趾数一下也能知道initcall_levels数组里面有9个元素,他们都是指针。哈哈
对于这个数组,我们先暂且到这儿,因为我们已经知道了数组的个数了,也就知道for循环的循环次数。(后面还会继续分析这个数组,所以要由印象)
我们再回来看看do_initcalls:
- static void __init do_initcalls(void)
- {
- int level;
- for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
- do_initcall_level(level);
- }
循环8此就调用了do_initcall_level(level) 8次。
do_initcall_level函数原型如下:
- static void __init do_initcall_level(int level)
- {
- extern const struct kernel_param __start___param[], __stop___param[];
- initcall_t *fn;
- strcpy(static_command_line, saved_command_line);
- parse_args(initcall_level_names[level],
- static_command_line, __start___param,
- __stop___param - __start___param,
- level, level,
- &repair_env_string);
- for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
- do_one_initcall(*fn);
- }
在do_initcall_level函数中,有如下部分是和内核初始化过程调用parse_args对选项进行解析并调用相关函数去处理的。其中的__start___param和__stop___param也是可以在内核链接脚本vmlinux.lds中找到的。
- extern const struct kernel_param __start___param[], __stop___param[];
- strcpy(static_command_line, saved_command_line);
- parse_args(initcall_level_names[level],
- static_command_line, __start___param,
- __stop___param - __start___param,
- level, level,
- &repair_env_string);
这个也很简单,不就是一个for循环嘛,so easy~!!
那么接下来我们就开始分析这个for循环:
1. for循环开始,fn = initcall_levels[level],initcall_levels是上面分析过的数组,数组里面存放着指针,所以fn也应该是指针咯。那么看看fn的定义
fn确实是一个initcall_t类型的指针,那initcall_t是什么?在文件include\linux\init.h文件中找到其定义
- /*
- * Used for initialization calls..
- */
- typedef int (*initcall_t)(void);
- typedef void (*exitcall_t)(void);
简单来说,fn是一个函数指针。
2. 每循环一次,fn++。循环执行的条件是fn < initcall_levels[level+1];
这里fn++就不是很容易理解了,毕竟不是一个普通的变量而是一个函数指针,那么fn++有何作用呢??
首先,fn = initcall_levels[level],所以我们还是有必要去再看看initcall_levels数组了(之前暂时没有分析的,现在开始分析了)
- static initcall_t *initcall_levels[] __initdata = {
- __initcall0_start,
- __initcall1_start,
- __initcall2_start,
- __initcall3_start,
- __initcall4_start,
- __initcall5_start,
- __initcall6_start,
- __initcall7_start,
- __initcall_end,
- };
这样一来,initcall_levels数组里面保存的元素都是数组指针啦。
很显然这是通过枚举的方式定义了数组initcall_levels,那么元素值是多少??(数组中元素是分别是 __initcall0_start __initcall1_start __initcall2_start … __initcall7_start __initcall_end)
通过寻找会发现在main.c文件中有如下的声明
- extern initcall_t __initcall_start[];
- extern initcall_t __initcall0_start[];
- extern initcall_t __initcall1_start[];
- extern initcall_t __initcall2_start[];
- extern initcall_t __initcall3_start[];
- extern initcall_t __initcall4_start[];
- extern initcall_t __initcall5_start[];
- extern initcall_t __initcall6_start[];
- extern initcall_t __initcall7_start[];
- extern initcall_t __initcall_end[];
下面再次把链接脚本中相关的内容拿出来:(相关的解释请参考 module_init 解析–上)
- __init_begin = .;
- . = ALIGN(4096); .init.text : AT(ADDR(.init.text) - 0) { _sinittext = .; *(.init.text) *(.cpuinit.text) *(.meminit.text) _einittext = .; }
- .init.data : AT(ADDR(.init.data) - 0) { *(.init.data) *(.cpuinit.data) *(.meminit.data) *(.init.rodata) *(.cpuinit.rodata) *(.meminit.rodata) . = ALIGN(32); __dtb_start = .; *(.dtb.init.rodata) __dtb_end = .; . = ALIGN(16); __setup_start = .; *(.init.setup) __setup_end = .; __initcall_start = .; *(.initcallearly.init) __initcall0_start = .; *(.initcall0.init) *(.initcall0s.init) __initcall1_start = .; *(.initcall1.init) *(.initcall1s.init) __initcall2_start = .; *(.initcall2.init) *(.initcall2s.init) __initcall3_start = .; *(.initcall3.init) *(.initcall3s.init) __initcall4_start = .; *(.initcall4.init) *(.initcall4s.init) __initcall5_start = .; *(.initcall5.init) *(.initcall5s.init) __initcallrootfs_start = .; *(.initcallrootfs.init) *(.initcallrootfss.init) __initcall6_start = .; *(.initcall6.init) *(.initcall6s.init) __initcall7_start = .; *(.initcall7.init) *(.initcall7s.init) __initcall_end = .; __con_initcall_start = .; *(.con_initcall.init) __con_initcall_end = .; __security_initcall_start = .; *(.security_initcall.init) __security_initcall_end = .; . = ALIGN(4); __initramfs_start = .; *(.init.ramfs) . = ALIGN(8); *(.init.ramfs.info) }
- . = ALIGN(4);
为了好理解,把其中的__initcall0_start单独拿出来 这里的意思是,__initcall0_start 是一段地址的开始,从这个地址开始链接所有 .initcall0.init和.initcall0s.init段的内容。那.initcall0.init和.initcall0s.init段有什么东东??这就是上篇博文中解释的。简单来说,就是我们通过module_init(xxx)添加的内容,只是module_init对应的level值默认为6而已。
总而言之,__initcallN_start(其中N = 0,1,2…7)地址开始存放了一系列优先级为N的函数。我们通过module_init注册的函数优先级为6
现在我们回过头再去看看上面的for循环
- for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
- <span style=”white-space: pre;”> </span>do_one_initcall(*fn);
一开始fn = initcall_levels[level],假设level = 0。也就是fn = initcall_levels[0] = __initcall0_start。所以fn指向了链接脚本中的__initcall0_start地址,每当fn++也就是fn逐次指向注册到.initcall0.init和.initcall0s.init段中的函数地址了。for循环的条件是fn < initcall_levels[level + 1] = initcall_levels[0 + 1] = initcall_level[1] = __initcall1_start。
为了能直观看出fn增加的范围,用如下的简易方式表达一下。
__initcall0_start __initcall1_start __initcall2_start __initcall3_start … … __initcall7_start __initcall_end
| <—– fn++ —->|| <—– fn++ —>| | <—– fn++ —>| | <—– fn++ —>|… … | <—– fn++ —>| END
了解这一点,我们已经接近胜利的彼岸~~
最后我们要了解的就是for循环每次执行的内容 do_one_initcall(*fn),其函数原型如下- int __init_or_module do_one_initcall(initcall_t fn)
- {
- int count = preempt_count();
- int ret;
- if (initcall_debug)
- ret = do_one_initcall_debug(fn);
- else
- ret = fn();
- msgbuf[0] = 0;
- if (preempt_count() != count) {
- sprintf(msgbuf, ”preemption imbalance ”);
- preempt_count() = count;
- }
- if (irqs_disabled()) {
- strlcat(msgbuf, ”disabled interrupts ”, sizeof(msgbuf));
- local_irq_enable();
- }
- WARN(msgbuf[0], ”initcall %pF returned with %s\n”, fn, msgbuf);
- return ret;
- }
因为fn就是函数指针,fn指向的是我们注册到__initcall0_start … __initcall7_start的一系列函数。所以 fn( ); 就是调用这些函数。当然也包括了驱动中module_init注册的函数啦,只是通过module_init注册的level等级是6,for循环是从level = 0开始的,这也能看出0是优先级最高,7是优先级最低的。
到现在,module_init的作用已经全部分析完毕~
- 本文已收录于以下专栏:
相关文章推荐
-
Linux内核很吊之 module_init解析 (下)
分析module_init 的作用。和module_init注册函数过程,以及注册函数被执行的过程。- Richard_LiuJH
- 2015-07-04 22:00
- 1998
-
Linux内核模块分析(module_init宏)
我们在学习Linux驱动开发时,首先需要了解Linux的模块化机制(module),但是module并不仅仅用于支撑驱动的加载和卸载。一个最简单的模块例子如下:// filename: HelloWo…- luckydarcy
- 2016-05-17 03:25
- 674
-
蚂蜂窝大数据平台架构及Druid引擎实践!
本文主要涉及蚂蜂窝大数据平台的架构设计、离线数据探索、实时数据探索、多维数据分析中Druid引擎引入的背景和带来的价值….
-
linux内核段属性机制(以subsys_initcall和module_init为例)
linux内核段属性机制 以subsys_initcall和module_init为例- TongxinV
- 2017-01-27 11:04
- 614
-
Linux内核module_param的使用
本博客转载于:http://blog.****.net/dysh1985/article/details/7802080 1.定义模块参数的方法: module_param(nam…- Explorer_day
- 2014-12-22 20:33
- 608
-
linux内核可装载模块(lkm)传参机制 module_param
对于如何向模块传递参数,Linux kernel 提供了一个简单的框架。其允许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,在驱动程序里,参数的用法如同全局变量。 使用下面的…- qq69696698
- 2012-07-19 11:38
- 758
-
linux内核结构体初始化时出现的.owner = THIS_MODULE
.owner = THIS_MODULE为什么加“点”的原因 多次看书、编程时又看到了对结构体这种定义的方法,如: struct file_operations scull_fops = {…- ychongx
- 2014-06-04 17:06
- 299
-
linux内核模块编译和安装–kni module移植的makefile
根据需要需要把依赖dpdk的kni module移植,所以就学习了下模块编译makefile,总结如下 Makefile内容如下 obj-m += rte_kni.o #要生成的module…- u013920085
- 2016-03-01 15:47
- 1080
-
Linux内核module_param的使用
1.定义模块参数的方法:module_param(name, type, perm);其中,name:表示参数的名字; type:表示参数的类型; perm:表示参数的访问权限…- viewsky11
- 2016-11-12 22:38
- 220
-
Linux内核module_param的使用
1.定义模块参数的方法: module_param(name, type, perm); 其中,name:表示参数的名字; type:表示参数的类型; perm:表示参数的…- dysh1985
- 2012-07-30 14:57
- 12868
-
linux内核入门之module
一、模块的概念 模块是一种向linux内核添加设备驱动程序、文件系统及其他组件的有效办法。无需重新编译新内核和重启系统就可以向内核加入功能。增加了linux的可扩展性。 优点: 将…- u012627278
- 2013-10-30 16:39
- 609
-
linux内核结构体初始化时出现的.owner = THIS_MODULE是什么?
如题 :在阅读Linux内核源码时候经常会遇到一种神秘的结构体初始化情况 像这种 .owner = THIS_MODULE 这到底是怎么回事呢? 其实这是Linux内核代码中一种特殊的结构体初…- bonnshore
- 2012-08-10 10:28
- 4540
-
很经典的linux内核模块编程指南The+Linux+Kernel+Module+Programming+Guide–2.2 && 2.6 && 2.4
- 2010-12-07 16:05
- 908KB
- 下载
-
linux内核及驱动开发中有关__init,__exit和__initdata的用法
要了解Linux Kernel代码的分段信息,需要了解一下gcc的__attribute__的编绎属性或定义的函数或数,__attribute__主要用于改变所声明据的特性,它有很多子项,用于改变作用…- hpu11
- 2017-05-22 13:27
- 81
-
Linux内核中的等待队列–init_waitqueue_head等
Linux内核的等待队列是以双循环链表为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。在Linux2.4.21中,等待队列在源代码树include/linux/wait.h…- hellolwl
- 2011-11-25 17:05
- 1853
-
linux内核cdev_init系列函数(字符设备的注册)
内核中每个字符设备都对应一个 cdev 结构的变量,下面是它的定义: linux-2.6.22/include/linux/cdev.h struct cdev { struct ko…- cfc1243570631
- 2015-04-15 20:32
- 420
-
linux内核cdev_init系列函数(字符设备的注册)
内核中每个字符设备都对应一个 cdev 结构的变量,下面是它的定义: linux-2.6.22/include/linux/cdev.h struct cdev { struct kobj…- a252292568
- 2014-09-26 19:39
- 85
-
Linux内核中的宏:__init and __exit
ZZ FROM: http://blog.****.net/musein/article/details/742609 ====================================…- dadoneo
- 2012-04-27 12:25
- 1829
-
linux内核cdev_init系列函数
内核中每个字符设备都对应一个 cdev 结构的变量,下面是它的定义: linux-2.6.22/include/linux/cdev.h struct cdev { struct kobject k…- longerzone
- 2012-08-08 21:34
- 490
-
linux内核及驱动开发中有关__init,__exit和__initdata的用法 .
这些宏包括 __init、__initdata、__initfunc()、asmlinkage、ENTRY()、FASTCALL()等等。它们的定义主要位于 Include\linux\linkage…- liyun422828
- 2011-09-22 23:30
- 477
-
linux内核及驱动开发中有关__init,__exit和__initdata的用法
要了解Linux Kernel代码的分段信息,需要了解一下gcc的__attribute__的编绎属性或定义的函数或数,__attribute__主要用于改变所声明据的特性,它有很多子项,用于改变作用…- LinuxEngineer
- 2013-12-03 14:28
- 556