Start_Kernel (Week 5)
Start_Kernel (Week 5)

Start_Kernel (Week 5)

카테고리
⚙️ Start Kernel
작성자
박용성박용성
작성일
2024년 06월 02일
태그
C
Linux
Slug
start-kernel-5
번외. ~ kernel_init() 🔍 참고사항🖥️ 전체 코드0. kernel_init(void *unused) 🔍 참고사항1. wait_for_completion(&kthreadd_done);🔍 참고사항💡 Completion Variables⚙️ 예제 코드2. kernel_init_freeable();🔍 참고사항2-1. gfp_allowed_mask = __GFP_BITS_MASK;2-2. set_mems_allowed(node_states[N_MEMORY]);2-3. cad_pid = get_pid(task_pid(current)); 2-4. smp_prepare_cpus(setup_max_cpus);2-5. workqueue_init();여기서부터도 전부 초기화 코드이므로 간단히 보고 넘어가겠습니다.2-13. page_alloc_init_late(); 2-14. do_basic_setup();💡 사용자 공간의 init 프로세스💡 Userspace? Namespace?Depth 1 : prepare_namespace2-19. integrity_load_keys();3. async_synchronize_full();Depth 1 : async_syncrhonize_full_domain(NULL);💡 세 동기화 함수의 차이점1. async_synchronize_full2. async_synchronize_full_domain3. async_synchronize_cookie_domain4. system_state = SYSTEM_FREEING_INITMEM;5. kprobe_free_init_mem(void);6. ftrace_free_init_mem()7. kgdb_free_init_mem()8. exit_boot_config()9. free_initmem()10. mark_readonly()depth 1 : rcu_barrier();11. pti_finalize()12. system_state = SYSTEM_RUNNING;13. numa_default_policy();💡 NUMA란? (복습)14. rcu_end_inkernel_boot();💡 RCU (Read-Copy-Update) (복습)⚙️ 작동 방식15. do_sysctl_args();16. 초기 RAM 디스크에 실행할 명령어가 지정된 경우 실행depth 1 : run_init_process17. 부트로더를 통해 전달된 init 명령이 있는 경우 실행18. 커널 설정에서 전달된 init 명령이 있는 경우 실행19. 위 명령들 모두 실패하면 복구 시도20. 복구 시도 실패하면 panic 출력📢 결론kernel_init 이 하는 작업

번외. ~ kernel_init()

noinline void __ref __noreturn rest_init(void) { struct task_struct *tsk; int pid; rcu_scheduler_starting(); /* * We need to spawn init first so that it obtains pid 1, however * the init task will end up wanting to create kthreads, which, if * we schedule it before we create kthreadd, will OOPS. * init 프로세스가 PID 1을 얻어야 하며, init 프로세스가 커널 스레드를 생성하려 할 때 * 이미 kthreadd(커널 스레드를 생성하는 데몬) 가 실행 중이어야 됨 * 실행 중이 아니라면 시스템 오류 (OOPS)가 발생 */ pid = user_mode_thread(kernel_init, NULL, CLONE_FS);

🔍 참고사항

📌 rest_init 하는 일

  1. 커널 스레드 생성
    1. 메인 초기화 작업 끝난 후, kthreadd 등 필수적 스레드 생성. kthreadd 스레드는 다른 스레드들의 부모 역할을 함
  1. 사용자 모드로 전환
    1. 첫 번째 사용자 모드 프로세스인 init 프로세스 생성
  1. 커널 모드에서 최종 정리
    1. 리소스 할당 해제 등 작업 수행
 

📌 코드 설명

pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
  1. kernel_init 함수를 새로운 사용자 모드 스레드로 실행, kernel_init 이 처음 사용자 공간 프로세스로 실행됨
  1. 그 프로세스 ID를 pid 변수에 저장
  1. CLONE_FS 를 통해 파일 시스템 관련 정보를 공유
 
kernel_init 함수에 NULL 매개변수를 전달한다.
 
 

🖥️ 전체 코드

// line:1441 start static int __ref kernel_init(void *unused) { int ret; /* * Wait until kthreadd is all set-up. */ wait_for_completion(&kthreadd_done); kernel_init_freeable(); /* need to finish all async __init code before freeing the memory */ 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(); /* * Kernel mappings are now finalized - update the userspace page-table * to finalize PTI. */ 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); } /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ 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."); }

0. kernel_init(void *unused)

static int __ref kernel_init(void *unused) { int ret; ...

🔍 참고사항

static

이 함수를 해당 파일 내에서만 접근 가능하도록 제한 (외부 파일에서 이 함수를 참조할 수 없도록 보호)

__ref

해당 함수가 초기화 섹션의 코드나 데이터를 참조할 수 있지만 컴파일러나 링커가 경고를 발생시키지 않도록 함?
→ 초기화 후에도 다른 데이터에 의해 참조될 수 있으므로 메모리에 유지하도록 지시함
/* * modpost check for section mismatches during the kernel build. * A section mismatch happens when there are references from a * code or data section to an init section (both code or data). * The init sections are (for most archs) discarded by the kernel * when early init has completed so all such references are potential bugs. * For exit sections the same issue exists. * * The following markers are used for the cases where the reference to * the *init / *exit section (code or data) is valid and will teach * modpost not to issue a warning. Intended semantics is that a code or * data tagged __ref* can reference code or data from init section without * producing a warning (of course, no warning does not mean code is * correct, so optimally document why the __ref is needed and why it's OK). * * The markers follow same syntax rules as __init / __initdata. */ #define __ref __section(".ref.text") noinline

int ret;

함수가 반환하는 값 또는 함수 내에서 발생한 작업의 성공 또는 실패를 나타내는 데 사용
 

1. wait_for_completion(&kthreadd_done);

💡
kthreadd가 모든 설정을 마칠 때까지 대기
void __sched wait_for_completion(struct completion *x) { wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE); } EXPORT_SYMBOL(wait_for_completion);

🔍 참고사항

📌 kthreadd_done 이란?

static __initdata DECLARE_COMPLETION(kthreadd_done);

  • __initdata : 변수가 커널 초기화 과정 중에 사용되는 데이터임을 명시, 커널이 초기화된 후 사용되지 않아 메모리 공간 절약 가능
  • DECLARE_COMPLETION() : completion 변수를 선언하고 초기화
 

💡 Completion Variables

프로그래밍 할 때, 특정 이벤트가 끝나기까지를 기다려야 하는 상황이 생길 수 있다. 예를 들어
void test_xxx_function(void) { struct semaphore sem; init_mutex_locked(&sem); // 세마포어 잠금 설정 start_external_task(&sem); // 외부 작업 시작 down(&sem); // 잠금 해제될때까지 대기 ... } void start_external_task(struct semaphore *sem) { ... up(sem); // 세마포어 잠금 해제 -> down 실행 가능해짐 }
이 코드의 문제점은
  1. 세마포어를 지역변수로 선언했기 때문에 문제가 생길 수 있음
    1. → 모종의 이유로 test_xxx_functionstart_external_task 보다 먼저 종료된다면 유효하지 않은 메모리 영역 참조하게 됨
  1. downup 이 여러 CPU에서 병렬적으로 수행할 수 있기에 문제가 생길 수 있음 (?)
 
Completion 변수는 세마포어와 굉장히 비슷하지만, 커널의 한 Task가 다른 Task에 특정 이벤트가 발생했다는 것을 알려줄 필요가 있을 때 쉽게 두 Task를 동기화시키는 방법
일회용으로 사용함!

⚙️ 예제 코드

// 1. semaphore 문제 일어나는 코드 #include <pthread.h> #include <semaphore.h> #include <stdio.h> #include <unistd.h> /* 이렇게 생김 typedef union { char __size[__SIZEOF_SEM_T]; long long int __align; } sem_t; */ sem_t sem1, sem2; void *thread1(void *arg) { sem_wait(&sem1); sleep(1); // 잠시 대기 sem_wait(&sem2); // 데드락 발생 가능성 printf("Thread 1: Both semaphores acquired\n"); sem_post(&sem2); sem_post(&sem1); return NULL; } void *thread2(void *arg) { sem_wait(&sem2); sleep(1); // 잠시 대기 sem_wait(&sem1); // 데드락 발생 가능성 printf("Thread 2: Both semaphores acquired\n"); sem_post(&sem1); sem_post(&sem2); return NULL; } int main() { pthread_t t1, t2; sem_init(&sem1, 0, 1); // 세마포어를 프로세스 간에 공유하지 않음, 1로 초기화 sem_init(&sem2, 0, 1); pthread_create(&t1, NULL, thread1, NULL); pthread_create(&t2, NULL, thread2, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); sem_destroy(&sem1); sem_destroy(&sem2); return 0; }
// 2. completion으로 해결 #include <pthread.h> #include <stdio.h> #include <unistd.h> // completion 구조체 정의 typedef struct { pthread_mutex_t mutex; pthread_cond_t cond; int done; } completion; completion comp; // completion 구조체 초기화 함수 void init_completion(completion *x) { pthread_mutex_init(&x->mutex, NULL); // 뮤텍스 초기화 pthread_cond_init(&x->cond, NULL); // 특정 조건을 만족할 때까지 대하는 동기화 기능 x->done = 0; // 완료 상태 초기화 } // completion 완료를 기다리는 함수 void wait_for_completion(completion *x) { pthread_mutex_lock(&x->mutex); // 잠금 while (x->done == 0) { // 완료 상태가 0인 동안 대기 pthread_cond_wait(&x->cond, &x->mutex); } pthread_mutex_unlock(&x->mutex); // 잠금 해제 } // completion 완료 상태를 설정하는 함수 void complete(completion *x) { pthread_mutex_lock(&x->mutex); // 잠금 x->done = 1; // 완료 상태를 1로 설정 pthread_cond_broadcast(&x->cond); // 모든 대기 스레드에 신호 전송 pthread_mutex_unlock(&x->mutex); // 잠금 해제 } // 첫 번째 스레드 함수 void *thread1(void *arg) { printf("스레드 1: Completion 대기...\n"); wait_for_completion(&comp); // completion 완료 대기 printf("스레드 1: Completion 완료!\n"); return NULL; } // 두 번째 스레드 함수 void *thread2(void *arg) { sleep(1); // 잠시 대기하여 thread1이 먼저 실행되게 함 printf("스레드 2: Completion 설정\n"); complete(&comp); // completion을 완료 상태로 return NULL; } // 메인 함수 int main() { pthread_t t1, t2; init_completion(&comp); // completion 초기화 pthread_create(&t1, NULL, thread1, NULL); // 스레드 1 생성 pthread_create(&t2, NULL, thread2, NULL); // 스레드 2 생성 pthread_join(t1, NULL); // 스레드 1이 끝날 때까지 대기 pthread_join(t2, NULL); // 스레드 2가 끝날 때까지 대기 return 0; }
CC=gcc CFLAGS=-Wall -g LIBS=-lpthread # -lrt : librt 링크 all: semaphore completion semaphore: semaphore.o $(CC) $(CFLAGS) -o semaphore semaphore.o $(LIBS) -lrt completion: completion.o $(CC) $(CFLAGS) -o completion completion.o $(LIBS) semaphore.o: semaphore.c $(CC) $(CFLAGS) -c semaphore.c completion.o: completion.c $(CC) $(CFLAGS) -c completion.c clean: rm -f *.o semaphore completion .PHONY: all clean
 
참고 링크
 

2. kernel_init_freeable();

💡
다양한 하위 시스템 초기화 후 사용자 공간 프로세스 시작하기 위한 준비 단계
static noinline void __init kernel_init_freeable(void) { /* Now the scheduler is fully set up and can do blocking allocations */ gfp_allowed_mask = __GFP_BITS_MASK; /* * init can allocate pages on any node */ set_mems_allowed(node_states[N_MEMORY]); cad_pid = get_pid(task_pid(current)); smp_prepare_cpus(setup_max_cpus); workqueue_init(); init_mm_internals(); rcu_init_tasks_generic(); do_pre_smp_initcalls(); lockup_detector_init(); smp_init(); sched_init_smp(); workqueue_init_topology(); padata_init(); page_alloc_init_late(); do_basic_setup(); kunit_run_all_tests(); wait_for_initramfs(); console_on_rootfs(); /* * check if there is an early userspace init. If yes, let it do all * the work */ if (init_eaccess(ramdisk_execute_command) != 0) { ramdisk_execute_command = NULL; prepare_namespace(); } /* * Ok, we have completed the initial bootup, and * we're essentially up and running. Get rid of the * initmem segments and start the user-mode stuff.. * * rootfs is available now, try loading the public keys * and default modules */ integrity_load_keys(); }

🔍 참고사항

💡 noinline ?

→ 특정 함수를 인라인 처리 하지 않도록 지시하는 데 사용

인라인 처리란?

  • 함수 호출의 오버헤드를 줄이기 위해, 함수 호출을 함수 코드 자체 로 대체하는 최적화 기법
  • 추가적인 비용 없이 호출 지점에 함수 코드가 직접 삽입됨
  • 일반적으로 함수 사이즈가 매우 크거나 재귀적으로 호출되는 상황에서는 noinline 을 명시해줌
#include <stdio.h> // noinline 키워드를 사용하여 인라인 처리를 방지하는 매크로 정의 #define noinline __attribute__((noinline)) // noinline이 적용된 함수 noinline void display_message() { printf("This is a noinline function.\n"); } // 인라인 처리가 가능한 일반 함수 void normal_function() { printf("This is a normal function.\n"); } int main() { display_message(); // noinline 함수 호출 normal_function(); // 일반 함수 호출 return 0; }
 

2-1. gfp_allowed_mask = __GFP_BITS_MASK;

  • 스케쥴러가 완전히 설정되었으므로 이제 블로킹 할당을 수행할 수 있음
  • 블로킹 할당
    • 프로세스가 필요한 메모리를 즉시 할당받지 못했을 때, 메모리가 사용 가능해질 때까지 대기하는 메모리 할당 방식을 일컬음
       
→ 스케쥴러가 모든 종류의 메모리 할당을 수행할 준비가 되었다는 것을 나타냄
 

2-2. set_mems_allowed(node_states[N_MEMORY]);

💡
초기화 프로세스(init)가 그 어떤 메모리 노드라도 페이지를 할당할 수 있게 설정
static inline void set_mems_allowed(nodemask_t nodemask) { unsigned long flags; task_lock(current); local_irq_save(flags); write_seqcount_begin(&current->mems_allowed_seq); current->mems_allowed = nodemask; write_seqcount_end(&current->mems_allowed_seq); local_irq_restore(flags); task_unlock(current); }
  • node_states[N_MEMORY]
    • 멀티 노드 시스템에서 각 메모리 노드의 상태를 추적하고 관리하는 데이터 구조
  • 멀티 노드 시스템에서는 여러 개의 노드가 하나의 시스템을 구성 (ex. NUMA)
    • → 각 노드는 다른 노드와 독립적으로 작동하며, 각각이 별도의 프로세서와 메모리를 가짐
  • 페이지를 할당한다는 말
    • → 물리적인 메모리 노드에 특정 프로세스의 가상 메모리를 할당한다는 말로도 이해 가능
 
 

2-3. cad_pid = get_pid(task_pid(current));

💡
현재 실행 중인 태스크(보통은 초기화 태스크)의 PID를 cad_pid 변수에 저장
 

2-4. smp_prepare_cpus(setup_max_cpus);

💡
SMP(다중 프로세서 구성)를 준비
  • setup_max_cpus
    • smp_prepare_cpus 함수가 어떤 동작을 수행할지를 결정, 시스템의 구성과 아키텍처에 따라 달라짐
       

2-5. workqueue_init();

💡
작업 큐 초기화, 커널 내에서 비동기 작업을 스케줄링 하는 데 사용
/** * workqueue_init - bring workqueue subsystem fully online * * This is the second step of three-staged workqueue subsystem initialization * and invoked as soon as kthreads can be created and scheduled. Workqueues have * been created and work items queued on them, but there are no kworkers * executing the work items yet. Populate the worker pools with the initial * workers and enable future kworker creations. */ void __init workqueue_init(void) { struct workqueue_struct *wq; struct worker_pool *pool; int cpu, bkt; wq_cpu_intensive_thresh_init(); mutex_lock(&wq_pool_mutex); /* * Per-cpu pools created earlier could be missing node hint. Fix them * up. Also, create a rescuer for workqueues that requested it. */ for_each_possible_cpu(cpu) { for_each_cpu_worker_pool(pool, cpu) { pool->node = cpu_to_node(cpu); } } list_for_each_entry(wq, &workqueues, list) { WARN(init_rescuer(wq), "workqueue: failed to create early rescuer for %s", wq->name); } mutex_unlock(&wq_pool_mutex); /* create the initial workers */ for_each_online_cpu(cpu) { for_each_cpu_worker_pool(pool, cpu) { pool->flags &= ~POOL_DISASSOCIATED; BUG_ON(!create_worker(pool)); } } hash_for_each(unbound_pool_hash, bkt, pool, hash_node) BUG_ON(!create_worker(pool)); wq_online = true; wq_watchdog_init(); }
 

여기서부터도 전부 초기화 코드이므로 간단히 보고 넘어가겠습니다.

// 메모리 관리자 내부 구조 초기화, 가상 메모리와 관련 (Depth 들어가도 모르겠습니다) init_mm_internals(); // RCU(Read-Copy-Update) 관련 태스크 초기화 // 커널 내부에서 데이터 구조 읽기/쓰기 작업을 효율적으로 관리 rcu_init_tasks_generic(); // SMP 환경을 설정하기 전에 필요한 콜백 함수 실행 do_pre_smp_initcalls(); // 시스템이 멈춰있는지 감지하는 lockup 탐지기 초기화 lockup_detector_init(); // SMP 시스템의 각 CPU를 초기화 smp_init(); // SMP 환경에서의 스케줄러 초기화 sched_init_smp(); // 중간 점검 : SMP를 지원하지 않는 아키텍처는 그냥 단일 CPU 최적화만 수행, 런타임 때 결정됨 // CONFIG_SMP 옵션을 사용한다고 함... // 작업 큐 시스템에, 하드웨어 정보를 반영해 초기화 // 지금 또 초기화할거면 왜 위에 먼저 초기화를 했는지? // 바운딩되지 않은 CPU를 초기화하기 위해?? workqueue_init_topology(); // 병렬 데이터 처리를 위한 시스템 초기화 // pdata는 병렬 작업 처리를 위한 프레임워크로, 많은 병렬 단일 스레드 작업 처리 padata_init(); // 페이지 할당자를 늦게 초기화 // -> 기본 시스템 서비스와 하드웨어 드라이버가 초기화 된 후, 메모리 관리 시스템 조정 page_alloc_init_late(); // 기본 시스템 맟 히위 시스템의 기본 설정 수행 // 파일 시스템, 드라이버, 네트워크 설정 등 // 시스템에서 입출력 장치가 서로 연결되기 위한 버스 초기화 do_basic_setup(); // KUnit 테스트를 실행, 잘 완료되었는지 자체 테스트를 수행함 kunit_run_all_tests(); // 초기 RAM 파일 시스템인 initramfs가 완전히 로드될 때까지 대기 // 커널이 부팅할 때 필요한 임시 파일 시스템으로, 초기 사용자 공간 환경을 제공함 wait_for_initramfs(); // 루트 파일 시스템에서 콘솔 활성화 // 디버깅 정보를 콘솔로 출력하기 위함 -> file open 등 필요 console_on_rootfs(); /* * check if there is an early userspace init. If yes, let it do all * the work */ // 초기 사용자 공간의 init 프로세스가 실행될 수 있는지 확인 // 실행할 수 없다면 네임스페이스 준비 if (init_eaccess(ramdisk_execute_command) != 0) { ramdisk_execute_command = NULL; prepare_namespace(); } /* * Ok, we have completed the initial bootup, and * we're essentially up and running. Get rid of the * initmem segments and start the user-mode stuff.. * * rootfs is available now, try loading the public keys * and default modules */ // 초기 부팅이 완료되었고, 시스템이 본격적으로 작동 // 초기 메모리 세그먼트를 제거하고 사용자 모드 관련 작업 시작, 임시 메모리 영역 해제 // root 파일 시스템 사용 가능, 공개 키와 기본 모듈 로드 // rootfs? // 커널이 처음으로 접근하는 파일 시스템. 초기 RAM 파일 시스템인 initramfs로부터 시작해 // 필요한 드라이버와 설정 로드, rootfs로 전환 integrity_load_keys(); }

2-13. page_alloc_init_late();

+/* + * This lock grantees that only one thread at a time is allowed to grow zones + * (decrease number of deferred pages). + * Protects first_deferred_pfn field in all zones during early boot before + * deferred pages are initialized. Deferred pages are initialized in + * page_alloc_init_late() soon after smp_init() is complete. + */ 한 번에 하나의 스레드만이 메모리 존을 확장할 수 있음 초기 부팅 동안 모든 메모리 존의 `first_deffered_pfn` 필드를 보호, 첫 번째로 연기된 PFN을 나타냄 연기된 페이지들은 smp_init()이 완료된 후 page_alloc_init_late()에서 초기화됨
 

2-14. do_basic_setup();

static void __init do_basic_setup(void) { cpuset_init_smp(); driver_init(); init_irq_proc(); do_ctors(); do_initcalls(); }
 

💡 사용자 공간의 init 프로세스

리눅스 시스템 부팅 과정에서, 마지막에 실행되는 첫 번째 사용자 레벨 프로세스입니다. 이 프로세스는 시스템의 나머지 부분을 초기화하는 데 사용되며, PID 1 입니다.
 

💡 Userspace? Namespace?

리눅스 커널의 기능으로, 다양한 시스템 리소스를 격리하는 기술이며, 추후 컨테이너라는 가상화 기술의 핵심 요소입니다. Namespace 를 사용하면 프로세스 그룹이 파일 시스템 마운트, 네트워크, 사용자 ID, 호스트 이름 등의 리소스를 공유하지 않고 독립적으로 사용 가능합니다.
 
일반 사용자들은 기본적으로 PID 1 의 네임스페이스들을 공유하게 됩니다.
 
찾아보기로는 네임스페이스 별로 PID 1 이 따로따로 존재한다는데 정확한 지 모르겠습니다.
일반적으로 단일 네임스페이스를 사용하고, 특별히 네트워크 테스트나 컨테이너 구현에서 다중 네임스페이스가 활용됩니다.
 
/prod/<PID>/ns 디렉터리에서 현재 프로세스에서 사용하고 있는 네임스페이스의 고유 ID를 확인하는 것이 가능합니다.
root@dacab6fd14a8:/app/_reo# ls -al /proc/1/ns total 0 dr-x--x--x 2 root root 0 May 9 11:59 . dr-xr-xr-x 9 root root 0 May 9 11:59 .. lrwxrwxrwx 1 root root 0 May 9 12:02 cgroup -> 'cgroup:[4026532510]' lrwxrwxrwx 1 root root 0 May 9 12:02 ipc -> 'ipc:[4026532393]' lrwxrwxrwx 1 root root 0 May 9 11:59 mnt -> 'mnt:[4026532391]' lrwxrwxrwx 1 root root 0 May 9 12:02 net -> 'net:[4026532395]' lrwxrwxrwx 1 root root 0 May 9 12:02 pid -> 'pid:[4026532394]' lrwxrwxrwx 1 root root 0 May 9 12:02 pid_for_children -> 'pid:[4026532394]' lrwxrwxrwx 1 root root 0 May 9 12:02 time -> 'time:[4026531834]' lrwxrwxrwx 1 root root 0 May 9 12:02 time_for_children -> 'time:[4026531834]' lrwxrwxrwx 1 root root 0 May 9 12:02 user -> 'user:[4026531837]' lrwxrwxrwx 1 root root 0 May 9 12:02 uts -> 'uts:[4026532392]'
 

Depth 1 : prepare_namespace

💡
파일 시스템 네임스페이스를 준비 루트 파일 시스템을 마운트하고, 필요한 경우 램디스크를 로드
/* * Prepare the namespace - decide what/where to mount, load ramdisks, etc. */ void __init prepare_namespace(void) { // 기다린 후 실행 if (root_delay) { printk(KERN_INFO "Waiting %d sec before mounting root device...\n", root_delay); ssleep(root_delay); } /* * wait for the known devices to complete their probing * * Note: this is a potential source of long boot delays. * For example, it is not atypical to wait 5 seconds here * for the touchpad of a laptop to initialize. */ // 디바이스들의 구성 요소 초기화가 완료될 때까지 기다림 wait_for_device_probe(); // 멀티 디스크 시스템 설정 md_run_setup(); // 루트 디바이스 이름을 가져와 ROOT_DEV에 설정 // 시스템이 부팅할 루트 디바이스를 지정함 if (saved_root_name[0]) ROOT_DEV = parse_root_device(saved_root_name); // 초기 램디스크인 initrd를 로드, 로드되었다면 out으로 점프 if (initrd_load(saved_root_name)) goto out; if (root_wait) wait_for_root(saved_root_name); mount_root(saved_root_name); out: // 장치 파일 관리하는 devtmpfs 마운트 devtmpfs_mount(); // ? init_mount(".", "/", NULL, MS_MOVE, NULL); init_chroot("."); }
 

2-19. integrity_load_keys();

💡
시스템의 무결성을 보호하기 위한 공개 키 로드, 신뢰할 수 있는 소스에서 왔다는 것을 검증
/* * Ok, we have completed the initial bootup, and * we're essentially up and running. Get rid of the * initmem segments and start the user-mode stuff.. * * rootfs is available now, try loading the public keys * and default modules */ // 커널이 이제 기본적인 부팅을 마치고 실행 중인 상태이며, // 초기 메모리 (initmem) 세그먼트를 정리하고 유저 모드로 전환할 준비를 마침 // 더욱 안전하게 사용자 모드로 전환? integrity_load_keys();
/* * integrity_load_keys - load integrity keys hook * * Hooks is called from init/main.c:kernel_init_freeable() * when rootfs is ready */ void __init integrity_load_keys(void) { ima_load_x509(); if (!IS_ENABLED(CONFIG_IMA_LOAD_X509)) evm_load_x509(); }
 
 

3. async_synchronize_full();

💡
리눅스 커널의 비동기 초기화 코드가 모두 완료될 때까지 대기하도록 함
/** * async_synchronize_full - synchronize all asynchronous function calls * * This function waits until all asynchronous function calls have been done. */ void async_synchronize_full(void) { async_synchronize_full_domain(NULL); } EXPORT_SYMBOL_GPL(async_synchronize_full);
 

Depth 1 : async_syncrhonize_full_domain(NULL);

/** * async_synchronize_full_domain - synchronize all asynchronous function * within a certain domain * @domain: the domain to synchronize * * This function waits until all asynchronous function calls for the * synchronization domain specified by @domain have been done. */ void async_synchronize_full_domain(struct async_domain *domain) { async_synchronize_cookie_domain(ASYNC_COOKIE_MAX, domain); } EXPORT_SYMBOL_GPL(async_synchronize_full_domain);
  1. async_synchronize_full_domain(NULL)
    1. : 함수는 도메인에 속한 비동기 작업들이 완료될 때까지 대기
      → 이 경우, NULL이 도메인 매개변수로 전달되어 있으므로 모든 도메인에 속한 비동기 작업들이 대기 대상이 됩니다.
  1. EXPORT_SYMBOL_GPL(async_synchronize_full)
    1. : 해당 함수를 GPL(GNU General Public License) 라이선스로 공개하고, 다른 커널 모듈에서 사용할 수 있도록 함
      → 커널에서 함수를 외부로 노출시킴, 따라서 이 함수는 GPL 라이선스를 따르는 코드나 다른 모듈에서 이 함수를 호출하여 동기화를 수행
       

💡 세 동기화 함수의 차이점

1. async_synchronize_full

void async_synchronize_full(void) { async_synchronize_full_domain(NULL); } EXPORT_SYMBOL_GPL(async_synchronize_full);
  • 등록된 모든 도메인에서 비동기 함수 호출들이 완료될 때까지 대기
    • 특정 도메인을 지정하지 않고 NULL 을 전달하여 모든 도메인을 대상으로 함
 

2. async_synchronize_full_domain

void async_synchronize_full_domain(struct async_domain *domain) { async_synchronize_cookie_domain(ASYNC_COOKIE_MAX, domain); } EXPORT_SYMBOL_GPL(async_synchronize_full_domain);
  • 특정 도메인에 대한 비동기 함수 호출들을 동기화
    • 도메인 매개변수는 비동기 작업이 수행되는 범위를 구분하는 데 사용되고, 해당 도메인에 속한 모든 비동기 호출이 완료될 때까지 대기
 

3. async_synchronize_cookie_domain

void async_synchronize_cookie_domain(async_cookie_t cookie, struct async_domain *domain) { // 현재 시간 선언 변수 ktime_t starttime; // 디버깅 메세지 출력 pr_debug("async_waiting @ %i\n", task_pid_nr(current)); // 현재 시간 저장 starttime = ktime_get(); // lowest_in_progress의 반환값이 cookie 이상이 될 때까지 대기 // 아직 완료되지 않은 가장 낮은 쿠키 값 반환 wait_event(async_done, lowest_in_progress(domain) >= cookie); pr_debug("async_continuing @ %i after %lli usec\n", task_pid_nr(current), microseconds_since(starttime)); } EXPORT_SYMBOL_GPL(async_synchronize_cookie_domain);
  • 특정 도메인에서, 지정된 쿠키를 기준으로 비동기 함수 호출들을 동기화
  • Cookie (쿠키)
    • 작업이 완료되었는지에 대한 순서를 기록하는 데 사용함, 체크포인트?
 

4. system_state = SYSTEM_FREEING_INITMEM;

💡
시스템의 상태를 SYSTEM_FREEING_INITMEM 로 설정, 시스템의 초기화 메모리를 해제 중임을 명시 이 아래 코드부터 시스템 초기화 메모리 해제를 수행하므로 상태를 명시함
/* * Values used for system_state. Ordering of the states must not be changed * as code checks for <, <=, >, >= STATE. */ extern enum system_states { SYSTEM_BOOTING, SYSTEM_SCHEDULING, SYSTEM_FREEING_INITMEM, SYSTEM_RUNNING, SYSTEM_HALT, SYSTEM_POWER_OFF, SYSTEM_RESTART, SYSTEM_SUSPEND, } system_state;
 

5. kprobe_free_init_mem(void);

💡
__init 섹션에 위치한 코드에 설정된 kprobes(커널 프로브)를 제거 프로브 : 특정 지점에 코드를 삽입하여 해당 지점에서 발생하는 이벤트를 감지하거나 조작할 수 있도록 하는 기술, 보통 디버깅 시 사용함
void kprobe_free_init_mem(void) { void *start = (void *)(&__init_begin); void *end = (void *)(&__init_end); struct hlist_head *head; struct kprobe *p; int i; mutex_lock(&kprobe_mutex); /* Kill all kprobes on initmem because the target code has been freed. */ for (i = 0; i < KPROBE_TABLE_SIZE; i++) { head = &kprobe_table[i]; hlist_for_each_entry(p, head, hlist) { if (start <= (void *)p->addr && (void *)p->addr < end) kill_kprobe(p); } } mutex_unlock(&kprobe_mutex); }
  • 초기화 코드가 실행된 후, 해당 코드가 위치한 메모리 영역을 더 이상 필요가 없으므로 해제해야 하는데, kprobes가 존재한다면 이를 먼저 제거해야 메모리를 안전하게 해제 가능
 

6. ftrace_free_init_mem()

💡
커널 함수 호출 추적을 위한 ftrace에 사용된 초기화 메모리를 해제
 

7. kgdb_free_init_mem()

💡
커널 디버깅을 위한 kgdb에 사용된 초기화 메모리를 해제
 

8. exit_boot_config()

💡
커널의 부팅 설정을 종료하고 정리, 부팅 중 생성되었던 임시 구성 데이터와 설정을 정리함
 

9. free_initmem()

💡
부팅 과정에서 사용했던 모든 초기화 메모리(__init 섹션)를 해제
notion image
  • 아키텍처 별로 다른 것 같습니다.

10. mark_readonly()

💡
커널 코드와 중요 데이터를 읽기 전용으로 설정, 안정성과 보안 향상
#ifdef CONFIG_STRICT_KERNEL_RWX static void mark_readonly(void) { if (rodata_enabled) { /* * load_module() results in W+X mappings, which are cleaned * up with call_rcu(). Let's make sure that queued work is * flushed so that we don't hit false positives looking for * insecure pages which are W+X. */ rcu_barrier(); mark_rodata_ro(); // 읽기 전용 데이터 섹션을 읽기 전용으로 설정, 데이터 무결성 보장 rodata_test(); } else pr_info("Kernel memory protection disabled.\n"); } #elif defined(CONFIG_ARCH_HAS_STRICT_KERNEL_RWX) static inline void mark_readonly(void) { pr_warn("Kernel memory protection not selected by kernel config.\n"); } #else static inline void mark_readonly(void) { pr_warn("This architecture does not have kernel memory protection.\n"); } #endif
  • CONFIG_STRICT_KERNEL_RWX , 즉 엄격한 커널 읽기/쓰기/실행 보호가 활성화되어 있다면 mark_readonly() 함수 실행
  • rodata_enabled 변수는 읽기 전용 데이터 섹션 보호가 활성화되어 있는지 확인
 

depth 1 : rcu_barrier();

💡
리눅스 커널의 RCU(Read-Copy-Update)에 등록된 모든 콜백이 완료될 때까지 기다림
/** * rcu_barrier - Wait until all in-flight call_rcu() callbacks complete. * * Note that this primitive does not necessarily wait for an RCU grace period * to complete. For example, if there are no RCU callbacks queued anywhere * in the system, then rcu_barrier() is within its rights to return * immediately, without waiting for anything, much less an RCU grace period. */ void rcu_barrier(void) { uintptr_t cpu; unsigned long flags; unsigned long gseq; struct rcu_data *rdp; unsigned long s = rcu_seq_snap(&rcu_state.barrier_sequence); rcu_barrier_trace(TPS("Begin"), -1, s); /* Take mutex to serialize concurrent rcu_barrier() requests. */ mutex_lock(&rcu_state.barrier_mutex); /* Did someone else do our work for us? */ // 이미 완료되었다면 반환 if (rcu_seq_done(&rcu_state.barrier_sequence, s)) { rcu_barrier_trace(TPS("EarlyExit"), -1, rcu_state.barrier_sequence); smp_mb(); /* caller's subsequent code after above check. */ mutex_unlock(&rcu_state.barrier_mutex); return; } /* Mark the start of the barrier operation. */ // 스핀락으로 보호, 시퀀스 시작 raw_spin_lock_irqsave(&rcu_state.barrier_lock, flags); rcu_seq_start(&rcu_state.barrier_sequence); gseq = rcu_state.barrier_sequence; rcu_barrier_trace(TPS("Inc1"), -1, rcu_state.barrier_sequence); /* * Initialize the count to two rather than to zero in order * to avoid a too-soon return to zero in case of an immediate * invocation of the just-enqueued callback (or preemption of * this task). Exclude CPU-hotplug operations to ensure that no * offline non-offloaded CPU has callbacks queued. */ // 완료 객체를 초기화, CPU 카운트 2로 설정? // 2로 설정하는 이유는 잘 모르겠습니다. init_completion(&rcu_state.barrier_completion); atomic_set(&rcu_state.barrier_cpu_count, 2); raw_spin_unlock_irqrestore(&rcu_state.barrier_lock, flags); /* * Force each CPU with callbacks to register a new callback. * When that callback is invoked, we will know that all of the * corresponding CPU's preceding callbacks have been invoked. */ // CPU에 대기중인 콜백이 있는지 확인 for_each_possible_cpu(cpu) { rdp = per_cpu_ptr(&rcu_data, cpu); retry: if (smp_load_acquire(&rdp->barrier_seq_snap) == gseq) continue; raw_spin_lock_irqsave(&rcu_state.barrier_lock, flags); if (!rcu_segcblist_n_cbs(&rdp->cblist)) { WRITE_ONCE(rdp->barrier_seq_snap, gseq); raw_spin_unlock_irqrestore(&rcu_state.barrier_lock, flags); rcu_barrier_trace(TPS("NQ"), cpu, rcu_state.barrier_sequence); continue; } if (!rcu_rdp_cpu_online(rdp)) { rcu_barrier_entrain(rdp); WARN_ON_ONCE(READ_ONCE(rdp->barrier_seq_snap) != gseq); raw_spin_unlock_irqrestore(&rcu_state.barrier_lock, flags); rcu_barrier_trace(TPS("OfflineNoCBQ"), cpu, rcu_state.barrier_sequence); continue; } raw_spin_unlock_irqrestore(&rcu_state.barrier_lock, flags); if (smp_call_function_single(cpu, rcu_barrier_handler, (void *)cpu, 1)) { schedule_timeout_uninterruptible(1); goto retry; } WARN_ON_ONCE(READ_ONCE(rdp->barrier_seq_snap) != gseq); rcu_barrier_trace(TPS("OnlineQ"), cpu, rcu_state.barrier_sequence); } /* * Now that we have an rcu_barrier_callback() callback on each * CPU, and thus each counted, remove the initial count. */ if (atomic_sub_and_test(2, &rcu_state.barrier_cpu_count)) complete(&rcu_state.barrier_completion); /* Wait for all rcu_barrier_callback() callbacks to be invoked. */ wait_for_completion(&rcu_state.barrier_completion); /* Mark the end of the barrier operation. */ rcu_barrier_trace(TPS("Inc2"), -1, rcu_state.barrier_sequence); rcu_seq_end(&rcu_state.barrier_sequence); gseq = rcu_state.barrier_sequence; for_each_possible_cpu(cpu) { rdp = per_cpu_ptr(&rcu_data, cpu); WRITE_ONCE(rdp->barrier_seq_snap, gseq); } /* Other rcu_barrier() invocations can now safely proceed. */ mutex_unlock(&rcu_state.barrier_mutex); } EXPORT_SYMBOL_GPL(rcu_barrier);
 

11. pti_finalize()

💡
Page Table Isolation(PTI)를 완전히 적용해 커널과 유저 레벨의 메모리 격리를 강화
void pti_finalize(void) { // 시스템의 CPU가 PTI 기능을 지원하는지 확인 if (!boot_cpu_has(X86_FEATURE_PTI)) return; /* * We need to clone everything (again) that maps parts of the * kernel image. */ // 커널 이미지의 일부를 매핑하는 모든 것을 다시 복제해야 함 // 커널 부팅 과정 중에, 커널 이미지의 일부 매핑 속성이 변경되었을 수 있기 때문 // ex) 읽기 전용으로 설정, 실행 금지로 설정 pti_clone_entry_text(); pti_clone_kernel_text(); // 유저 공간에서 WX 권한이 부여된 페이지가 없는지 확인 // 쓰기 권한이 없어야 함! debug_checkwx_user(); }
 

12. system_state = SYSTEM_RUNNING;

💡
시스템의 상태를 SYSTEM_RUNNING 으로 설정, 이제 시스템이 실행 상태임을 명시 시스템이 모든 초기화 작업을 완료했고, 이제 정상적으로 작동
 

13. numa_default_policy();

💡
리눅스 커널에서 NUMA(Non-Uniform Memory Access) 아키텍처에 기반한 시스템의 기본 메모리 할당 정책 설정 → NUMA 시스템에서 각 프로세스의 메모리 접근 패턴을 최적화
/* Reset policy of current process to default */ void numa_default_policy(void) { do_set_mempolicy(MPOL_DEFAULT, 0, NULL); }
 

💡 NUMA란? (복습)

  • 멀티프로세서 컴퓨터 시스템 아키텍처에서 사용하는 모델 중 하나
  • 각 프로세서가 자신의 로컬 메모리에 더 빠르게 접근할 수 있도록 설계하지만, 다른 프로세서의 메모리에 접근할 때는 더 고비용
    • → 메모리 접근 시간이 일관되지 않음
notion image
 
 

14. rcu_end_inkernel_boot();

💡
커널의 부팅 과정에서 사용된 RCU 시스템을 정상 모드로 전환 → 부팅 중에는 RCU가 최적화된 방식으로 동작할 수 있도록 처리되지만, 모든 초기화 작업이 종료된 후에는 일반 방식으로 전환
/* * Inform RCU of the end of the in-kernel boot sequence. */ void rcu_end_inkernel_boot(void) { rcu_unexpedite_gp(); rcu_async_relax(); if (rcu_normal_after_boot) WRITE_ONCE(rcu_normal, 1); rcu_boot_ended = true; }
 

💡 RCU (Read-Copy-Update) (복습)

  • 리눅스 커널에서 사용되는 동기화 메커니즘 중 하나
    • 주로 읽기가 많고 쓰기가 적은 데이터 구조에 대한 효율적 접근 제공
  • 데이터를 읽는 동안 락을 걸지 않아도 되어 굉장히 빠르고, 많은 수의 CPU를 사용하는 대규모 시스템에 유용
 

⚙️ 작동 방식

  1. RCU 읽기
    1. 락을 사용하지 않고, 데이터 읽기는 완전히 병렬로 실행
  1. RCU 쓰기
    1. 데이터를 변경할 때, 먼저 기존 데이터의 복사본 생성
    2. 변경 사항은 이 복사본에 적용된 후, 완료되면 원본 대신 복사본을 사용하도록 포인터를 전환
    3. 이 과정에서도 읽기 작업 수행 가능
  1. RCU 동기화
    1. 데이터의 변경 사항을 적용한 후, 기존 데이터에 대한 모든 참조가 종료될 때까지 기다려야 함
    2. 이는 synchronize_rcu() 함수를 호출하여 수행, 모든 기존 RCU 리더가 완료될 때까지 대기
    3. 이후 안전하게 기존 데이터를 해제하거나 재사용
 
 

15. do_sysctl_args();

💡
시스템 부팅 시 주어진 커널 매개변수를 처리해 sysctl 설정 초기화 → 시스템 제어 변수들 설정
void do_sysctl_args(void) { char *command_line; struct vfsmount *proc_mnt = NULL; command_line = kstrdup(saved_command_line, GFP_KERNEL); if (!command_line) panic("%s: Failed to allocate copy of command line\n", __func__); parse_args("Setting sysctl args", command_line, NULL, 0, -1, -1, &proc_mnt, process_sysctl_arg); if (proc_mnt) kern_unmount(proc_mnt); kfree(command_line); }
 

16. 초기 RAM 디스크에 실행할 명령어가 지정된 경우 실행

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); }
 

depth 1 : run_init_process

// line:1345 static int run_init_process(const char *init_filename) { const char *const *p; argv_init[0] = init_filename; pr_info("Run %s as init process\n", init_filename); pr_debug(" with arguments:\n"); for (p = argv_init; *p; p++) pr_debug(" %s\n", *p); pr_debug(" with environment:\n"); for (p = envp_init; *p; p++) pr_debug(" %s\n", *p); return kernel_execve(init_filename, argv_init, envp_init); } // 여기서 bash 쉘도 실행
 

17. 부트로더를 통해 전달된 init 명령이 있는 경우 실행

/* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ // 하나씩 성공할 때까지 시도! // 정말 고장난 컴퓨터를 복구하려는 경우, 초기화 대신 Bourne shell 사용 가능 if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret); }
 

18. 커널 설정에서 전달된 init 명령이 있는 경우 실행

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; }
 

19. 위 명령들 모두 실패하면 복구 시도

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;
 

20. 복구 시도 실패하면 panic 출력

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

📢 결론

kernel_init 이 하는 작업

  1. 커널의 나머지 부분 초기화
    1. 메모리 할당자, 스케줄러, 드라이버, 서브시스템 설정
  1. root 파일 시스템 마운트
    1. 필수 파일 시스템 마운트해 시스템이 파일을 읽고 쓸 수 있도록 함
  1. init 프로세스 실행
    1. 사용자 공간에서 실행될 첫 번째 프로세스인 init 실행
       
→ 이후 유저 공간의 init 프로세스로 연결됨?
 

댓글

guest