프로세스와 스레드의 관계
리눅스에서 프로세스는 자원 할당의 기본 단위이며, 스레드는 실행의 기본 단위이다. 모든 스레드는 프로세스 내에서 동작하며, 같은 주소 공간과 시스템 자원을 공유한다. 커널 입장에서는 스레드도 하나의 독립된 작업 단위(task)로 간주되며,
task_struct 구조체로 표현된다.
프로세스의 내부 표현: task_struct
커널은 각 프로세스를
struct task_struct 타입의 구조체로 관리한다. 이 구조체는 프로세스 상태, 우선순위, 메모리 맵핑, 파일 디스크립터 테이블, 부모/자식 관계 등 수많은 정보를 포함한다. 모든 활성 프로세스는 이 구조체 인스턴스들로 구성되어 있으며, 이들은 연결 리스트를 통해 전역적으로 연결되어 있다.
메모리 할당 방식: Slab 할당기
task_struct 인스턴스는 커널 스택 근처가 아니라 Slab 할당기를 통해 할당된다. 이는 객체 재사용을 통한 성능 향상과 메모리 조각화 방지를 위한 설계이다. 고정 크기의 캐시에서 빠르게 할당 및 해제가 가능하다.
현재 실행 중인 프로세스 참조하기
x86 아키텍처와 같이 전용 레지스터가 없는 플랫폼에서는 커널 스택 하단에
thread_info 구조체를 배치하고, 스택 포인터의 특정 비트 마스크 연산을 통해 현재 프로세스의
task_struct를 계산해낸다. 이를 추상화한 매크로가
current이며, 어떤 코드가 실행 중일 때 그를 수행하는 프로세스를 가리킨다.
프로세스 상태 모델
프로세스는 여러 상태를 가지며, 대표적인 상태는 다음과 같다:
- 실행 대기(Ready): CPU 할당 가능 상태
- 실행 중(Running): 현재 CPU에서 실행 중
- 대기 중(Waiting): I/O 완료 또는 신호 도착 대기
- 인터럽트 가능 대기 (TASK_INTERRUPTIBLE)
- 인터럽트 불가 대기 (TASK_UNINTERRUPTIBLE)
- 중지됨(Stopped): SIGSTOP 등의 신호로 일시 정지
- 종료됨(Zombie): 종료되었으나 부모가 종료 상태를 수거하지 않음
상태 전이는
set_current_state() 또는
set_task_state() 함수로 제어된다.
시스템 호출과 컨텍스트
애플리케이션이 시스템 호출을 수행하면 커널 모드로 전환되며, 이때 커널은 해당 프로세스의 "컨텍스트"에서 동작하게 된다. 이 컨텍스트 내에서는
current 매크로를 통해 현재 작업의 상태를 조회하거나 수정할 수 있다.
프로세스 계층 구조
모든 프로세스는 트리 형태의 계층 구조를 형성하며, 루트는
init 프로세스(pid=1)이다. 다음은 관련 순회 예제이다:
// 자식 프로세스 순회
struct task_struct *child;
struct list_head *pos;
list_for_each(pos, ¤t->children) {
child = list_entry(pos, struct task_struct, sibling);
printk(KERN_INFO "Child: %s [%d]\n", child->comm, child->pid);
}
// 전체 프로세스 리스트 순회
struct task_struct *task;
for_each_process(task) {
printk(KERN_INFO "Process: %s [%d]\n", task->comm, task->pid);
}
// init 프로세스까지 거슬러 올라가기
struct task_struct *walker = current;
while (walker->parent != walker) {
walker = walker->parent;
}
// walker 는 이제 init_task
복제 전략: Copy-on-Write
fork() 시점에서 물리적 메모리 복사는 발생하지 않는다. 부모의 페이지 테이블만 복사되고, 모든 페이지는 읽기 전용으로 설정된다. 이후 어느 한쪽이 쓰기 접근을 시도하면 페이지 폴트가 발생하고, 그때 비로소 실제 데이터 복사가 이뤄진다. 이를 통해 초기 오버헤드를 최소화한다.
복제 기반 시스템 호출
모든 프로세스 생성은 내부적으로
do_fork()로 수렴되며, 차이점은 클론 플래그에 있다:
- fork():
clone(SIGCHLD, 0) — 자원 전부 복사, COW 적용
- vfork():
clone(CLONE_VM | CLONE_VFORK, 0) — 자식이 exec() 하거나 종료할 때까지 부모는 블로킹, 주소 공간 공유
- pthread_create():
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, ...) — 스레드처럼 행동
커널 스레드
커널 내부에서 백그라운드 작업을 수행하는 특수한 프로세스로, 사용자 영역 주소 공간이 없다(
mm 필드가 NULL). 다음 매크로로 생성 가능:
#define kthread_run(fn, arg, fmt, ...) \
({ \
struct task_struct *__k = \
kthread_create(fn, arg, fmt, ##__VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
종료는
kthread_stop()로 요청되며, 타겟 스레드는 스스로 종료 루프를 체크해야 한다.
프로세스 종료와 정리 과정
exit() 시스템 호출은 다음과 같은 절차를 따른다:
do_exit() 진입 후 더 이상 스케줄링되지 않도록 설정
- 자원 해제 (파일 디스크립터, 메모리 맵 등)
exit_notify() 호출로 부모에게 종료 알림
- 상태가 ZOMBIE로 전환됨
자식 프로세스 상태 수거
부모는
wait(),
waitpid() 등을 통해 종료된 자식의 종료 코드를 수거하고, 커널은 이때
release_task()를 호출하여
task_struct와 스택 메모리를 완전히 해제한다. 수거되지 않은 자식은 고아 존버(zombie) 상태로 남아 시스템 리소스를 소모한다.
고아 프로세스의 보호
부모가 먼저 종료된 경우, 자식은 고아가 된다. 커널은
find_new_reaper()를 호출하여 새로운 보호자(parent reaper)를 지정한다. 일반적으로
init 프로세스가 이를 담당하며, 주기적으로
wait()를 호출해 고아 자식들을 정리한다.
PID 관리
PID는 시스템 내 유일한 식별자로, 기본 최대값은 32768이다.
/proc/sys/kernel/pid_max를 수정하면 최대 4194304까지 확장 가능하다. 커널은 PID 해시 테이블(
pidhash)을 사용해 빠른 검색을 지원하며,
detach_pid()를 통해 리스트에서 제거한다.