0. 介紹
在linux kernel啟動過程中,通過initcall機制調用初始化函數。initcall作為kernel經典設計機制之一延續至今。在2018年,Steven Rostedt
為了跟蹤各個初始化函數的執行時間,增加了tracing功能。
在本篇文章中,將會介紹initcall的意義和使用方法、實現原理、執行流程以及調試方法。
1. 意義和使用方法
正如文章最開始的地方所描述的那樣,其直接意義是在kernel啟動過程中執行不同的初始化函數,涉及到不同架構下的CPU初始化以及各種外設驅動的初始化。
由于使用initcalls不需要顯示的傳遞、存儲和調用函數指針,我們只需要將函數標記為合適的initcall類型,內核代碼就幫助我們完成了各函數的遍歷執行,因此,基于initcall機制,可以使得代碼更具模塊化屬性以及更高的可維護性。
kernel中的基于initcall機制定義的初始化代碼遵循固定的規則:使用__init進行修飾,然后通過xxx_initcall聲明為不同的類型。
static int __init register_cpufreq_notifier(void)
{
...
}
core_initcall(register_cpufreq_notifier);
每一個initcall函數都通過不同的前綴加以修飾,例如:
pure_initcall
subsys_initcall
core_initcall
fs_initcall
arch_initcall
...
在kernel代碼中存在著大量的*_initcall
修飾的函數。不同種類的initcall函數進行統計,如下圖所示:
initcall統計
2. 實現原理
initcall設計思想如下:
- 在生成vmlinux的鏈接階段為initcall創建特定的section
- 開發者創建相關的initcall函數,并使用xxx_initcall聲明為不同類型
- 每一類initcall對應一組section
- 遍歷執行initcall section中的initcalls
xxx_initcall的定義位于include/linux/init.h
中,從這個文件的名字也可以看出xxx_initcall是針對初始化操作的。
#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)
從上面的宏定義可以發現,所有的xxx_initcall都是基于__define_initcall的,后者的定義位于同一個文件中,通過__define_initcall將各個xxx_initcall統一到一起,基于ID編號鏈接到不同的subsection,在同一個subsection中各個initcall的排序以鏈接的順序為準。
另外,__define_initcall
中的ID編號還有另外一個作用,就是防止不同類型的xxx_initcall調用相同的符號引起編譯錯誤。
#define __define_initcall(fn, id) \\
static initcall_t __initcall_##fn##id __used \\
__attribute__((__section__(".initcall" #id ".init"))) = fn; \\
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
以rockchip_grf_init()為例拆解分析xxx_initcall的實現細節,如下圖所示,注意,在倒數第二個框圖內可以看出來initcall機制使用到了GNU編譯工具鏈的屬性。
initcall實現細節
4. 執行流程
根據前面的介紹,當xxx_initcall被鏈接到目標文件后,會生成不同類別的section,包含不同的initcall函數,如下所示:
.initcallearly.init 0000000000000008 __initcall_trace_init_flags_sys_exitearly
.initcall0.init 0000000000000008 __initcall_ipc_ns_init0
.initcall1.init 0000000000000008 __initcall_map_entry_trampoline1
.initcall2.init 0000000000000008 __initcall_bdi_class_init2
.initcall3.init 0000000000000008 __initcall_dma_bus_init3
.initcall4.init 0000000000000008 __initcall_fbmem_init4
.initcall5.init 0000000000000008 __initcall_chr_dev_init5
.initcall6.init 0000000000000008 __initcall_hwrng_modinit6
.initcall7.init 0000000000000008 __initcall_deferred_probe_initcall7
.initcallrootfs.init 0000000000000008 __initcall_populate_rootfsrootfs
同一類的initcall執行順序由編譯順序決定,不同類的initcall執行順序在init/main.c中定義,如下所示:
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
在include/asm-generic/vmlinux.lds.h
中將xxx_start和.initcall*.init
鏈接到了一起,do_initcalls()遍歷不同ID的initcall時,基于xxx_start找到相對應的.initcall entry,之后遍歷各個initcalls。
#define INIT_CALLS_LEVEL(level) \\
VMLINUX_SYMBOL(__initcall##level##_start) = .; \\
*(.initcall##level##.init) \\
*(.initcall##level##s.init) \\
#define INIT_CALLS \\
VMLINUX_SYMBOL(__initcall_start) = .; \\
*(.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) = .;
在arch/arm64/kernel/vmlinux.lds
中可以看到initcall的符號排布如下圖所示,基于*_start
可以定位到各個initcall函數所對應的符號。
initcall符號表排布
基于以上分析,整理出initcalls的完整執行流程如下:
initcall完整執行流程
5. 調試方法
你可能會遇到kernel啟動時間特別長,而在啟動過程中會加載很多的initcalls,此時,該如何下手呢?
5.1 initcall_debug
CMDLINE中增加initcall_debug選項
console=ttyS0,115200...initcall_debug
打開CMDLINE選項
結果:
[root@rk3399:/]# dmesg | grep initcall
[ 0.000000] Kernel command line: initcall_debug storagemedia=emmc androidboot.storagemedia=emmc androidboot.mode=normal androidboot.slot_suffix= androidboot.serialno=d3143e5cd395b593 rw rootwait earlycon=uart8250,mmio32,0xff1a0000 swiotlb=1 console=ttyFIQ0 root=PARTUUID=614e0000-0000 rootfstype=ext4 coherent_pool=1m
[ 0.126902] initcall trace_init_flags_sys_exit+0x0/0x1c returned 0 after 0 usecs
......
[ 0.227475] initcall rockchip_grf_init+0x0/0x12c returned 0 after 976 usecs
[ 0.227515] initcall rockchip_pm_domain_drv_register+0x0/0x20 returned 0 after 0 usecs
......
[ 10.106112] initcall hci_uart_init+0x0/0x1000 [hci_uart_aw] returned 0 after 2840 usecs
[root@rk3399:/]#
雖然initcall_debug是一個不錯的調試手段,可以用來檢測各initcall的執行時間。然而,當內核打印級別設置的不合適時,這些調試日志會直接打印在控制臺上,并且和其他日志信息混雜到了一起,便會顯得雜亂無章。
5.2 ftrace
如果是2018年以后的內核(4.16.0-rc4
),則可以基于ftrace分析initcall的執行情況。
author Steven Rostedt (VMware) < rostedt@goodmis.org > 2018-03-23 10:18:03 -0400
committer Steven Rostedt (VMware) < rostedt@goodmis.org > 2018-04-06 08:56:54 -0400
commit 4ee7c60de83ac01fa4c33c55937357601631e8ad (patch)
---
init, tracing: Add initcall trace events
Being able to trace the start and stop of initcalls is useful to see where
the timings are an issue. There is already an "initcall_debug" parameter,
but that can cause a large overhead itself, as the printing of the
information may take longer than the initcall functions.
Adding in a start and finish trace event around the initcall functions, as
well as a trace event that records the level of the initcalls, one can get a
much finer measurement of the times and interactions of the initcalls
themselves, as trace events are much lighter than printk()s.
打開trace相關功能,CMDLINE中增加trace選項
console=ttyS0,...trace_event=initcall:initcall_level,initcall:initcall_start,initcall:initcall_finish
結果:
# mount -t debugfs nodev /sys/kernel/debug
# cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 1090/1090 #P:4
#
# _-----= > irqs-off
# / _----= > need-resched
# | / _---= > hardirq/softirq
# || / _--= > preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
-0 [000] .... 0.000125: initcall_level: level=console
-0 [000] .... 0.000136: initcall_start: func=con_init+0x0/0x220
-0 [000] .... 0.000232: initcall_finish: func=con_init+0x0/0x220 ret=0
-0 [000] .... 0.000235: initcall_start: func=univ8250_console_init+0x0/0x3c
-0 [000] .... 0.000246: initcall_finish: func=univ8250_console_init+0x0/0x3c ret=0
swapper/0-1 [000] .... 0.002016: initcall_level: level=early
swapper/0-1 [000] .... 0.002026: initcall_start: func=trace_init_flags_sys_exit+0x0/0x24
...
[...]
-
Linux系統
+關注
關注
4文章
593瀏覽量
27392 -
LINUX內核
+關注
關注
1文章
316瀏覽量
21644 -
GNU
+關注
關注
0文章
143瀏覽量
17492
發布評論請先 登錄
相關推薦
評論