멀티프로세스의 이해
멀티프로세스는 운영 체제에서 하나의 프로그램이 여러 개의 프로세스를 생성하여 병렬 작업을 수행하는 기법입니다.
프로세스와 시스템 호출
프로세스는 실행 중인 프로그램의 인스턴스입니다. 멀티프로세스를 구현하기 위해서는 프로세스를 생성하거나 관리할 수 있는 시스템 호출이 필요합니다. 아래는 주요 시스템 호출입니다:
- fork(): 현재 프로세스(부모 프로세스)의 코드를 복제하여 자식 프로세스를 생성합니다. 반환값은 부모와 자식에서 다르게 동작합니다:
- 부모: 생성된 자식 프로세스의 PID 반환
- 자식: 0 반환
- exit(int status): 현재 프로세스를 종료하고 상태 코드를 반환합니다.
- getpid(): 현재 프로세스의 PID를 반환합니다.
- getppid(): 현재 프로세스의 부모 프로세스 PID를 반환합니다.
fork() 예제
void fork_example() {
pid_t pid = fork();
if (pid == 0) {
printf("This is the child process.\n");
} else {
printf("This is the parent process.\n");
}
}
프로세스 종료와 정리
프로세스 종료 시, 운영 체제는 프로세스가 점유한 자원을 반환해야 합니다. 이를 위한 함수와 메커니즘을 이해해야 합니다.
- atexit(handler): 프로세스 종료 시 실행할 함수를 등록합니다. 부모와 자식 모두 별도로 실행됩니다.
- 좀비 프로세스: 자식 프로세스가 종료되었지만, 부모가 이를 정리하지 않아 프로세스 정보가 남아 있는 상태입니다. wait()와 waitpid()를 사용하여 좀비 프로세스를 방지할 수 있습니다.
wait()와 waitpid()
- wait(): 하나의 자식 프로세스가 종료될 때까지 대기합니다.
- waitpid(pid, *status, options): 특정 자식 프로세스를 기다립니다. 옵션으로 WNOHANG을 설정하면 대기하지 않고 즉시 반환할 수 있습니다.
void wait_example() {
pid_t pid = fork();
if (pid == 0) {
exit(42);
} else {
int status;
wait(&status);
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
}
}
}
좀비 프로세스 예제
void zombie_example() {
pid_t pid = fork();
if (pid == 0) {
printf("Child process terminating. PID: %d\n", getpid());
exit(0);
} else {
printf("Parent process running. PID: %d\n", getpid());
while (1) {
sleep(1);
}
}
}
위 코드에서 부모 프로세스가 종료되지 않고 자식 프로세스의 종료를 처리하지 않으면 자식은 좀비 상태로 남게 됩니다. 이를 방지하려면 부모가 wait() 또는 waitpid()를 호출하여 자식의 종료를 처리해야 합니다.
고아 프로세스와 좀비 프로세스
고아 프로세스
고아 프로세스는 부모 프로세스가 먼저 종료되어 더 이상 부모가 없는 상태가 되는 프로세스입니다. 이 경우 운영 체제의 init 프로세스가 해당 고아 프로세스의 부모 역할을 대신하며, 자식 프로세스가 종료되면 init이 이를 수거하여 자원을 반환합니다. 고아 프로세스는 일반적으로 시스템에 문제를 일으키지 않으나, 프로세스 관리 구조가 복잡해질 수 있습니다.
좀비 프로세스
좀비 프로세스는 자식 프로세스가 종료된 후에도 부모가 자식의 종료 상태를 수거하지 않아 프로세스 테이블에 남아 있는 상태입니다. 좀비 프로세스는 시스템 자원을 소비하며, 지속적으로 남아 있으면 프로세스 테이블의 공간을 소모하여 새로운 프로세스 생성을 방해할 수 있습니다.
자원 수거와 wait()
운영 체제에서 좀비 프로세스를 방지하려면 부모 프로세스가 wait() 또는 waitpid()를 호출하여 자식 프로세스의 종료 상태를 확인하고, 이를 수거(reap)해야 합니다. 이 과정을 통해 자식 프로세스의 종료 상태가 부모에게 전달되며, 운영 체제가 프로세스 테이블에서 해당 정보를 제거합니다.
void prevent_zombie() {
pid_t pid = fork();
if (pid == 0) {
printf("Child process exiting.\n");
exit(0);
} else {
int status;
wait(&status); // 자식 프로세스 종료 상태를 수거
printf("Child process reaped.\n");
}
}
고아 및 좀비 방지
- 부모가 자식 종료를 wait() 또는 waitpid()로 처리해야 합니다.
- 자식 프로세스가 SIGCHLD 신호를 통해 부모에게 자신의 종료를 알리도록 설정할 수 있습니다.
- 부모가 비동기로 자식 종료를 처리하려면 signal(SIGCHLD, handler)와 같은 방식으로 핸들러를 등록하면 됩니다.
비동기 종료 처리 예제
void sigchld_handler(int signum) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
void async_wait_example() {
signal(SIGCHLD, sigchld_handler);
if (fork() == 0) {
printf("Child process exiting.\n");
exit(0);
}
printf("Parent process continuing without waiting.\n");
sleep(5); // 부모는 다른 작업 수행 가능
}
exec 패밀리 함수
exec 함수는 현재 프로세스를 새로운 프로그램으로 대체합니다. 반환값은 없으며, 호출이 성공하면 기존 코드의 실행은 종료됩니다.
- execl(): 인자를 리스트로 전달합니다.
- execv(): 인자를 배열로 전달합니다.
- execlp(): 파일명을 경로 없이 전달 가능합니다.
- execle(): 환경 변수를 명시적으로 설정 가능합니다.
- execvp(): 배열 인자와 경로 없이 파일명 사용 가능합니다.
- execvpe(): 배열 인자와 환경 변수를 사용할 수 있습니다.
exec 예제
void exec_example() {
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);
perror("exec failed"); // exec 호출 실패 시 실행
}
종합 예제: 간단한 쉘 구현
아래는 execve를 활용하여 간단한 쉘을 구현한 예제입니다:
void eval(char* cmdLine) {
char* argv[100];
pid_t pid;
if (parse_command(cmdLine, argv) == 0) {
if ((pid = fork()) == 0) {
if (execve(argv[0], argv, environ) < 0) {
perror("exec failed");
exit(1);
}
}
wait(NULL);
}
}
'CS 지식 > 시스템 프로그래밍' 카테고리의 다른 글
Assembly(어쌤블리어)의 기초 (0) | 2024.10.18 |
---|---|
[C언어] Signal 프로그래밍에서의 Race Condition과 해결법 (0) | 2024.10.17 |
[C언어] Signal 프로그래밍 개념과 예제 (0) | 2024.10.17 |
댓글