기록공간

2-1장. 프로세스 API 본문

OS

2-1장. 프로세스 API

입코딩 2020. 2. 14. 10:53
반응형

이번 장에서는 UNIX 시스템의 프로세스 생성에 관해 논의한다. UNIX는 프로세스를 생성하기 위해서 fork()exec() 시스템 콜을 사용한다. wait()는 프로세스가 자신이 생성한 프로세스가 종료되기를 기다리기 원할 때 사용된다. 그러면 본격적으로 이 인터페이스에 대해 예제를 통해 더 자세하게 알아보자.


fork() 시스템 콜

프로세스 생성에는 fork() 시스템 콜이 사용된다. 다음 코드는 fork()를 호출하는 코드(p1.c)이다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	printf("hello world (pid:%d)\n", (int) getpid());
	int rc = fork();
	if(rc < 0) // fork 실패; 종료
	{
		fprintf(stderr, "fork failed\n");
		exit(1);
	}
	else if(rc == 0) // 자식 (새 프로세스)
	{
		printf("hello, I am child (pid:%d)\n", (int) getpid());
	}
	else  // 부모 프로세스는 이 경로를 따라 실행한다 (main)
	{
		printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
	}
}

출력 값

prompt> ./p1
hello world (pid:29146)
hello. I am parent of 29147 (pid:29146)
hello, I am child (pid:29147)
prompt>

프로그램에서 어떤 일들이 벌어지는지 더 자세히 보도록 하자. 실행이 시작될때 프로세스는 "hello world ..." 메시지를 출력 한다. 이 메시지에는 PID로도 알려진 프로세스 식별자(process identifier)가 포함된다. 이 프로세스는 29146이라는 PID를 가진다. 지금 까지는 별 문제가 없다.

 

프로세스는 fork() 시스템 콜을 호출한다. 운영체제는 프로세스 생성을 위해 이 시스템 콜을 제공한다. 이상한 부분이 있는데, 생성된 프로세스가 호출한 프로세스의 복사본이라는 것이다. 이 복사본은 p1.c의 코드와 동일하지만 fork() 함수 호출 된 위치 이후 부터 코드를 수행한다.

 

fork() 호출 직후를 잘 살펴보자. 운영체제 입장에서 보면, 프로그램 p1이 2개가 존재한다. 두 프로세스 모두 fork()에서 리턴하기 직전이다. 새로 생성된 프로세스는 main() 함수 첫 부분부터 시작하지 않았다는 것을 알 수 있다. 자식 프로세스는 fork()를 호출하면서부터 시작되었다.

 

자식 프로세스는 부모 프로세스와 완전히 동일하지 않다. 자식 프로세스는 자신의 주소 공간, 레지스터, PC(Program Counter)값을 갖는다. 자식과 부모의 차이점은 fork() 시스템 콜의 반환 값이 서로 다르다는 것이다. fork()로 부터 부모 프로세스는 생성된 자식 프로세스의 PID를 반환받고, 자식 프로세스는 0을 반환 받는다. 이 반환 값의 차이로 인해 위와 같은 프로그램을 쉽게 작성 할 수 있는 것이다.

 

프로그램의 출력 결과는 항상 동일하지 않다. 그 반대의 경우도 발생 할 수 있다.

prompt> ./p1
hello world (pid:29146)
hello, I am child (pid:29147)
hello. I am parent of 29147 (pid:29146)
prompt>

CPU 스케줄러(Scheduler)는 실행할 프로세스를 선택한다. 스케줄러는 곧 자세하게 다룰 것이다. 스케줄러의 동작은 일반적으로 상당히 복잡하고 상황에 따라 다른 선택이 이루어지기 때문에, 어느 프로세스가 먼저 실행된다라고 단정하는 것은 매우 어렵다. 이러한 비결정성으로 인해 멀티 스레드 프로그램 실행 시 다양한 문제가 발생한다.


wait() 시스템 콜

이제까지 메시지를 출력하는 자식 프로세스를 만들어 보았다. 부모 프로세스가 자식 프로세스의 종료를 대기해야 하는 경우도 발생할 수 있다. 이러한 작업을 위해 wait() 시스템 콜이 있다. 다음 코드(p2.c)를 보자. 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	printf("hello world (pid:%d)\n", (int) getpid());
	int rc = fork();
	if(rc < 0) // fork 실패; 종료
	{
		fprintf(stderr, "fork failed\n");
		exit(1);
	}
	else if(rc == 0) // 자식 (새 프로세스)
	{
		printf("hello, I am child (pid:%d)\n", (int) getpid());
	}
	else  // 부모 프로세스는 이 경로를 따라 실행한다 (main)
	{
		int wc = wait(NULL);
		printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
	}
}

부모 프로세스는 wait() 시스템 콜을 호출하여 자식 프로세스 종료 시점까지 자신의 실행을 잠시 중지시킨다. 자식 프로세스가 종료하면 wait()은 리턴한다. 그래서 다음 코드처럼 wait() 호출을 추가하면 프로그램은 항상 동일한 결과를 출력한다.

 

출력 값

prompt> ./p2
hello world (pid:29266)
hello, I am child (pid:29267)
hello. I am parent of 29267 (wc:29267) (pid:29266)
prompt>

exec() 시스템 콜

프로세스 생성 관련 API 중에서 마지막으로 중요한 시스템 콜은 exec() 시스템 콜이다. 이 시스템 콜은 자기 자신이 아닌 다른 프로그램을 실행해야 할 때 사용한다. p2.c의 fork() 시스템 콜은 자신의 복사본을 생성하여 실행한다. 자신의 복사본이 아닌 다른 프로그램을 실행해야 할 경우에는 exec() 시스템 콜이 그 일을 한다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	printf("hello world (pid:%d)\n", (int) getpid());
	int rc = fork();
	if(rc < 0) // fork 실패; 종료
	{
		fprintf(stderr, "fork failed\n");
		exit(1);
	}
	else if(rc == 0) // 자식 (새 프로세스)
	{
		printf("hello, I am child (pid:%d)\n", (int) getpid());
		char *myargs[3];
		myargs[0] = strdup("wc");		// 프로그램 "wc"
		myargs[1] = strdup("p3.c");		// 인자 : 단어 셀 파일
		myargs[2] = NULL;				// 배열의 끝 표시
		execvp(myargs[0], myargs);		// "wc" 실행
		printf("this shouldn't print out");
	}
	else  // 부모 프로세스는 이 경로를 따라 실행한다 (main)
	{
		int wc = wait(NULL);
		printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
	}
}

출력 값

prompt> ./p3
hello world (pid:29383)
hello, I am child (pid:29384)
	29	107	1030	p3.c
hello. I am parent of 29384 (wc:29384) (pid:29383)
prompt>

wc 프로그램은 단어를 세는 프로그램이다. 예제 프로그램은 자신의 소스 파일인 p3.c를 인자로 하여 wc를 실행하고 소스 코드의 행 개수, 단어의 개수, 바이트의 개수를 알려준다.

 

fork() 시스템 콜만큼 exec() 시스템 콜도 이해하기 어렵다. exec() 시스템 콜은 다음과 같은 과정으로 수행된다. 실행 파일의 이름(wc)과 약간의 인자(p3.c)가 주어지면 해당 실행 파일의 코드와 정적 데이터를 읽어 들여 현재 실행 중인 프로세스의 코드 세그멘트와 정적 데이터 부분을 덮어쓴다. 힙과 스택 및 프로그램 다른 주소 공간들로 새로운 프로그램의 실행을 위해 다시 초기화 된다. 그런 다음 운영체제는 프로세스의 argv와 같은 인자를 전달하여 프로그램을 실행시킨다. 새로운 프로세스를 생성하지는 않는다. 현재 실행 중인 프로그램(p3)을 다른 실행중인 프로그램(wc)으로 대체하는 것이다. 자식 프로세스가 exec()를 호출한 후에는 p3.c는 전혀 실행되지 않은 것처럼 보인다. exec() 시스템 콜이 성공하게 되면 p3.c는 절대로 리턴하지 않는다.


왜, 이런 API를?

이런 의문이 생길 수 있다. 새로운 프로세스를 생성하는 간단한 작업 같은데, 왜 이런 이상한 인터페이스를 사용할까? UNIX의 을 구현하기 위해서는 fork()와 exec()을 분리해야 한다. 그래야만 쉘이 fork()를 호출하고 exec()를 호출하기 전에 코드를 실행할 수 있다. 이때 실행하는 코드에서 프로그램의 환경을 설정하고, 다양한 기능을 준비한다.

 

쉘은 단순한 사용자 프로그램이다. 쉘은 프롬프트를 표시하고 사용자가 무언가 입력하기를 기다린다. 그리고 명령어를 입력한다. 대부분의 경우 쉘은 파일 시스템에서 실행 파일의 위치를 찾고 명령어를 실행하기 위하여 fork()를 호출하여 새로운 자식 프로세스를 만든다. 그런 후 exec()의 변형 중 하나를 호출하여 프로그램을 실행시킨 후 wait()을 호출하여 명령어가 끝나기를 기다린다. 자식 프로세스가 종료되면 쉘은 wait()으로 부터 리턴하고 다시 프롬프트를 출력하고 다음 명령어를 기다린다. 

반응형

'OS' 카테고리의 다른 글

2-3장. 스케줄링 : 개요  (0) 2020.02.21
2-2장. Mechanism : 제한적 직접 실행 원리  (0) 2020.02.19
2장. 가상화  (0) 2020.02.07
1-4장. 운영체제 개요 - 설계 목표와 역사  (0) 2020.02.04
1-3장. 운영체제 개요 - 영속성  (2) 2020.02.04
Comments