파일과 마찬가지로 프로세스는 유닉스(UNIX®) 운영체제에서 기초다. 프로세스는 실행 가능한 파일의 명령어를 수행하는 살아있는 존재다. 명령 수행 이외에, 프로세스는 특히 열린 파일, 프로세서 문맥, 주소 공간, 프로그램과 관련한 자료 등을 관리한다. 리눅스 커널은struct task_struct
로 정의된 프로세스 기술자에서 완전한 프로세스 관련 정보를 보관한다. 리눅스 커널 원시 코드인 include/linux/sched.h에 들어있는 struct task_struct
의 다양한 필드를 확인하자.
생명 주기 동안, 프로세스는 상호 배타적인 상태 집합을 오간다. 커널은 프로세스 상태를 struct task_struct
의 state 필드에 보관한다. 그림 1은 프로세스 상태 사이에 일어나는 전이 상태를 보여준다.
그림 1. 프로세스 상태 전이
다양한 프로세스 상태를 검토해보자.
TASK_RUNNING
: 프로세스는 CPU에서 동작하고 있거나 스케줄을 위해 실행 큐에서 대기하고 있다.TASK_INTERRUPTIBLE
: 프로세스는 잠든 상태이며, 사건이 일어나기를 기다리고 있다. 프로세스는 시그널이 인터럽트하도록 열려 있다. 일단 시그널을 받거나 명시적인 깨어나기 호출로 깨어나면, 프로세스 상태가TASK_RUNNING
으로 전이한다.TASK_UNINTERRUPTIBLE
: 프로세스 상태는TASK_INTERRUPTIBLE
과 비슷하다. 하지만 이 상태에서 프로세스는 시그널을 처리하지 않는다. 몇몇 중요한 작업을 완료하는 중간에 있기에 이 상태에 있을 경우 프로세스를 인터럽트하는 상황이 그리 바람직하지 않을 가능성도 있다. 기다리고 있던 사건이 발생할 때, 프로세스는 명시적인 깨어나기 호출로 깨어난다.TASK_STOPPED
: 프로세스 수행이 멈췄으며 동작하지 않으며 동작할 수도 없다.SIGSTOP
,SIGTSTP
같은 시그널을 받으면, 프로세스는 이 상태에 도달한다. 프로세스가SIGCONT
시그널을 받으면 다시 한번 동작 가능한 상태로 바뀐다.TASK_TRACED
: 디버거와 같은 다른 프로세스가 감시 중일 때 프로세스가 이 상태에 도달한다.EXIT_ZOMBIE
: 프로세스가 종료되었다. 자식 프로세스는 어버이가 몇 가지 통계 정보를 수집하기를 기다리면서 질질 끄는 상태다.EXIT_DEAD
: (글자 그대로) 최종 상태다. 어버이가wait4()
나waitpid()
시스템 호출을 사용해 모든 통계 정보를 수집했기에 프로세스를 시스템에서 제거 중일 때 이 상태에 도달한다.
프로세스 상태 전이에 대한 상세한 정보는 참고자료에 소개한 The Design of the UNIX Operating System을 읽어보자.
이미 언급했듯이 TASK_UNINTERRUPTIBLE
과 TASK_INTERRUPTIBLE
이 프로세스 잠들기 상태다. 이제 프로세스를 잠들도록 만들기 위해 커널이 제공하는 메커니즘을 살펴볼 차례다.
리눅스 커널은 프로세스를 잠들기 상태로 만드는 두 가지 방법을 제공한다.
프로세스를 잠들게 만들려면 프로세스 상태를 TASK_INTERRUPTIBLE
이나 TASK_UNINTERRUPTIBLE
로 설정하고 스케줄러 함수인schedule()
을 호출하는 일반적인 방식을 따른다. 이렇게 하면 프로세스가 CPU 실행 큐에서 제거된다. (상태를 TASK_INTERRUPTIBLE
로 설정해) 프로세스가 인터럽트 가능한 모드에서 잠들고 있다면, 명시적인 깨우기 호출(wakeup_process()
)이나 처리가 필요한 시그널로 깨울 수 있다.
하지만 (상태를 TASK_UNINTERRUPTIBLE
로 설정하는 방법으로) 프로세스가 인터럽트 불가능 모드에서 잠들고 있다면, 명시적인 깨우기 호출로만 깨울 수 있다. (시그널 처리가 아주 어려울 때 디바이스 I/O 수행 중인 경우처럼) 진짜로 필요하지 않는 이상 인터럽트가 불가능한 프로세스 잠들기 모드보다는 인터럽트가 가능한 프로세스 잠들기 모드를 사용하는 편이 바람직하다.
인터럽트 가능한 상태에서 잠든 태스크가 시그널을 받으면, (이미 마스크되지 않았다면!) 태스크는 시그널을 처리할 것이다. 이 때 진행하던 일을 남겨두고(물론 정리 코드가 필요하다) 사용자 영역으로 -EINTR
을 반환한다. 당연한 이야기지만, 반환 코드를 검사해서 적절하게 처리할 책임은 프로그래머에게 있다. 따라서 게으른 프로그래머가 프로세스를 인터럽트 불가능한 잠들기 모드로 두는 편을 선호할지도 모르는 이유는 시그널이 태스크를 깨우지 못하기 때문이다. 하지만 이유 불문하고 인터럽트가 불가능한 잠들기 상태에 있는 프로세스에 깨우기 호출이 일어나지 않는 경우를 주의해야 한다. 이렇게 되면 프로세스에 kill을 보낼 수 없으므로 궁극적으로는 시스템 재시동이 유일한 해결책이 되어버리는 황당한 상황이 벌어진다. 커널과 사용자 영역에 버그를 심을 가능성이 있기 떄문에 몇 가지 세부 사항에 신경을 써야 한다. 또한 (차단된 상태에서 kill이 불가능한 프로세스인) 불사신을 만들어 낼지도 모른다.
이제 커널에서 잠들기와 관련한 새로운 방식을 소개할 차례다!
리눅스 커널 2.6.25는 새로운 프로세스 잠들기 상태인 TASK_KILLABLE
을 도입했다. 프로세스가 이런 새로운 상태에서 kill이 가능한 잠들기에 빠져 있다면, TASK_UNINTERRUPTIBLE
특성에다 치명적인 시그널에 반응하는 보너스까지 더한 방식으로 동작한다. Listing 1에 커널 2.6.18에서 2.6.26으로 올라가면서 변경된 프로세스 상태(include/linux/sched.h에 정의된 내용)를 비교해놓았다.
Listing 1. 2.6.18과 2.6.26 사이에 달라진 프로세스 상태 비교
리눅스 커널 2.6.18 리눅스 커널 2.6.26 ================================= =================================== #define TASK_RUNNING 0 #define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define TASK_UNINTERRUPTIBLE 2 #define TASK_STOPPED 4 #define __TASK_STOPPED 4 #define TASK_TRACED 8 #define __TASK_TRACED 8 /* in tsk->exit_state */ /* in tsk->exit_state */ #define EXIT_ZOMBIE 16 #define EXIT_ZOMBIE 16 #define EXIT_DEAD 32 #define EXIT_DEAD 32 /* in tsk->state again */ /* in tsk->state again */ #define TASK_NONINTERACTIVE 64 #define TASK_DEAD 64 #define TASK_WAKEKILL 128 |
TASK_INTERRUPTIBLE
과 TASK_UNINTERRUPTIBLE
상태는 바뀌지 않았음에 주목하자. TASK_WAKEKILL
은 치명적인 시그널을 받을 때 프로세스를 깨우도록 설계되어 있다.
Listing 2는 (TASK_KILLABLE
정의와 더불어) TASK_STOPPED
와 TASK_TRACED
상태가 바뀐 방식을 보여준다.
Listing 2. 커널 2.6.26에 등장한 새로운 상태 정의
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) #define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED) #define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED) |
다시 말해, TASK_UNINTERRUPTIBLE
+ TASK_WAKEKILL
= TASK_KILLABLE
이다.
TASK_KILLABLE을 사용하는 새로운 커널 API
새로운 상태를 사용하는 몇 가지 새로운 함수를 살펴보자.
int wait_event_killable(wait_queue_t queue, condition);
이 함수는 include/linux/wait.h에 정의되어 있다. 이 함수는condition
이 참이 될 때까지queue
에서 kill 가능한 상태로 호출한 프로세스를 잠들게 만든다.long schedule_timeout_killable(signed long timeout);
이 함수는 kernel/timer.c에 정의되어 있다. 이 루틴은 기본적으로 현재 태스크 상태를TASK_KILLABLE
로 설정하고schedule_timeout()
을 호출한다. 이렇게 되면 호출한 태스크가jiffiestimeout
숫자만큼 잠든다(유닉스 시스템에서, jiffy는 연속적인 두 클럭 틱 사이에 벌어진 시간이다).int wait_for_completion_killable(struct completion *comp);
kernel/sched.c에 정의되어 있다. 이 루틴은 이벤트 완료를 위해 kill이 가능한 상태에서 대기하도록 사용된다. 이 함수는 기다리고 있는 치명적인 시그널이 없을 경우MAX_SCHEDULE_TIMEOUT
(LONG_MAX
와 똑같은 값으로 정의되어 있다) jiffies가 지나고schedule_timeout()
을 호출한다.int mutex_lock_killable(struct mutex *lock);
kernel/mutex.c에 정의되어 있다. 이 루틴은 뮤텍스 잠금을 획득하는 데 사용한다. 하지만 잠금을 얻지 못하는 상황에서 태스크가 잠금을 얻으려고 기다리는 도중에 치명적인 시그널을 받으면, 태스크는 시그널 처리를 위해 뮤텍스 잠금을 기다리는 대기 목록에서 제거된다.int down_killable(struct semaphore *sem);
kernel/semaphore.c에 정의되어 있다. 이 루틴은 세마포어sem
을 획득하는 데 사용한다. 세마포어를 얻지 못하면 잠들기에 빠진다. 치명적인 시그널이 도착하면, 대기자 목록에서 빠지며, 시그널에 반응해야 한다. 세마포어를 획득하는 다른 두 가지 메서드는down()
과down_interruptible()
이다.down()
은 이제 한물간 함수이기에down_killable()
이나down_interruptible()
을 사용해야 한다.
이런 새로운 프로세스 상태를 사용하도록 NFS 클라이언트 코드에 몇 가지 변화가 필요하다. Listing 3은 리눅스 커널 2.6.18과 2.6.26 사이에 달라진 nfs_wait_event
매크로의 차이점을 보여준다.
Listing 3. TASK_KILLABLE 때문에 nfs_wait_event에서 달라진 사항
리눅스 커널 2.6.18 리눅스 커널 2.6.26 ========================================== ============================================= #define nfs_wait_event(clnt, wq, condition) #define nfs_wait_event(clnt, wq, condition) ({ ({ int __retval = 0; int __retval = wait_event_killable(wq, condition); if (clnt->cl_intr) { __retval; sigset_t oldmask; }) rpc_clnt_sigmask(clnt, &oldmask); __retval = wait_event_interruptible(wq, condition); rpc_clnt_sigunmask(clnt, &oldmask); } else wait_event(wq, condition); __retval; }) |
Listing 4는 리눅스 커널 2.6.18과 2.6.26을 비교해 달라진 nfs_direct_wait()
함수 정의를 보여준다.
Listing 4. TASK_KILLABLE 때문에 nfs_direct_wait()에서 달라진 사항
리눅스 커널 2.6.18 ================================= static ssize_t nfs_direct_wait(struct nfs_direct_req *dreq) { ssize_t result = -EIOCBQUEUED; /* Async requests don't wait here */ if (dreq->iocb) goto out; result = wait_for_completion_interruptible(&dreq->completion); if (!result) result = dreq->error; if (!result) result = dreq->count; out: kref_put(&dreq->kref, nfs_direct_req_release); return (ssize_t) result; } 리눅스 커널 2.6.26 ===================== static ssize_t nfs_direct_wait(struct nfs_direct_req *dreq) { ssize_t result = -EIOCBQUEUED; /* Async requests don't wait here */ if (dreq->iocb) goto out; result = wait_for_completion_killable(&dreq->completion); if (!result) result = dreq->error; if (!result) result = dreq->count; out: return (ssize_t) result; } |
이런 새로운 기능을 활용하기 위해 NFS 클라이언트에 변경된 사항을 좀 더 살펴보려면 참고자료에 나온 리눅스 커널 메일링 리스트를 참조하자.
초기 NFS 마운트 옵션인 intr
은 몇 가지 사건을 기다리는 NFS 클라이언트 프로세스를 인터럽트하는 길을 열어 놓았지만 (TASK_KILLABLE
처럼) 심각한 시그널뿐만 아니라 모든 인터럽트를 허용한다.
심지어 이 기사에서 소개하는 기능이 현존하는 다른 옵션에 비해 일반적인 개선이 있었더라도, 결국 프로세스가 꼼짝달싹하지 못하는 상황을 피하는 다른 수단에 불과하며, 일반적으로 활용되기까지는 시간이 필요하다. (전통적인 TASK_UNINTERRUPTIBLE
을 사용해) 명시적인 깨우기 호출 이외에 다른 인터럽트를 허용하지 않도록 만드는 정책이 진짜로 필요하지 않은 이상 새롭게 도입된TASK_KILLABLE
을 활용하는 방식이 바람직하다는 사실만 기억하자.
교육
TASK_KILLABLE
프로세스 상태는 2002년 David Howells가 제시한 쟁점에서 출발했다. Howells는 OpenAFS 파일 시스템 드라이버가 사건을 기다리는 동안 인터럽트가 가능한 상태에서 모든 시그널을 차단하는 방법을 설명했다. 여기서 프로세스는 실제로TASK_UNINTERRUPTIBLE
상태에서 대기해야 한다는 사실이 중요하다.
- Jonathan Corbet이 쓴
TASK_KILLABLE
(LWN.net, 2008년 7월)은 유용한 소개 기사다.
- "Kernel Korner: Sleeping in the Kernel"(Linux Journal, 2005년 7월)은 리눅스 커널에서 잠들기 활용법을 설명한다.
- The Design of the UNIX Operating System(Prentice Hall, 1986년, Maurice J. Bach)에서 6장은 프로세스 상태 전이를 아주 자세하게 다룬다.
- 리눅스 커널 메일링 리스트에서 "NFS Killable tasks request comments on patch" 스레드는 새로운 2.6.26
TASK_KILLABLE
함수의 장점을 활용하기 위해 설계된 NFS 클라이언트 변경 사항을 소개한다.
- "리눅스 커널 분석"(한국 developerWorks, 2008년 4월): 커널 구조를 만든 이유와 방법을 개괄한다.
- developerWorks 리눅스 영역에서, (리눅스 프로그래밍과 시스템 관리자를 맡은 초보 개발자를 포함하여) 리눅스 개발자를 위한 자료를 찾아보고, 인기있는 기사와 튜토리얼을 검색해보자.
- 리눅스 팁과 리눅스 튜토리얼을 developerWorks에서 읽어보자.
- developerWorks 기술 행사와 웹 캐스트를 놓치지 말자.
'Enginius > Linux' 카테고리의 다른 글
CFS(completely fair scheduler) vs DWRR(distributed weighted round robin) (0) | 2011.08.12 |
---|---|
Load balance in Linux source level bottom-up analysis (0) | 2011.08.09 |
리눅스 커널 실시간 스케줄링 우선순위 (0) | 2011.08.01 |
리눅스 커널 스케쥴링 영역과 클래스 (0) | 2011.08.01 |
perf top 사용하기 (0) | 2011.08.01 |