KoreanFoodie's Study

프로세스 스케줄링과 시그널 본문

OS

프로세스 스케줄링과 시그널

GoldGiver 2019. 4. 25. 20:21

프로세스 스케줄링과 시그널에 대해 알아보자


 

 

먼저, 프로세스가 어떤 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;
Comments