자주 사용되는 Process API 3가지에 대한 예시 코드를 살펴보자
#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(); // 자식 process 생성
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()는 자식 Process를 생성하는 API이다.
현재 A라는 Process가 위의 코드를 실행하고 있다고 생각해보자
- fork() System call
- A의 자식 process인 B가 생성
- A의 code, data, stack, heap가 상속, PCB의 대부분 상속 (pid 같은 요소는 상속X)
위의 과정을 거쳐 자식 process인 B가 생성되고 A와 B가 병렬적으로 실행된다.
또한 fork()는 해당 process가 자식인 경우 0을, 부모인 경우 자식의 pid를 반환한다.
따라서 부모 process인 A가 실행될 때는 else 절에 해당하는 코드를 실행하게 되고
자식 process인 B가 실행될 때는 rc 값이 0이므로 else if 절에 해당하는 코드를 실행하게 된다.
위 코드의 출력은 다음과 같다.
2개의 출력 중 non-deterministic하게 결정된다.
그 이유는 A와 B process중 어떤 process가 먼저 실행될 지 모르기 때문이다.
부모 process인 A가 먼저 실행되었다면 위의 결과가, 자식 process인 B가 먼저 실행되었다면 밑의 결과가 출력된다.
wait()는 fork()를 통해 생성한 자식 process가 죽을 때까지(사라질 때까지) 코드의 실행을 멈추는 API이다.
예제 코드를 살펴보자
#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(); // 자식 process 생성
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;
}
fork()를 통해 자식 process를 생성하고 있다.
앞선 코드와의 차이는 else절의 첫 문장에서 wait()을 사용하고 있다는 점이다.
- A process에서 fork()를 통해 자식 process B를 생성
- A가 else절을 실행, wait때문에 B가 죽을 때까지 대기
- B가 else if절을 실행, return을 지나며 종료
- B가 종료되고 A가 남은 else절을 실행
wait()을 사용하면서 출력이 deterministic하게 1개만 나오는 것을 확인할 수 있다.
exec()는 fork()가 등장한 이후에 사용될 수 있다.
우리가 fork()를 call하면 자식 process가 생성되며 부모 process의 code, data, stack, heap이 상속된다.
exec()를 사용하면 자식 process의 code, data를 DISK에 저장된 새로운 내용으로 바꿔줄 수 있다.
그림으로 간략하게 표현하면 위와 같다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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 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;
}
코드를 살펴보면, else if절에서 exec()의 한 종류인 execvp()를 call하고 있다.
execvp()의 인자는 원하는 program의 이름과 그 program의 main함수의 2번째 인자인 argv이다.
2번째 인자를 통해 execvp()의 내부에서 NULL이 등장하기 전까지의 배열 요소의 개수를 argc로 판단하여 main함수에 인자로 전달해준다. 그래서 myargs[2] = NULL; 문장이 필요한 것이다.
따라서 else if절에서 execvp()를 call하기 전에 myargs라는 char* 배열을 만들고 있다.
strdup은 인자로 전달된 문자열을 heap 영역에 할당하고 해당 문자열의 주소를 반환하는 함수이다.
자식 process가 else if절을 실행할 때 execvp()를 call하면 code가 새롭게 바뀌기 때문에(wc라는 프로그램의 code, data로, stack과 heap도 초기화) 다음 printf 문장은 사라지며 출력되지 않는다.
29 107 1030 p3.c는 execvp()에서 출력되는 결과이다.
새로운 program을 실행하려고 할 때 앞의 예제에서는
fork() -> exec() 순서로 call하는 과정을 거쳤다.
fork와 exec 사이에 시간 간극이 존재하는데 이 때 자식 process에게 적용되는 다른 부가적인 일을 할 수 있다.
다음 예제 코드는 화면에 출력하는 것이 아니라 파일에 출력되는, redirection의 과정을 추가했다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>
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);
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;
}