原文链接:https://blog.csdn.net/richard_liujh/article/details/46758073

————————————————

从上一篇博文 介绍了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();
在上面的调用过程中,通过kernel_thread注册了一个任务kernel_init,kernel_thread的函数原型如下。
/*
 * 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);
}
kernel_thread创建了一个内核线程,也就是创建一个线程完成kernel_init的任务。通过kernel_init的逐层调用,最后调用到我们目前最应该关心的函数 do_initcalls;
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 = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
这句for循环很简单,循环执行条件是 level < ARRAY_SIZE(initcall_levels)。
ARRAY_SIZE是一个宏,用于求数组元素的个数,在文件include\linux\kernel.h文件中

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))
当然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,
};
这个数组可不能小看他,如果看过 module_init解析(上)的朋友,对数组里面的名字“__initcall0 __initcall1 ... __initcall7”有一点点印象吧。
谈到数组,我们知道是元素的集合,那么initcall_levels数组中得元素是什么???(看下面的分析前,请先弄清楚数组指针 和指针数组的区别,不然容易走火入魔...)

static initcall_t *initcall_levels[] __initdata = {
很显然,这个数组定义非常高大上。不管如何高大上,总离不开最基本的知识吧。所以我先从两点去探索:
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);
}
ARRAY_SIZE求出了数组initcall_levels的元素个数为9,所以 level变量从 0 ~ 7都是满足 level < ARRAY_SIZE(initcall_levels) - 1既 level < 9 - 1。一共循环了8次。
循环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 (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(*fn);
这个也很简单,不就是一个for循环嘛,so easy~!!

那么接下来我们就开始分析这个for循环:

1. for循环开始,fn = initcall_levels[level],initcall_levels是上面分析过的数组,数组里面存放着指针,所以fn也应该是指针咯。那么看看fn的定义

initcall_t *fn;
fn确实是一个initcall_t类型的指针,那initcall_t是什么?
在文件include\linux\init.h文件中找到其定义

/*
 * Used for initialization calls..
 */
typedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);
从上面的定义可以知道,initcall_t原来是一个 函数指针的类型定义。函数的返回值是int类型,参数是空 void。从注释也可以看出,initcall_t是初始化调用的。
简单来说,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_t类型的,上面刚刚分析过initcall_t是函数指针的类型定义。
这样一来,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[];
所以__initcall0_start __initcall1_start __initcall2_start ... __initcall7_start __initcall_end都是initcall_t类型的数组名,数组名也就是指针。只是这些都是extern声明的,所以在本文件里面找不到他们的定义出。那么他们在哪一个文件??答案还是 链接脚本 vmlinux.lds,而且我们已经看过这些名字很多次了...
下面再次把链接脚本中相关的内容拿出来:(相关的解释请参考 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);
所以在main.c文件中extern声明的那些数组__initcall0_start  ... __initcall7_start __initcall_end其实就是上面链接脚本vmlinux.lds中定义的标号(也可以暂且简单粗暴认为是地址)。
为了好理解,把其中的__initcall0_start单独拿出来
__initcall0_start = .; *(.initcall0.init) *(.initcall0s.init)
这里的意思是,__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 (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(*fn);
最后我们要了解的就是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;
}
do_one_initcall函数就非常简单了,让我们看看最重要的内容如下
    if (initcall_debug)
        ret = do_one_initcall_debug(fn);
    else
        ret = fn();
这里就是判断是不是debug模式,无非debug会多一些调试的操作。但是不管是哪一种,他们都执行 ret = fn( );
因为fn就是函数指针,fn指向的是我们注册到__initcall0_start  ... __initcall7_start的一系列函数。所以 fn( ); 就是 调用这些函数。当然也包括了驱动中module_init注册的函数啦,只是通过module_init注册的level等级是6,for循环是从level = 0开始的,这也能看出0是优先级最高,7是优先级最低的。
到现在,module_init的作用已经全部分析完毕~

Linux内核之 module_init解析 (下)相关推荐

  1. 从入门到精通ARM(4412)-Linux内核驱动编程【下】-李志勇-专题视频课程

    从入门到精通ARM(4412)-Linux内核驱动编程[下]-247人已学习 课程介绍         嵌入式绝对是当前IT领域最炙手可热的话题了.其主要应用领域涵盖与人类相关的各行各业: * 消费电 ...

  2. arm的2级页表在Linux内核创建过程解析

    系统DDR的基地址为0x0,内存为1GB,所以TTB的基地址为0x4000.下面要创建虚拟地址0xfe700000到物理地址0xffff0000之间的映射,映射大小为64KB,即16页.由于物理地址不 ...

  3. linux系统内核参数命令,Linux内核启动参数解析及添加

    1.环境: Linux linuxidc 4.4.0-89-generic #112-Ubuntu SMP Mon Jul 31 19:38:41 UTC 2017 x86_64 x86_64 x86 ...

  4. ubuntu生成Linux内核解压,Ubuntu下生成linux内核

    写在前面:由于老师所给的指导文档经过测试之后发现已经不太适用于现在ubuntu下面的实际操作情况(貌似这个是按照RED HAT下面的情况来写的),具体体现在一些指令不适用(mkinitrd),还有引导 ...

  5. Linux 内核宏 time_after解析

    同学们留言回复答案看看 可能很多老鸟对这样的Linux 内核宏已经见惯不怪了,但是作为新手的Linux内核开发者,我觉得非常有必要了解其中的原理和作用. jiffies 这个想必大家已经非常熟悉,ji ...

  6. linux内核input子系统解析,ARM Linux内核Input输入子系统浅解

    struct list_head      node;      //该链表头用于将设备链接到input_dev_list }; Input_dev是一个很强大的结构体,它把所有的input设备(触摸 ...

  7. linux内核input子系统解析

    作者:刘洪涛,华清远见嵌入式学院讲师. Android.X windows.qt等众多应用对于linux系统中键盘.鼠标.触摸屏等输入设备的支持都通过.或越来越倾向于标准的input输入子系统. 因为 ...

  8. linux内核input子系统解析【转】

    转自:http://emb***/Column/Column289.htm 时间:2017-01-04作者:华清远见 Android.X windows.qt等众多应用对于linux系统中键盘.鼠标. ...

  9. debian 编译linux内核源码,Debian下为本机编译内核模块的方法

    改用Debian之后发现用之前在Red Hat下编译内核模块的Makefile不好使了,原因是在/lib/modules/`uname -r`/下没有了build目录,也没有内核的源代码.上网查了一些 ...

  10. Linux 内核调用栈解析

    1.pushl和popl pushl和popl指令来说操作的是栈,栈的基地址由%ebp来指定,栈顶元素由%esp来指定,%esp指向的就是栈顶元素.将一个双字压入栈中,首先要将%esp减4,然后将双字 ...

最新文章

  1. 什么才是真正的L3自动驾驶?
  2. 4G 信令中的 PCO 字段
  3. html5场景编辑工具,3款容易上手的HTML5编辑工具推荐~
  4. seo从入门到精通_SEO入门到精通(七):SEO工作的流程是什么?
  5. QML基础类型之geoshape
  6. Epoll 的tcp通信代码(服务器+客户端)
  7. mysql5.7.1.16出现[Err] 1146 - Table 'performance_schema.session_status' doesn't exist的解决办法
  8. 保险营销观察报告:保险直播“带货”的现状、风险分析与未来研判
  9. python的argparse模块
  10. 设计师面试提前准备好这10个面试问题,助你面试成功
  11. 微信公众号开发--Emoji表情(可用于关注自动回复等)
  12. 一种新的UI测试方法:视觉感知测试
  13. hic-pro运行报错Makefile执行过程中出错:make: *** No rule to make target ` ‘Stop
  14. [转载]Matlab绘图-很详细,很全面(包含各种标示符的输入方法)
  15. GAN动漫人像生成实现(附带源码)
  16. ESXi无法直通显卡
  17. 重磅推荐:一个也许能骗到黑客的系统
  18. 获取页面scroll高度
  19. FastClick 填坑及源码解析
  20. android 桌面插件 语录,句子控桌面小插件-句子控APPv2.5.3 安卓版_永辉资源网

热门文章

  1. WIN10作为服务器操作系统可以吗,服务器可以装win10吗
  2. poco mysql 登录_POCO数据库操作简介
  3. 梁念坚致辞Tech ED2009 主推Windows7
  4. Win10关闭显示器后立即锁定
  5. C# 温故而知新:Stream篇(五)
  6. 130 个相见恨晚的超实用网站,一次性分享出来。
  7. juniper防火墙定义策略生效时间
  8. 成语——》谁不曾浑身是伤,谁不曾彷徨迷惘
  9. 芯片常见的三种封装形式
  10. 课本剧剧本和计算机专业相关,《滥竽充数》课本剧剧本