-
프로세스는 프로그램 코드를 실행하면서 생기는 모든 결과물이다.
- 일반적인 의미: 실행 중인 프로그램
- 포괄적인 의미: 사용 중인 파일, 대기 중인 시그널, 커널 내부 데이터, 프로세서 상태, 메모리 주소 공간, 실행 중인 하나 이상의 스레드 정보 등
-
프로세스는
fork()
호출 시 생성되고, 기능을 수행한 뒤,exit()
를 호출해 종료된다. 부모 프로세스는wait()
호출로 자식 프로세스 종료 상태를 확인할 수 있다. -
스레드는 프로세스 내부에서 동작하는 객체이고, 개별적인 PC, Stack, Register(context)를 가지고 있다.
-
리눅스 커널은 프로세스와 스레드를 구분하지 않는다.
-
리눅스 커널에 대한 접근은 오직 시스템 콜과 ISR로만 가능하다.
-
커널은 프로세스를 ‘task list’라는 **circular bidirctional linked list **자료구조로 저장한다.
-
‘Task list’는
<linux/sched.h>
내task_struct
구조체로 정의되어있다.-
task_struct
는 프로세스를 관리하는 데 필요한 모든 정보를 가지고 있어서 정의만 약 300줄 가량이며 32-bit 시스템 기준 약 1.7KB에 달하는 상당히 큰 구조체다. -
task_struct
는 ‘slab allocator’(객체 재사용 및 캐시 기능 지원)를 이용해 동적할당 한다.- 커널 2.6 버전 이전에는 각 프로세스 커널의 최하단(또는 최상단)에 task_struct를 뒀다. 그래야 x86처럼 레지스터 개수가 적은 아키텍처에서 레지스터에 따로 task_struct의 주솟값을 저장하지 않고 바로 접근할 수 있었기 때문이다.
struct task_struct { volatile long state; void *stack; atomic_t usage; unsigned int flags; unsigned int ptrace; int lock_depth; #ifdef CONFIG_SMP #ifdef __ARCH_WANT_UNLOCKED_CTXSW ... }
-
-
프로세스는 다음과 같은 5가지 상태를 가진다.
TASK_RUNNING
: Ready queue에서 대기중이거나, 현재 동작 중인 프로세스다.TASK_INTERRUPTIBLE
/TASK_UNINTERRUPTIBLE
: 특정 조건이 발생하기를 기다리며 중단된 상태에 있는 프로세스다. 조건 발생 시TASK_RUNNING
으로 바뀐다. Signal 수신 여부로 두 상태를 구분한다.TASK_TRACED
: 디버거 같은 장비를 사용하는 외부 프로세스가 ptrace를 사용해 해당 프로세스를 추적하고 있는 상태다.TASK_STOPPED
: 프로세스가 SIGSTOP 같은 signal을 받아 실행이 정지된 상태다.
-
프로세스의 상태는
<linux/sched.h>
의set_task_state()
함수로 설정 가능하다.
- 모든 프로세스는 PID 1인 init 프로세스의 자식 프로세스다.
task_struct
는 부모-형제-자식 프로세스의 관계를 표현하고 있다.- 또한,
task_struct
는 (Bidirectional circular linked list의 요소를 가리키는)*next
,*prev
포인터를 갖고 있다.
-
UNIX에서는
fork()
로 프로세스를 생성할 때 부모 프로세스의 모든 리소스를 그대로 자식 프로세스에 복사하는 식으로 구현했다. 일반적으로 자식 프로세스는 생성된 후exec()
을 이용해 다른 프로그램을 실행하는 경우가 많으므로 이러한 방법은 굉장히 비효율적이었다. -
리눅스는 ‘Copy-and-write’를 이용해서 이 문제를 해결했다.
- 자식 프로세스가 공유자원에 write을 시도할 때 부모 프로세스 → 자식 프로세스 리소스 복사한다.
- 자식 프로세스가 공유자원에 write을 하지 않는 경우 (대부분 생성 후 바로
exec()
하는 경우), 큰 최적화 효과를 얻을 수 있다.
-
프로세스가 생성되는 세부적인 과정은 다음과 같다.
-
리눅스의 glibc 속
fork()
는clone()
이라는 시스템콜을 다양한 플래그를 적용해 부모-자식 프로세스간 공유자원을 지정한 뒤 호출한다.- linux - Which file in kernel specifies
fork()
,vfork()
... to usesys_clone()
system call - Unix & Linux Stack Exchange
- linux - Which file in kernel specifies
-
clone()
은do_fork()
함수를 호출하고do_fork()
는copy_process()
를 호출해 내부적으로 아래 과정을 수행한다.dup_task_struct()
함수 호출- 새로운 프로세스 스택 공간 할당, 새로운 thread_info, task_struct 구조체를 생성한다.
- 생성할 때 부모의 process descriptor를 그대로 가져와서 생성한다.
- 프로세스 개수 제한을 넘었는지 검사한다.
자식 프로세스 구조체의 일부 멤버변수를 초기화. (상태=
TASK_UNINTERRUPTED
) copy_flag()
함수 호출task_struct
의flags
내용을 정리한다.PF_SUPERPRIV
플래그 초기화: 현재 수행하는 작업이 관리자 권한임을 의미.PF_FORKNOEXEC
플래그 초기화: 프로세스가 exec() 함수를 호출하지 않았음을 의미.
alloc_pid()
함수 호출- 자식 프로세스에게 새로운 PID값을 할당한다.
clone()
의 매개변수로 전달된 플래그에 따라 파일시스템 정보, signal handler, 주소공간, namespace 등을 share하거나 copy한다. (보통 스레드는 share를, 프로세스는 copy 한다.) 생성한 자식 프로세스의 포인터를 반환한다.
vfork()
시스템콜은 부모 프로세스의 page table을 copy하지 않는다는 점을 제외하면fork()
와 동일. 그러나 copy-and-write을 사용하는 리눅스 특성상fork()
대비 이득이 적어서 거의 사용하지 않는다.
- 대표적인 modern-programming 기법인 스레드는 공유 메모리를 가진 여러 프로그램을 ‘동시에’(concurrent) 수행해 multi-processor 환경에서는 진정한 병렬처리를 구현할 수 있다.
- 스레드는 개별
task_struct
를 갖고 메모리를 부모 프로세스와 공유하고 있는 정상 프로세스다. (리눅스는 프로세스와 스레드를 구분하지 않는다.) - 따라서 스레드도 내부적으로는 프로세스 생성 때와 똑같이
clone()
시스템콜을 이용한다. 여러 플래그를 parameter로 넘겨서 스레드의 특성을 부여할 뿐이다. (i.e.clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
;) <linux/sched.h>
의 최상단에 스레드 생성 관련 clone flags가 정의되어있다.
-
커널도 일부 동작은 백그라운드에서 실행하는 것이 좋은데, 이때 커널 공간에서만 존재하는 특별한 스레드인 ‘커널 스레드’를 이용한다.
-
가장 큰 차이점은 주소 공간이 없다는 점이다. (프로세스의 주소 공간을 가리키는 mm 포인터가 NULL이다.)
-
커널 스레드는
<linux/kthread.h>
에 정의돼있고,kthreadd
라는 최상위 부모 스레드가 모든 하위 커널 스레드를 만드는 방식으로 동작한다. -
커널 스레드는
kthread_run
매크로로kthread_create()
를 호출해clone()
시스템콜을 호출해 만든다.
- 프로세스는
main()
이 끝날 때 묵시적으로 또는 명시적으로exit()
를 호출하여 종료된다. exit()
함수는 내부적으로<kernel/exit.c>
에 정의된do_exit()
함수를 호출한다.- 프로세스 종료 과정은 아래와 같다.
void __noreturn do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
...
exit_signals(tsk); /* 1 - sets PF_EXITING */
acct_update_integrals(tsk); /* 2 */
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead) {
/*
* If the last thread of global init has exited, panic
* immediately to get a useable coredump.
*/
if (unlikely(is_global_init(tsk)))
panic("Attempted to kill init! exitcode=0x%08x\n",
tsk->signal->group_exit_code ?: (int)code);
...
tsk->exit_code = code; /* 3 */
taskstats_exit(tsk, group_dead);
exit_mm(); /* 4 */
if (group_dead)
acct_process();
trace_sched_process_exit(tsk);
exit_sem(tsk); /* 5 */
exit_shm(tsk);
exit_files(tsk); /* 6 */
exit_fs(tsk);
...
exit_notify(tsk, group_dead); /* 7 */
proc_exit_connector(tsk);
mpol_put_task_policy(tsk);
...
lockdep_free_task(tsk);
do_task_dead(); /* 8 */
}
- current의 flags의
PF_EXITING
플래그를 설정한다. acct_update_integrals()
함수를 호출해 종료될 프로세스 정보를 기록한다. current의 exit_code에exit()
함수에서 지정한 값에 따른 종료코드가 저장된다.exit_mm()
함수를 호출해 프로세스의 mm_struct를 반환해 자원 해제한다.exit_sem()
함수를 호출해 프로세스의 세마포어를 반환해 대기 상태를 해제한다.exit_files()
,exit_fs()
함수를 호출해 file descriptor 및 file system의 참조 횟수를 하나 감소한다. 참조 횟수가 0이면 해당 객체를 사용하는 프로세스가 없다는 의미이므로 자원 해제한다.exit_notify()
함수를 호출해 부모 프로세스에 signal을 보낸다. 이때 해당 프로세스가 자식 프로세스를 가지고 있었다면, 자신의 부모 프로세스 or 자신이 속한 스레드 group의 다른 스레드 or init 프로세스 중 하나를 부모로 설정한다.- current의 state을
TASK_DEAD
로 설정해 좀비 프로세스로 만든다.
- 부모 프로세스의 동작은 다음과 같다.
release_task()
함수를 호출해 더는 자식 좀비 프로세스가 필요없다고 커널에게 signal을 보낸다.release_task()
->__exit_signal()
->__unhashed_process()
->detach_pid()
__exit_signal()
에서 좀비 프로세스의 남은 정보도 완전히 메모리 반환한다.release_task()
는put_task_struct()
함수를 호출해 좀비 프로세스의stack
,thread_info
구조체,task_struct
구조체가 들어있던 페이지 및 slab cache를 반환한다. 이제 프로세스와 연관된 모든 자원이 해제돼 완전히 종료됐다.
-
부모 프로세스가 좀비가 된 자식 프로세스를 책임지고 종료하지 못할 때 리눅스의 유명한 문제인 ‘좀비 프로세스 문제’가 발생한다.
-
시스템 메모리를 낭비하는 문제가 발생하는 것이다.
-
따라서, 위 과정 중 8번에서 다뤘듯, 부모 프로세스가 없을 때 다른 부모 프로세스 후보들 중 하나를 선택해 부모로 설정해주는 과정이 반드시 필요하다.
-
do_exit()
함수에서exit_notify()
함수를 호출한다. -
exit_notify()
함수에서forget_original_parent()
함수를 호출한다.forget_original_parent()
함수는 종료할 프로세스의 부모 프로세스를 반환하는 함수다.- 이때, 부모 프로세스가 먼저 종료된 ‘문제 좀비 프로세스’인 경우, 적당한 부모 프로세스를 선택해주는 역할도 함께 한다.종료 프로세스가 속한 스레드 group 내에서 다른 스레드를 찾는다. 찾았다면, 해당 스레드를 부모로 만들고 반환한다.
- 만일 다른 스레드가 없다면, init 프로세스를 찾고 init 프로세스를 부모로 만들어서 반환한다.
-
부모 프로세스를 찾았으니 종료할 프로세스의 모든 자식 프로세스의 부모로 설정한다.
-
-
-
이로써, 좀비 프로세스를 적절히 종료하지 못해 발생하는 문제를 미연에 방지할 수 있다.
참고