[Linux Kernel慢慢學]Linux modules載入及載入順序

Posted by John on 2020-10-17
Words 1.6k and Reading Time 7 Minutes
Viewed Times

前言

我們都知道(或是我知道),Linux modules的入口點在module_init(),透過module_init()把我們的程式掛載到kernel去。

不過有時候會看到有些module用的是late_initcall()或是arch_initcall() 這類macro,他們跟一般的module_init()又差在哪裡?

本文會透過trace linux source code介紹以下幾個概念

  1. module_init()這個macro背後做了什麼事情
  2. 不同的init macro背後的差異是什麼
  3. 怎麼透過不同的init macro來控制modules的載入先後順序,以及為何要控制載入的順序

(本文的linux code 版本為linux4.14.201)

module_init code trace

首先我們來看一下每個module的entry,module_init()到底在幹嘛,如果不想跟我一起追code的可以追我喔…不是,我下面有懶人包總結,可以直接跳下去看。

module_init()的macro被定義在linux/include/module.h中

89
90
91
92
93
94
95
96
97
98
/* linux/include/module.h, line=85 */
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
85
86
/* linux/include/init.h, line=85 */
#define __initcall(fn) device_initcall(fn)

發現實際上是調用了device_initcall(fn),所以來追一下這個部分會發現有許多相關的func都同樣地呼叫了__define_initcall(fn, id)

176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/* linux/include/init.h, line=176 */
/*
* Early initcalls run before initializing SMP.
*
* Only for built-in code, not modules.
*/
#define early_initcall(fn) __define_initcall(fn, early)

/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
* Keep main.c:initcall_level_names[] in sync.
*/
#define pure_initcall(fn) __define_initcall(fn, 0)

#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)

懶人包總結: module_init到底做了什麼

好,先到這裡停住,先講結論,講完有興趣的可以繼續往下追。

簡單說module_init()透過實際上呼叫__device_initcall(fn, id)將對應的init function放到linker script的某個section中,而kernel在載入的時候會根據數字的順序來依序init所有的init functions完成各個modules的初始化。

而我們可以發現我們常用的module_init()實際上優先權是6(device_initcall(fn)的id是6),所以如果有人用了比module_init()還要前面的,那他就會比較早被載入,反之依然。

  • 對於一些載入順序很重要的modules來說,透過不同的xxxx_initcall來控制載入順序很重要

繼續追__define_initcall(fn, id)

上面各個不同的initcall都都呼叫了__define_initcall(),所以來看一下

172
173
174
175
/* include/linux/init.h. line=172 */
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn;

這個一坨真的很可怕,舉個例子來個別解釋,如果fn=hello, id=6的話:

  • __initcall_##fn##id就會變成__initcall_hello6
  • __attribute__(__section__)是編譯器的編譯屬性,就是說將fn放到linker script的”.initcall6.init” section中

比方說,觀察arch/powerpc/kernel/vmlinux.lds.S的initcall.init section:

215
216
217
218
/* arch/powerpc/kernel/vmlinux.lds.S, line=215 */
.initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) {
INIT_CALLS
}

INIT_CALLS被定義在include/asm-generic/vmlinux.lds.h

744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
/* include/asm-generic/vmlinux.lds.h, line=749 */
#define INIT_CALLS_LEVEL(level) \
VMLINUX_SYMBOL(__initcall##level##_start) = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \

#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
KEEP(*(.initcallearly.init)) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
VMLINUX_SYMBOL(__initcall_end) = .;

發現他做的事情就是將*(.initcall0.init), *(.initcall0s.init)…依照順序去放在.initcall.init section中。

實際上,arch/powerpc/kernel/vmlinux.lds.S的initcall.init section展開後是長下面這樣:

215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
/* arch/powerpc/kernel/vmlinux.lds.S, line=215 */
.initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) {
__initcall_start = .;
__initcall0_start = .;
*(.initcall0.init);
*(.initcall0s.init);
__initcall1_start = .;
*(.initcall1.init);
*(.initcall1s.init);
__initcall2_start = .;
*(.initcall2.init);
*(.initcall2s.init);
...
__initcall7_start = .;
*(.initcall7.init);
*(.initcall7s.init);
__initcall_end = .;
}

好,到這裡回顧一下,__define_initcall(fn, id)會根據你的id將你的init function放到linker script中對應的section內。

到這裡module_init()終於全部追完了,比追20集的韓劇還累。

如果有跟我一起追到這邊的朋友,可以再回去回顧一下懶人包,是不是對module_init()做的事情更加清楚了呢?

到這裡我們已經可以回答一開始的兩個問題了

  1. module_init()做的事情是將init func放到對應優先權的init section中,等kernel載入時依序呼叫
  2. 不同xxxx_initcall()給了不同的優先順序,kernel載入時會根據id來依序載入,所以可以達到控制module載入先後順序的效果

為何要控制module載入的順序

引用一些網站上遇到的內容來說明模組先後順序會有什麼影響:

举个例子,在2.6.24的内核中:gianfar_device使用的是arch_initcall,而gianfar_driver使用的是module_init,因为arch_initcall的优先级大于module_init,所以gianfar设备驱动的device先于driver在总线上添加。

这是我在调背光的时候出现的问题,因为键盘驱动是会在一个遥控手柄之前加载,导致驱动出现冲突;把两者先后顺序换一下就可以了;

補充: Kernel載入init function的過程

在kernel載入的過程中,會依序呼叫start_kernel()->rest_init()->kernel_init()->kernel_init_freeable()->do_basic_setup()->do_initcalls()來依序載入init function

不過這個要trace下去也很痛苦,我就把東西放在這裡了,有興趣的自己去慢慢看: Linux内核很吊之 module_init解析 (下)

platform_device有多個module時加載的順序

以下引用linux 设备驱动加载的先后顺序的內容:

【問題】背光驱动初始化先于LCD驱动初始化,导致LCD驱动初始化时出现闪屏的现象。

考慮下面的platform_device array

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* platform devices */
static struct platform_device *athena_evt_platform_devices[] __initdata = {
//&xxx_led_device,
&xxx_rtc_device,
&xxx_uart0_device,
&xxx_uart1_device,
&xxx_uart2_device,
&xxx_uart3_device,
&xxx_nand_device,
&xxx_i2c_device,

&xxx_lcd_device,
&xxxpwm_backlight_device,
...
};

發現lcd_device在backlight_device前面,但加載順序並不一致

  • 注意PM resume/suspend ops和這個順序是一致的

看了一下該module的Makefile: backlight在display前面

obj-$(CONFIG_VT)   += console/
obj-$(CONFIG_LOGO) += logo/
obj-y += backlight/ display/
...
obj-$(CONFIG_FB_xxx) += xxxfb.o ak_logo.o
obj-$(CONFIG_FB_AK88) += ak88-fb/

產生的System.map如下

c001f540 t __initcall_pwm_backlight_init6
c001f544 t __initcall_display_class_init6
c001f548 t __initcall_xxxfb_init6

如果把Makefile順序改一下
obj-$(CONFIG_VT)   += console/
obj-$(CONFIG_LOGO) += logo/
obj-y += display/
...
obj-$(CONFIG_FB_xxx) += xxxfb.o ak_logo.o
obj-$(CONFIG_FB_AK88) += ak88-fb/
obj-y += backlight/

System.map順序就對了

c001f53c t __initcall_display_class_init6
c001f540 t __initcall_xxxfb_init6
c001f544 t __initcall_genericbl_init6
c001f548 t __initcall_pwm_backlight_init6

加載後就會發現xxxpwm_backlight_device的probe会在xxx_lcd_device的probe之后执行,即LCD初始化先于PWM的初始化。

References


>