[리눅스] 3. Process Management

Nanggu_Pine·2023년 9월 19일
0

linux-kernel

목록 보기
1/12

Ch3. Process Management

프로세스란 무엇이며, 커널에서 어떻게 생성되고 죽는지

1. Process

프로세스: 프로그램 코드를 실행시키는 것 + 할당받은 리소스(메모리, 프로세서 상태 등)
쓰레드: 프로세스 간의 활동을 담은 객체로, 고유한 program counter, 프로세스 스택, 프로세서 레지스터를 포함한다.
커널은 쓰레드를 스케줄링하는게 일반적이지만, 리눅스는 프로세스와 쓰레드를 따로 구별하지 않는다.
쓰레드는 가상 메모리는 공유하지만, 가상 프로세서는 각각 소유한다.
fork -> exec -> exit / parent 프로세스는 wait4를 통해 child 프로세스의 상태를 확인할 수 있다.

2. Process Descriptor and the Task Structure

커널은 프로세스 리스트를 circular doubly linked list로 관리한다. 각각의 요소는 process descriptor이다.

2.1. Allocating the Process Descriptor

task_struct 구조체는 slab allocator를 통해 할당된다. 과거에는 각 프로세스의 마지막 커널 스택에 저장되었지만, 현재는 slab allocator를 통해 동적으로 할당되면서 thread_info 구조체가 새로 출현하였다.

struct thread_info {
	struct task_struct 		*task;
    struct exec_domain 		*exec_domain;
    __u32 					flags;
    __u32 					status;
    __u32 					cpu;
    int 					preemp_count;
    mm_segment_t 			addr_limit;
    struct restart_block 	restart_block;
    void 					*sysenter_return;
    int 					uaccess_err;
}

2.2. Storing the Process Descriptor

시스템은 프로세스를 PID로 구분한다. 보통 int로 처리하는게 일반적이지만, 하위 호환성을 위해 short int의 최대치인 32,768까지가 기본값이다. 최근 실행중인 태스크에 접근하기 위해 current 매크로를 이용한다. 레지스터가 많은 아키텍쳐에서는 레지스터에 해당 값을 저장하고, 그렇지 않은 (x86과 같은) 경우는 current를 계산한다.

2.3. Process State

  • TASK_RUNNING: 프로세스가 실행중이거나, 큐에서 실행 대기중인 상태
  • TASK_INTERRUPTIBLE: 프로세스가 sleeping. 신호를 받으면 TASK_RUNNING 상태로 돌아감
  • TASK_UNINTERRUPTIBLE: 신호를 받아도 깨어나지 않음. interrupt 받지 않고 기다려야 하거나, 금방 이벤트가 발생하는 경우 등에 사용함.
  • __TASK_TRACED: ptrace 시스템 콜을 통해 다른 프로세스에 의해 추적되고 있는 상태 (디버거)
  • __TASK_STOPPED: 태스크가 실행중도 아니고, 실행될 것도 아닌 상태(종료)

set_task_state(task, state) 함수를 통해 task의 상태를 state로 바꿀 수 있다.

2.4. Process Context

보통 프로그램의 실행은 user-space에서 발생되며, system call이나 예외를 발생시키는 경우 프로그램은 kernel-space로 진입하게된다. Kernel-space에 진입한 현재 상황을 process context에 있다고 표현한다. Process context에 있을 때 current 매크로를 사용할 수 있다. Kernel-space에서 나가면 스케줄러의 정책에 따라 우선순위가 높은 프로세스가 실행된다.

2.5. Process Family Tree

최상단에 init 프로세스가 있다 (PID 1). 각각의 task_struct에는 부모 프로세스의 정보를 담은 parent 포인터가 있고, 자식 프로세스의 리스트를 담은 children 포인터가 있다.
Task의 계층 구조에 따라 탐색할 수도 있지만, task_list 자체가 circular linked list이기 때문에 간단하게 탐색할 수 있고, 대부분 이 방식을 선호한다.

3. Process Creation

Unix는 fork()와 exec()의 두 함수를 통해 프로세스를 생성한다. fork() 단계에선 자식 프로세스를 생성한다. exec() 단계에선 새로운 실행가능한 것을 불러와 실행한다.

3.1. Copy-on-Write

전통적으로 fork()를 통해 부모 프로세시의 모든 자원이 자식 프로세스에 복제된다. 이는 굉장히 비효율적이기 때문에 Linux는 copy-on-write 기술을 이용한다. copy-on-write는 데이터의 복제를 막거나 지연시키고, 대신 부모와 자식 프로세스가 공유하는 기법이다. 만약 데이터에 쓰기 작업이 필요한 경우, 그 때 각 프로세스에 복제를 시작한다. 즉, 평소에는 공유된 Read-only 데이터였다가, 데이터에 쓰기 작업이 필요한 때에 데이터를 복제한다.

3.2. Forking

Linux는 clone 시스템 콜을 이용해 fork()를 구현한다.

copy_process():
1. 새로운 커널 스택, thread_info 구조체, task_struct를 생성한다. 이 시점에서는 부모와 자식 process descriptor가 동일하다.
2. 자식 프로세스가 리소스 제한을 초과하지 않는지 확인한다.
3. 자식 프로세스의 process descriptor가 초기화된다.
4. 자식 프로세시의 상태가 TASK_UNINTERRUPTIBLE로 설정된다.
5. copy_flags()가 호출되어 task_struct의 flags가 업데이트 된다. PF_SUPERPRIV 플래그(super user 권한을 이용했는지에 대한 플래그)는 초기화되고, PF_FORKNOEXEC 플래그(프로세스가 exec()을 실행 안했는지에 대한 플래그)는 세팅된다.
6. alloc_pid()를 호출해 태스크에 PID를 할당한다.
7. flags에 의거하여 open files, 파일시스템 정보, 신호 핸들러, 프로세스 주소 공간과 네임스페이스가 공유 혹은 복제된다.
8. copy_process()가 마무리 되고, 포인터를 자식 프로세스에게 전달한다.

자식 프로세스는 이후 바로 exec()을 호출하기 때문에 커널은 자식 프로세스를 먼저 실행한다. 이로 인해 copy-on-write 오버헤드를 제거할 수 있다.

3.3. vfork()

fork()와 동일하나, 부모 프로세스의 page table이 복사되지 않는다. 대신, 단독 쓰레드로써 부모 프로세스의 주소 공간에서 실행되며, 그 동안 부모 프로세스는 블락되어있다. 자식 프로세스는 쓰기 작업을 할 수 없고, 이는 Copy-on-Write가 없던 과거에는 강점을 지녔다.

4. Linux Implementation of Threads

쓰레드의 등장으로 같은 프로그램에서 여러 쓰레드가 동시에 실행될 수 있는 concurrent programming이 가능하게 되었다. 리눅스는 모든 쓰레드를 일반 프로세스로서 구현한다. 리눅스에 있어 쓰레드는 특정 리소스를 다른 프로세스와 공유하는 프로세스일 뿐이다. 다른 시스템들이 쓰레드를 경량화 프로세스로 빠르고 가벼운 실행을 보여준다면, 리눅스는 쓰레드를 프로세스간 자원을 공유하는 방법으로 여긴다.

4.1. Creating Threads

일반 프로세스를 생성하는 것과 비슷하게, clone() 시스템 콜을 이용한다.

// Thread
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

// fork()
clone(SIGCHLD, 0);

// vfork()
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

4.2. Kernel Threads

커널에서는 kernel threads를 통해 백그라운드에서 동작을 수행한다. Kernel Threads는 address space가 없다. Kernel Threads는 유저공간으로 context switch하지 않지만, 다른 프로세스와 마찬가지로 스케줄링 할 수 있고, preemptable하다.

Kernel Threads는 또 다른 커널 쓰레드인 kthread에 의해서만 생성 가능하다.

5. Process Termination

프로세스가 종료될 때 커널은 자원을 해제하고, 부모 프로세스에 프로세스의 종료를 알린다. do_exit()을 통해 종료되면, 태스크는 EXIT_ZOMBIE라는 exit state 상태가 된다. 커널 스택의 thread_info, task_struct만이 메모리에 남아있고, 부모 프로세스에 전달한다. 전달이 완료되면 메모리 역시 반환된다.

5.1. Removing the Process Descriptor

프로세스를 정리하는 것과 process descriptor를 삭제하는 것은 구분되어있다. 부모 프로세스가 자식 프로세스의 정보를 다 가져가거나, 커널이 필요없다고 판단하면 자식 프로세스의 task_struct가 해제된다.

5.2. Dilemma of the Parentless Task

자식 프로세스가 끝나기 전에 부모 프로세스가 종료되면, 해당 자식 프로세스들을 다른 태스크에 reparent 해주어야한다. 보통은 최근 이용한 쓰레드 그룹 혹은 init 프로세스에 reparent 해준다.
커널 2.6 이상부터는 ptraced 리스트와 자식 리스트 모두를 확인한다.


References

  • Linux Kernel Development (3rd Edition) by Robert Love
profile
학부생 기록남기기!

0개의 댓글