본문 바로가기

기술/Linux

리눅스 커널 심층 분석 개정 3판 Linux Kernel Development Third Edition) Chapter 3. 프로세스 관리 Process Management 정리

리눅스 커널 심층 분석 개정 3판 : 3장. 프로세스 관리 

Linux Kernel Development Third Edition : Chapter 3. Process management

 

리눅스 커널 심층분석

이 책은 리눅스 커널의 핵심을 간결하면서도 심도있게 다루고 있다. 일반적인 운영체제에 대한 이해를 넘어, 여타 유닉스 시스템과 다른 리눅스만의 특징적인 부분에 대한 설계, 구현, 인터페이

book.naver.com

Notion에서 보기 


  1. Process (=task)

: 실행 중인 프로그램

사용 중인 파일, 대기중인 시그널, 커널 내부 데이터, 프로세서 상태, 실행 중인 스레드 등 모든 자원 포함

  • 리눅스에서 fork()로 프로세스 생성

: 존재하는 프로세스를 duplicate / 부모 프로세스를 호출

-> 새로운 프로세스 = 자식 프로세스

그래서 fork()는 커널로부터 반환 두번 (부모 + 자식)

  • 리눅스에서 exit()로 프로세스 종료 + 할당받은 자원 해제

: 부모 프로세스가 wait4() 시스템콜을 통해 종료된 자식 프로세스의 상태를 얻어옴

**wait4() = 프로세스가 특정 프로세스의 종료를 기다리게 하는 시스템콜

부모 프로세스가 wait()를 호출할떄까지 종료된 프로세스(자식 프로세스) = 좀비 프로세스

  • 스레드 : 프로세스 내부에서 동작하는 객체
  • 개별적으로 프로그램 카운터, 스택, 레지스터 가짐
  • 스택을 제외한 모든 address space 공유
  • 커널이 개별적 스레드를 관리 (프로세스 X)
  • 리눅스에서 스레드 = 프로세스
  1. 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 : 각 아키텍쳐마다 독립적으로 구현되어야 함.

  1. 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되는 상태
  1. ex) 디버거

using ptrace

  • _TASK_STOPPED: 프로세스 실행이 멈춘 상태, 실행하기에 적절하지 않은 상태

SIGTSTP, SIGTTIN, SIGTTOU 신호를 받은 경우 발생

디버깅 중에 어떤 SIGNAL이라도 받은 경우

  • 커널 코드가 프로세스 상태 바꾸는 매커니즘:

set_task_state (task, state);

= task를 state 상태로 만들어라

  1. 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로를 통해서만 접근가능

  1. 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, &current->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들은 환형 이중 연결리스트로 연결되어 있기 때문에 단순 반복문을 통해 시스템의 모든 프로세스를 살펴 보는 것이 특정 프로세스로 바로 찾는 것보다 좋음 / 너무 많은 프로세스가 있는 경우에 반복문은 비효율적

  1. 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 생성
  1. 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를 얻기 때문에 더 이상 이점이 없음 + 구현 어려움

  1. 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

  1. 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()을 호출하고 좀비 상태 치워버림