일반적으로 새로운 프로세스를 만들기 위해서는 부모 프로세스로부터 자식 프로세스를 만들어야한다.
zsh, bash 같은 쉘 프로세스에서 쉘 명령어를 실행시키는 동작을 예시로 들어서 살펴보자.
쉘은 다양한 쉘 명령어를 실행시킬 수 있다. 예를 들어 ls 명령어를 사용하면 현재 디렉토리의 파일 목록을 출력할 수 있다.
명령을 입력 받은 쉘 프로세스는 ls 명령어를 실행시키기 위한 자식 프로세스를 만든다. 이는 다음과 같은 과정을 거쳐서 만들어진다.
- ls 명령어가 실행되면 쉘은 fork() 시스템 콜을 통해 자식 프로세스를 만든다.
- 자식 프로세스의 프로그램 코드는 exec() 시스템 콜을 통해 /bin/ls 위치에 있는 프로그램 코드로 교체된다.
- 자식 프로세스는 ls 프로그램에 있는 명령어들을 실행하여 할일을 마치고 종료된다.
쉘 프로세스는 부모 프로세스로부터 생성된 자식 프로세스와 병행하여 계속 실행된다. 즉 자식 프로세스가 완료되기 이전에 새로운 명령어를 입력 받을 수 있다. 부모 프로세스는 wait() 시스템 콜을 통해 자식 프로세스가 종료되기를 기다리고, 새롭게 입력된 명령어는 먼저 실행했던 자식 프로세스가 끝난 다음 순차적으로 실행된다.
/Users/kimsj > sleep 5 && echo "first" # 5초 후에 "first" 출력하는 명령어
echo "second" # 5초가 지나기 전에 다음 명령어 입력
first # 5초 후에 "first"가 출력됨
/Users/kimsj > echo "second" # "first" 출력 후 다음 명령어가 실행됨
second
프로세스를 만들기 위한 시스템 콜
운영체제의 커널은 하드웨어를 제어하기 위한 API인 시스템 콜을 제공한다. 그 중 프로세스를 만들기 위해 다음과 같은 시스템 콜들을 사용할 수 있다.
- fork()
- 부모 프로세스의 메모리 공간을 복제하여 자식 프로세스를 만든다.
- fork()의 반환값(pid)은 자식 프로세스인 경우 0, 부모 프로세스인 경우 자식 프로세스의 pid이다.
- wait()
- 부모 프로세스는 wait() 시스템 콜을 사용하여 자식 프로세스의 실행이 종료될 때까지 기다릴 수 있다.
- wait()이 실행되면 부모 프로세스를 대기 큐(wait queue)로 보낸다.
- exec()
- 현재 프로세스의 메모리 공간을 새로운 프로그램으로 교체한다.
- 자식 프로세스의 메모리 공간을 다른 프로그램으로 바꿀 때 사용한다.
자식 프로세스를 만드는 프로그램 예시(C 언어)
부모 프로세스로부터 자식 프로세스가 만들어지면 둘은 어떻게 구분할 수 있을까?
fork()를 호출하면 프로세스 아이디 값(pid)이 반환된다. 이 값이 0이면 자식 프로세스이고, 0보다 큰 값이면 부모 프로세스로 식별할 수 있다.
부모 프로세스가 반환 받는 아이디 값은 자식 프로세스의 pid이다.
// pid.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid, parent_pid, child_pid;
pid = fork();
if (pid > 0) {
wait(NULL); // 자식 프로세스 종료될 때까지 블록됨
parent_pid = getpid(); // 현재 프로세스의 pid를 가져옴
printf("pid: %d\\n", pid); // fork()에서 반환 받은 값은 자식 프로세스의 pid
printf("parent pid: %d\\n", parent_pid);
printf("I am parent process\\n");
}
else if (pid == 0) {
child_pid = getpid(); // 현재 프로세스의 pid를 가져옴
printf("child pid: %d\\n", child_pid);
printf("I am child process\\n");
}
return 0;
}
실행 결과는 다음과 같다.
> gcc pid.c # pid.c 파일을 실행파일로 컴파일
> ./a.out # a.out 실행파일 실행
child pid: 50550
I am child process
pid: 50550
parent pid: 50548
이번에는 fork()를 이용해 자식 프로세스를 만들고, exec()을 이용해 자식 프로세스를 다른 프로그램으로 대체하는 코드를 작성해보자. 여기서 execlp()는 exec()의 한 버전이다. execlp()는 첫번째 인자로 대체하고자 하는 프로그램이 저장된 경로를 받는다.
// exec_ls.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid == 0) {
execlp("/bin/ls", "ls", NULL); // 자식 프로세스를 ls 프로그램으로 변경
printf("여기는 실행되지 않음");
}
else if (pid > 0) {
wait(NULL); // 자식 프로세스가 종료되기 전까지 기다린다.
printf("부모 프로세스 완료\\n");
}
return 0;
}
실행 결과는 다음과 같다.
> gcc exec_ls.c
> ./a.out
a.out exec_ls.c # 자식 프로세스가 ls 프로그램을 실행한 결과
부모 프로세스 완료 # 부모 프로세스에서 출력된 것
기존에 있는 프로그램 말고도 내가 만든 프로그램으로 자식 프로세스를 교체해보았다. 먼저 다음과 같은 쉘 명령어 파일을 만든다. 주의할 점은 해당 파일을 실행할 수 있도록 실행 권한을 설정해야한다. chmod +x hello 명령어로 파일에 대한 실행 권한을 추가할 수 있다.
# hello
echo "Hello World"
execlp()에 인자로 넣는 경로를 hello 파일의 경로로 설정해서 자식 프로세스를 해당 쉘 명령어 프로그램으로 변경한다.
// exec_hello.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid == 0) {
execlp("/Users/kimsj/Desktop/.../hello", "hello", NULL); // 자식 프로세스를 hello 프로그램으로 변경
printf("여기는 실행되지 않음");
}
else if (pid > 0) {
wait(NULL);
printf("부모 프로세스 완료\\n");
}
return 0;
}
실행 결과는 다음과 같다.
> gcc exec_hello.c
> ./a.out
Hello World # hello 프로그램이 실행된 결과
부모 프로세스 완료
'운영체제' 카테고리의 다른 글
[OS] 가상 메모리 & 요구 페이징 (0) | 2024.06.12 |
---|---|
[OS] 경쟁 상태(Race Condition) (0) | 2024.05.14 |
쉘(Shell)과 커널(Kernel) (0) | 2023.09.18 |
[OS] 프로세스 스케줄링 (3) | 2023.07.20 |
크론탭(crontab)을 이용한 스케줄링 (0) | 2023.07.11 |