리눅스 커널에서 initcalls는 초기화 함수를 실행하는 메커니즘으로 커널 초기화 단계에서 시스템의 다양한 구성 요소를 초기화하는 데 사용된다. 이번 글에서는 initcalls의 동작 원리와 사용 방법에 대해 알아보고, 커널 코드 및 예제를 통해 실제로 initcalls를 구현하는 방법을 알아보도록 한다.
initcalls 동작 원리
리눅스 커널은 부팅 초기에 initcalls 메커니즘을 통해 초기화 함수를 순서대로 실행한다. initcalls는 정의된 초기화 함수(심볼)들을 특정 섹션에 위치시켜 관리한다. 아래와 같이 arch_initcall(reserve_memblock_reserved_regions)
하게 되면 reserve_memblock_reserved_regions
심볼은 arch_initcall
에 해당하는 섹션에 위치하게 된다. 이를 예로 들어 initcalls 메커니즘에서 심볼이 어디에, 어떻게 위치하게 되는지 따라가 보자.
static int __init reserve_memblock_reserved_regions(void)
{
....
}
arch_initcall(reserve_memblock_reserved_regions);
아래와 같이 aarch64 vmlinux.lds.S 에서 .init.data
부분을 보면 SECTIONS
에서 INIT_CALLS
를 확인할 수 있다.
...
SECTIONS
{
....
.init.data : {
INIT_DATA
INIT_SETUP(16)
INIT_CALLS
CON_INITCALL
INIT_RAM_FS
*(.init.rodata.* .init.bss) /* from the EFI stub */
}
.exit.data : {
ARM_EXIT_KEEP(EXIT_DATA)
}
....
INIT_CALLS
는 include/asm-generic/vmlinux.lds.h 에 다음과 같이 정의되고 있다.
#define INIT_CALLS_LEVEL(level) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \
#define INIT_CALLS \
__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) \
__initcall_end = .;
INITCALLS
는 각 INIT_CALLS_LEVEL
에 따라 각 레벨에 대당하는 __initcall##level##_start
라는 심볼을 만들고, .initcall##level##.init
섹션을 정의한다. arch_initcall(fn)
은 include/linux/init.h에 정의되어 있다.
#define ___define_initcall(fn, id, __sec) \
__ADDRESSABLE(fn) \
asm(".section \"" #__sec ".init\", \"a\" \n" \
"__initcall_" #fn #id ": \n" \
".long " #fn " - . \n" \
".previous \n");
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#define arch_initcall(fn) __define_initcall(fn, 3)
결국, arch_initcall(reserve_memblock_reserved_regions)
은 arch_initcall(fn)
으로 인해 .initcall3.init
섹션에 __initcall_reserve_memblock_reserved_regions3
이라는 심볼을 위치하게 한다. 커널의 심볼에서 확인해 보면 아래와 같이 __initcall3_star
와 __initcall4_start
사이에 __initcall_reserve_memblock_reserved_regions3
가 위치해 있음을 알 수 있다.
ffffffc011718508 t __initcall_reserve_dvr_memory2
ffffffc01171850c t __initcall_kobject_uevent_init2
ffffffc011718510 T __initcall3_start
ffffffc011718510 t __initcall_debug_traps_init3
ffffffc011718514 t __initcall_reserve_memblock_reserved_regions3
ffffffc011718518 t __initcall_aarch32_alloc_vdso_pages3
ffffffc01171851c t __initcall_vdso_init3
ffffffc011718520 t __initcall_arch_hw_breakpoint_init3
ffffffc011718524 t __initcall_realtek_boards_sysfs_init3
ffffffc011718528 t __initcall_kcmp_cookies_init3
ffffffc01171852c t __initcall_cryptomgr_init3
ffffffc011718530 t __initcall_iommu_dma_init3
ffffffc011718534 t __initcall_dmi_id_init3
ffffffc011718538 t __initcall_of_platform_default_populate_init3s
ffffffc01171853c T __initcall4_start
ffffffc01171853c t __initcall_topology_init4
ffffffc011718540 t __initcall_uid_cache_init4
initcalls의 초기화 순서
리눅스 커널은 8가지 레벨의 initcalls를 제공한다. init/main.c에 정의되어 있는 initcall_levels
을 보면 INIT_CALLS
로 인해 추가된 __initcall##level##_start
엔트리 심볼을 확인할 수 있다.
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];
static initcall_entry_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
각 initcalls 레벨에 해당하는 레별명은 아래와 같이 initcall_level_names
문자열 배열로 정의된다.
static const char *initcall_level_names[] __initdata = {
"pure",
"core",
"postcore",
"arch",
"subsys",
"fs",
"device",
"late",
};
initcalls는 레벨 순서대로 실행하게 된다. 아래는 레벨별 init을 등록하기 위한 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)
리눅스 커널은 부팅 중 reset_init()
과정에서 kernel_init()
을 호출하게 되고, do_initcalls()
호출을 통해 do_initcall_level()
을 통해 각 레벨 섹션 등록되어 있는 entry들을 do_init_call()
함수를 통해 차례대로 실행하게 된다.
static void __init do_initcall_level(int level){
initcall_entry_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,
NULL, &repair_env_string);
trace_initcall_level(initcall_level_names[level]);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels); level++) {
do_initcall_level(level);}
static void __init do_basic_setup(void)
{
...
do_initcalls();
}
...
static int __init kernel_init(void *unused)
{
...
/* Run the initcalls */
if (ramdisk_execute_command)
run_init_process(ramdisk_execute_command);
else
kernel_init_freeable();
...
}
static noinlin void __init kernel_init_freeable(void)
{
...
do_basic_setup();
...
}
module_init: 모듈 초기화
커널 내장 모듈의 module_init
은 아래와 같이 확장되어 device_initcall
과 레벨이 같게 된다.
#define module_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
결국 initcalls는 pure_initcall
부터 시작해서 late_initcall_sync
순으로 호출된다.
initcalls 사용방법
initcalls를 사용하려면 초기화 함수를 정의하고, 해당 함수를 initcalls 섹션에 등록되도록 해야 한다. __init
매크로는 컴파일러에게 해당 함수가 초기화 함수로 .init.text
섹션에 위치함을 표기한다.
/* These are for everybody (although not all archs will actually
discard it in modules) */
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
#define __initdata __section(.init.data)
#define __initconst __section(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)
module_init
매크로는 초기화 함수를 initcalls 섹션에 위치하게 하는 역할을 한다.
아래는 간단한 예제로, my_init 초기화 함수를 정의하고 initcalls 섹션에 등록한다.
#include <linux/init.h>
#include <linux/module.h>
static int __init my_init(void) {
printk("My initialization function\n");
return 0;
}
static void __exit my_exit(void) {
printk("My exit function\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");