[운영체제] Chapter 3.3 Operations on Processes

강현주·2025년 2월 7일

대부분의 시스템의 프로세스들은 동시에 실행될 수 있고, 동적으로 생성되고 삭제될 수 있다. 따라서, 시스템은 프로세스 생성과 종료 메커니즘을 제공해야한다.

3.3.1 Process Creation

실행 과정에서, 프로세스는 여러 개의 새로운 프로세스를 생성할 수 있다. 앞서 언급했듯이, 생성하는 프로세스를 부모 프로세스라고 하며, 새로운 프로세스는 해당 프로세스의 자식이라고 한다. 이러한 새 프로세스 각각은 다른 프로세스를 생성하고, 프로세스 트리를 형성할 수 있다.
대부분의 운영체제(UNIX, Linux, Windows 포함)는 프로세스를 일반적으로 정수인 고유한 프로세스 식별자(또는 pid)에 따라 식별한다. pid는 시스템의 각 프로세스에 고유한 값을 제공하며, 커널 내에서 프로세스의 다양한 속성에 접근하기 위한 인덱스로 사용될 수 있다.

그림 3.7은 Linux 운영체제의 일반적인 프로세스 트리를 보여주며, 각 프로세스의 이름과 pid를 보여준다. (이 상황에서 process라는 용어를 다소 느슨하게 사용한다. Linux는 대신에 task라는 용어를 선호하기 때문이다.) systemd 프로세스(항상 pid가 1)는 모든 유저 프로세스의 루트 부모 프로세스의 역할을 하며, 시스템이 부팅될 때 생성되는 첫 번째 유저 프로세스이다. 시스템이 부팅되면, systemd 프로세스는 웹 또는 프린트 서버, ssh 서버 등과 같은 추가적인 서비스를 제공하는 프로세스를 생성한다. 그림 3.7에서 systemd의 두 자식 logind와 sshd를 볼 수 있다. logind 프로세스는 시스템에 직접 로그인하는 클라이언트 관리를 책임진다. 이 예에서, 클라이언트가 로그인하고 pid 8416가 할당된 bash shell을 사용하고 있다. bash 명령어-라인 인터페이스를 사용하여 이 유저는 ps 프로세스와 vim 에디터를 생성했다. sshd 프로세스는 ssh(secure shell의 약자)를 사용하여 시스템에 연결하는 클라이언트 관리를 책임진다.

UNIX와 Linux 시스템에서, ps 명령어를 사용하여 프로세스 목록을 얻을 수 있다. 예를 들어, 명령어 ps -el은 시스템에서 현재 활성화된 모든 프로세스의 완전한 정보를 나열한다. 그림 3.7과 유사한 프로세스 트리는 부모 프로세스를 systemd 프로세스까지 재귀적으로 추적하여 구성될 수 있다. (추가적으로, Linux 시스템은 시스템의 모든 프로세스 트리를 표시하는 pstree 명령어를 제공한다.)

일반적으로 프로세스가 자식 프로세스를 생성할 때, 자식 프로세스는 작업을 완료하기 위해 특정 리소스 (CPU 시간, 메모리, 파일, I/O 장치)가 필요하다. 자식 프로세스는 운영체제에서 직접 리소스를 얻거나, 부모 프로세스의 리소스 하위 집합으로 제한될 수도 있다. 부모는 리소스를 자식 사이에 분할해야 할 수도 있고, 여러 자식 사이에 메모리나 파일같은 일부 리소스를 공유해야 수도 있다. 자식 프로세스를 부모 리소스의 하위 집합으로 제한하면 어떤 프로세스도 너무 많은 자식 프로세스를 생성해서 시스템을 과부하시키는 것을 막을 수 있다.

다양한 물리적 및 논리적 리소스를 제공하는 것 이외에도, 부모 프로세스는 자식 프로세스에게 초기화 데이터(입력)를 전달할 수 있다. 예를 들어, 터미널 화면에 파일(에:hw1.c)의 내용을 표시하는 기능을 가진 프로세스를 생각해 봐라. 프로세스가 생성되면, 그것의 부모 프로세스로부터 파일 hw1.c의 이름을 입력으로 받는다. 이 파일의 이름을 사용하여, 파일을 열고 내용을 작성한다. 또한 출력 장치의 이름을 받을 수도 있다. 또는 일부 운영체제는 자식 프로세스에게 리소스를 전달한다. 이러한 시스템에서, 새 프로세스는 hw1.c와 터미널 장치 두 개의 열린 파일을 받고, 둘 사이에 데이터를 전송할 수 있다.

프로세스가 새 프로세스를 생성할 때, 실행에 두 가지 가능성이 존재한다:
1. 부모는 자식과 동시에 계속 실행한다.
2. 부모는 자식 중 일부 또는 전부 종료될 때까지 기다린다.

또한 새 프로세스에 두 가지 주소-공간 가능성이 있다.
1. 자식 프로세스는 부모 프로세스의 복제본이다(부모와 동일한 프로그램과 데이터를 가짐).
2. 자식 프로세스에 새 프로그램이 로드되었다.

이러한 차이점을 설명하기 위해, 먼저 UNIX 운영 체제를 고려해 보자. UNIX에서 살펴본 대로, 각 프로세스는 프로세스 식별자로 식별되며, 이는 고유한 정수이다. 새 프로세스는 fork() 시스템 콜로 생성된다. 새 프로세스는 원본 프로세스의 주소 공간 사본으로 이루어져 있다. 이 메커니즘은 부모 프로세스가 자식 프로세스와 쉽게 통신할 수 있도록 한다. 두 프로세스(부모와 자식)는 fork() 이후 명령어 실행을 계속하지만, 한 가지 차이점이 있다: fork()의 반환 코드는 새(자식) 프로세스의 경우 0인 반면에, (0이 아닌) 자식의 프로세스 식별자는 부모에게 반환된다.

fork() 시스템 콜 이후, 두 프로세스 중 하나는 일반적으로 exec() 시스템 콜을 사용하여 프로세스의 메모리 공간을 새 프로그램으로 대체한다. exec() 시스템 호출은 메모리에 바이너리 파일를 로드하고(exec() 시스템 호출이 포함된 프로그램의 메모리 이미지를 파괴) 실행을 시작한다. 이 방법으로, 두 프로세스는 통신하고 각자의 길을 갈 수 있다. 그런 다음 부모는 더 많은 자식을 만들 수 있다. 또는 자식이 실행되는 동안 할 일이 없으면, wait() 시스템 콜을 발행하여 자식이 종료될 때까지 ready queue(준비 대기열)에서 벗어날 수 있다. exec() 호출은 프로세스의 주소 공간을 새 프로그램에 오버레이하기 때문에, exec()는 에러 발생없이 제어를 반환하지 않는다.

그림 3.8에 나와 있는 C 프로그램은 이전에 설명한 UNIX 시스템 콜을 보여준다. 같은 프로그램의 복사본을 실행하는 두 개의 다른 프로세스가 있다. 유일한 차이점은 자식 프로세스의 pid 변수 값은 0인 반면에, 부모는 0보다 큰 정수 값이라는 것이다.(사실, 자식 프로세스의 실제 pid이다.) 자식 프로세스는 부모에게 권한과 스케줄링 속성, 그리고 열려 있는 파일 같은 특정 리소스를 상속받는다. 그런 다음 자식 프로세스는 execlp() 시스템 콜(execlp()는 exec() 시스템 콜의 한 버전)을 사용하여 UNIX 명령어 /bin/ls(디렉토리 목록을 가져오는 데 사용)로 주소 공간을 오버레이한다. 부모는 wait() 시스템 호출로 자식 프로세스가 완료될 때까지 기다린다. 자식 프로세스가 완료되면(암시적이거나 명시적으로 exit()를 호출하여), 부모 프로세스는 wait() 호출에서 다시 시작하여, exit() 시스템 콜을 사용하여 완료한다. 이는 그림 3.9에도 나와있다.

물론, 자식이 exec()를 호출하지 않고, 대신 부모 프로세스의 사본을 계속 실행하는 것을 막을 수 있는 것은 없다. 이 시나리오에서, 부모와 자식은 동일한 코드 명령어를 실행하는 동시 프로세스다. 자식은 부모의 사본이기 때문에, 각 프로세스는 모든 데이터의 자체 사본을 가진다.

또 다른 예로, Windows의 프로세스 생성을 고려해 보자. 프로세스는 CreateProcess() 함수를 사용하여 Windows API에서 생성된다. 이것은 부모가 새 자식 프로세스를 생성한다는 점에서 fork()와 유사하다. 하지만, fork()는 자식 프로세스가 부모의 주소 공간을 상속하는 반면, CreateProcess()는 프로그램 생성 시 자식 프로세스의 주소 공간에 특정 프로그램을 로드해야 한다. 또한, fork()는 매개변수를 전달받지 않는 반면, CreateProcess()는 최소 10개의 매개변수를 예상한다.

그림 3.10에 나와있는 C 프로그램은 CreateProcess() 함수를 보여준다. 이는 mspaint.exe 애플리케이션을 로드하는 자식 프로세스를 생성한다. 우리는 CreateProcess()로 전달된 10개의 매개변수 기본값 중 많은 것을 선택한다.

CreateProcess() 함수로 전달되는 두 개의 파라미터는 STARTUPINFO와 PROCESS_INFORMATION 구조의 인스턴스이다. STARTUPINFO는 윈도우의 사이즈와 모양 그리고 표준 입출력 파일에 대한 핸들같은 새 프로세스의 많은 속성을 지정한다. PROCESS_INFORMATION 구조는 핸들과 새로 생성된 프로세스 및 해당 스레드의 식별자를 포함한다. CreateProcess()를 진행하기 전에 ZeroMemory() 함수를 호출하여 이러한 각 구조에 대한 메모리를 할당한다.

CreateProcess()에 전달된 처음 두 파라미터는 애플리케이션 이름과 명령어-라인 파라미터이다. 만약 애플리케이션 이름이 NULL이면, 명령어-라인 파라미터는 로드할 애플리케이션을 지정한다. 이 경우, Microsoft Windows mspaint.exe 애플리케이션을 로드한다. 이 두 개의 초기 파라미터 외에도, 프로세스 및 스레드 핸들 상속뿐아니라 생성 플래그가 없도록 지정하는 기본 파라미터를 사용한다. 또한, 부모의 기존 블록 환경과 시작 디렉토리를 사용한다. 마지막으로, 프로그램 시작 시 생성된 STARTUPINFO와 PROCESS_INFORMATION 구조에 대한 두 가지 포인터를 제공한다. 그림 3.8에서, 부모 프로세스는 wait() 시스템 콜을 호출하여 자식이 완료될 때까지 기다린다. Windows에서 이와 동일한 기능은 WaitForSingleObject()로, 자식 프로세스의 핸들인 pi.hProcess가 전달되고 이 프로세스가 완료될 때까지 기다린다. 자식 프로세스가 종료되면, 부모 프로세스의 WaitForSingleObject() 함수에서 제어가 반환된다.

  • 환경 블록
    • 프로세스 환경 블록(PEB: Process Environment Block): 윈도우 NT에서의 프로세스의 환경 정보를 담고 있는 데이터 구조체. 운영체제 내부에서 사용되며, 각 프로세스마다 PEB가 존재.
    • 스레드 환경 블록(TEB: Thread Environment Block): 스레드의 환경 정보를 담고 있는 구조체. 각 프로세스마다 가지고 있는 스레드마다 TEB가 존재
  • 프로세스 핸들(Process Handle)은 프로세스를 식별하는 값으로 프로세스 핸들을 사용하는 함수로 다음과 같은 것들이 있다.
    • CreateProcess 함수: 시스템 전체에서 프로세스를 고유하게 식별하는 식별자 반환
    • GetCurrentProcessId 함수: 프로세스 식별

3.3.2 Process Termination

프로세스는 최종 명령문 실행을 마치고 운영체제에 exit() 시스템 콜을 사용하여 삭제를 요청하면 종료된다. 그 포인트에서, 프로세스는 대기 중인 부모 프로세스의 상태 값(일반적으로 정수)을 반환할 수 있다(wait() 시스템 콜을 통해). 물리적 및 가상 메모리, 열린 파일, I/O 버터를 포함하는 프로세스의 모든 리소스는 운영체제에 의해 할당 해제되고 회수된다.

종료는 다른 상황에서도 발생할 수 있다. 프로세스는 적절한 시스템 콜(예:Windows의 TerminateProcess())을 통해 다른 프로세스의 종료를 일으킬 수 있다. 일반적으로, 시스템 콜은 오직 종료될 프로세스의 부모에서만 호출할 수 있다. 그렇지 않으면, 유저 또는 오작동하는 애플리케이션이 임의로 다른 유저의 프로세스를 종료할 수 있다. 부모가 자식을 종료하려면 자식의 ID를 알아야 한다는 점을 유의해라. 따라서, 하나의 프로세스가 새로운 프로세스를 만들면, 새로 생성된 프로세스의 ID가 부모에게 전달된다.

부모는 다음과 같은 다양한 이유로 자식 중 하나의 실행을 종료할 수 있다:

  • 자식이 할당된 리소스 중 일부의 사용량을 초과했다.(이것이 발생했는지 확인하려면, 부모가 자식의 상태를 검사할 수 있는 메커니즘이 있어야 한다.)
  • 자식에게 할당된 작없이 더 이상 필요하지 않다.
  • 부모가 종료 중이며, 운영체제는 부모가 종료되면 자식이 계속하는 것을 허용하지 않는다.

일부 시스템은 부모가 종료되면 자식이 존재하는 것을 허용하지 않는다. 이러한 시스템에서 프로세스가 종료되면(정상적이든 비정상적이든) 모든 자식도 종료되어야 한다. 이 현상은 cascading termination(연쇄 종료)라고 하며, 일반적으로 운영체제에 위해 시작된다.

프로세스 실행 및 종료를 설명하기 위해 Linux와 UNIX 시스템에서 다음을 고려해라. exit() 시스템 콜을 사용하여 프로세스를 종료할 수 있으며, 파라미터로 종료 상태를 제공한다:

/* exit with status 1 */
exit(1);

사실, 정상적인 종료에서 exit()는 직접적으로(위에서 보여준 대로) 또는 간접적으로 호출된다. 왜냐하면 C 런타임 라이브러리(UNIX 실행 파일에 추가되는)가 기본적으로 exit()에 대한 호출을 포함하기 때문이다.

부모 프로세스는 wait() 시스템 콜을 사용하여 자식 프로세스의 종료를 기다릴 수 있다. wait() 시스템 호출은 부모가 자식의 종료 상태를 얻을 수 있도록 하는 파라미터를 전달받는다. 이 시스템 콜은 또한 종료된 자식의 프로세서 식별자를 반환하여 부모가 자식 중 어느 것이 종료되었는지 알 수 있도록 한다.

pid_t pid;
int status;

pid = wait(&status);

프로세스가 종료되면, 해당 리소스는 운영 체제에 의해 할당 해제된다. 그러나, 프로세스 테이블에 있는 항목은 부모가 wait()를 호출할 때까지 유지되어야 한다. 프로세스 테이블에 프로세스의 종료 상태가 포함되어 있기 때문이다. 종료되었지만, 부모가 아직 wait()를 호출하지 않은 프로세스를 좀비 프로세스라고 한다. 모든 프로세스는 종료될 때 이 상태로 전환되지만, 일반적으로 잠시동안만 좀비로 존재한다. 부모가 wait()를 호출하면, 좀비 프로세스의 프로세스 식별자와 프로세스 테이블의 항목이 해방된다.

만약 부모가 wait()를 호출하지 않고 대신 종료되어, 자식 프로세스를 고아로 남겨두면 어떻게 될지 생각해봐라. 전통적인 UNIX 시스템은 이 시나리오를 init 프로세스를 고아 프로세스의 새 부모로 할당하여 해결했다. (3.3.1절에서 init이 UNIX 시스템에서 프로세스 계층의 루트 역할을 한다는 것을 기억해라.) init 프로세스는 정기적으로 wait()를 호출하여 고아 프로세시의 종료 상태를 수집하고 고아 프로세스의 프로세스 식별자와 프로세스 테이블 항목을 해제할 수 있다.

대부분의 Linux 시스템이 init을 systemd로 대체했지만, 후자의 프로세스는 여전히 동일한 역할을 수행할 수 있다. 그러나 Linux에서 systemd 이외의 프로세스가 고아 프로세스를 상속하고 해당 프로세스의 종료를 관리하는 것을 허용한다.

3.3.2.1 Android Process Hierarchy

제한된 메모리와 같은 리소스 제약으로 인해, 모바일 운영 체제는 제한된 시스템의 자원을 회수하기 위해 기존 프로세스를 종료해야 할 수 있다. 임의의 프로세스를 종료하는 대신, 안드로이드는 프로세스의 중요도 계층을 식별했고, 시스템이 새롭거나 더 중요한 프로세스에 리소스를 제공하기 위해 프로세스를 종료해야 하는 경우, 중요도가 증가하는 순서로 프로세스를 종료한다. 가장 중요한 것부터 가장 덜 중요한 것까지 프로세스 분류는 다음과 같다:

  • Foreground process: 화면에 표시되는 현재의 프로세스는, 유저가 현재 상호작용하고 있는 애플리케이션을 나타낸다.
  • Visible process: 포그라운드에 직접 보이지 않지만 포그라운드 프로세스가 참조하는 활동을 수행하는 프로세스(즉, 포그라운드 프로세스에 상태가 표시되는 활동을 수행하는 프로세스)
  • Service process: 백그라운드 프로세스와 유사하지만 유저에게 명확하게 보이는 활동(예:음악 스트리밍)을 수행하는 프로세스
  • Background process: 활동을 수행할 수 있지만 유저에게는 보이지 않은 프로세스
  • Empty process: 어떤 애플리케이션과 연관된 활성 구성 요소도 보유하지 않는 프로세스

시스템 리소스를 회수해야 하는 경우, Android는 먼저 빈 프로세스를 종료한 다음, 백그라운드 프로세스를 종료하는 식으로 진행한다. 프로세스에 중요도 순서가 할당되고, Android는 가능한 높은 순위의 프로세스를 할당하려고 시도한다. 예를 들어, 프로세스가 서비스를 제공하고 또한 보이는 경우, 더 중요한 가시적 분류가 할당될 것이다.

또한, Android 개발 관행은 프로세스 수명 주기의 가이드라인을 따르는 것을 제안한다. 이러한 가이드라인을 따르면, 프로세스의 상태는 종료되기 전에 저장되고 유저가 애플리케이션으로 돌아가면 저장된 상태에서 재개된다.

0개의 댓글