0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

linux内核中do_initcalls函数的执行逻辑分析

嵌入式小生 来源:嵌入式小生 2023-01-13 09:20 次阅读

一、导读

linux内核启动过程中,会向终端打印出很多的日志信息,从这些信息中可以得到许多内核的行为。如果在启动阶段出现了问题,那么很多的提示信息也会从终端打印出。

这些信息的输出与具体模块功能的执行都归功于一个函数:do_initcalls,本文将主要分析这个函数的执行逻辑,且从这个函数延伸到linux各个子系统初始化背后的机制。

本文所有源码分析基于linux内核版本:4.1.15

二、do_initcalls

do_initcalls由do_basic_setup()调用:

ebe49646-92da-11ed-bfe3-dac502259ad0.png

do_basic_setup()由kernel_init()代表的内核init线程函数间接调用(在kernel_init_freeable()被调用)。

在调用do_basic_setup之前,处理器已经被初始化了,CPU子系统已经启动并且运行,内存和进程管理也工作正常,但是系统中的设备还没有被初始化,故而do_basic_setup正作用于此,本文主要描述do_initcalls,所以不再进而分析其他的函数。

do_initcalls在/init/main.c文件中实现:

staticvoid__initdo_initcalls(void)
{
intlevel;

for(level=0;level< ARRAY_SIZE(initcall_levels) - 1; level++)
  do_initcall_level(level);
}

函数中内容比较少,是一个for循环结构,循环的对象是initcall_levels数组,该数组用于描述初始化调用的级别,定义如下:

externinitcall_t__initcall_start[];
externinitcall_t__initcall0_start[];
externinitcall_t__initcall1_start[];
externinitcall_t__initcall2_start[];
externinitcall_t__initcall3_start[];
externinitcall_t__initcall4_start[];
externinitcall_t__initcall5_start[];
externinitcall_t__initcall6_start[];
externinitcall_t__initcall7_start[];
externinitcall_t__initcall_end[];

staticinitcall_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类型的指针,回到do_initcalls()函数中,该函数的核心操作是:按顺序从__initcall0_start开始,到__initcall_end结束的节段(称为初始化调用段)中取出不同段之间的函数,并执行。

存在这几个初始化调用段之间的函数都是内核中各个模块的初始化函数,而这些函数是如何加入到初始化调用段中的呢?又是如何设置调用级别的,会在后文中描述到。

在do_initcalls()函数中,会根据initcall_levels初始化调用级别的数量调用do_initcall_level(),该函数实现如下:

staticvoid__initdo_initcall_level(intlevel)
{
initcall_t*fn;

strcpy(initcall_command_line,saved_command_line);
parse_args(initcall_level_names[level],
initcall_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循环结构,该循环的操作对象为函数指针,且会将对应的函数指针传递到do_one_initcall中,在该函数执行函数指针所指向的函数:

ec54107a-92da-11ed-bfe3-dac502259ad0.png

三、构造section并添加函数

(3-1)构造初始化调用section

在linux内核中,不同架构(ARCH)下的kernel目录中,都会有一个名为vmlinux.lds.S的链接脚本,初始化调用section的构造则在这个链接脚本中完成。

本文以ARM32架构为例

在/arch/arm/kernel/vmlinux.lds.S中的链接脚本中,.init.data输出节段则需要INIT_CALLS作为输入节段:

ec7ceb1c-92da-11ed-bfe3-dac502259ad0.png

INIT_CALLS定义在/include/asm-generic/vmlinux.lds.h文件中:

eca878e0-92da-11ed-bfe3-dac502259ad0.png

而在内核的makefile中有以下语句:

LDFLAGS_vmlinux+=-Tarch/$(ARCH)/kernel/vmlinux.lds.s

用于指定构建linux内核镜像时所使用的链接脚本,基于此,则会构造好初始化调用section。

当初始化调用section构造完成后,是如何向该section中添加函数的呢?继续往下看。

(3-2)向section中添加函数

向section中添加函数的本质操作则是__define_initcall(),定义如下:

ecd103aa-92da-11ed-bfe3-dac502259ad0.png

并且linux内核基于__define_initcall()封装出了多个宏定义接口,供内核中各个模块使用,接口如下:

ece83110-92da-11ed-bfe3-dac502259ad0.png

__define_initcall()宏定义的本质则是定义一个initcall_t函数指针类型的变量并命名为__initcall_##fn##id,其中fn为赋值给该变量的函数名称,id为初始化调用级别,然后将fn赋值给该变量。接着就是最为重要的技术点:使用__attribute__将该变量加入到命名为"initcall##id.init"的section中,其中id为初始化调用级别,所以将fn添加到初始化调用section中则是通过这一点实现。例如:如果有以下类似的代码:

staticvoid__initshow_info(void)
{
printk("I'miriczhao
")
}

core_initcall(show_info);

经过层层宏替换后,本质上则变成:

staticinitcall_t__initcall_core_initcall1__used
__attribute__((__section__(".initcall1.init")))=show_info;

四、总结

综上,linux内核中使用基于__define_initcall封装出的多个接口API初始化内核的各个模块,使用这些API接口会将指定的函数放到名称为.initcall##id.init的section中,id为初始化调用级别,内核中定义了14种调用级别:分别为1~7和1s~7s(linux 3.0后增加的扩展)。这些调用级别是按照先后顺序依次排列的。

(4-1)linux内核中,对于内核的各个模块的初始化,正是通过使用__define_initcall()的衍生宏定义API将初始化函数放置到__initcall##id.initsection中,不同模块的初始化函数按照调用级别顺序排列。在内核启动阶段,这些放置到这个section中的函数指针将被do_initcalls()按顺序依次调用,进而完成各个模块的初始化操作。

linux内核系统非常庞大,各个子系统也非常多,他们的初始化函数从源码上是不需要在内核启动过程中去主动调用的,从设计上这一点也不现实,随着内核功能的增加,越来越复杂的驱动程序,从而linux内核基于编译器section技术,设计了初始化调用机制,将各个模块的初始化与linux内核启动主线分离。

(4-2)当使用基于__define_initcall封装出的多个API接口时,函数指针放置到哪个子section由具体的宏定义API接口的level参数确定,较小的level参数对应的函数指针则被放置在前面。而位于同一个子section内的函数指针顺序不定,由编译器按照编译的顺序随机指定。所以,如果一个模块的初始化函数想要越早被调用执行,则需要有较小的调用级别。





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 处理器
    +关注

    关注

    68

    文章

    18621

    浏览量

    224899
  • Arch
    +关注

    关注

    0

    文章

    18

    浏览量

    9613
  • LINUX内核
    +关注

    关注

    1

    文章

    313

    浏览量

    21440

原文标题:惊呆了,linux内核中的这机制 | do_initcalls

文章出处:【微信号:嵌入式小生,微信公众号:嵌入式小生】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Linux内核自解压过程分析

    uboot完成系统引导以后,执行环境变量bootm的命令;即,将Linux内核调入内存并调用do
    的头像 发表于 12-08 14:00 559次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>自解压过程<b class='flag-5'>分析</b>

    linux内核do_fork函数创建新进程

    前面已经谈了内核加载与系统引导过程,下面我们来看看内核do_fork() 函数是如何创建一个新的进程的。
    发表于 08-06 08:40

    Linux内核源码之我见——内核源码的分析方法

    ——找不到main函数。对于简单的demo程序,我们可以从头至尾的分析代码的含义,但是分析内核代码这招就彻底失效了,因为没有人能把Linux
    发表于 05-11 07:00

    简单分析linux内核的结构体使用方法

    所谓linux驱动编程可以理解为linux内核的编程。既然在内核编程那就必须要符合内核逻辑和各
    发表于 01-19 08:26

    逻辑代数与逻辑函数

    逻辑代数与逻辑函数:本章主要讨论分析和设计数字逻辑功能的数学。首先介绍逻辑代数
    发表于 09-01 09:11 0次下载

    Linux内核GPIO操作函数的详解分析

    本文档的主要内容详细介绍的是Linux内核GPIO操作函数的详解分析免费下载。
    发表于 01-22 16:58 28次下载

    Linux内核热补丁安全隐患的探索

    修复的函数替换掉内核存在问题的函数从而达到修复目的。 函数替换的思想比较简单,就是在执行
    的头像 发表于 10-11 11:54 1535次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>热补丁安全隐患的探索

    Linux内核系统调用概述及实现原理

    本文介绍了系统调用的一些实现细节。首先分析了系统调用的意义,它们与库函数和应用程序接口(API)有怎样的关系。然后,我们考察了Linux内核如何实现系统调用,以及
    的头像 发表于 05-14 14:11 2031次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>系统调用概述及实现原理

    linux内核启动过程会执行用户空间的init进程

    linux内核启动过程的后期,在kernel_init()函数代表的init线程,会尝试执行用户空间的init进程
    的头像 发表于 10-14 09:12 845次阅读

    Linux内核模块参数传递与sysfs文件系统

    Linux应用开发,为使应用程序更加灵活地执行用户的预期功能,我们有时候会通过命令行传递一些参数到main函数,使得代码
    发表于 06-07 16:23 1661次阅读

    Linux内核SoftIrq源代码分析

    我们在分析linux内核中断剖析时,简单的聊了一下SOFTIRQ, 而没有进行深入分析. Linux内核
    发表于 06-23 15:22 362次阅读

    万千设备,linux内核如何知道?

    linux内核设备的注册由device_register()函数完成,这个函数linux设备驱动模型的核心
    的头像 发表于 07-12 08:52 605次阅读
    万千设备,<b class='flag-5'>linux</b><b class='flag-5'>内核</b>如何知道?

    Linux内核如何使用结构体和函数指针?

    我将结合具体的Linux内核驱动框架代码来展示Linux内核如何使用结构体和函数指针。
    的头像 发表于 09-06 14:17 668次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>如何使用结构体和<b class='flag-5'>函数</b>指针?

    Linux驱动是如何挂载的

    | --- >do_initcalls | --- >do_initcall_level | --- >do_one_initcall 注意,这里就是驱动的初始化和驱动模块的加载。 我们知道在 rest_
    的头像 发表于 09-28 16:48 921次阅读
    <b class='flag-5'>Linux</b>驱动是如何挂载的

    bootm命令的执行流程

    Bootm命令用来从memory启动内核,bootm命令的执行流程如下图所示。 在串口终端输入bootm命令后,执行do_bootm函数来完
    的头像 发表于 12-04 17:33 679次阅读
    bootm命令的<b class='flag-5'>执行</b>流程