Linux 模块初始化优化
在阅读 Linux 内核驱动源码时经常会看到驱动初始化模块使用了 “__init” 和 “__exit” 修饰,例如下面展示的 Linux 内核支持的 LED 驱动就是这样的。
static int __init leds_init(void)
{
leds_class = class_create("leds");
......
return 0;
}
static void __exit leds_exit(void)
{
class_destroy(leds_class);
}
好奇吧,驱动初始化函数添加 “__init” 和 “__exit” 修饰到底意欲何为,该不会是内核开发者随便添加的装饰吧。
1. 作用
实际上 “__init” 和 “__exit” 是一种空间优化机制,大概内核从 v2.1.23 版本开始就被支持了,原理运用到了 GCC 编译器提供的一些编译属性和编译指令,这个稍后再讲。之所以说是优化机制,是因为初始化代码执行之后,包含初始化代码的函数就被丢弃,以及内存就被释放了不再占用内存。
2. 定义
要知道这种优化的原理我们还需要看下 “__init” 和 “__exit” 是如何定义的,在内核源码的 “init.h” 文件中可以看到下面这样的定义:
#define __section(section) __attribute__((__section__(section)))
#define __init __section(".init.text") __cold __latent_entropy __noinitretpoline
#define __section(section) __attribute__((__section__(section)))
#define __exit __section(".exit.text") __exitused __cold notrace
可以看到了 “__init” 和 “__exit” 是一个宏定义,被定义为了 “__section” 宏定义,这个 “__section” 宏定义使用到了 GCC 编译器提供的 “__attribute__” 指令,这个指令用来设置一些变量或函数的属性,比如这里就是用来设置 section 这个属性。
section 属性的作用:
section 属性告诉编译器(对于 Linux 这里指的是 GCC 编译器)将被修饰的变量或函数编译到特定的一块位置,注意这里说的特定位置不是物理存储器上的特定内存位置,而是在编译出来的可执行文件的特定段内(这里指的是 ELF 可执行文件的某个段中,可以查看 Linux ELF 可执行文件的组成结构)。
ELF 文件段:
现在我们来稍微了解一下 ELF 文件的段表,查看一下 ELF 文件包含哪些段,段表就是保存 ELF 文件中各种各样段的基本属性的结构。段表是 ELF 除了文件以外的最重要结构体,它描述了 ELF 的各个段的信息,ELF 文件的段结构就是由段表决定的,看下面某个 ELF 可执行文件的段表。
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040 000000000000002c 0000000000000000 AX 0 0 4
[ 2] .rela.text RELA 0000000000000000 00000218 0000000000000078 0000000000000018 I 8 1 8
[ 3] .data PROGBITS 0000000000000000 0000006c 0000000000000004 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 00000070 0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 00000070 0000000000000008 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 00000078 0000000000000031 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000a9 0000000000000000 0000000000000000 0 0 1
[ 8] .symtab SYMTAB 0000000000000000 000000b0 0000000000000150 0000000000000018 9 11 8
[ 9] .strtab STRTAB 0000000000000000 00000200 0000000000000017 0000000000000000 0 0 1
[10] .shstrtab STRTAB 0000000000000000 00000290 0000000000000052 0000000000000000 0 0 1
通常编译器默认将函数放在 “.text” 段,变量根据状态放在 “.data”,“.bss” 或 “.rodata” 段,使用 section 属性,可以让编译器将函数或变量放在指定的节中。那么例如:前面对 “__init” 的定义便表示将它修饰的代码放在 “.init.text” 段。
为什么上面这个 ELF 文件没有看到 “.init.text” 段?因为上面的 ELF 文件只是普通的可执行文件,实际内核把段分的非常细致,是因为它会在运行过程中去定位相应的数据和代码,这样细致分段将更加方便处理。就像 “__init” 修饰的所有代码都放在 “.init.text” 段,它只在启动阶段会被内核调用到,当初始化结束后就会释放这部分内存,以便充分利用内存,这个就是属于内存管理的部分了。
3. __init 函数怎么执行
使用 “__init” 修饰后相关的函数全部放在一起,比如这里将相关的初始化时需要运行的函数全部放到 “.init.text” 数据段。
最终所有的 “__init” 函数在区段 “.initcall.init” 段中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些__init函数指针,并在整个初始化完成后,释放整个 init 区段(“.init.text” 和 “.initcall.init” 段)。
注意,这些函数在内核初始化过程中的调用顺序只和这里的函数指针的顺序有关,和这些函数本身在 “.init.text” 区段中的顺序无关。
4. __exit
同理使用 “__init” 修饰设备初始化操作相关的函数,那么一些取消设备初始化相关的函数则使用 “__exit” 修饰,比如驱动被卸载不再使用时,为该驱动申请的内存就需要释放,那释放内存的操作就可以放到取消设备初始化函数中执行。
使用 “__exit” 修饰后,用于取消初始化操作的函数被放到 “.exit.text” 段,取消初始化操作的代码执行之后,包含取消初始化代码的函数就被丢弃。具体原理和"__init" 是相同的,这里不再重复说明。