운영체제 기본/x86-64 Linux의 BIOS 부팅/PID 1 생성: 두 판 사이의 차이
출처·문체 정리: 커널 원문은 git.kernel.org, em dash는 콜론으로 변경 |
BIOS GRUB 부팅 경로 보강과 각론 형식 통일 |
||
| 1번째 줄: | 1번째 줄: | ||
[[운영체제 기본|운영체제 기본 목차]] · [[운영체제 기본/x86-64 Linux의 부팅|부팅 목차]] | [[운영체제 기본|운영체제 기본 목차]] · [[운영체제 기본/x86-64 Linux의 부팅|부팅 목차]] | ||
Linux v6.12에서 부트 태스크는 <code>rest_init()</code>에 도달하면 PID 1과 <code>kthreadd</code>를 만든다. PID 1은 커널의 나머지 초기화를 맡고, 기존 부트 태스크인 PID 0은 idle 태스크로 남는다. | |||
<span id="1._PID_0이_PID_1로_바뀌는_것은_아니다"></span> | |||
== PID 0이 PID 1로 바뀌는 것은 아니다 == | |||
== | |||
<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> | <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> | ||
즉 <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>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> | ||
<pre> | <pre> | ||
PID 0: start_kernel | PID 0: start_kernel -> rest_init -----------------> idle | ||
| | |||
+-- create PID 1 -> kernel_init | |||
| | |||
+-- create PID 2 -> kthreadd | |||
</pre> | </pre> | ||
= | <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"/> | ||
| 28번째 줄: | 27번째 줄: | ||
권한 수준과 태스크 종류도 같은 말이 아니다. 사용자 프로그램을 실행할 태스크 역시 커널에 진입해 커널 코드를 실행할 수 있다. PID 1은 아직 첫 사용자 실행 이미지를 얻기 전이다. 보호 경계 자체는 [[운영체제 기본/보호|보호]]를 참고한다. | 권한 수준과 태스크 종류도 같은 말이 아니다. 사용자 프로그램을 실행할 태스크 역시 커널에 진입해 커널 코드를 실행할 수 있다. 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"/> | ||
= | <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"/> | ||
| 41번째 줄: | 42번째 줄: | ||
<code>kernel_init_freeable()</code>에서 돌아온 뒤에도 비동기 초기화의 완료를 기다린다. 그래야 아직 실행 중인 초기화 코드가 있는데 그 코드를 담은 메모리를 먼저 해제하는 일이 없다. 이후 초기화용 메모리를 정리하고, 다음 장에서 다룰 init 선택과 exec로 진행한다.<ref name="main"/> | <code>kernel_init_freeable()</code>에서 돌아온 뒤에도 비동기 초기화의 완료를 기다린다. 그래야 아직 실행 중인 초기화 코드가 있는데 그 코드를 담은 메모리를 먼저 해제하는 일이 없다. 이후 초기화용 메모리를 정리하고, 다음 장에서 다룰 init 선택과 exec로 진행한다.<ref name="main"/> | ||
'''이전:''' [[운영체제 기본/x86-64 Linux의 부팅/커널 초기화|커널 초기화]] · '''다음:''' [[운영체제 기본/x86-64 Linux의 부팅/init 실행과 사용자 모드 진입|init 실행과 사용자 모드 진입]] | '''이전:''' [[운영체제 기본/x86-64 Linux의 부팅/커널 초기화|커널 초기화]] · '''다음:''' [[운영체제 기본/x86-64 Linux의 부팅/init 실행과 사용자 모드 진입|init 실행과 사용자 모드 진입]] | ||
== 참고 문헌 == | |||
<references /> | <references /> | ||
2026년 10월 31일 (토) 08:38 판
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.0 1.1 1.2 1.3 1.4 1.5 v6.12 init/main.c, rest_init.
- ↑ 2.0 2.1 2.2 v6.12 kernel/fork.c, user_mode_thread, kernel_thread, kernel_clone.
- ↑ v6.12 fs/exec.c, kernel_execve.
- ↑ v6.12 init/initramfs.c, populate_rootfs 및 rootfs_initcall 등록.