본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

운영체제 기본/x86-64 Linux의 BIOS 부팅/PID 1 생성: 두 판 사이의 차이

~ldmsys
부팅 3장: PID 0·1·2의 분리, user_mode_thread, completion과 initcall 설명
 
BIOS 부팅·i8042 키보드 각론의 새 문서명으로 내부 링크 갱신
 
(같은 사용자의 중간 판 4개는 보이지 않습니다)
1번째 줄: 1번째 줄:
[[운영체제 기본|운영체제 기본 목차]] · [[운영체제 기본/x86-64 Linux의 부팅|부팅 목차]]
[[운영체제 기본|운영체제 기본 목차]] · [[운영체제 기본/x86-64 Linux의 BIOS 부팅|부팅 목차]]


'''시작 상태:''' 부트 태스크가 <code>start_kernel()</code>의 주요 초기화를 마치고 <code>rest_init()</code>에 도달했다.
Linux v6.12에서 부트 태스크는 <code>rest_init()</code>에 도달하면 PID 1과 <code>kthreadd</code>를 만든다. PID 1은 커널의 나머지 초기화를 맡고, 기존 부트 태스크인 PID 0은 idle 태스크로 남는다.


'''끝 상태:''' 별도로 생성된 PID 1이 <code>kernel_init()</code>에서 나머지 커널 초기화를 마치고 사용자 프로그램을 선택할 준비를 한다. 기존 PID 0은 idle 경로로 들어간다.
<span id="1._PID_0이_PID_1로_바뀌는_것은_아니다"></span>
== PID 0이 PID 1로 바뀌는 것은 아니다 ==


기준은 '''Linux v6.12'''다. 함수 호출과 그 함수를 시작점으로 하는 태스크 생성은 별개의 작업이다.
<code>rest_init()</code>은 먼저 <code>user_mode_thread(kernel_init, NULL, CLONE_FS)</code>를 호출해 PID 1을 만든다. 이어 <code>kernel_thread()</code>로 <code>kthreadd</code>(PID 2)를 만든다. 기존 부트 태스크는 스케줄러를 거쳐 <code>cpu_startup_entry()</code>의 idle 경로로 진행한다.<ref name="main">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/init/main.c?h=v6.12 v6.12 init/main.c], rest_init.</ref>


== 1. PID 0이 PID 1로 바뀌는 것은 아니다 ==
즉 <code>rest_init()</code>이 자기 스택에서 평범하게 <code>kernel_init()</code>을 호출하고 그대로 init이 되는 구조가 아니다. 새 태스크에는 별도 실행 문맥이 생긴다. 실행 상태와 자원의 구분은 [[운영체제 기본/프로세스와 스레드|프로세스와 스레드]]에서 설명한다. 이후 어떤 태스크가 CPU를 사용할지는 스케줄링에 따라 달라진다. PID 1은 새로 생겼지만 기존 PID 0이 사라진 것은 아니다.<ref name="fork">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/fork.c?h=v6.12 v6.12 kernel/fork.c], user_mode_thread, kernel_thread, kernel_clone.</ref>


<code>rest_init()</code>은 먼저 <code>user_mode_thread(kernel_init, NULL, CLONE_FS)</code>를 호출해 PID 1을 만든다. 이어 <code>kernel_thread()</code>로 <code>kthreadd</code>(PID 2)를 만든다. 기존 부트 태스크는 스케줄러를 거쳐 <code>cpu_startup_entry()</code>의 idle 경로로 진행한다.<ref name="main">[https://github.com/torvalds/linux/blob/v6.12/init/main.c v6.12 init/main.c], rest_init.</ref>
즉 <code>rest_init()</code>이 자기 스택에서 평범하게 <code>kernel_init()</code>을 호출하고 그대로 init이 되는 구조가 아니다. 새 태스크에는 별도 실행 문맥이 생긴다. 이후 어떤 태스크가 CPU를 사용할지는 스케줄링에 따라 달라진다. PID 1은 새로 생겼지만 기존 PID 0이 사라진 것은 아니다.<ref name="fork">[https://github.com/torvalds/linux/blob/v6.12/kernel/fork.c v6.12 kernel/fork.c], user_mode_thread, kernel_thread, kernel_clone.</ref>
'''동기화와 중간 처리를 생략한 태스크 분기: '''
<pre>
<pre>
PID 0: start_kernel → rest_init ─────────────→ idle
PID 0: start_kernel -> rest_init -----------------> idle
                         ├─ 새 PID 1 → kernel_init
                         |
                         └─ 새 PID 2 → kthreadd
                        +-- create PID 1 -> kernel_init
                         |
                        +-- create PID 2 -> kthreadd
</pre>
</pre>


== 2. user_mode_thread인데 왜 커널 함수를 실행하는가 ==
<span id="2._user_mode_thread인데_왜_커널_함수를_실행하는가"></span>
== user_mode_thread인데 왜 커널 함수를 실행하는가 ==


함수 이름의 user mode를 “이 호출 직후 CPU가 사용자 모드가 된다”로 읽으면 안 된다. <code>user_mode_thread()</code>는 시작 함수와 인자를 <code>kernel_clone_args</code>에 담아 <code>kernel_clone()</code>에 넘긴다. 이 경우 시작 함수는 커널 내부 함수인 <code>kernel_init()</code>이다.<ref name="fork"/>
함수 이름의 user mode를 “이 호출 직후 CPU가 사용자 모드가 된다”로 읽으면 안 된다. <code>user_mode_thread()</code>는 시작 함수와 인자를 <code>kernel_clone_args</code>에 담아 <code>kernel_clone()</code>에 넘긴다. 이 경우 시작 함수는 커널 내부 함수인 <code>kernel_init()</code>이다.<ref name="fork"/>


<code>kernel_thread()</code>와의 중요한 차이는 <code>kthread</code> 속성을 설정하지 않는다는 점이다. 계속 커널 스레드로 남을 태스크와 나중에 사용자 실행 이미지를 갖게 될 태스크의 생성 방식이 구별된다. 실제로 <code>kernel_execve()</code>는 <code>PF_KTHREAD</code>가 설정된 태스크의 호출을 거부한다. 따라서 “PID 1은 처음에 커널 코드를 실행한다”는 설명은 맞지만, 이를 곧바로 “일반 커널 스레드를 만든 뒤 exec한다”로 바꾸면 v6.12 구현과 어긋난다.<ref name="fork"/><ref>[https://github.com/torvalds/linux/blob/v6.12/fs/exec.c v6.12 fs/exec.c], kernel_execve.</ref>
<code>kernel_thread()</code>와의 중요한 차이는 <code>kthread</code> 속성을 설정하지 않는다는 점이다. 계속 커널 스레드로 남을 태스크와 나중에 사용자 실행 이미지를 갖게 될 태스크의 생성 방식이 구별된다. 실제로 <code>kernel_execve()</code>는 <code>PF_KTHREAD</code>가 설정된 태스크의 호출을 거부한다. 따라서 “PID 1은 처음에 커널 코드를 실행한다”는 설명은 맞지만, 이를 곧바로 “일반 커널 스레드를 만든 뒤 exec한다”로 바꾸면 v6.12 구현과 어긋난다.<ref name="fork"/><ref>[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/exec.c?h=v6.12 v6.12 fs/exec.c], kernel_execve.</ref>


권한 수준과 태스크 종류도 같은 말이 아니다. 사용자 프로그램을 실행할 태스크 역시 커널에 진입해 커널 코드를 실행할 수 있다. PID 1은 아직 첫 사용자 실행 이미지를 얻기 전이다. 보호 경계 자체는 [[운영체제 기본/보호|보호]]를 참고한다.
권한 수준과 태스크 종류도 같은 말이 아니다. 사용자 프로그램을 실행할 태스크 역시 커널에 진입해 커널 코드를 실행할 수 있다. PID 1은 아직 첫 사용자 실행 이미지를 얻기 전이다. 보호 경계 자체는 [[운영체제 기본/보호|보호]]를 참고한다.


== 3. 먼저 만든 PID 1을 왜 기다리게 하는가 ==
<span id="3._먼저_만든_PID_1을_왜_기다리게_하는가"></span>
== 먼저 만든 PID 1을 왜 기다리게 하는가 ==


PID 1이라는 번호를 얻도록 init을 먼저 생성하지만, init이 진행하며 사용할 커널 스레드 생성 기반인 <code>kthreadd</code>는 그다음 만들어진다. 단순히 소스에서 생성 호출을 두 줄 연속 적었다고 새 태스크들이 반드시 원하는 순서로 실행되는 것은 아니다.<ref name="main"/>
PID 1이라는 번호를 얻도록 init을 먼저 생성하지만, init이 진행하며 사용할 커널 스레드 생성 기반인 <code>kthreadd</code>는 그다음 만들어진다. 단순히 소스에서 생성 호출을 두 줄 연속 적었다고 새 태스크들이 반드시 원하는 순서로 실행되는 것은 아니다.<ref name="main"/>


이 의존 관계를 표현하는 것이 <code>kthreadd_done</code> completion이다. '''completion'''은 한쪽이 작업 완료를 알리고 다른 쪽이 그 사건을 기다리는 동기화 수단이다. <code>kernel_init()</code>은 시작하자마자 기다리고, <code>rest_init()</code>은 kthreadd 준비 후 완료를 알린다. “태스크를 먼저 만들었다”와 “필요한 기반이 준비되었으니 진행해도 된다”를 분리한 것이다.<ref name="main"/>
이 의존 관계를 표현하는 것이 <code>kthreadd_done</code> completion이다. '''completion'''은 한쪽이 작업 완료를 알리고 다른 쪽이 그 사건을 기다리는 동기화 수단이다. 조건을 기다리는 작업과 깨우기의 관계는 [[운영체제 기본/동시성|동시성]]과 연결된다. <code>kernel_init()</code>은 시작하자마자 기다리고, <code>rest_init()</code>은 kthreadd 준비 후 완료를 알린다. “태스크를 먼저 만들었다”와 “필요한 기반이 준비되었으니 진행해도 된다”를 분리한 것이다.<ref name="main"/>


== 4. PID 1에서 커널 초기화를 계속한다 ==
<span id="4._PID_1에서_커널_초기화를_계속한다"></span>
== PID 1에서 커널 초기화를 계속한다 ==


대기가 끝나면 <code>kernel_init_freeable()</code>이 SMP와 나머지 초기화를 진행한다. 여기에는 <code>do_basic_setup()</code>을 통한 initcall 실행도 포함된다.<ref name="main"/>
대기가 끝나면 <code>kernel_init_freeable()</code>이 SMP와 나머지 초기화를 진행한다. 여기에는 <code>do_basic_setup()</code>을 통한 initcall 실행도 포함된다.<ref name="main"/>


'''initcall'''은 부팅 중 실행할 초기화 함수를 정해진 단계에 등록하는 방식이다. 소스의 각 부분이 등록한 함수들을 초기화 경로가 순회하며 실행하므로, <code>start_kernel()</code> 안에 모든 드라이버의 함수 이름이 직접 나열될 필요는 없다. 예를 들어 rootfs에 initramfs를 푸는 <code>populate_rootfs()</code>도 <code>rootfs_initcall</code>로 등록되어 있다.<ref name="main"/><ref>[https://github.com/torvalds/linux/blob/v6.12/init/initramfs.c v6.12 init/initramfs.c], populate_rootfs 및 rootfs_initcall 등록.</ref>
'''initcall'''은 부팅 중 실행할 초기화 함수를 정해진 단계에 등록하는 방식이다. 소스의 각 부분이 등록한 함수들을 초기화 경로가 순회하며 실행하므로, <code>start_kernel()</code> 안에 모든 드라이버의 함수 이름이 직접 나열될 필요는 없다. 예를 들어 rootfs에 initramfs를 푸는 <code>populate_rootfs()</code>도 <code>rootfs_initcall</code>로 등록되어 있다.<ref name="main"/><ref>[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/init/initramfs.c?h=v6.12 v6.12 init/initramfs.c], populate_rootfs 및 rootfs_initcall 등록.</ref>


<code>kernel_init_freeable()</code>에서 돌아온 뒤에도 비동기 초기화의 완료를 기다린다. 그래야 아직 실행 중인 초기화 코드가 있는데 그 코드를 담은 메모리를 먼저 해제하는 일이 없다. 이후 초기화용 메모리를 정리하고, 다음 장에서 다룰 init 선택과 exec로 진행한다.<ref name="main"/>
<code>kernel_init_freeable()</code>에서 돌아온 뒤에도 비동기 초기화의 완료를 기다린다. 그래야 아직 실행 중인 초기화 코드가 있는데 그 코드를 담은 메모리를 먼저 해제하는 일이 없다. 이후 초기화용 메모리를 정리하고, 다음 장에서 다룰 init 선택과 exec로 진행한다.<ref name="main"/>


'''소스 탐색:''' [https://elixir.bootlin.com/linux/v6.12/ident/rest_init rest_init] · [https://elixir.bootlin.com/linux/v6.12/ident/user_mode_thread user_mode_thread] · [https://elixir.bootlin.com/linux/v6.12/ident/kernel_init kernel_init].
'''이전:''' [[운영체제 기본/x86-64 Linux의 BIOS 부팅/커널 초기화|커널 초기화]] · '''다음:''' [[운영체제 기본/x86-64 Linux의 BIOS 부팅/루트 파일시스템과 init 선택|루트 파일시스템과 init 선택]]
 
'''이전:''' [[운영체제 기본/x86-64 Linux의 부팅/커널 초기화|커널 초기화]] · '''다음:''' [[운영체제 기본/x86-64 Linux의 부팅/init 실행과 사용자 모드 진입|init 실행과 사용자 모드 진입]]


== 참고 문헌 ==
<references />
<references />

2026년 10월 31일 (토) 09:02 기준 최신판

운영체제 기본 목차 · 부팅 목차

Linux v6.12에서 부트 태스크는 rest_init()에 도달하면 PID 1과 kthreadd를 만든다. PID 1은 커널의 나머지 초기화를 맡고, 기존 부트 태스크인 PID 0은 idle 태스크로 남는다.

PID 0이 PID 1로 바뀌는 것은 아니다

rest_init()은 먼저 user_mode_thread(kernel_init, NULL, CLONE_FS)를 호출해 PID 1을 만든다. 이어 kernel_thread()로 kthreadd(PID 2)를 만든다. 기존 부트 태스크는 스케줄러를 거쳐 cpu_startup_entry()의 idle 경로로 진행한다.[1]

즉 rest_init()이 자기 스택에서 평범하게 kernel_init()을 호출하고 그대로 init이 되는 구조가 아니다. 새 태스크에는 별도 실행 문맥이 생긴다. 실행 상태와 자원의 구분은 프로세스와 스레드에서 설명한다. 이후 어떤 태스크가 CPU를 사용할지는 스케줄링에 따라 달라진다. PID 1은 새로 생겼지만 기존 PID 0이 사라진 것은 아니다.[2]

PID 0: start_kernel -> rest_init -----------------> idle
                         |
                         +-- create PID 1 -> kernel_init
                         |
                         +-- create PID 2 -> kthreadd

user_mode_thread인데 왜 커널 함수를 실행하는가

함수 이름의 user mode를 “이 호출 직후 CPU가 사용자 모드가 된다”로 읽으면 안 된다. user_mode_thread()는 시작 함수와 인자를 kernel_clone_args에 담아 kernel_clone()에 넘긴다. 이 경우 시작 함수는 커널 내부 함수인 kernel_init()이다.[2]

kernel_thread()와의 중요한 차이는 kthread 속성을 설정하지 않는다는 점이다. 계속 커널 스레드로 남을 태스크와 나중에 사용자 실행 이미지를 갖게 될 태스크의 생성 방식이 구별된다. 실제로 kernel_execve()는 PF_KTHREAD가 설정된 태스크의 호출을 거부한다. 따라서 “PID 1은 처음에 커널 코드를 실행한다”는 설명은 맞지만, 이를 곧바로 “일반 커널 스레드를 만든 뒤 exec한다”로 바꾸면 v6.12 구현과 어긋난다.[2][3]

권한 수준과 태스크 종류도 같은 말이 아니다. 사용자 프로그램을 실행할 태스크 역시 커널에 진입해 커널 코드를 실행할 수 있다. PID 1은 아직 첫 사용자 실행 이미지를 얻기 전이다. 보호 경계 자체는 보호를 참고한다.

먼저 만든 PID 1을 왜 기다리게 하는가

PID 1이라는 번호를 얻도록 init을 먼저 생성하지만, init이 진행하며 사용할 커널 스레드 생성 기반인 kthreadd는 그다음 만들어진다. 단순히 소스에서 생성 호출을 두 줄 연속 적었다고 새 태스크들이 반드시 원하는 순서로 실행되는 것은 아니다.[1]

이 의존 관계를 표현하는 것이 kthreadd_done completion이다. completion은 한쪽이 작업 완료를 알리고 다른 쪽이 그 사건을 기다리는 동기화 수단이다. 조건을 기다리는 작업과 깨우기의 관계는 동시성과 연결된다. kernel_init()은 시작하자마자 기다리고, rest_init()은 kthreadd 준비 후 완료를 알린다. “태스크를 먼저 만들었다”와 “필요한 기반이 준비되었으니 진행해도 된다”를 분리한 것이다.[1]

PID 1에서 커널 초기화를 계속한다

대기가 끝나면 kernel_init_freeable()이 SMP와 나머지 초기화를 진행한다. 여기에는 do_basic_setup()을 통한 initcall 실행도 포함된다.[1]

initcall은 부팅 중 실행할 초기화 함수를 정해진 단계에 등록하는 방식이다. 소스의 각 부분이 등록한 함수들을 초기화 경로가 순회하며 실행하므로, start_kernel() 안에 모든 드라이버의 함수 이름이 직접 나열될 필요는 없다. 예를 들어 rootfs에 initramfs를 푸는 populate_rootfs()도 rootfs_initcall로 등록되어 있다.[1][4]

kernel_init_freeable()에서 돌아온 뒤에도 비동기 초기화의 완료를 기다린다. 그래야 아직 실행 중인 초기화 코드가 있는데 그 코드를 담은 메모리를 먼저 해제하는 일이 없다. 이후 초기화용 메모리를 정리하고, 다음 장에서 다룰 init 선택과 exec로 진행한다.[1]

이전: 커널 초기화 · 다음: 루트 파일시스템과 init 선택

참고 문헌

  1. ↑ 1.0 1.1 1.2 1.3 1.4 1.5 v6.12 init/main.c, rest_init.
  2. ↑ 2.0 2.1 2.2 v6.12 kernel/fork.c, user_mode_thread, kernel_thread, kernel_clone.
  3. ↑ v6.12 fs/exec.c, kernel_execve.
  4. ↑ v6.12 init/initramfs.c, populate_rootfs 및 rootfs_initcall 등록.