리눅스 커널 심층 분석 개정 3판 : 3장. 프로세스 관리
Linux Kernel Development Third Edition : Chapter 3. Process management
Notion에서 보기
- Process (=task)
: 실행 중인 프로그램
사용 중인 파일, 대기중인 시그널, 커널 내부 데이터, 프로세서 상태, 실행 중인 스레드 등 모든 자원 포함
- 리눅스에서 fork()로 프로세스 생성
: 존재하는 프로세스를 duplicate / 부모 프로세스를 호출
-> 새로운 프로세스 = 자식 프로세스
그래서 fork()는 커널로부터 반환 두번 (부모 + 자식)
- 리눅스에서 exit()로 프로세스 종료 + 할당받은 자원 해제
: 부모 프로세스가 wait4() 시스템콜을 통해 종료된 자식 프로세스의 상태를 얻어옴
**wait4() = 프로세스가 특정 프로세스의 종료를 기다리게 하는 시스템콜
부모 프로세스가 wait()를 호출할떄까지 종료된 프로세스(자식 프로세스) = 좀비 프로세스
- 스레드 : 프로세스 내부에서 동작하는 객체
- 개별적으로 프로그램 카운터, 스택, 레지스터 가짐
- 스택을 제외한 모든 address space 공유
- 커널이 개별적 스레드를 관리 (프로세스 X)
- 리눅스에서 스레드 = 프로세스
- process desciptor = struct task_struct <linux/sched.h>
: 특정 프로세스의 모든 정보를 담고 있는 자료 구조
/ circular doubly linked list = task list에 프로세스 목록 저장
- 실행중인 프로그램을 기술하는 데이터 담고 있음
=> open files, 프로세스의 주소 공간, pending signal, 프로세스 상태 등
- slab allocator로 할당
=> 객체 재사용, cache coloring을 위해 (chap 12)
- 2.6 kernel 이전) task_struct는 각 프로세스의 커널 스택의 끝에 저장됨.
-> 레지스터가 스택 포인터로 task_struct의 위치 계산 가능
- 2.6 kernel 이후) slab allocator로 task_struct를 동적 creation
- strcut_thread_info를 스택 bottom과 top에서 grow
<asm/thread_info.h>에 구조체 정의
: info 구조체는 실제 task_struct 포인터를 가지고 있음
- task struct -> tasks (직접 참조)
- 프로세스에 대한 직접적인 작업을 다루는 구조체
- 실행중인 task의 task_struct를 빠르게 loop up하는 것이 좋다
=> using current macro
** macro : 각 아키텍쳐마다 독립적으로 구현되어야 함.
- ex) 어떤 아키텍쳐는 task_struct에 대한 포인터를 레지스터에 저장여 빠른 접근 지원 /
x86은 레지스터의 개수가 적기 때문에 thread_info구조체를 만들어 사용한다. thread_info에는 thread_info의 위치를 계산하여 task_struct에 접근할 수 있는 커널 스택을 저장한다.
- 시스템은 process identification (PID)로 프로세스 식별
: PID = opaque type pid_t (numerical value, int)
**opaque: struct나 class를 완전히 정의하지 않은 상태로 갖고 있는 타입
- 리눅스 이전버전과의 호환성을 위해
default max value = short int
=> 커널은 pid 값을 각 task_struct에 저장
- max value = 시스템에 동시에 존재할 수 있는 프로세스의 최대 개수
- /proc/sys/kernel/pid_max 에서 값을 수정하여 프로세스 max 개수 조정 가능
- Process State
: task_struct 중 state에 담겨 있음 /
current condition of the process / 5가지
- TASK_RUNNING: 프로세스 실행할 수 있는 상태
현재 실행 중이거나 실행되고자 실행큐에서 대기 중
커널 + 유저 스페이스에서 프로세스를 실행할 수 있는 유일한 상태
(실행 중 다른 프로세스에 의해 선점당해도 대기하면서 RUNNING 유지)
- TASK_TNERRUPTIBLE: sleeping, blocked / 어떤 조건을 기다리는 중
조건 충족 -> 커널은 해당 프로세스를 running 시킴 = awake
신호를 받으면 미리 awake하고 runnable
- TASK_UNINTERRUPTIBLE:
신호를 받으면 runnable하지만 wake up되지 않은 상태
프로세스가 intrerruption이 없으면 기다리거나 이벤트가 빠르게 발생할 것으로 예상될때 사용되는 상태
(자주 사용되진 않음)
- _TASK_TRACED: 다른 프로세스에 의해 traced되는 상태
- ex) 디버거
using ptrace
- _TASK_STOPPED: 프로세스 실행이 멈춘 상태, 실행하기에 적절하지 않은 상태
SIGTSTP, SIGTTIN, SIGTTOU 신호를 받은 경우 발생
디버깅 중에 어떤 SIGNAL이라도 받은 경우
- 커널 코드가 프로세스 상태 바꾸는 매커니즘:
set_task_state (task, state);
= task를 state 상태로 만들어라
- process context
- 프로세스의 코드는 executable file에서 read
- 프로세스는 프로그램의 address space에서 실행됨.
- 보통 프로그램 실행은 user-space에서 일어남.
그런데 프로그램이 system call 실행 / trigger an exception
-> kernel-space로 들어감
system call, exception handler는 커널에 interface로 정의됨
즉, 커널 스페이스는 system call, exception handler interface로를 통해서만 접근가능
- Process Family Tree
- 유닉스 시스템, 리눅스에서 프로세스간 계층 구조 존재
- 모든 프로세스는 init process의 자식
=> 즉, 모든 프로세스는 정확한 하드의 부모 프로세스가 있음
이러한 프로세스간 관계도 task_struct에 저장됨.
task_struct는 부모 task_struct에 대한 포인터 parent, 자식 프로세스 task_struct 리스트에 대한 포인터 children을 가지고 있음.
struct task_struct *my_parent = current -> parent;
struct task_struct *task;
struct list_head *list;
list_for_each(list, ¤t->children){
task = list_entry(list, struct task_struct, sibling);
-> task는 current의 자식 프로세스 중 하나 포인팅
}
**current = 현재 프로세스
- kernel은 boot process의 마지막 단계에서 init이 시작된다.
**boot process: 전원버튼을 누르면 캐시메모리에 있던 부트로더 프로그램으로 전원이 보내지고 운영체제에 로드된다.
-> 운영체제 활성화 시키는 프로세스 시작
- init 프로세스 시스템의 initscripts를 read, 다른 프로그램을 실행시키고, boot process를 완성시킴.
init task의 task_struct는 init_task에 정적 할당
struct task_struct *task;
for (task = current; task != &init_task; task = task->parent) ;
현재 task부터 init_task가 아닌 동안 계속해서 상위계층 프로세스로 이동 -> 현재 task가 init을 가리키면 반복 끝
**task list들은 환형 이중 연결리스트로 연결되어 있기 때문에 단순 반복문을 통해 시스템의 모든 프로세스를 살펴 보는 것이 특정 프로세스로 바로 찾는 것보다 좋음 / 너무 많은 프로세스가 있는 경우에 반복문은 비효율적
- Process Creation
- 대부분의 운영체제 (!=unix): spawn 매커니즘 구현
-> 새로운 주소공간에 새로운 프로세스를 생성하고 executable 을 read하고 실행시킴
- Unix: fork() + exec() / 위의 creation을 두개로 분리
- fork(): current task를 복제한 자식 프로세스 생성
생성된 자식 프로세스들은 pid로 식별하고 나머지는 부모 프로세스와 같음 (ppid), 특정 자원, 통계는 상속 X
- exec(): 주소 공간에 새로운 executable을 로드하고 실행시킴
- Copy-on-Write
부모가 가진 모든 자원을 자식에게 복제하는 것은 공유할 수 있는 데이터도 복사하기 때문에 비효율적
=> copy-on-write pages 사용
: (COW) 데이터의 복제를 방지하거나 지연시키는 기법
-> 부모와 자식이 하나의 데이터를 공유하도록
write했을 때에만 해당 자원을 복제, write 없으면 read-only로 공유
- fork()의 유일한 오버헤드 = 부모 page table 복제, 자식에게 고유한 task_struct 생성
- Forking
리눅스는 clone() system call로 fork()를 구현
clone() -> 어떤 자원을 부모, 자식이 공유해야 하는지 알려주는 flag
** fork(), vfork(), _clone() library call -> clone() system call 발생시팀 -> do_fork() 호출 -> forkinng의 작업 대부분 수행
- do_fork() <kernel/fork.c>에 정의되어 있음
-> copy_process() 함수 호출 -> 프로세스 실행 시작
- copy_process() 하는 작업
- dup_task_struct() 호출 :
새로운 커널 스택, thresd_info 구조체, task_struct 생성
=> 이떄, 자식과 부모 프로세스는 완전히 동일
- 새로 생성된 자식 프로세스가 현재 사용중인 프로세스 개수의 제한 넘지 않는지 확인
- 부모와 차별화 두기
: task_struct의 다양한 멤버들이 값을 초기화함.
(그렇지만 대부분 unchanged)
- 자식 프로세스의 state = TASK_UNINTERRUPTIBLE로 바꿈
= 아직 실행되지 않았음을 명시
- copy_flags() 호출
: task_struct의 flage들을 갱신
PF_SUPERPRIV flag = clear 0
(task가 슈퍼유저 권한을 가지고 있는지 표시)
PF_FORKNOEXEC flag = set 1
(프로세스가 exec()에 호출되지 않았음을 표시)
- alloc_pid() 호출: 새로운 task에 사용가능한 PID 부여
- clone()을 통해 전달된 flag에 따라서 copy_process()는 open file, filesystem information, signal handlers, process address space, namespace를 공유하거나 복제한다.
- copy_process 정리되고 새로운 프로세스에 대한 포인터 반환
=> 새로운 프로세스 woken up and run
- do_fork()를 마치고 자식프로세스가 exec()를 호출하면 COW 오버헤드가 제거된다 (부모 프로세스가 처음으로 실행되면 주소 공간에 write해야되니까)
- vfork()
: fork()와 같음 / 부모프로세스의 page table entry들이 복사되지 않음
자식 프로세스가 독자적으로 부모 프로세스의 주소 공간에서 스레드를 실행시키고 부모는 자식 프로세스가 exec()를 호출하거나 나갈때까지 block 당함. -패륜이네
-> 자식 프로세스는 주소공간에 write 못함
=> COW 기법으로 페이지 테이블 entry를 얻기 때문에 더 이상 이점이 없음 + 구현 어려움
- The Linux Implementation of Threads
- Thread = programming abstraction
: 공유 메모리 공간에서 같은 프로그램을 여러개의 스레드로 실행 가능
- open file, 자원 공유 가능
=> concurrent programming, 멀티프로세서 시스템(parallelism)
- 리눅스의 thread
**리눅스에는 스레드의 개념이 없다. 스레드 = 프로세스
: 다른 프로세스와 특정 자원을 공유하는 프로세스
- thread는 각각 독자적인 task_struct를 가진다.
- 커널은 thread를 일반적인 process로 본다.
- creating thread
- task creation과 같음 / clone() system call이 공유해야 하는 특정 자원에 대한 flag를 넘겨준다.
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
주소공간, 파일시스템, 파일 디스크립터, signal handler 공유
**일반적인 fork() = clone(SIGCHLD, 0);
**일반적인 vfork() = clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
- clone flag list <linux/sched.h>에 정의
- kernel threads
: kernel space에 독자적으로 존재하는 표준 프로세스
- 주소공간 X
(mm pointer=주소 공간 포인터 = NULL)
- 커널 공간에서 user-space와 context switch를 하지 않음
- scheduled, preemptable O
- 커널은 kernel thread를 통해 연산을 수행
- 리눅스는 몇개의 task를 kernel thread에게 위임
flush task, ksoftirqd task
- ps -ef 명렁으로 리눅스 시스템에 있는 kernel thread를 볼 수 있음
- kernel thread는 오직 다른 kernel thread에 의해서만 생성가능
: 자동으로 handling
(모든 새로운 커널스레드는 kthreadd 커널프로세스를 fork하여 생성)
- kernel thread 생성 >> <linux/kthread.h>에 정의된 함수
struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
kthread 커널프로세스에 의해 clone() system call 을 통해 새로운 task 생성
-> 새로운 프로세스가 threadfn 함수 실행(인자값 전달)
-> 프로세스는 namefmt에 의해 이름이 붙여짐 (print-style formatting argument를 가짐)
-> 프로세스는 unrunnable state로 생성됨
-> wake_up_process()로 woken up 되어야 실행됨
**kthread_run() 하나의 함수로 생성 + 실행 한번에 가능
do_exit()가 호출되거나 커널의 다른 부분이 kthread_stop()을 호출하기 전까지 커널 스레드 계속 실행됨
- Process Termination
ㅋㅋㅋㅋㅋㅋㅋㅋ
: 프로세스 스스로 종료 exit() system call / 처리할 수 없는 signal, execption을 받으면 비자발적 종료 /
- 프로세스 종료 -> 커널은 프로세스가 소유했던 자원을 반납하고 해당 프로세스의 부모에게 demise 알림
- do_exit()은 강제 종료 <kernel/exit.c>에 정의
- task_struct의 flag 중 PF_EXITING flag = 1로 set
- del_timer_sync() 호출 -> kernel timer제거
=> timer handler, timer X
- acct_update_integrals() 호출 -> accounting information write
- exit_mm() 호출 -> 해당 프로세스가 가지고 있던 mm_struct release
(이 구조체를 공유하고 있는 프로세스가 없다면 소멸시킴)
- exit_sem() 호출-> 해당 프로세스가 IPC 세마포어를 기다리기 위해 큐에 대기중이라면 dequeue
- exit_files(), exit_fs() 호출 -> 파일 디스트립터, 파일시스템 데이터 관련 객체 usage count --
각 count = 0 이면 객체는 더 이상 사용중이 아니므로 소멸
- task's exit code = 1 >> task_struct의 exit_code 에 저장
- exit_notify() 호출 -> task의 부모에 signal보냄, task의 자식들을 같은 스레드 그룹에 있는 스레드에게 reparent
-> exit state set하고 이를 task_struct 의 exit_state에 저장
-> exit_zombie
- schedule() 호출 -> 새로운 프로세스로 switch하기 위해
** task에 연관된 모든 object들 freed
- 커널 스택, thread_info, task_struct는 메모리에 존재
-> 부모 프로세스가 정보를 찾아가고 더 이상 필요 없다고 kernel에게 알리면 남은 메모리를 모두 시스템에 반납
- Removing the Process Descriptor
do_exit()이 완료 후, 종료된 task의 process descriptor는 여전히 존재 / process = exit_zombie state (실행 불가능)
-> 종료된 후에 자식 프로세스에 대한 정보를 시스템이 가져갈 수 있도록 함.
-> kernel 에 이제 이 프로세스를 지워도 된다고 signalㄹ 보냄
-> child's task_struct 할당 해제
- wait() 함수 - wait4() system call로 구현
-> exit하는 자식 프로세스의 PID를 가진 함수가 반환할때 자식 프로세스 중 하나라도 exit하기 전까지 호출한 task의 실행을 중단시켜놓는 것
- process descriptor deallocation >> release_task()
_exit_signal() 호출 -> _unhash_process() 호출 -> detach_pid() 호출 -> pidhash와 task list에서 해당 프로세스를 제거
-> _exit_signal() -> 종료된 프로세스가 사용했던 남은 자원 반환, statistic, bookkeeping 마무리
-> thread group의 마지막 task 이면 zombie 상태의 leader의 부모에게 release_task()알림
-> put_task_struct() 호출 -> 프로세스의 kernel stack과 thread_info가 가지고 있던 page free, task_struct가 가지고 있던 slab cache 할당 해제
== 해당 프로세스만 가지고 있던 모든 자원, 프로세스 디스크립터 freed
- The Dilemma of the Paraentless Task
자식 프로세스를 가진 부모 프로세스는 exit하기 전에 반드시 자식 task들을 새로운 프로세스에 reparent해야 함
그렇지 않으면 parentless task는 terminate 과정에서 영원히 zombie로 남게됨. -> 자원 낭비
- reparent:
current thread group / init process에 reparent
- do_exit() -> exit_notify() -> forget_original_parent() -> find_new_reaper() -> reparenting해당 프로세스의 thread group의 다른 task를 return 없다면 init process return
적절한 부모 프로세스 찾으면 child를 reaper
-> ptrace_exit_finish() 호출
-> reparenting to ptraced children list
**task가 ptraced되면 디버깅 과정에서 일시적으로 reparenting
(2.6 kernel의 새로운 특징)
이전의 커널에서는 reparenting을 위해 시스템의 모든 프로세스를 loop
-> ptraced되는 자식 프로세스 리스트를 분리해 두어 위의 상황 방지
- reparenting하면 자식 프로세스에 대새 init process가 wait()을 호출하고 좀비 상태 치워버림