프로세스 제어

mydailylogs
|2022. 12. 7. 00:50

Unix는 C 프로그램으로부터 프로세스를 제어하기 위한 많은 시스템 콜을 제공한다. 해당 글에서는 프로세스를 제어하는 많은 함수들을 설명하고 이를 어떻게 사용하는지 예제를 통해 알아보고자 한다.

프로세스의 생성과 종료

프로그래머의 관점에서 프로세스는 다음의 세 가지 상태 중의 하나로 분류할 수 있다.

그림 1. 프로세스의 상태 전이도

  • 실행중(Running): 프로세스는 CPU에서 실행하고 있거나 실행을 기다리고 있으며, 궁극적으로 커널에 의해서 스케쥴될 것이다. 해당 그림에서 ready와 running을 묶은 개념이다.
  • 정지(Stopped): 프로세스의 실행은 정지된 상태이고 스케쥴되지 않는다. 프로세스는 SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 시그널을 바게되면 그 결과로 정지며, SIGCONT 시그널을 받을 때까지 정지 상태로 남아있으며, 이 시그널을 받은 시점에 다시 시작할 수 있다. (Waiting 상태라고 생각할 수 있다.)
  • 종료(Terminated): 프로세스가 영구적으로 정지된 상태이다. 프로세스는 다음의 세가지 이유 중 하나로 종료된다. (1) 프로세스를 종료하는 시그널, SIGTERM, SIGINT 을 받았을 때, (2) 메인 루틴에서 리턴하여 정해진 동작이 끝났을 때, (3) exit 함수를 호출할 때

fork()

부모 프로세스는 fork 함수를 불러서 자식프로세스를 생성할 수 있다.

새롭게 생성된 자식 프로세스는 완벽히는 아니지만 부모와 동일하다. 자식은 텍스트, 데이터, bss 세그먼트, 힙, 사용자 스택을 포함하는 부모의 사용자 수준 가상 주소공간과 동일한 복사본을 갖는다.

자식은 또한 부모가 오픈한 파일 디스크립터에 대해서 동일한 사본을 갖는다. 이는 부모가 fork를 호출했을 때 자식이 부모가 오픈한 파일 모두를 읽고 쓸 수 있다는 것을 의미한다. 부모와 새롭게 생성된 자식 간의 중요한 차이는 PID가 다르다는 점이다.

1) fork 함수는 한 번 호출되지만 두 번 리턴된다. 이 부분이 fork를 가장 헷갈리게하는데. 사실 당연하지만 처음 들으면 한 번쯤 헷갈릴 만한 주제이다. fork를 호출하면 부모에서 자식의 PID를 리턴한다. 한편, 자식에서는 fork의 리턴 값으로 0을 받아 본인이 해당 fork로부터 생성된 프로세스라는 것을 알게 된다.

2) fork 함수에서 부모와 자식은 동시에 실행된다. fork는 부모와 자식 프로세스가 동시적으로 실행되게 한다. 이때 각각의 프로세스는 독립된 논리 흐름으로서 별도의 작업 없이는 어떤 작업이 먼저 수행될지 알 수 없다. 이는 전적으로 커널의 스케쥴러의 역할이다.

3) 자식은 부모의 사적 주소 공간을 복사한다. 만일 부모와 자식을 각 프로세스에서 fork 함수가 리턴한 직후에 중단할 수 잇다면, 각 프로세스의 주소공간이 동일하다는 것을 알 수 있을 것이다. 그러나 fork가 호출된 이후, 부모와 자식 프로세스는 각자의 진행에 따라 사적 주소 공간의 구조가 변화하게 된다. 이러한 변화는 본인의 컨텍스트에서만 확인할 수 있으며, 다른 프로세스의 메모리에서는 확인이 불가능할 것이다.

4) 파일 디스크립터 테이블은 공유된다. 자식은 부모가 오픈한 모든 파일을 상속받는다. 부모가 fork를 호출할 때를 기점으로 파일 디스크립터 테이블은 그대로 상속되어 복사되며, 분기 이후의 파일 디스크립터 테이블은 당연하게도 서로 독립적으로 관리된다.


자식 프로세스의 청소

프로세스가 어떤 이유로 종료할 때, 커널은 프로세스를 즉시 제거하지 않는다. 대신 프로세스는 부모가 청소할 때까지 종료된 상태로 남아 있는다. 부모가 종료된 자식을 청소할 때 커널은 자식의 exit 상태를 부모에게 전달하며 그 후 종료된 프로세스를 없애며 이 시점에서 프로세스가 사라지게 된다.
만약 청소 과정이 생략된 채 프로세스가 종료되었다면 이를 좀비 프로세스라고 부르며, 비록 좀비들은 실행되고 있지 않을 지라도 이들은 여전히 계속해서 시스템 메모리 자원을 점유하게 된다. 만일 부모 프로세스가 좀비가 된 자신의 자식 프로세스를 청소하지 않고 종료하면, 커널은 init 프로세스로 하여금 이들을 청소하도록 한다. 

init 프로세스는 PID가 1인 프로세스이며, 시스템 초기화 시에 커널에 의해 생성되는 프로세스이다.

 

자식 프로세스를 청소하는 방법에는 두가지(wait, waitpid)가 있는데, 해당 글에서는 waitpid 를 활용하여 자식 프로세스를 청소하는 과정을 소개하고자 한다. 

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
        Returns: PID of child if OK, 0 if WNOHANG and no child ready, -1 on error

 

1) pid : 각각의 경우 waitpid에서의 처리가 다르다

  • pid > 0 : pid 값이 동일한 child process를 기다린다.
  • pid == 0 : group pid (gpid)가 같은 프로세스를 기다린다.
  • pid == -1 : 임의의 child process를 기다린다.
  • pid < -1 : pid와 절댓값이 같은 group pid를 가진 자식 프로세스를 기다린다.

2) status : waitpid에 의해 기다려진 프로세스의 정보를 담게 된다.

3) options : waitpid의 동작을 설정할 수 있다.
설정하지 않을 경우 0이다.

  • WNOHANG : 기다리고자 pid의 프로세스에 대해 모두 종료되지 않았으나 기다리지 않고 waitpid 이후의 코드를 실행하고자 할 때 사용하는 옵션이다. 기다리지 않았을 경우 waitpid의 반환 값은 0이다. (NON-BLOCKING)
  • WUNTRACED : Stop 된 proccess들에 대해서도 waitpid에서 처리를 한다.
  • WCONTINUED : Continue 된 process들에 대해서도 waitpid에서 처리를 한다.

에러조건

호출하는 자식 프로세스가 없다면 -1 리턴, errno를 ECHILD로 설정. 어떤 시널에 의해 waitpid가 중단되었다면 -1을 리턴, errno를 EINTR로 설정

 

'CS > OS' 카테고리의 다른 글

쓰레드  (1) 2022.12.07
프로세스를 이용한 동시성 프로그래밍  (0) 2022.12.07
시스템 콜의 에러 처리  (0) 2022.12.07
컨텍스트 스위치  (0) 2022.12.06
프로세스  (0) 2022.12.06