반응형
Linux/UNIX 운영체제 환경에서 새로운 프로세스를 생성하는 대표적인 시스템 콜인 fork()에 대해 알아보자.
fork()
- fork()를 호출하면 부모 프로세스의 주소 공간이 그대로 복사돼서 자식 프로세스가 만들어진다.
- 부모 프로세스의 메모리 공간 즉, Code(Text), Data(Data, BSS), Heap, Stack 영역을 자식이 똑같이 가진 상태로 시작한다.
- 부모 프로세스와 자식 프로세스는 같은 코드 위치(fork() 리턴 이후)에서 실행을 이어간다.
- 부모와 자식 프로세스는 fork()의 반환값이 다르게 나오기 때문에 개발가자 구분하여 실행 경로를 나눌 수 있다.
- 부모 프로세스는 자식의 PID(> 0) 반환
- 자식 프로세스는 0 반환
- 실패시 -1 반환
- 복제된 시점 이후로 부모 프로세스와 자식 프로세스는 서로 독립된 메모리 공간을 사용한다.
- 단, 성능 최적화를 위한 Copy-On-Write 메커니즘을 사용하기 때문에 실제로 부모/자식이 동일한 메모리를 공유하다가 어느 한쪽이 수정하는 순간에 복사가 일어난다.
fork() 사용 예제
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
int value = 10;
if (pid == 0) {
// 자식 프로세스 실행 영역
value += 5;
printf("Child: My PID = %d, value = %d\n", getpid(), value);
} else {
// 부모 프로세스 실행 영역
value -= 5;
printf("Parent: My PID = %d, Child PID = %d, value = %d\n", getpid(), pid, value);
}
return 0;
}
Parent: My PID = 1992, Child PID = 1993, value = 5
Child: My PID = 1993, value = 15
자식 프로세스의 종료 처리 : wait / waitpid
자식 프로세스가 종료되면 커널은 그 정보를 부모가 수거하기 전까지 좀비 프로세스로 남겨두기 때문에, 부모는 wait() 또는 waitpid()를 호출하여 자식의 종료 상태를 회수할 필요가 있다.
(위 예제에서는 부모와 자식 둘 다 printf 만 찍고 바로 return 0; 코드에 도달하므로 거의 동시에 둘 다 종료되는 케이스이다.)
wait 함수는 임의의 자식 프로세스가 종료될 때까지 블로킹한다.
#include <sys/wait.h>
pid_t wait(int *stat_loc);
- stat_loc: 이 인자로 전달된 주소의 변수에 자식의 종료 상태가 저장된다. 매크로를 통해 해석하여야 하며, 자식의 종료 상태가 중요하지 않은 경우 NULL을 넘기면 된다.
- WIFEXITED(stat_loc) : 정상 종료한 경우 true(1) 반환
- WEXITSTATUS(stat_loc) : 자식이 exit() 또는 return으로 전달한 값 확인 (0~255)
- WIFSIGNALED(stat_loc) : 시그널로 종료되었는지 확인
- WTERMSIG(stat_loc) : 어떤 시그널로 종료되었는지 확인
- 리턴 값 : 종료된 자식의 PID, 오류 발생 시 -1
만약 자식 프로세스가 여럿이라면 while (wait(NULL) > 0) { } 으로 모든 자식 프로세스가 종료될 때까지 대기할 수 있다.
waitpid는 wait 함수보다 유연하게 자식의 종료를 대기할 수 있는 함수이다.
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *stat_loc, int options);
- pid : 종료를 확인하고자 하는 자식의 PID, -1을 전달하면 wait 함수와 마찬가지로 임의의 자식 프로세스가 종료되길 기다린다.
- stat_loc : wait()과 동일
- options : 인자로 상수 WNOHANG를 전달하면 논 블로킹 모드로 동작하여, 종료된 자식이 없는 경우 즉시 0을 리턴한다.
- 리턴 값 : wait()과 동일
fork + exec 조합
실무에서는 fork()로 자식을 만든 뒤, exec 계열 함수로 새로운 프로그램을 실행하는 경우가 많다. 주로 쉘 명령을 실행하거나, 서버 또는 데몬 프로세스에서 외부 프로그램을 실행할 때 이 방식을 많이 쓴다.
exec 계열 함수들에는 다음과 같은 것들이 있다. 보통 execlp 또는 execvp 함수가 많이 쓰이며, 함수들 간의 차이는 인자 전달 방식과 PATH 탐색 여부이다.
- exec 계열 함수들 : execl, execlp, execle, execv, execvp, execve
- l : 인자를 가변 인자 형식으로 나열
- v : 인자를 배열(char *argv[])으로 전달
- p : PATH 환경변수에서 실행 파일 검색
- e : 환경변수 배열을 직접 전달
#include <unistd.h>
int execlp(const char *file, const char *arg0, ... /*, (char *)NULL */);
- file : 실행할 프로그램 이름(또는 파일 경로)
- arg0 : argv[0] 값으로 보통 실행할 프로그램 이름을 넣는다
- arg1, arg2, ... 가변인자 : 실행할 프로그램에 전달할 인자들
- NULL : 인자 리스트의 끝을 알리기 위해 마지막 인자로는 반드시 NULL을 넣어야 한다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 자식 프로세스 실행 영역: exec로 새로운 프로그램 실행
execlp("ls", "ls", "-l", (char *)NULL);
// 자식 프로세스의 메모리 공간이 새로운 프로그램으로 교체되었기 때문에
// exec 실패 시에만 이 코드에 도달한다.
perror("execlp failed");
exit(1);
} else if (pid > 0) {
// 부모 프로세스 실행 영역: 자식 종료 대기
wait(NULL);
printf("Child process finished.\n");
} else {
perror("fork failed");
exit(1);
}
return 0;
}
- exec*() 함수가 성공하면 자식의 프로세스의 메모리 공간 전체가 교체되어, 기존의 코드, 데이터, 힙, 스택 모두 사라지고 지정한 프로그램의 이미지로 덮어씌워져 새 프로그램의 main()부터 실행한다.
- 따라서 exec*() 호출이 실패한 경우가 아닌 이상 원래 코드 흐름은 더이상 실행되지 않는다.
- 단, exec*() 호출이 성공하더라도 자식의 PID는 변하지 않기 때문에 부모든 여전히 자식 PID를 추적 가능하다.
참고로 위 예시 케이스에선 자식의 표준 출력이 부모의 터미널로 그대로 나오는데, 그걸 원하지 않은 경우 stdout, stderr를 리다이렉션하는 방법이 있다.
반응형
'프로그래밍 > 리눅스 시스템 프로그래밍' 카테고리의 다른 글
| Linux ] 파일 append는 정말 atomic 할까? (0) | 2025.11.12 |
|---|---|
| Linux ] dup2() 함수를 사용한 표준입출력 리다이렉션 (0) | 2025.11.03 |
| Linux에서 현재 프로세스가 모니터가 연결된 GUI 세션인지 확인하는 법 (0) | 2025.10.24 |
| POSIX C ] root 권한 체크하기 (0) | 2025.10.21 |
| errno == EINTR (0) | 2025.10.15 |