개념
프로세스란 실행 중인 프로그램(실행 파일)이자, 현대의 컴퓨팅 시스템에서 작업의 단위이다.
프로세스는 실행되는 동안 여러 개의 새로운 프로세스들을 생성할 수 있다.
생성하는 프로세스를 부모 프로세스라 부르고, 새로운 프로세스는 자식 프로세스라고 부른다.
새로운 프로세스들은 각각 다시 자식 프로세스들을 생성할 수 있으며, 이는 프로세스 트리를 형성한다.
UNIX의 운영체제는 고유한 프로세스 식별자(PID)를 사용하여 프로세스를 구분한다.
자식 프로세스는 부모 프로세스와 동일한 프로그램을 가지며, 주소 공간(fd 테이블 포함)이 복사된다.
프로세스 생성
#include <unistd.h>
#include <sys/types.h> // pid_t
pid_t fork(void);
fork() 시스템 콜로 자식 프로세스를 생성한다. pid_t는 int와 동일하다.
fork()가 호출된 이후 두 프로세스가 나뉘며, 각 프로세스는 fork()의 반환 값을 다르게 받는다.
성공하면 부모 프로세스는 자식 프로세스의 PID가 반환되고, 자식 프로세스는 0이 반환된다.
실패하면 -1이 반환되고, 자식 프로세스가 생성되지 않으며, 오류를 나타내는 errno가 설정된다.
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h> // pid_t
int main()
{
pid_t pid = fork();
if (pid < 0)
{
printf("fork error\n");
return 1;
}
if (pid == 0)
{
printf("I'm Child Process\n");
}
else
{
printf("I'm Parent Process\n");
}
return 0;
}
fork()는 위와 같이 사용 가능하며, 실행 결과는 아래와 같다.
자식 프로세스 제어
wait / waitpid
#include <sys/wait.h>
#include <sys/type.h> // pid_t
pid_t wait(int *_Nullable wstatus);
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
부모 프로세스는 자식 프로세스가 종료될 때까지 wait() 시스템 콜로 기다릴 수 있다.
프로세스가 종료되면 사용하던 자원은 운영체제가 회수하지만,
프로세스의 종료 상태가 저장되는 프로세스 테이블에는 부모 프로세스가 wait() 할 때까지 남아있게 된다.
종료되었지만 부모 프로세스가 wait() 하지 않은 자식 프로세스를 좀비 프로세스라고 한다.
또한, 자식 프로세스가 종료되기 이전에 부모 프로세스가 종료되는 경우, 자식 프로세스를 고아 프로세스라고 한다.
UNIX는 고아 프로세스의 부모를 init 프로세스로 재설정함으로 인해 문제를 해결한다.
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
int status;
if (pid < 0)
{
printf("fork error\n");
return 1;
}
if (pid == 0)
{
printf("I'm Child Process\n");
int meaninglessWork[1000000];
for (int i=0; i<1000000; ++i)
meaninglessWork[i] = i;
printf("Finished work\n");
}
else
{
printf("I'm Parent Process\n");
wait(&status);
if (WIFEXITED(status))
{
printf("Wait Status : %d\n", WEXITSTATUS(status));
}
}
return 0;
}
wait()은 위와 같이 사용 가능하며, 실행 결과는 아래와 같다.
int 변수의 주소 값을 인자로 받으며 이는 자식 프로세스의 종료 상태를 나타낸다.
자식의 종료 상태를 알고 싶지 않는 경우, NULL을 전달 할 수 있다.
해당 종료 상태는 매크로 함수를 통해 쉽게 판단이 가능하다.
매크로 함수 |
설명 |
WIFEXITED(wstatus) |
참이 반환된 경우, 자식 프로세스는 정상적으로 종료 |
WEXITSTATUS(wstatus) |
WIFEXITED에서 참이 반환됐을 경우 사용 가능하며, 어느 코드로 종료되었는지 파악 가능 (main() return value) |
WIFSIGNALED(wstatus) |
참이 반환된 경우, 자식 프로세스는 시그널에 의해 종료 |
WTERMSIG(wstatus) |
WIFSIGNALED에서 참이 반환됐을 경우 사용 가능하며, 어느 시그널로 종료되었는지 파악 가능 (kill signal) |
waitpid()는 wait() 보다 더욱 많은 기능을 제공한다.
wait()은 먼저 종료되는 자식 프로세스를 감지하는 반면, waitpid()는 pid를 지정하여 감지가 가능하다.
더 많은 종료 상태 확인 매크로와 waitpid()에 대한 정보는 메뉴얼 확인
waitpid()를 wait()과 동일하게 사용하고 싶은 경우, 아래와 같이 인자를 전달한다.
waitpid(-1, &wstatus, 0);
kill
#include <signal.h>
#include <sys/types.h> // pid_t
int kill(pid_t pid, int sig);
프로세스간 통신 방법 중에 시그널(signal)이 존재한다.
kill() 시스템 콜로 특정 프로세스나 프로세스 그룹에게 시그널을 보낼 수 있다.
fork()의 반환 값으로 자식 프로세스의 pid를 알고 있으므로, 종료 시그널을 보내 자식 프로세스를 종료시킬 수 있다.
sig에는 원하는 시그널 값을 넣는다. 시그널의 종류는 아래 링크 참고
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
int main()
{
pid_t pid = fork();
int status;
if (pid < 0)
{
printf("fork error\n");
return 1;
}
if (pid == 0)
{
printf("I'm Child Process\n");
while (1)
{
// infinity work
}
printf("Finished work\n");
}
else
{
printf("I'm Parent Process\n");
usleep(100000);
kill(pid, SIGINT);
wait(&status);
if (WIFSIGNALED(status))
{
printf("Child Process Killed : %d\n", WTERMSIG(status));
}
}
return 0;
}
위는 kill()로 무한 루프에 빠진 자식 프로세스를 종료시키는 코드다.
(자식 프로세스가 무한 루프에 도달하기 이전 kill()하는 것을 막기 위해 usleep()을 주어 잠시 block 시켰다.)
아래는 실행 결과.
프로세스 간 통신 (IPC)
부모 프로세스와 자식 프로세스는 독립적인 메모리 공간이므로 데이터를 공유할 때 특정 방법을 이용해야한다.
IPC (Inter-Process Communication)는 프로세스가 데이터를 공유하기 위해 운영 체제에서 제공하는 메커니즘이다.
파일, 시그널, 소켓, 메시지 큐, 파이프, 공유 메모리 등이 있다.
파일
파이프
세마포어
소켓
참고 자료
https://man7.org/linux/man-pages/man2/fork.2.html
https://man7.org/linux/man-pages/man2/waitpid.2.html
https://man7.org/linux/man-pages/man2/kill.2.html
개인 공부용 포스팅인 점을 참고하시고, 잘못된 부분이 있다면 댓글로 남겨주시면 감사드리겠습니다.