UNIX 상에서 프로세스 관리를 위한 시스템 콜, API는 어떤 것을 사용할까.
fork(), exec()를 통해 프로세스를 생성하고, wait()를 통해 자식 프로세스가 끝날 때 까지 대기한다.
fork
fork() 함수는 현재 프로세스를 복사한다. 현재 프로세스(부모)에서 새로운 프로세스인 자식 프로세스를 만든다.
#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 {
// 부모 프로세스
printf("hello, I am parent of %d (pid:%d)\n", rc, (int)getpid());
}
return 0;
}
코드 실행 시 hello world (pid:~~)는 한번 밖에 나오지 않는다. 자식 프로세스는 fork()가 실행된 시점 부터 생성된다.
자식 프로세스와 부모 프로세스는 완전히 동일하지는 않다. 할당된 레지스터, 주소 공간, PC 값 등 다르다. 또한 부모 프로세스는 자식 프로세스 PID를 반환하지만 자식 프로세스는 0을 반환한다.
위 코드에서 프로세스 출력 결과 순서는 항상 동일하지 않다. CPU Scheduler 에 따라서 달라 질 수 있다. 비결정성(nondeterminism)으로 인해 멀티 쓰레드 프로그램시 문제가 발생한다.
wait
wait()함수는 말 그대로 대기를 한다. 부모 프로세스가 자식 프로세스가 끝내기 까지 기다리는 것이며 이를 통해 자식 프로세스가 항상 먼저 끝내게 할 수 있다.
#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 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());
sleep(1);
} else {
// parent goes down this path (original process)
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
rc, wc, (int) getpid());
}
return 0;
}
위 코드 실행 시 결과 확인 가능하다.
exec
exec()는 fork()와 달리 부모 프로세스가 자식 프로세스를 만들어주는 게 아니라 현재 프로세스가 외부 프로그램을 실행 시킬 때 사용된다. 외부 프로그램 실행 시 현재 프로세스는 외부 프로세스로 코드 세그먼트와 정적 데이터 부분을 덮어쓴다.
#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 (original process)
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
rc, wc, (int) getpid());
}
return 0;
}
위 코드를 실행하면 printf("this shouldn't print out"); 이것이 출력되지 않을 것이다.
그럼 왜 이렇게 나눈건가?
프로세스 구성을 위한 특정한 API의 정의가 있는 것은 아니다. 하지만 위와 같이 API를 구성함으로 UNIX Shell을 사용함에 있어 유용하다.
ls -al > test.txt 와 같은 명령어를 실행한다 가정하자. fork로 새로운 프로세스를 만들고 ls -al 실행하기 전 표준 출력을 닫고, test.txt 파일을 연다. 표준 출력을 닫으면 자동으로 미사용 중인 파일을 찾아 연결된다. 그러면 ls -al을 실행하더라도 화면이 아닌 test.txt 파일에 저장된다.
외부 프로그램 실행 전 자식 프로세스를 통해 파일 디스크립터 등 다양한 설정을 할 수 있어 fork()로 자식 프로세스 생성 후 exec()를 실행하는 경우가 많다.
pipe
UNIX의 pipe() 시스템 콜도 위의 ls -al > test.txt 처럼 양쪽 파일 끝에서 연동되게 하는 기능이 있다. 주로 fork(), exec()와 함께 사용된다.
이외 명령어
kill(): 프로세스에 시그널(signal)을 보낸다. 시그널은 프로세스를 중단, 삭제하는 작업 등에 사용된다.ps(): 어떤 프로세스 실행 중인 지 확인하는 명령어man():man [알고 싶은 명령어]와 같이 사용되며 메뉴얼을 알 수 있다.top(): 현재 프로세스와 자원이 많이 사용되는 프로세스를 보임.