커널 스레드
는 커널 공간에서만 실행되는, 유저 공간과 상호 작용하지 않는 스레드이다. 따라서 커널 스레드는 실행과 휴면 등의 모든 동작을 커널에서 직접 제어 · 관리하며, 대부분의 커널 스레드는 시스템이 부팅할 때 생성되고 시스템이 종료할 때까지 백그라운드로 실행된다.
kthreadd
프로세스kthreadd()
(kernel/kthread.c
)이며 커널 스레드를 생성하는 역할을 수행한다.kworker
스레드worker_thread()
(kernel/workqueue.c
) 이며, process_one_work()
(kernel/workqueue.c
) 함수를 호출해서 워크를 실행하는 기능을 수행한다.ksoftirqd
프로세스Soft IRQ
를 위해 실행하는 프로세스이다. 프로세스 이름 뒤에 붙는 숫자는 실행 중인 CPU 의 번호이다. ksoftirqd/3
프로세스는 CPU3 에서만 동작한다.irq
스레드irq
/irq-num
-irq-name
과 같은 형태의 이름을 가진 스레드를 IRQ 스레드
라 부르며 인터럽트 후반부 처리를 위해 쓰이는 프로세스이다. 커널 스레드의 생성 과정은 크게 2단계로 나뉜다:
kthreadd
프로세스에게 커널 스레드의 생성을 요청kthread_create()
kthread_create_on_node()
kthreadd
프로세스가 커널 스레드를 생성kthreadd()
create_kthread()
kthread_create()
는 kthread_create_on_node()
의 래퍼 매크로인데 별거 없다. 그냥 인자를 풀어서 넘겨준다.
kthread_create_on_node()
함수도 그냥 인자를 va_list
로 읽어서 넘겨준다. 실질적인 함수의 본체는 __kthread_create_on_node()
이다.
책에서는 full_name
과 관련된 내용이 빠져있는데 실제 코드에는 있다. 핵심적인 내용은 다음과 같다:
kmalloc()
함수로 kthread_create_info
구조체를 할당하고, 구조체의 멤버를 채워 넣는다.kthread_create_list
연결 리스트에 새롭게 생성한 kthread_create_info
구조체를 연결한다.kthread_create_list
는 전역 변수이다.wake_up_process
로 kthreadd_task
를 깨운다.kthread_create_list
와 마찬가지로 kthreadd_task
역시 전역 변수로 등록되어 있다. 앞서 말했듯이 kthreadd
프로세스의 핸들러 함수는 kthreadd()
이므로 이 프로세스가 실행될 것이다.kthreadd()
함수는 짧고 간명하다:
kthread_create_list
가 비어있을 것이므로 schedule()
을 호출하여 deep sleep 에 빠진다.kthreadd()
의 코털을 wake_up_process()
로 건드린다.kthread_create_list
)가 깨웠는지 확인하다.while
문 돌면서 한 놈도 빠짐없이 전부 다 조진다.kthread_create_lock
을 풀고 잠그는 과정을 계속 반복하는데 왜 이렇게 했는지도 의문임. 단순한 추측인데, 새롭게 생성한 커널 스레드가 한번 더 스레드를 생성하면서 spin_lock
의 이중 잠금이 걸리는 것을 방지? 하려는 게 아닌가 싶다.kthread_create()
를 호출할 수 있기 때문이 아닌가 하는 추측을 한다.)create_kthread()
함수 분석별거 없다. 그냥 kernel_thread()
를 수행한다. 다만 여기에는 kernel_thread()
실패에 대한 예외처리 코드가 함께 붙어있다.
kernel_thread()
함수 분석kernel_thread()
함수는 kernel_clone
함수 호출을 위해 args
를 구성해서 인자로 넘긴다. 결국 핵심 코드는 kernel_clone()
이다. 책에는 _do_fork()
로 나오는데 이건 과거의 구현이다.
![]() |
---|
출처: https://opensource.com/article/18/1/analyzing-linux-boot-process |
궁금해서 kthreadd()
는 누가 생성하는지 확인해봤는데, 아무래도 커널 부팅 과정에서 생성되는 것 같다. init/main.c
의 rest_init()
함수에서 kernel_thread()
함수로 kthreadd
를 생성한다.
책에서는 _do_fork()
로 소개되고 있는 kernel_clone()
함수의 동작은 크게 2단계로 분류할 수 있다:
copy_process()
함수를 호출해서 프로세스를 생성한다. 함수의 이름에서 볼 수 있듯이 부모 프로세스의 리소스를 자식 프로세스에게 복제한다.copy_process()
함수를 호출해 프로세스를 만든 후 wake_up_new_task()
함수를 호출해서 프로세스를 깨운다. 이는 스케줄러에게 프로세스의 실행을 요청한다는 뜻이다.kernel_clone()
함수copy_process()
함수를 통해 프로세스를 생성wake_up_new_task()
함수로 생성한 프로세스를 깨움get_task_pid()
, pid_vnr()
함수로 pid
를 생성하고 반환.copy_process()
함수엄청나게 많은 라인을 스킵했는데... 핵심 동작은 다음과 같다:
dup_task_struct()
함수로 task_struct
구조체와 프로세스가 실행될 스택 공간을 할당한다.sched_fork()
함수를 통해 task_struct
구조체에서 스케줄링 관련 정보를 초기화한다.copy_XXX()
류 함수로 프로세스의 자원(파일 디스크립터, 시그널 핸들러, 스레드, etc.)을 초기화한다. 이때, 생성 플래그에 따라 부모 프로세스로부터의 관계가 설정된다. (시스템 프로그래밍을 하다보면 signal 관련 정보나 file descriptor 의 공유 여부(Close-on-exec) 등등을 설정할 수 있는데 이에 해당하는 것 같다)wake_up_new_task()
함수여기서부턴 책과 코드가 약간 다른데 핵심적인 내용은 다음과 같다:
TASK_RUNNING
으로 변경WRITE_ONCE(p->__state, TASAK_RUNNING);
thread_info
구조체의 cpu
필드에 저장__set_task_cpu(p, select_task_rq(p, task_cpu(p), WF_FORK));
rq = __task_rq_lock(p, &rf);
...
activate_task(rq, p, ENQUEUE_NOCLOCK);
프로세스는 크게 두 가지 흐름으로 종료된다:
exit()
함수를 호출할 때유저 프로세스가 정해진 시나리오에 따라 종료해야 될 때에는 exit()
함수를 호출하여 종료하고, 종료 시그널을 전달받은 경우 유저/커널 프로세스 관계 없이 커널 내부에서 소멸된다.
do_exit()
함수do_exit()
함수는 kernel/exit.c
에 정의되어 있고 다음과 같이 선언되어 있다:
void __noreturn do_exit(long code);
do_exit()
함수는 실행 후 반환되지 않으므로 __noreturn
annotation 이 붙는다. 전달되는 code
인자는 프로세스의 종료 코드를 의미한다. 동작방식은 다음과 같다:
init
프로세스가 종료하면 강제 커널 패닉 유발task_struct
구조체의 state
필드에 TASK_DEADE
로 설정do_task_dead()
함수를 호출해 스케줄링을 실행책에서는 재호출 여부에 대한 내용도 있는데 rpi-6.6.y
기준으로는 해당 코드가 빠졌기 때문에 생략한다. (이 부분은 do_exit()
을 호출하는 make_task_dead()
에서 수행된다. do_group_exit()
에서는 수행되지 않는다. 일반적인 상황은 아닌듯 함)
830번째 라인에서 exit_signals(tsk);
로 task_struct
구조체의 flags
에 PF_EXITING
플래그를 설정해서 현재 프로세스가 do_exit()
을 실행 중임을 알린다.
exit_mm()
함수를 호출해서 mm_struct
구조체의 리소스를 해제하고 메모리 디스크립터의 사용 카운트를 1만큼 감소시킨다. 이후에 프로세스가 사용하고 있는 자원을 반납한다.
exit_notify(tsk, group_dead)
함수를 호출해서 부모 프로세스에게 현재 프로세스가 종료 중이라는 사실을 통지한다.
마지막으로 do_task_dead()
함수를 호출한다. 당연히 종료되는 프로세스는 본인의 스택 영역을 해제할 수 없으므로 __schedule()
을 호출하여 해제하지 못한 남은 자원들의 반납을 수행하게 된다.
do_task_dead()
함수 호출 뒤의 동작 __schedule()
함수가 호출된 뒤에는 context_switch()
함수가, 그 다음으로 finish_task_switch()
함수를 호출하게 된다. 위 함수들이 실행되는 과정에서 프로세스가 소멸하게 된다.
do_exit()
함수에서 대부분의 자신의 리소스를 커널에게 반납하고 자신의 상태를 TASK_DEAD
로 변경한다.finish_task_switch()
함수에서 이전에 실행했던 프로세스의 상태가 TASK_DEAD
라면 프로세스의 스택 공간을 해제한다.__schedule()
함수가 context_switch()
함수를 호출한다.
context_switch()
함수가 finish_task_switch()
함수를 호출한다. switch_to
를 호출한 시점에서 이전 프로세스에서 다음 프로세스의 전환이 이뤄졌으므로 finish_task_switch()
는 다음에 스케쥴링된 프로세스가 실행하게 된다.
finish_task_switch()
함수는 put_task_stack()
함수를 호출해서 프로세스의 스택 메모리 공간을 해제하고 커널 메모리 공간에 반환한다.
ftrace
로 확인해보면 __schedule()
함수가 반환된 이후 (프로세스가 변경된 이후) finish_task_switch()
함수가 호출되고 이어서 put_task_stack()
함수가 되어 최종적으로 스택 메모리 공간이 해제된다.