운영체제

process API (system call)

땅콩콩 2023. 3. 12. 21:01
fork()

fork()는 process를 생성하는 유일한 방법이다. 예시 코드를 살펴보자.

int
main(int argc, char *argv[])
{
	printf("hello world (pid:%d)\n", (int) getpid());
	int rc = fork();
	if (rc < 0) { // fork failed; exit
	fprintf(stderr, "fork failed\n");
	exit(1);
} else if (rc == 0) { 
	// child (new process)
	printf("hello, I am child (pid:%d)\n", (int) getpid());
	} else { 
    // parent goes down this path (main)
	printf("hello, I am parent of %d (pid:%d)\n",
		rc, (int) getpid());
	}
	return 0;
}

fork()의 리턴값을 rc라는 변수로 받게 되는데, 이 값이 무엇인지에 따라 실행이 달라지는 코드이다.

int rc = fork();

fork()의 리턴값이 0보다 작으면 일반적으로 error이고, 0이 아닐때는 다른 동작을 한다.

fork()의 리턴값으로, 새로 생성된 프로세스에는 0을 주고 부모 프로세스에는 자기가 만든 자식 프로세스의 pid를 준다.

따라서 실행해보면 다음과 같은 결과가 나온다. 

(parents pid : 29146)

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

여기서 중요한 점은 fork()라는 system call이 호출되면 커널은 복사하는 일을 하기 시작한다는 것이다. 부모 프로세스의 내용을 그대로 물리적 공간에 복사해서 만들어낸다(memory copy). 따라서 두 프로세스의 pid는 다르지만 내용은 완전히 동일하다. (여기서는 fork의 리턴값인 rc값만 다름.)

 

하지만 부모와 자식 두 프로세스 중 어떤 것이 먼저 실행될지는 결정되어 있지 않다. (Non-determinstic)

그렇기 때문에 wait()가 필요한 것이다.

 

 

wait()

wait()는 말 그대로 기다리는 역할을 하는 함수이다.

그래서 자기가 생성한 child process가 끝나기를 기다리고, wait()의 리턴값은 자기가 기다린 프로세스(자식)의 pid가 된다.

int
main(int argc, char *argv[]){
	printf("hello world (pid:%d)\n", (int) getpid());
	int rc = fork();
	if (rc < 0) { // fork failed; exit
		fprintf(stderr, "fork failed\n");
		exit(1);
	} else if (rc == 0) { // child (new process)
		printf("hello, I am child (pid:%d)\n", (int) getpid());
	} else { // parent goes down this path (main)
		int wc = wait(NULL); //자식 프로세스를 기다린다.
		printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
		rc, wc, (int) getpid());
	}
	return 0;
}

앞서 말했듯이 부모 프로세스와 자식 프로세스의 실행순서는 비결정적이다. 그리고 wait()를 통해 그 순서를 보장할 수 있다. (자식>부모)

int wc = wait(NULL);

해당 부분의 코드를 보면 wait()의 파라미터로 NULL이 주어진 것이 확인할 수 있다. 

이 파라미터는 내가 기다리는 child process의 exit status값을 어디에 담아올지를 결정하기 위해 존재한다.

만일 integer변수 s를 만들어 이 파라미터로 주면, child process의 exit status를 s에 담을 수 있는 것이다.

Exit status란?

프로세스의 종료에는 다음과 같이 두 종류가 있다. 
정상종료 = 중간에 갑자기 종료되지 않고 끝까지 정상실행되어서 main()에서 리턴되거나 exit()이 호출되는 경우.
비정상종료 = 중간에 interrupt나 error로 인해 비정상적으로 끝나는 경우.

그리고 정상종료는 프로세스의 기준이 아닌 사람의 기준으로 다시 한번 나뉜다.
프로그래머의 의도대로 잘 수행되고 끝났으면 성공 = exit(0)
의도와 다르게 수행되어 끝났으면 실패 = exit(1), exit(2) ... 등등

따라서 사람의 기준이 아닌 프로세스의 기준으로, 성공적으로 끝까지 정상 실행된 모든 프로세스는 끝날때의 상태값을 가지고, 이것을 exit status라고 한다.

즉, wait()가 하는 역할을 다시 정리해보면 두가지로 정리할 수 있다.

1.child process가 끝나기를 기다리는 역할.

2.child process가 끝날 때, 그 exit status값을 받아오기 위한 역할.

 

 

exec()

fork()로 인해 새로운 process환경이 생성되었을 때, 그 process가 아예 코드가 다른 일(다른 프로그램)을 하게 하고 싶을 때, exec()을 통해 다른 program으로 해당 process를 덮어쓸 수 있다.

즉 code, data, stack, memory 모든 부분이 초기화되고, 완전히 다른 일을 수행할 수 있게 되는것이다.

int
main(int argc, char *argv[]){
	printf("hello world (pid:%d)\n", (int) getpid());
	int rc = fork();
	if (rc < 0) { // fork failed; exit
		fprintf(stderr, "fork failed\n");
		exit(1);
	} else if (rc == 0) { // child (new process)
	printf("hello, I am child (pid:%d)\n", (int) getpid());
	char *myargs[3];
	myargs[0] = strdup("wc"); // program: "wc" (word count)
	myargs[1] = strdup("p3.c"); // argument: file to count
	myargs[2] = NULL; // marks end of array
	execvp(myargs[0], myargs); // runs word count
	printf("this shouldn’t print out");
	} else { // parent goes down this path (main)
		int wc = wait(NULL);
		printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
		rc, wc, (int) getpid());
	}
	return 0;
}

위의 코드를 살펴보자.

myargs[0] = strdup("wc"); 
myargs[1] = strdup("p3.c");
myargs[2] = NULL;
execvp(myargs[0], myargs);

myarg[0]이라는 커맨드, 즉 wc라는 프로그램을 실행하는데 그 파라미터는 myargs에 있다는 뜻이다.

또한 p3.c 이외에는 파라미터가 더이상 없다는 것을 나타내기 위해 NULL을 마지막에 넣어준 것이다.

 

이 코드를 실행하면 다음과 같은 결과를 볼 수 있다.

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>

hello world를 출력할때까지만 해도 process가 하나였지만, fork()를 통해 프로세스가 두개가 된다.

그리고 wait()이 child process를 기다리고 있기 때문에 child 먼저 실행된다. 근데 이때 child process가 들어가는 분기문에서 exec()이 실행되어 program이 아예 바뀌어버린다. 한마디로 process가 천지개벽을 한 것이다.

그래서 분기문 마지막의 printf("this shouldn’t print out"); 코드는 실행되지 않는다. 거기까지 닿기도 전에 exec()이 실행되면서 프로세스의 세상이 바뀌어 버렸기 때문이다. 그리고나서는 parent precess가 실행되고 전체 실행이 끝난다.

 

 

Motivating The API

계속해서 프로세스를 제어하는 API에 대해 살펴보자.

컴퓨터가 프로세스를 복사할때는 fork()를 사용하고, 프로그램을 실행하려고 로딩할때는 exec()을 사용한다. 그런데 왜 fork()와 exec()을 분리했을까?

이것은 shell에서 우리가 명령어를 실행할때를 떠올려보면 된다.

shell에서 사용자로부터 받은 명령을 처리하기 위해서는 사전에 처리해야 하는 일들이 많이 있는데, 그런것들을 위해서 실행환경을 만드는 일(fork)과 실행환경에 해당 명령어를 실행하도록 하는 일(exec)을 분리해 둔 것이다.

그래서 주로 실행환경을 약간 수정하는 일에 사용하게 되는데, 예를 들어 출력을 프롬프트창이 아닌 파일로 redirect하고 싶으면 다음과 같이 실행하면 된다.

prompt> wc p3.c > newfile.txt

프로그램 실행시 디폴트, 즉 standard input, output은 키보드입력, 모니터 출력이다.

따라서 내가 wc라는 프로그램을 실행할 때 아무런 옵션없이 실행할 경우 입력은 키보드로, 출력은 모니터로 하게 된다.

하지만 내가 원하는 옵션을 줘서 입/출력에 연결된 것을 수정할 수도 있다.

$wc //디폴트
$wc p3.c //입력 변경
$wc p3.c > p4.output //입출력 모두 변경

그럼 다음 코드를 살펴보자.

int
main(int argc, char *argv[]){
	int rc = fork();
	if (rc < 0) { 
    // fork failed; exit
	fprintf(stderr, "fork failed\n");
	exit(1);
	} else if (rc == 0) { 
    // child: redirect standard output to a file
	close(STDOUT_FILENO);
	open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

	// now exec "wc"...
	char *myargs[3];
	myargs[0] = strdup("wc"); // program: "wc" (word count)
	myargs[1] = strdup("p4.c"); // argument: file to count
	myargs[2] = NULL; // marks end of array
	execvp(myargs[0], myargs); // runs word count
	} else { 
    // parent goes down this path (main)
	int wc = wait(NULL);
	}
	return 0;
}
File descripter table
파일시스템에서 미리 예약되어있는 숫자를 file descript number, fd라고 한다.

코드를 보면 file descipt number 1번에 해당되는 stdout을 닫아서 fd의 1번이 비게 만드는 부분이 있다.

close(STDOUT_FILENO);

그리고나서 file을 open하면 fd중 인덱스가 최소인 위치에 연결하게 되므로, p4.output이 1에 연결된다.

open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

즉, output의 방법에 교체가 일어난 것이다.

그리고는 코드가 계속 실행되면서 exec()를 만나면 program은 이러한 교체상황을 모르고 그냥 0으로 입력, 1로 출력을 한다.

execvp(myargs[0], myargs); // runs word count

* 2023 국민대학교 소프트웨어학부 황선태 교수님의 운영체제 수업을 듣고 정리한 내용입니다.

* 원서 출처 : https://pages.cs.wisc.edu/~remzi/OSTEP/