본문 바로가기
CS/운영체제

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

by 베어 그릴스 2022. 8. 18.
본 정리는 운영체제(Operating System: Concepts) 9th edition과 22학년도 1학기 건국대학교 운영체제 수업을 바탕으로 하고 있습니다.

 

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

 

프로세스의 생성


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

 

프로세스는 다른 프로세스를 생성할 수 있고, 이렇게 생성하는 프로세스를 부모 프로세스, 생성된 새로운 프로세스를 자식 프로세스라고 한다.

 

부모 프로세스와 자식 프로세스는 1:N 관계를 가지기 때문에 위 그림과 같은 트리 구조를 형성한다.

 

각 프로세스는 프로세스 식별자로 보통 정수의 PID가 할당된다.

 

* 원래 부팅 시작 시 가장 먼저 시작되는 프로세스인 init 프로세스가 PID 1을 할당받았었다. 속도적으로 느린 init의 한계를 극복하기 위해 대신 systemd를 사용한다. (system daemon : init과 다르게 병렬로 시행되어 부팅 속도가 빨라짐)

 

 

fork() 시스템 호출

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

 

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

 

자식 프로세스도 프로세스이기 때문에 PCB가 필요한데, PCB가 생성되고 나서 부모 프로세스로부터 상속받는 것들은 다음과 같다.

 

1. 속성, 자원 상속

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

한 프로세스가 너무 많은 자식 프로세스 생성으로 과중한 부하가 발생하지 않도록 부모가 소유한 자원의 일부만 상속되도록 할 수도 있음

 

2. 레지스터 문맥 복제

부모 프로세스의 특수레지스터 및 범용 레지스터 값도 자식 프로세스의 초기값으로 복제되나 각자 수행되는 즉시 값들은 달라질 것임

 

3. 사용자 수준 문맥 복제

변경되지 않는 부분인 text 영역은 부모와 자식 프로세스가 공유

프로세스에 의해 변경되는 data(bss 포함), heap, stack 영역은 복사되어 별도 할당됨 → 이 때문에 이렇게 생성되는 자식 프로세스를 heavy weight process라고 부른다

 

 

fork() 함수는 부모 프로세스에게는 자식 프로세스의 PID를 반환하고, 자식 프로세스에게는 0을 반환하여 총 2개의 반환 값을 가진다.

 

다음 코드를 보자.

int a = 3;
int main()
{
	int pid;

	do something;

	if ((pid = fork()) == 0) { //child
		a = 4; // child’s variable a
		printf ("child's a = %d\n", a);
		exit(0); // child 종료(exit)
	} 
    
    else if (pid != 0) { // parent	
		a = 5; // parent’s variable a
		printf ("parent's a = %d\n", a);
		wait(); // parent는 child의 exit 까지 대기
	}
	
    do something;
}

 

pid 변수는 부모 프로세스에서는 자식 프로세스의 pid를 가지기 때문에 0이 아니고 그렇다면 해당 코드 부분은 부모 프로세스가 실행할 부분이고, 자식 프로세스는 pid로 0을 가지기 때문에 해당 부분은 자식 프로세스가 실행할 부분이다.

 

여기서 알 수 있는 점은 자식 프로세스와 부모 프로세스는 한 코드를 공유한다는 점이다. 즉, 사용자 문맥의 text 영역은 함께 공유하고, stack영역에 있는 pid변수와 data 영역에 있는 a변수의 공간은 실질적인 데이터만 copy 하고 공간은 부모 프로세스와 별도로 할당된다는 점이다.

 

정리하면, 부모프로세스와 공유하는 주소는 text 영역이고, 값만 복제되어 다른 주소를 갖는 것은 커널 수준 문맥과 text 부분을 제외한 사용자 수준 문맥이다.

 

 

 

* wait() 기능

부모 프로세스가 자식 프로세스가 종료될 때까지 기다리게 해주는 기능

 

따라서

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

2. 준비 완료 큐에서 부모 프로세스 자신을 제거 대기 큐로 들어가게 된다.

3. 자식 프로세스가 종료되었을 경우 wait() 호출로부터 부모 프로세스가 준비 큐로 돌아와 재개된다.

 

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

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

 

 

exec() 시스템 호출

위 fork에선 부모 프로세스의 코드가 너무 복잡해진다는 단점이 있다.

 

이를 위해 아예 새로운 프로그램을 자식 프로세스로 불러올 수 있는 exec() 시스템 호출을 사용한다.

 

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

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

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

 

PCB의 정보 및 부모 자식 관계 등은 유지한 채, 프로세스의 text, data, stack 영역이 새로운 프로그램을 위한 것으로 교체되어 새로운 문맥이 만들어진다.

 

다음 코드를 보자.

#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;
}

fork 하여 자식 프로세스에 stack과 data를 위한 새로운 주소 공간을 할당받고, 그 주소에 exec("~") 시스템 호출을 하여 ~에 있는 프로그램을 실행한다. 그렇다면 stack과 data 영역도 모두 ~에 있는 프로그램의 stack과 data로 바뀌고 text영역도 해당 부분의 코드로 교체된다.

 

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

 

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

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

 

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