系统初始化函数集(subsys_initcall)和初始化段应用

本文解析了Linux内核中驱动程序初始化函数(subsys_initcall)的工作原理,包括函数定义、宏展开过程以及如何通过__attribute__设置函数属性和段定位。介绍了内核启动时的调用顺序,并讨论了不同初始化函数的优先级。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考博文:

系统初始化函数集(subsys_initcall)和初始化段应用_subsys_initcall函数csdn-优快云博客

C语言__attribute__的使用-优快云博客

https://www.cnblogs.com/downey-blog/p/10486653.html

1.函数定义(subsys_initcall/late_initcall是什么)

我们都知道,linux对驱动程序提供静态编译进内核和动态加载两种方式,当我们试图将一个驱动程序编译进内核时,开发者通常提供一个xxx_init()函数接口以启动这个驱动程序同时提供某些服务。

那么,根据常识来说,这个xxx_init()函数肯定是要在系统启动的某个时候被调用,才能启动这个驱动程序。

当然,对于linus大神而言,这些都不是事,linux的做法是:

底层实现上,在内核镜像文件中,自定义一个段,这个段里面专门用来存放这些初始化函数的地址,内核启动时,只需要在这个段地址处取出函数指针,一个个执行即可。

对上层而言,linux内核提供xxx_init(init_func)宏定义接口,驱动开发者只需要将驱动程序的init_func使用xxx_init()来修饰,这个函数就被自动添加到了上述的段中,开发者完全不需要关心实现细节。
对于各种各样的驱动而言,可能存在一定的依赖关系,需要遵循先后顺序来进行初始化,考虑到这个,linux也对这一部分做了分级处理

内核代码4.14.99/include/linux/init.h,需要注意的是这里只针对build-in code,所以代码加上了#ifndef MODULE的限制

我们用C代码展示一下宏展开是怎样的:(gcc -E)

  1 #include <stdio.h>
  2 
  3 typedef int (*initcall_t)(void);
  4 
  5 #define __define_initcall(fn, id) \
  6     static initcall_t __initcall_##fn##id __used \
  7     __attribute__((__section__(".initcall"#id".init"))) = fn;
  8 
  9 #define subsys_initcall(fn) __define_initcall(fn, 4)
 10 #define late_initcall(fn) __define_initcall(fn, 7)
 11 #define late_initcall_sync(fn) __define_initcall(fn, 7s)
 12 
 13 int ljw(void);
 14 int main(void)
 15 {
 16     subsys_initcall(ljw)
 17     late_initcall(ljw)
 18     late_initcall_sync(ljw)
 19     return 0;
 20 }
# 2 "initcall.c" 2

typedef int (*initcall_t)(void);
# 13 "initcall.c"
int ljw(void);
int main(void)
{
    static initcall_t __initcall_ljw4 __used __attribute__((__section__(".initcall""4"".init"))) = ljw;
    static initcall_t __initcall_ljw7 __used __attribute__((__section__(".initcall""7"".init"))) = ljw;
 static initcall_t __initcall_ljw7s __used __attribute__((__section__(".initcall""7s"".init"))) = ljw;
 return 0;
}

简单来说,如上红框的注释所示,__define_initcall(fn,id)定义并初始化了一个静态函数指针__initcall_fnid,并初始化为fn

也就是说完成了如下效果:

static initcall_t __initcall_fnid = fn

然后将__initcall_fnid放入.initcallid.init段中等待内核初始化时的调用

这里需要注意的是不同的xxx_initcall中的fn可以相同,id的不同避免了同名符号的编译错误,例如:

int my_i2c_func(void);

subsys_initcall(my_i2c_func) ==> static initcall_t __initcall_my_i2c_func4 = my_i2c_func;

late_initcall(my_i2c_func) ==> static initcall_t __initcall_my_i2c_func7 = my_i2c_func;

xxx_init_call(fn)的原型其实是__define_initcall(fn, n),n是一个数字或者是数字+s,这个数字代表这个fn执行的优先级,数字越小,优先级越高,带s的fn优先级低于不带s的fn优先级。

2.系统初始化函数集如何使用

这里我们参考内核标准驱动(./drivers/i2c/busses/i2c-tegra.c),注意这个分号其实不该添加

既然我们知道了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的位置在./init/main.c

分析以上代码:

1). initcall_levels[9]一共有9个元素,每个元素都是initcall_t*类型

2). initcall_t __initcall0_start[n]~initcall_t __initcall7_start[n]为.initcall0.init~.initcall7.init段,每个元素都是initcall_t类型

     可以想象,在__define_initcall(fn,id)的时候,有__initcallid_start[x] = __initcall_fnid;

3).所以do_initcalls()的流程是,用一个for循环去遍历initcall_levels[9]数组的initcall_levels[0]~initcall_levels[7]八个元素

    对每一个initcall_levels[n]元素,又去遍历其指向的__initcalln_start[]数组的每个元素,调用__initcalln_start[x]所指向的函数

    最终调完所有段中的函数

4).那么为什么__initcall0_start[]~__initcall7_start[]与.initcall0.init~.initcall7.init段有关联呢?

    答案在./include/asm-generic/vmlinux.lds.h

将上述代码再整理翻译一下就是:

VMLINUX_SYMBOL(__initcall_start) = .;

KEEP(*(.initcallearly.init))

VMLINUX_SYMBOL(__initcall0_start) = .;

KEEP(*(initcall0.init))

KEEP(*(initcall0s.init))

......

VMLINUX_SYMBOL(__initcall4_start) = .;

KEEP(*(initcall4.init))

KEEP(*(initcall4s.init))

......

我们拿__initcall4_start来举例子,结合数组定义initcall_t __initcall4_start[]与lds链接文件的段信息,我们就知道了__initcall4_start[]数组的元素就是initcall4.init段与initcall4s.init段里面的内容,它们正是你代码里面xxx_initcall(yyy)时定义的函数指针

我们再来看一下System.map中__initcallx_start之间的关系:

可以看到这些段全部是连续的,所以整个调用的过程就相当于一个二维数组元素的遍历过程

可以看到这里将__initcall0_start与.initcall0.init及.initcall0s.init段关联起来,1~7也同理关联起来了

这里又有另外一个问题:

既然不同的段有优先级之分: 数字越小,优先级越高,那么在同一个段有没有优先级,优先级是怎么分的呢?

将就上图,我们拿__initcall0_start来举例,可以看到函数指针仍然是按照顺序排列,所以说调用仍然是有顺序的,但是这种顺序应该是由编译顺序来决定的。所以要知道确切的顺序,看System.map文件就好了。

3.__attribute__是什么

GNU C 的一大特色就是__attribute__ 机制。__attribute__ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )。

__attribute__ 书写特征是:__attribute__ 前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__ 参数。

__attribute__ 语法格式为:__attribute__ ((attribute-list))

__attribute__((_section_(".initcall" #id ".init")))表示编译时将目标符号放置在括号指定的段中。

而#在宏定义中的作用是将目标字符串化,##在宏定义中的作用是符号连接,将多个符号连接成一个符号,并不将其字符串化。

__used是一个宏定义,

#define  __used  __attribute__((__used__))

使用前提是在编译器编译过程中,如果定义的符号没有被引用,编译器就会对其进行优化,不保留这个符号,而__attribute__((_used_))的作用是告诉编译器这个静态符号在编译的时候即使没有使用到也要保留这个符号

提到section,就得说RO RI ZI了,在ARM编译器编译之后,代码被划分为不同的段,RO Section(ReadOnly)中存放代码段和常量,RW Section(ReadWrite)中存放可读写静态变量和全局变量,ZI Section(ZeroInit)是存放在RW段中初始化为0的变量。
于是本文的大体意思就清晰了,__attribute__((section("section_name"))),其作用是将作用的函数或数据放入指定名为"section_name"对应的段中。

__attribute__((section("name")))
RealView Compilation Tools for µVision Compiler Reference Guide Version 4.0 
 
Home > Compiler-specific Features > Variable attributes > __attribute__((section("name"))) 
 
4.5.6. __attribute__((section("name")))
Normally, the ARM compiler places the objects it generates in sections like data and bss. However, you might require additional data sections or you might want a variable to appear in a special section, for example, to map to special hardware. The section attribute specifies that a variable must be placed in a particular data section. If you use the section attribute, read-only variables are placed in RO data sections, read-write variables are placed in RW data sections unless you use the zero_init attribute. In this case, the variable is placed in a ZI section.
 
Note
This variable attribute is a GNU compiler extension supported by the ARM compiler.
 
Example
/* in RO section */
const int descriptor[3] __attribute__ ((section ("descr"))) = { 1,2,3 };
/* in RW section */
long long rw[10] __attribute__ ((section ("RW")));
/* in ZI section */
long long altstack[10] __attribute__ ((section ("STACK"), zero_init));

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值