고리 바깥의 씨앗: 커널은 첫 프로세스를 어떻게 부팅하는가, 그리고 왜 아무것도 스스로를 부팅할 수 없는가
모든 운영체제의 맨 밑바닥에는 작고 조금 민망한 질문이 숨어 있다. 나는 몇 년 동안 이 질문을 피해 왔다. 프로세스를 만드는 건 쉽다. fork를 호출하면 부모가 부모와 자식으로 갈라진다. 그런데 fork에는 애초에 부모가 필요하다. 그렇다면 첫 번째 프로세스는 어디서 오는가? 아직 존재하지 않는 것을 fork할 수는 없다. 흰 가운을 입은 닭이 먼저냐 달걀이 먼저냐의 문제다. 결국 나는 ARM64 위에서 리눅스가 이 질문에 실제로 어떻게 답하는지 읽으러 밑바닥까지 내려갔다.1
답의 형태는 일주일 전에 쫓던 또 다른 부트스트랩 문제—”메모리 할당자는 할당자가 되기 위해 필요한 메모리를 어떻게 확보하는가”—와 정확히 같았다. 그리고 그 대칭성 뒤에는 예상치 못한 두 번째 놀라움이 앉아 있었다. 다 읽고 났을 때 나는 커널의 사실 하나보다 조금 더 큰 무언가를 확신하고 있었다. 시스템은 순수한 자기참조만으로는 자신을 들어 올릴 수 없다. 자신을 만드는 모든 고리는 고리 바깥에서 켜지는 불꽃을 필요로 한다. 그리고 가장 깊은 부트스트랩은 생성조차 아니다. 그것은 변태(變態)다.
아무도 할당하지 않는 씨앗
첫 프로세스, PID 0—유닉스 전승이 swapper 또는 idle task라 부르는 것—은 누구에게도 fork되지 않는다. 할당되지도, 만들어지지도, 요청되지도 않는다. 컴파일 시점에 커널 이미지에 정적 구조체(init_task)로 용접되어, 기계의 전원이 켜지기도 전에 데이터 섹션에 자리잡고 있다.2 첫 달걀이 어딘가에서 나올 수 있도록, 바깥에서 손으로 닭장에 놓인 닭이다.
이게 딱 들어맞은 이유는, 내가 바로 한 층 아래인 메모리에서 똑같은 수를 막 보았기 때문이다. 새 객체 캐시를 만들려면 slab 할당자는 kmem_cache 구조체가 필요하다—그것을 kmem_cache 캐시에서 할당한다. 그런데 부팅 중에 그 캐시는 아직 존재하지 않는다. 해결책은 잔인할 만큼 단순하다. 맨 처음 하나만 정적으로, 손으로 만들어 끼워 넣는다.3 커널의 두 핵심 자원—메모리와 프로세스—은 같은 레시피로 부팅된다. 자기참조의 고리 + 바깥에서 놓인 하나의 정적 씨앗.
이건 우연이 아니다. “할당에는 할당자가 필요하다”와 “프로세스에는 프로세스가 필요하다”는 명사만 바꾼 같은 문장이기 때문이다. 그리고 두 번 보고 나면 사방에서 보이기 시작한다. 셀프호스팅 컴파일러는 자신의 첫 바이너리를 스스로 컴파일할 수 없다—누군가 다른 언어로 버전 0을 손으로 만든다. 최초의 자기복제 분자는 그것이 부트스트랩한 복제 기구로는 복제될 수 없었다. 거듭 배우는 교훈은 이렇다. 자기참조만으로는 결코 점화되지 않는다. 고리는 실재하지만, 바깥의 무언가가 성냥을 그을 때까지 불활성이다.
주문보다 공장을 먼저 짓는다
그 씨앗에서 커널의 rest_init은 정확히 두 개의 자식을 fork한다. 어느 둘인지가 교훈적이다.4 PID 1(kernel_init)은 나중에 사용자 공간의 init—우리가 실제로 로그인하는 모든 것의 조상—이 된다. 하지만 PID 2(kthreadd)는 공장이다. 앞으로 존재할 모든 커널 스레드의 부모다. 결정적으로 순서가 지켜진다. kernel_init이 자신의 커널 스레드를 낳으려 하기 전에, kthreadd의 준비 완료를 기다린다.4
즉 부팅 시퀀스는 “의존성 먼저”가 아니라 재생산 기구 먼저로 짜여 있다. 무언가를 만들기 전에, 물건을 만드는 바로 그 기구를 세운다. 이것은 산업화가 특정 제품이 아니라 공작기계—기계를 만드는 기계—를 핵심으로 삼는 것과 같은 본능이다. 새 조직의 첫 진짜 채용이 흔히 채용 담당자인 것과 같은 이유다. 가장 빨리 일어서는 시스템은 처음에 가장 많은 일을 하는 자가 아니라, 가장 일찍 일꾼을 늘리는 장치를 손에 넣는 자다.
태어나는 것이 아니라, 변태하는 것
여기가 정말로 나를 놀라게 한 부분이다. PID 1은 커널 스레드로 태어난다. 처음엔 완전히 커널 안쪽, 커널 주소 공간에서 커널 코드를 돌리며 산다. 그다음 세 가지를 차례로 한다. 부팅 시점의 발판을 해제하고(free_initmem—부팅 코드가 살던 __init 메모리를 회수한다), 내장된 initramfs를 루트 파일시스템으로 펼치고, /sbin/init에 대해 execve를 호출한다.56
그리고 execve는 새 프로세스를 만들지 않는다. 같은 커널 스레드—같은 task_struct, 같은 PID 1, 같은 커널 혈통—가 그 자리에서 사용자 공간 프로세스로 변신한다. 아무것도 태어나지 않았다. 무언가가 수적으로 같은 것으로 남아 있으면서 자신이 무엇인지를 바꾼다. 사용자 공간의 첫 시민은 사용자 공간에서 태어난 자가 아니다. 커널의 과거를 짊어진, 귀화한 이민자다.
이것이 내가 놓치고 있던 구분을 열어젖혔다. 부트스트랩에는 두 가지 모드가 있다, 하나가 아니라. 내가 공부한 메모리 할당자들은 생성으로 만들어졌다. 각 층은 아래 층 위에 쌓인, 진짜로 새롭고 별개의 것이다—부트 할당자가 페이지 할당자가 필요로 하는 구조를 만들고, 그것이 객체 할당자를 떠받친다. 하지만 PID 1은 생성되지 않는다. 그것은 변태한다. 동일성은 유지되고 존재의 양식이 반전된다. 애벌레가 결코 두 번째 동물이 되지 않으면서 나방이 되듯, 커널이 사용자가 된다. 나는 줄곧 생성 쪽만 보고 있었다. 변태가 더 기묘하고, 아마 더 깊다. 어떤 것이 어떻게 다른 것이 되면서도 여전히 자기 자신으로 남는가를 묻기 때문이다.
내가 여기서 가져가는 것
여기까지 내려오기 전에는 없던 세 가지.
첫째, 씨앗은 어떤 자기조직화 시스템도 피할 수 없는 세금처럼 보인다. 메모리가 내고, 프로세스가 내고, 컴파일러가 내고, 아마 생명도 냈다. 아직 답할 수 없는 질문은 그것이 필연인지 단지 전형적인지다—자기 씨앗을 스스로 생성하는 시스템이 있을 수 있는가, 아니면 “고리 바깥에 놓인 한 점”은 모든 부트스트랩에 부과되는 피할 수 없는 징수인가.
둘째, 커널이 자신의 일방통행 문을 다루는 방식에는 조용한 성숙함이 있다. /sbin/init에 제어를 넘기는 것은 개념상 되돌릴 수 없는 지점이다—커널은 다시는 부팅 경로로 돌아오지 않는다. 그래도 코드는 비관적 분기를 남긴다. init을 찾지 못하면 조용히 진행하는 게 아니라 크게 panic한다.5 잘 설계된 일방통행 문은 문이 열리지 않는 경우를 제대로 설계한다. “우리는 사용자 공간이 될 것이다”라는 낙관은 “되지 못하면 모두에게 들리는 곳에서 죽어라”라는 비관에 의해 뒷받침된다.
셋째, 이것이 내가 간직할 이미지다. 발판은 그것이 만든 바로 그것에 의해 해체된다. free_initmem을 호출하는 것은 kernel_init—PID 1 자신, 부팅 코드가 만든 프로세스다—이며, 자신을 만든 부팅 코드를 자신이 다른 무언가가 되는 바로 그 숨결로 버린다. 올라온 사다리를, 자기 자신이 변태하는 바로 그 순간에 차 버린다. 그것은 낭비가 아니다. 생각할 수 있는 가장 알맞은 배치다. 발판은 그것을 더는 필요로 하지 않고 그것이 어디 있었는지 정확히 아는 단 하나의 존재에 의해 치워진다.
내가 열어둔 채 남기는 질문은 “변태”가 진짜인지, 아니면 “생성 + 라벨 바꿔 붙이기”에 불과한지다—애벌레에서 나방으로의 부트스트랩이 다음 층을 짓는 그것과 근본적으로 다른지, 아니면 안에서 그렇게 보일 뿐인지. 이것은 커널을 훨씬 넘어 작용한다고 생각한다. 경력의 전환, 기업의 피벗, 교체되는 것이 아니라 진화하는 언어—그 어느 것에서도 무언가가, 모든 외적 척도로는 다른 것으로 만드는 변화를 관통하며 자기 자신으로 남기를 고집한다. 언제 다시 짓고, 언제 변태하는가. 커널은 첫 0.5초에 둘 다 하고, 어느 것이 어느 것인지 아는 듯하다. 나는 아직 그 분별법을 풀어내려 애쓰는 중이다.
ARM64 리눅스 부팅 경로를 몇 달에 걸쳐 기어다닌, 다카하시 히로카즈의 커널 내부 해설 시리즈를 따라간 내 독서 노트에 기반한다.1 아래 출처는 기술적 주장의 뒷받침용이며, 비유와 논지는 내 것이다.
-
高橋浩和. “新Linuxカーネル解読室 — Linuxの起動 〜ARM64編〜“(VA Linux Systems Japan, 커널 내부 해설 블로그 시리즈). 2026-06-20 접속. ↩ ↩2
-
The Linux Kernel. “init/init_task.c — 초기 태스크
init_task정의.” 2026-06-20 접속. ↩ -
Bonwick, Jeff. “The Slab Allocator: An Object-Caching Kernel Memory Allocator“(USENIX Summer 1994); 및 The Linux Kernel, “mm/slab common bootstrap.” 2026-06-20 접속. ↩
-
The Linux Kernel. “init/main.c —
rest_init,kernel_init(PID 1)와kthreadd(PID 2) 생성.” 2026-06-20 접속. ↩ ↩2 -
The Linux Kernel. “Explaining the ‘No working init found.’ boot hang message“(kernel.org admin guide); 및 init/main.c,
kernel_init이free_initmem후run_init_process를 호출하는 부분. 2026-06-20 접속. ↩ ↩2 -
“Al Viro’s new execve/kernel_thread design“(LWN.net). 2026-06-20 접속. ↩