← Kernel Series Linux 6.18.37 LTS init/main.c

Linux 6.18.37 LTS · init/main.c · Research Note

start_kernel() 연구 노트

최신 LTS 기준: kernel.org longterm 6.18.37 기준으로 파일 좌표와 링크를 맞춘 문C식 독해 노트입니다. head.S가 MMU와 스택을 준비한 뒤 넘겨주는 C entry입니다. 여기서 arch setup, command line, memory, scheduler, IRQ, time, VFS, process, initcall의 큰 줄기가 한 번씩 등장합니다. 박사급으로 읽을 때는 호출 목록보다 초기화 partial order, object lifetime, IRQ enable boundary, initcall dependency graph를 중심에 둡니다.

연구 관점

start_kernel()은 커널 초기화 함수들의 단순한 나열이 아니라, subsystem 간 의존성을 선형화한 schedule입니다. 이 함수의 연구 대상은 “어떤 초기화가 어떤 전제 조건을 만들고, 그 전제 조건을 어떤 다음 subsystem이 소비하는가”입니다.

특히 중요한 경계는 setup_arch(), mm_core_init(), sched_init(), local_irq_enable(), arch_call_rest_init()입니다. 이 경계들은 단순 milestone이 아니라, 사용할 수 있는 allocator, interrupt context, scheduler state, RCU state, init memory lifetime이 바뀌는 지점입니다.

초기화 지도

start_kernel()을 문C식으로 읽으려면 긴 호출 목록을 먼저 외우지 말고, 각 호출이 어떤 subsystem의 사용 가능 조건을 만드는지부터 그려야 합니다. 아래 그림에서 왼쪽은 head.S가 넘긴 조건이고, 오른쪽으로 갈수록 일반 커널 런타임에 가까워집니다.

그림 1. head.S 이후 start_kernel()의 큰 경계
head.SMMU, stack, BSS, FDT pointer
early CIRQ off, boot CPU, command line
setup_arch()memblock, DT/FDT, paging, CPU feature
generic coremm, scheduler, irq, time, VFS
rest_init()pid 1, kthreadd, idle handoff

이 그림의 핵심은 “호출 순서”가 아니라 “전제 조건의 생산과 소비”입니다. 예를 들어 mm_core_init() 이후와 이전의 allocation 모델은 같은 C 함수 호출처럼 보여도 의미가 다릅니다.

경계생기는 전제 조건다음 소비자
setup_arch()architecture-specific memory map, FDT, CPU feature, paging statesetup_command_line(), mm_core_init(), driver init
mm_core_init()page allocator, slab/vmalloc 초기 기반scheduler, VFS, driver core, initcall
sched_init()runqueue, idle task, scheduler class 초기화kthread 생성, wakeup, rest_init
local_irq_enable()timer/interrupt가 system progress에 참여 가능timekeeping, softirq, scheduler tick
arch_call_rest_init()boot CPU가 idle로 넘어갈 준비pid 1 kernel_init, kthreadd

상태 전이

start_kernel()의 위험한 부분은 “아직 안 되는 일을 되는 것처럼 부르는” 순간입니다. 그래서 아래 상태표처럼 현재 context에서 sleep, allocation, interrupt, init memory 참조가 가능한지를 같이 적어 두면 버그 위치가 훨씬 빨리 보입니다.

상태의미깨지는 지점
early boot, IRQ offboot CPU 하나가 대부분의 진행을 만들고, interrupt handler는 일반적으로 실행되지 않습니다.timer interrupt나 softirq가 이미 돈다고 가정하는 코드
memblock 중심physical memory는 interval set으로 예약/할당되고, page allocator는 아직 완전한 기준이 아닙니다.일반 GFP allocation 실패 경로처럼 읽는 해석
scheduler preparedrunqueue와 idle task가 준비되지만, userspace task가 아직 system progress를 만들지는 않습니다.kthread wakeup, completion, RCU scheduler state의 시점 착각
IRQ/time enabledclockevent, timer, interrupt가 커널 진행에 참여합니다.IRQ enable 전후 latency를 같은 기준으로 비교하는 계측
init handoffpid 1과 kthreadd가 살아 있고 boot CPU는 idle 역할로 전환됩니다.__init lifetime과 initcall dependency를 무시한 late reference
start_kernel correctness = partial order + lifetime boundary + IRQ enable boundary + initcall dependency

경계별 실패 케이스

start_kernel()은 커널 전체가 아직 완성되지 않은 상태에서 커널 전체를 조립합니다. 그래서 여기서의 버그는 보통 한 함수 내부의 계산 오류보다, 어떤 subsystem이 준비되기 전에 다른 subsystem이 그 상태를 소비하는 ordering violation으로 나타납니다.

그림 2. start_kernel()에서 특히 위험한 소비-before-produce 경계
producersetup_arch, mm_core_init, sched_init
published statememblock, page allocator, rq, irq desc
consumerdriver init, initcall, kthread, timer
failure symptomsilent hang, panic, deferred probe, bad IRQ

이 그림은 단순 순서도가 아니라 검증용 체크리스트입니다. 오른쪽 증상이 보이면 왼쪽 producer가 정말 필요한 상태를 만들었는지 역추적합니다.

경계확인할 생산물실패 증상검증 방법
setup_arch()FDT, memblock, initrd, CPU feature, paging 전제early panic, 잘못된 memory reservation, DT 기반 device 미생성earlycon, memblock=debug, FDT dump
mm_core_init()page allocator, slab, vmalloc로 넘어갈 최소 기반초기 kmalloc 실패, page flag 이상, boot 중 OOM처럼 보이는 panicpage allocator trace, slub_debug, page_owner
sched_init()runqueue, idle task, class chain, scheduler clockkthread 생성 이후 hang, wakeup 손실, idle 진입 전 lockupsched_switch, /proc/sched_debug, function graph
local_irq_enable()interrupt handler가 실행될 수 있는 boundaryIRQ flood, timer interrupt 미동작, softirq backlog/proc/interrupts, irq tracepoint, timerlat
do_basic_setup()initcall graph, driver core, bus/device registrationprobe 미호출, deferred probe 누적, rootfs device 없음initcall_debug, devices_deferred, ftrace really_probe
주의

start_kernel()의 줄 순서를 그대로 subsystem readiness로 읽으면 안 됩니다. 함수가 호출됐다는 사실과 그 subsystem이 외부 callback을 받아도 되는 상태는 다릅니다.

주의

__init code/data는 “부팅 때 쓴다”가 아니라 “부팅 후 버려질 수 있다”는 lifetime annotation입니다. callback, workqueue, function pointer에 남기면 나중에 터집니다.

주의

IRQ enable 전후 로그를 같은 시간축으로 비교하면 해석이 틀어집니다. IRQ가 켜진 뒤에는 timer, softirq, driver interrupt가 boot CPU의 선형 흐름을 끊을 수 있습니다.

불변조건

  • local_irq_enable() 이전에는 timer interrupt와 일반 IRQ handler가 실행된다는 전제를 두면 안 됩니다.
  • mm_core_init() 이전과 이후의 allocation model은 다릅니다. early bootmem/memblock 의존 코드를 일반 allocator 코드처럼 읽으면 안 됩니다.
  • sched_init() 이전에는 scheduler 자료구조가 완전하지 않으므로 sleep, wakeup, kthread 생성의 의미가 제한됩니다.
  • free_initmem() 이후에는 __init text/data를 참조하면 lifetime violation입니다.
  • rest_init() 이후 boot CPU는 idle thread가 되고, pid 1과 kthreadd가 system progress를 이어받습니다.

계측 / 검증

부팅 순서 검증 포인트

Instrumentation:
initcall_debug
earlycon / keep_bootcon
ftrace=function_graph
trace_event=initcall:*,sched:*,irq:*
memblock=debug
rcupdate.rcu_cpu_stall_timeout=...

설명: 박사급 분석에서는 “어디까지 실행됐다”보다 “어떤 invariant가 성립하기 전에 누가 그 invariant를 사용했는가”를 확인해야 합니다. initcall latency, IRQ enable 시점, memblock reservation, scheduler activation, RCU scheduler start를 같은 timeline에 놓고 봅니다.

원본 코드

이 글은 Linux 6.18.37 LTS의 init/main.c를 기준으로 씁니다. 아래 raw 파일은 홈페이지 폴더에 같이 둔 로컬 복사본입니다.

init/main.c · Linux 6.18.37 LTS 로컬 원본 열기 →

head.S 다음에 무슨 일이 생기나

head.S__primary_switched는 stack, vector, BSS, FDT pointer를 정리하고 마지막에 start_kernel()을 호출합니다. 여기부터는 어셈블리 부트 코드가 아니라 C 코드의 큰 초기화 순서입니다.

이 함수는 커널 전체 목차에 가깝습니다. 한 줄씩 따라가면 memory allocator, scheduler, interrupt, timekeeping, VFS, process, security, network namespace, driver init, initcall이 모두 한 번씩 나옵니다. 그래서 이 글은 모든 세부 구현을 끝까지 파지 않고, 다음 토픽으로 갈 입구를 표시하는 데 초점을 둡니다.

870-899: start_kernel 진입부

L869-L899: boot CPU와 arch setup 전 준비

원본코드:
asmlinkage __visible __init __no_sanitize_address __noreturn __no_stack_protector
void start_kernel(void)
{
	char *command_line;
	char *after_dashes;

	set_task_stack_end_magic(&init_task);
	smp_setup_processor_id();
	debug_objects_early_init();
	init_vmlinux_build_id();

	cgroup_init_early();

	local_irq_disable();
	early_boot_irqs_disabled = true;

	/*
	 * Interrupts are still disabled. Do necessary setups, then
	 * enable them.
	 */
	boot_cpu_init();
	page_address_init();
	pr_notice("%s", linux_banner);
	early_security_init();
	setup_arch(&command_line);
	setup_boot_config();
	setup_command_line(command_line);
	setup_nr_cpu_ids();
	setup_per_cpu_areas();
	smp_prepare_boot_cpu();	/* arch-specific boot-cpu hooks */
	boot_cpu_hotplug_init();

설명: C 코드에 들어왔지만 아직 일반 커널 런타임은 아닙니다. IRQ는 꺼져 있고, allocator와 scheduler도 완전히 준비되지 않았습니다.

더 읽기: 함수 attribute가 먼저 눈에 들어옵니다. __init은 부팅 뒤 버릴 수 있는 init section에 들어간다는 뜻이고, __noreturn은 이 함수가 호출자에게 돌아가지 않는다는 뜻입니다. head.S에서 bl start_kernel 뒤에 ASM_BUG()가 있었던 이유와 맞습니다.

set_task_stack_end_magic(&init_task)는 init task stack 끝에 overflow 감지용 magic을 심습니다. 아직 process가 여러 개 있는 상태는 아니지만, 이미 init_task를 current의 기준으로 삼고 있습니다.

smp_setup_processor_id()boot_cpu_init()은 boot CPU identity를 커널 전역 상태에 반영합니다. 나중에 scheduler, per-cpu area, SMP bring-up에서 “현재 CPU가 누구냐”를 일관되게 봐야 하기 때문입니다.

setup_arch(&command_line)는 architecture-specific 초기화의 큰 문입니다. arm64라면 DTB 해석, memory region, memblock, paging, CPU feature, initrd, command line 포인터 같은 것들이 여기서 정리됩니다. 이 함수는 별도 글로 빼서 길게 봐야 합니다.

901-918: kernel command line 처리

L901-L918: early param과 일반 param 분리

원본코드:
	pr_notice("Kernel command line: %s\n", saved_command_line);
	/* parameters may set static keys */
	jump_label_init();
	parse_early_param();
	after_dashes = parse_args("Booting kernel",
				  static_command_line, __start___param,
				  __stop___param - __start___param,
				  -1, -1, NULL, &unknown_bootoption);
	print_unknown_bootoptions();
	if (!IS_ERR_OR_NULL(after_dashes))
		parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
			   NULL, set_init_arg);
	if (extra_init_args)
		parse_args("Setting extra init args", extra_init_args,
			   NULL, 0, -1, -1, NULL, set_init_arg);

	/* Architectural and non-timekeeping rng init, before allocator init */
	random_init_early(command_line);

설명: 커널 command line을 early param, kernel param, init process 인자로 나눠 처리합니다.

더 읽기: parse_early_param()early_param()으로 등록된 옵션을 먼저 처리합니다. 대표적으로 earlycon, loglevel, debug처럼 아주 이른 시점에 의미가 있는 옵션들이 이쪽입니다.

parse_args("Booting kernel", ...)__param section에 등록된 일반 kernel parameter를 훑습니다. 커널 코드의 여러 곳에서 module_param, core_param, __setup 계열로 등록한 값들이 여기서 걸립니다.

command line의 -- 뒤쪽은 커널이 아니라 userspace init에 넘길 인자로 취급됩니다. 그래서 after_dashes를 다시 set_init_arg로 parse합니다. bring-up 중 init argument가 이상하게 들어간다면 이 경계를 확인해야 합니다.

920-940: early memory와 scheduler

L920-L940: page allocator 전후의 큰 초기화

원본코드:
	/*
	 * These use large bootmem allocations and must precede
	 * initalization of page allocator
	 */
	setup_log_buf(0);
	vfs_caches_init_early();
	sort_main_extable();
	trap_init();
	mm_core_init();
	poking_init();
	ftrace_init();

	/* trace_printk can be enabled here */
	early_trace_init();

	/*
	 * Set up the scheduler prior starting any interrupts (such as the
	 * timer interrupt). Full topology setup happens at smp_init()
	 * time - but meanwhile we still have a functioning scheduler.
	 */
	sched_init();

설명: 메모리와 scheduler가 여기서 처음 큰 형태를 갖춥니다. 이 이후부터 커널은 훨씬 일반적인 실행 환경에 가까워집니다.

더 읽기: setup_log_bufvfs_caches_init_early는 page allocator가 완전히 서기 전에 큰 bootmem allocation을 써야 해서 앞에 옵니다. 순서가 그냥 취향이 아니라 allocator 준비 단계와 직접 연결됩니다.

mm_core_init()은 메모리 관리의 큰 전환점입니다. memblock 중심의 early allocation에서 buddy/slab/vmalloc 쪽으로 넘어가기 위한 기반이 잡힙니다. 이 함수는 memory 파트의 별도 글로 분리해야 합니다.

sched_init()은 timer interrupt를 켜기 전에 반드시 필요합니다. timer interrupt가 들어오면 scheduler tick과 time accounting이 동작할 수 있어야 하기 때문입니다. full topology setup은 나중에 smp_init()에서 하지만, 일단 단일 boot CPU 기준 scheduler는 여기서 준비됩니다.

945-980: IRQ, timer, timekeeping

L945-L980: interrupt와 time 기반 시설

원본코드:
	radix_tree_init();
	maple_tree_init();

	housekeeping_init();
	workqueue_init_early();

	rcu_init();
	trace_init();

	if (initcall_debug)
		initcall_debug_enable();

	context_tracking_init();
	/* init some links before init_ISA_irqs() */
	early_irq_init();
	init_IRQ();
	tick_init();
	rcu_init_nohz();
	init_timers();
	srcu_init();
	hrtimers_init();
	softirq_init();
	timekeeping_init();
	time_init();

설명: IRQ와 시간 기반 subsystem이 여기서 순서대로 올라옵니다. 이 순서가 맞아야 뒤에서 IRQ를 켤 수 있습니다.

더 읽기: rcu_init()이 비교적 이른 곳에 있습니다. 커널의 수많은 자료구조가 RCU read-side critical section을 전제로 움직이기 때문에, 나중에 driver와 network가 올라오기 전에 준비되어야 합니다.

early_irq_init()은 generic IRQ descriptor 기반을 만들고, init_IRQ()는 arch interrupt controller 초기화로 들어갑니다. arm64에서는 결국 GIC irqchip, irq domain, percpu interrupt 같은 주제로 이어집니다.

tick_init(), init_timers(), hrtimers_init(), timekeeping_init(), time_init()은 시간의 여러 층입니다. jiffies 기반 timer, high resolution timer, clocksource/clockevent, arch timer가 서로 맞물립니다.

softirq_init()은 bottom half 기반입니다. network RX/TX, timer, tasklet, RCU callback 등이 softirq를 타므로 network 파트로 가기 전에 여기서 한 번 끊어 읽는 게 좋습니다.

981-1068: IRQ enable 이후 core subsystem

L981-L1068: console, process, VFS, namespace, security, net

원본코드:
	random_init();
	kfence_init();
	boot_init_stack_canary();

	perf_event_init();
	profile_init();
	call_function_init();
	WARN(!irqs_disabled(), "Interrupts were enabled early\n");

	early_boot_irqs_disabled = false;
	local_irq_enable();

	kmem_cache_init_late();
	console_init();

	lockdep_init();
	locking_selftest();

	setup_per_cpu_pageset();
	numa_policy_init();
	acpi_early_init();
	if (late_time_init)
		late_time_init();
	sched_clock_init();
	calibrate_delay();

	arch_cpu_finalize_init();

	pid_idr_init();
	anon_vma_init();
	thread_stack_cache_init();
	cred_init();
	fork_init();
	proc_caches_init();
	uts_ns_init();
	key_init();
	security_init();
	dbg_late_init();
	net_ns_init();
	vfs_caches_init();
	pagecache_init();
	signals_init();
	seq_file_init();
	proc_root_init();
	nsfs_init();
	cpuset_init();
	cgroup_init();
	taskstats_init_early();
	delayacct_init();

	acpi_subsystem_init();
	arch_post_acpi_subsys_init();
	kcsan_init();

	/* Do the rest non-__init'ed, we're now alive */
	arch_call_rest_init();

설명: IRQ를 켠 뒤부터는 subsystem 초기화가 넓게 펼쳐집니다. 여기서 커널 전체 토픽이 거의 다 갈라집니다.

더 읽기: local_irq_enable()은 큰 경계입니다. 그 전까지는 interrupt가 들어오면 안 되는 초기화가 많았고, 이 뒤부터는 timer, IRQ, lockdep selftest 같은 코드가 실제 interrupt enabled 상태를 전제로 움직일 수 있습니다.

console_init()은 일부러 이른 시점에 호출됩니다. PCI나 driver core가 모두 올라온 뒤가 아니지만, 이후 문제가 생겼을 때 로그를 뽑아야 하므로 early console에서 정식 console로 넘어가는 길을 엽니다.

fork_init(), cred_init(), signals_init()은 process 파트의 입구입니다. pid allocator, task_struct 관련 cache, credential, signal 자료구조가 준비됩니다.

vfs_caches_init(), pagecache_init(), proc_root_init()은 filesystem 파트의 입구입니다. dentry/inode/file cache, page cache, procfs root가 여기서 올라옵니다.

net_ns_init()은 network namespace의 시작점입니다. 네트워크 장치와 protocol stack은 뒤 initcall에서 더 올라오지만, namespace 기반은 여기서 먼저 잡힙니다.

680-727: rest_init(), pid 1과 kthreadd

L680-L727: init thread, kthreadd, idle thread

원본코드:
noinline void __ref __noreturn rest_init(void)
{
	struct task_struct *tsk;
	int pid;

	rcu_scheduler_starting();
	pid = user_mode_thread(kernel_init, NULL, CLONE_FS);

	rcu_read_lock();
	tsk = find_task_by_pid_ns(pid, &init_pid_ns);
	tsk->flags |= PF_NO_SETAFFINITY;
	set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
	rcu_read_unlock();

	numa_default_policy();
	pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
	rcu_read_lock();
	kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
	rcu_read_unlock();

	system_state = SYSTEM_SCHEDULING;
	complete(&kthreadd_done);

	schedule_preempt_disabled();
	cpu_startup_entry(CPUHP_ONLINE);
}

설명: 여기서 pid 1이 될 init thread와 kernel thread를 낳는 kthreadd가 만들어집니다. boot CPU는 이후 idle loop로 들어갑니다.

더 읽기: user_mode_thread(kernel_init, ...)가 먼저 호출됩니다. init task가 pid 1을 얻어야 하기 때문입니다. 다만 이 thread가 곧바로 kthread를 만들려고 하면 kthreadd가 아직 없으므로, 뒤에서 completion으로 동기화합니다.

kernel_thread(kthreadd, ...)는 kernel thread 생성의 중심이 될 kthreadd를 만듭니다. 이후 대부분의 kthread는 직접 생성되는 것이 아니라 kthreadd를 통해 만들어집니다.

system_state = SYSTEM_SCHEDULING 이후 커널은 scheduling 가능한 상태로 넘어갑니다. boot idle thread는 schedule_preempt_disabled()를 한 번 호출하고, 마지막에는 cpu_startup_entry(CPUHP_ONLINE)로 idle loop에 들어갑니다.

1323-1330: driver_init과 initcall

L1323-L1330: do_basic_setup()

원본코드:
static void __init do_basic_setup(void)
{
	cpuset_init_smp();
	driver_init();
	init_irq_proc();
	do_ctors();
	do_initcalls();
}

설명: driver core와 initcall이 여기서 본격적으로 올라옵니다. 대부분의 driver probe는 결국 initcall 흐름을 통해 시작됩니다.

더 읽기: driver_init()은 driver core의 큰 틀을 세웁니다. bus, class, device, kobject, sysfs 같은 토픽은 이 함수에서 갈라집니다.

do_initcalls()는 커널 전역에 흩어진 initcall section을 level 순서대로 실행합니다. subsys_initcall, device_initcall, late_initcall처럼 보던 매크로들이 실제로 실행되는 곳입니다.

embedded bring-up에서는 probe가 왜 안 도는지 추적할 때 initcall level을 먼저 봐야 합니다. device tree matching이 틀린 건지, driver가 initcall에 등록되지 않은 건지, probe defer인지가 여기서 갈립니다.

1432-1500: kernel_init과 userspace init 실행

L1432-L1500: initmem 해제 후 /init 또는 /sbin/init 실행

원본코드:
wait_for_completion(&kthreadd_done);

kernel_init_freeable();
async_synchronize_full();

system_state = SYSTEM_FREEING_INITMEM;
kprobe_free_init_mem();
ftrace_free_init_mem();
kgdb_free_init_mem();
exit_boot_config();
free_initmem();
mark_readonly();
pti_finalize();

system_state = SYSTEM_RUNNING;
numa_default_policy();

rcu_end_inkernel_boot();

do_sysctl_args();

if (ramdisk_execute_command) {
	ret = run_init_process(ramdisk_execute_command);
	if (!ret)
		return 0;
	pr_err("Failed to execute %s (error %d)\n",
	       ramdisk_execute_command, ret);
}

if (execute_command) {
	ret = run_init_process(execute_command);
	if (!ret)
		return 0;
	panic("Requested init %s failed (error %d).",
	      execute_command, ret);
}

if (CONFIG_DEFAULT_INIT[0] != '\0') {
	ret = run_init_process(CONFIG_DEFAULT_INIT);
	if (ret)
		pr_err("Default init %s failed (error %d)\n",
		       CONFIG_DEFAULT_INIT, ret);
	else
		return 0;
}

if (!try_to_run_init_process("/sbin/init") ||
    !try_to_run_init_process("/etc/init") ||
    !try_to_run_init_process("/bin/init") ||
    !try_to_run_init_process("/bin/sh"))
	return 0;

panic("No working init found.  Try passing init= option to kernel. "
      "See Linux Documentation/admin-guide/init.rst for guidance.");

설명: 커널 초기화가 끝나면 init section을 해제하고, 마지막으로 userspace init을 실행합니다.

더 읽기: wait_for_completion(&kthreadd_done)은 rest_init에서 만든 kthreadd가 준비될 때까지 기다립니다. init thread가 kthread를 만들 수 있으려면 kthreadd가 먼저 살아 있어야 합니다.

kernel_init_freeable() 안에서 SMP, scheduler SMP, workqueue topology, late page allocator, initcall, rootfs 준비가 진행됩니다. 이 함수는 initcall과 driver probe를 따로 다루는 글에서 다시 봐야 합니다.

free_initmem() 이후에는 __init 코드와 데이터가 해제됩니다. 그래서 init 함수 포인터를 나중에 잘못 잡고 있으면 use-after-free 성격의 문제가 됩니다.

userspace init 실행 순서는 rdinit으로 잡힌 ramdisk init, init=으로 지정한 command, CONFIG_DEFAULT_INIT, 그리고 /sbin/init, /etc/init, /bin/init, /bin/sh fallback입니다. initramfs bring-up 문제는 이 순서대로 로그를 봐야 합니다.