KoreanFoodie's Study
프로세스 스케줄링과 시그널 본문
프로세스 스케줄링과 시그널에 대해 알아보자
먼저, 프로세스가 어떤 state를 가질 수 있는지부터 알아보도록 하자. 위 그림을 통해 하나하나 각 상태가 어떤 의미를 가지는지 나열해 보겠다.
- Running/Runnable(R)
이름에서 쉽게 유추할 수 있듯이, 이 상태는 프로세스가 동작하거나, 동작할 준비가 되었다는 것을 의미한다. 동작하고 있다는 것은 쉽게 이해가 가는데 동작할 준비가 되었다는 것은 무슨 뜻일까?
Runnable
상태의 프로세스는 runqueue
에 들어가서 실행되기를 기다린다. 이것이 유저 스페이스에서 프로세스가 실행될 수 있는 유일한 상태이다. 스케줄러는 해당 runqueue
를 확인한 후 CPU가 어떤 작업을 수행할지를 결정하게 된다.
- UnInterruptible Sleep(D)
이 상태는 시그널을 Blocking 한 상태로 잠을 자는 상태이다. 다만 SIGKILL과 SIGSTOP 시그널은 block 할 수 없다.
- Interruptible Sleep(S)
이 상태는 프로세스가 UnInterruptible Sleep과는 달리 incoming signal을 받을 수 있는 상태임을 의미한다.
- Stopped (T)
프로세스가 SIGSTOP시그널을 받았을 경우, 프로세스는 실행을 중지한 상태가 된다. 이 상태의 프로세스는 SIGCONT 시그널과 SIGKILL 시그널밖에 핸들링하지 않는다. 전자는 프로세스를 다시 실행상태로 돌려줄 것이고, 후자는 해당 프로세스를 종료시킬 것이다.
- Zombie (Z)
마지막으로 Zombie 상태는 조금 독특한데, 이는 해당 프로세스가 종료되었으나 아직 커널에 해당 프로세스에 대한 정보를 담고있는 리소스가 사라지지 않고 남아 있음을 의미한다. 프로세스가 실행되기 위해서는 task_struct
(프로세스 PID, 상태 등등의 정보를 담고 있는 구조체) 등의 리소스가 커널에 존재해야 하는데, 해당 프로세스가 종료했을 때 바로 이러한 리소스들이 사라지지 않는다.
이를 해결하기 위해서는 wait()
이나 waitpid()
함수를 이용하여 해당 프로세스를 reap
해 주는 과정이 필요하다.
프로세스가 시그널을 받았을 경우, 하는 행동은 총 3가지의 경우로 나뉘게 된다.
해당 시그널에 대한 default action
해당 시그널을 block (sigprocmask
등을 이용. 즉 시그널이 프로세스에 영향을 끼치지 않는다.)
custom handler가 signal을 핸들링 (프로그래머가 원하는 방식으로 동작하도록 코드를 짤 수 있다)
리눅스에서 시그널의 종류는 총 64개인데, 이 중 자주 쓰이는 몇가지만 살펴보도록 하자. 시그널에 대한 더 자세한 포스트는 여기를 참고하자.
SIGKILL : 프로세스를 강제 종료시킨다. Block되거나 핸들링될 수 없다.
SIGSTOP : 현재 프로세스를 중지시킨다.
<Ctrl>+z
를 통해 터미널에서 이 시그널을 보낼 수 있다.SIGCONT : 중지된 프로세스를 다시 실행시킨다.
SIGINT : 현재 프로세스를 interrupt시키고 사용자의 다음 command를 기다리도록 한다.
<Ctrl>+c
를 통해 이 시그널을 보낼 수 있다.SIGCHLD : 자식 프로세스가 종료했을때, 부모 프로세스로 이 시그널을 보낸다.
이제 예제를 통해 프로세스의 상태가 변하는 과정을 살펴보자.
일반적으로 커널은 단 하나의 프로세스만을 사용하지 않는다. 코어의 갯수 등 기타 여러 변수에 따라 다르겠지만, 여러 프로세스가 동시에 동작하는 경우, 스케줄러라고 불리는 녀석은 일정한 time slice
에 맞춰 어떤 프로세스가 언제 실행될지를 조정해 준다. 즉, 매우 짧은 간격을 두고 프로세스 A가 실행되었다가 프로세스 B가 실행되었다가를 반복함으로써 사용자에게는 두 프로세스가 동시에 일을 수행하고 있는 것처럼 보이는 것이다.
보통 프로세스를 재울 때는 이런 방식을 이용한다.
set_current_state(TASK_INTERRUPTIBLE); // signal을 받을 경우. 일반적으로 이 경우가 많이 쓰인다
// set_current_state(TASK_UNINTERRUPTIBLE); // signal을 받지 않을 경우
schedule(); // 프로세스가 자기 시작한다
깨울 때는 잠든 프로세스가 스스로 일어날 수 없으며, 다른 프로세스가 잠이 든 프로세스를 깨워 주어야 한다.wake_up_process();
함수를 사용하면 된다.
다만 multicore processor의 경우, 두 프로세스가 원하지 않는 동작을 수행할 수도 있다.
예를 들어, 프로세스 A가 set_current_state()
를 호출하기 전에 프로세스 B가 wake_up_process()
를 호출한다면 프로세스 A는 잠에서 깨어나지 않는다.
이 경우를 Lost Wakeup
이라고 부르기도 하는데, 간단한 lock을 이용함으로써 이 문제를 해결할 수 있다.
프로세스 A 코드를 조금 바꿔보자.
// At this point, condition_check = 1;
set_current_state(TASK_INTERRUPTIBLE); // signal을 받지 않을 경우
mutex_lock(&lock);
if (condition_check) {
mutex_unlock(&lock);
schedule();
mutex_lock(&lock);
}
set_current_state(TASK_RUNNING);
mutex_unlock(&lock);
마찬가지로 B의 코드도 조금 바꿔보자.
mutex_lock(&lock);
condition_check = 0;
mutex_unlock(&lock);
wake_up_process(process_a_ts);
이렇게 되면, wake_up_process()
함수는 프로세스의 상태를 TASK_RUNNING
으로 바꾸므로, Lost Wakeup
문제가 해결된다.
리눅스는 이 문제를 해결하기 위해 아래와 같이 sched.c 파일을 구현했다. (linux-2.6.11/kernel/sched.c: 4254) 더 자세한 정보는 이 링크에서 알아보면 좋다.
4253 /* Wait for kthread_stop */
4254 set_current_state(TASK_INTERRUPTIBLE);
4255 while (!kthread_should_stop()) {
4256 schedule();
4257 set_current_state(TASK_INTERRUPTIBLE);
4258 }
4259 __set_current_state(TASK_RUNNING);
4260 return 0;
'OS' 카테고리의 다른 글
윈도우즈 시스템 프로그래밍 - 파일 입출력 예제 (0) | 2019.04.25 |
---|---|
리눅스 쉘에 환경변수 경로 추가하기 (0) | 2019.04.25 |
리눅스에서 프로세스 강제 종료하기 + 포트 번호 조회하기 (0) | 2019.04.25 |
Implement a simple linux shell with pipe and more! (0) | 2019.04.23 |