봉황대 in CS

[Chapter 3. 프로세스] 프로세스 생성과 종료 본문

Computer Science & Engineering/Operating System

[Chapter 3. 프로세스] 프로세스 생성과 종료

등 긁는 봉황대 2022. 7. 9. 16:43

* 본 글은 '운영체제(Operating System: Concepts) 9th edition'의 내용과 2021학년도 1학기에 수강한 '운영체제' 과목 강의 내용을 함께 정리하여 작성하였습니다.

 

 

프로세스는 한 프로세스에 의해 새로 생성될 수 있으며, 생성된 프로세스는 자신에 의해서(수행을 마쳤을 경우) 또는 외부의 요청에 의해서 종료한다.

 

프로세스 생성


프로세스는 다른 프로세스를 생성할 수 있다.

이때 프로세스를 생성하는 프로세스를 부모 프로세스라고 하며, 생성된 새로운 프로세스는 자식 프로세스라고 한다.

 

부모 프로세스와 자식 프로세스는 1:N 관계이기에 전체적으로 트리가 구성된다.

Linux 운영체제의 프로세스 트리

 

프로세스 각각에게는 고유 번호 즉, 프로세스 식별자(PID)가 할당된다. (보통 정수 값)

 

위의 트리 그림에서 root 노드인 systemd 프로세스를 보자.

이 프로세스의 PID는 1이며, Linux 운영체제에서 모든 사용자 프로세스의 루트 부모 프로세스 역할을 한다.

 

* 원래는 init 프로세스(부팅 시작 시 가장 먼저 시작되는 프로세스)가 PID = 1을 할당받았었는데,

   init의 한계를 극복하기 위해 대신 systemd를 사용한다. (system daemon : init과 다르게 병렬로 시행되어 부팅 속도가 빨라짐)

 

fork() 시스템 호출

UNIX, Linux 운영체제에서는 fork() 시스템 호출을 통하여 새로운 프로세스를 생성한다.

 

부모 프로세스에서 fork() 시스템 호출 시 부모 프로세스로부터 주소 공간을 상속받는 자식 프로세스가 생성된다.

자식 프로세스의 PCB가 생성되고 나서 부모 프로세스로부터 상속(Cloning) 받는 것들은 다음과 같다.

 

1. 속성과 자원

     부모가 open 한 파일, 동기화 자원, 우선순위 정보 등

 

2. 레지스터 문맥

     자식 프로세스의 초기값으로 부모 프로세스의 특수 레지스터 및 범용 레지스터 값이 복제된다.

     (하지만 각자 수행되는 즉시 값들은 달라질 것)

 

3. 사용자 수준 문맥

     (1) text 영역은 부모 프로세스와 자식 프로세스가 공유한다.

           두 프로세스가 같은 코드를 수행하며, 코드 자체가 변경되어서는 안 되는 부분이기 때문이다.

     (2) 나머지 data 영역(bss 포함)과 heap, stack 영역은 변경되는 부분이기 때문에 복사되어 별도로 할당된다.

 

(이를 heavy weight process라고 한다. ↔︎ 스레드의 light weight process)

 

* 동일한 프로그램을 수행하는 가상적인 CPU가 하나 만들어져서 따로 실행을 시작하게 된 것으로 생각하면 이해가 쉽다.

 

 

 

fork() 함수로부터의 return은 (1) 부모 프로세스와 (2) 자식 프로세스 두 곳에서 일어난다.

단, (1) 부모 프로세스에게는 자식 프로세스의 PID가 return, (2) 자식 프로세스에는 0이 return 된다.

 

아래의 그림을 함께 보자.

 

 

프로세스 A에서 fork() 함수를 호출하였더니, A를 복제한 프로세스 B가 생성되었다.

프로세스 A와 B는 공유된 text를 각각 실행한다. (그림 상 PC의 위치를 유의해서 보자.)

 

프로세스 B가 생성되었을 때

     (1) 프로세스 A : fork() 함수에서 return 되어 pid 변수에 저장되는 값은 프로세스 B의 PID 값

     (2) 프로세스 B : fork() 함수에서 return 되어 pid 변수에 저장되는 값은 0

이라는 것이다.

 

이렇게 다른 return 값을 반환하는 이유는

한 프로그램으로 부모 프로세스와 자식 프로세스가 서로 다른 코드 블록을 수행할 수 있게 하기 위해서이다. (아래 코드 예시)

 

 

* (pid = fork() == 0)인 경우 : 자식 프로세스

* (pid = fork() != 0)인 경우 : 부모 프로세스

 

그렇다면 자식 프로세스 생성 후 부모 프로세스의 프로그램이 아닌, 새로운 프로그램을 실행시키려면 어떻게 해야 하는가?

이는 exec() 시스템 호출을 통해 진행된다.

 

exec() 시스템 호출

자식 프로세스를 생성한 후 해당 주소 공간에 새로운 프로그램을 적재하기 위해서

fork() 시스템 호출 → exec() 시스템 호출을 사용한다.

 

* 정확히는 exec family의 execl(), execve() 등의 시스템 호출을 사용한다.

   exec family : exec~로 시작하는 시스템 호출들을 모두 엮어서 exec family라고 부른다.

   해당 시스템 호출들은 모두 프로세스를 실행시킨다는 공통점을 가지고 있다.

 

exec() 시스템 호출이 불려지면

     (1) 해당 프로세스의 PCB 정보와 프로세스의 부모 자식 관계 등은 유지

     (2) 프로세스의 text, data, stack 영역이 새로운 프로그램을 위한 것으로 교체된다.

 

즉, 사용자 수준의 문맥이 전부 새로운 것으로 교체된다는 것이다.

(이진 파일을 메모리로 적재(load), 원래 프로그램의 메모리 이미지를 파괴한 후 프로그램 실행을 시작)

 

아래는 fork() 후 execlp() 시스템 호출을 통해 별도의 프로세스를 생성하는 예시 코드이다.

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

main(int argc, char *argv[])
{
	int pid;
    
	/* 새 프로세스 생성 (fork) */
	pid = fork();
    
	/* 오류가 발생했을 경우 */
	if (pid < 0) {
		fprintf(stderr, "Fork Failed");
		return 1;
	}
    
	/* 자식 프로세스 */
	else if (pid == 0) {
		execlp("/bin/ls","ls",NULL); /* 자신의 주소 공간 덮어쓰기 */
	}
        
	/* 부모 프로세스 */
	else {
		/* 부모가 자식이 완료되기를 기다림 */
		wait(NULL);
		printf("Child Complete");
	}
    
	return 0;
}

 

wait() 시스템 호출

위의 코드에서 부모 프로세스가 수행하는 코드 중 wait() 시스템 호출을 볼 수 있다.

이는 부모 프로세스가 그의 자식 프로세스가 종료될 때까지 기다리겠다는 것이다.

 

따라서

1. wait() 시스템 호출이 불려지면

2. 준비 완료 큐에서 부모 프로세스 자신을 제거

3. 자식 프로세스가 종료되었을 경우 wait() 호출로부터 부모 프로세스가 재개

된다.

 

부모 프로세스는 자식 프로세스의 종료 상태를 얻을 수 있도록 하나의 인자를 전달받는데,

종료된 자식의 PID를 반환받는 것을 통해서 부모는 어느 자식 프로세스가 종료되었는지를 구별할 수 있다.

 

CreateProcess()

위는 UNIX, Linux 운영체제에서의 예시였다.

Windows에서는 자식 프로세스의 생성을 위하여 Windows API의 CreateProcess 함수를 사용한다.

 

동일하게 자식 프로세스를 생성한 후 주소 공간에 명시된 프로그램을 적재하는데,

fork() 시스템 호출과는 다르게 매개변수를 요구한다. (10개 이상)

 

아래는 CreateProcess()를 사용하여 자식 프로세스를 생성하는 예시 코드이다.

#include <stdio.h>
#include <windows.h>

int main(VOID)
{
	STARTUPINFO si;
	PROCESS INFORMATION pi;

	/* 메모리 할당 */
	ZeroMemory(&si, sizeof(si));
	si.cb = sizeof(si);
	ZeroMemory(&pi, sizeof(pi));
    
	/* 자식 프로세스 생성 */
	if (!CreateProcess(NULL, /* 명령어 라인 사용 */
	 "C:∖∖WINDOWS∖∖system32∖∖mspaint.exe", /* 명령어 라인 */
	 NULL, /* 프로세스를 상속하지 말 것 */
	 NULL, /* 스레드 핸들을 상속하지 말 것 */
	 FALSE, /* 핸들 상속 디제이블 */
	 0, /* 생성 플래그 없음 */
 	 NULL, /* 부모 환경 블록 사용 */
	 NULL, /* 부모 프로세스가 존재하는 디렉토리 사용 */
	 &si,
	 &pi))
	{
    	fprintf(stderr, "Create Process Failed")
	}
    
	/* 부모 프로세스가 자식 프로세스가 끝나기를 기다림 */
	WaitForSingleObject(pi.hProcess, INFINITE);
	printf("Child Complete");
    
	/* 핸들 닫기 */
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);
}

 

프로세스 종료


UNIX, Linux 운영체제에서는 프로세스는 exit() 시스템 호출을 통해 자신의 종료를 요청하거나,

abort()와 같은 시스템 호출을 통해 다른 프로세스의 종료를 유발할 수 있다.

 

exit() 시스템 호출

운영체제에게 프로세스 자신의 종료를 요청한다.

 

exit() 시스템 호출 시 부모 프로세스에게 자식 프로세스가 종료되었다는 것을 알리고(사건),

해당 프로세스의 모든 자원이 운영체제로 반납된다.

 

* 자식 프로세스는 자신의 부모 프로세스에 있는 wait() 시스템 호출을 통해 자신의 상태 값(exit code)을 반환할 수 있다.

 

좀비(zombie) 프로세스

하지만 만약 자식 프로세스는 종료되었으나 부모 프로세스가 wait() 호출을 하지 않아 대기 상태에 있지 않다면

자식 프로세스의 주소 공간과 할당된 자원은 반납되지만, PCB는 wait()을 호출할 때까지 그대로 유지하게 된다.

이 상태에 있는 프로세스를 좀비(zombie) 프로세스라고 부른다.

 

좀비 상태에 있는 프로세스의 부모가 wait() 시스템 호출을 부르게 된다면

그제야 좀비 프로세스의 PID와 PCB가 운영체제에게 반납된다.

고아(orphan) 프로세스

만약 부모 프로세스가 wait() 호출을 하지 않고 종료하였다면

그 부모 프로세스의 자식 프로세스들을 고아(orphan) 프로세스라고 부른다.

 

UNIX, Linux 운영체제에서는 이들을 해결하기 위하여 고아 프로세스의 새로운 부모 프로세스로 init 또는 systemd 프로세스(root)를 지정하며, root 프로세스는 주기적으로 wait() 시스템 호출을 부른다.

 

다른 프로세스의 종료를 유발하는 경우

이는 종료시킬 프로세스의 부모 프로세스만이 이를 유발할 수 있다.

 

다른 프로세스의 종료를 유발하는 시스템 호출에는

UNIX, Linux 운영체제에서는 abort(), Windows에서는 TerminateProcess() 함수가 존재한다.

 

부모 프로세스가 자식 프로세스를 종료시키는 데는 대표적으로 다음의 이유들이 있다.

 

1. 자식 프로세스가 자신에게 할당된 자원을 초과하여 사용하였을 경우

2. 자식 프로세스에게 할당된 task가 더 이상 필요 없을 경우

3. 부모 프로세스가 종료하려는데, 운영체제는 부모가 종료한 후 자식이 실행을 계속하는 것을 허용하지 않는 경우

연쇄식 종료(casacding termiantion)

3번에서 처럼 부모 프로세스가 종료한 후 자식이 실행을 계속하는 것을 허용하지 않는 운영체제의 경우

운영체제 자체가 연쇄식 종료(casacding termiantion)를 시행한다.

 

즉, 프로세스가 정상적/비정상적으로 종료된다면 그로부터 비롯된 모든 자식 프로세스들도 종료시키는 것이다.

 

 

반응형
Comments