0. 시작하기전에
- 이전에 배운 fork()와 exec()에 대해서 간략하게 복습해보자.
▶ fork()
- 기존의 프로세스를 복사하여 새로운 프로세스를 생성한다.
- 기존의 프로세스를 부모 프로세스, 새로 생성된 프로세스를 자식 프로세스라고 한다.
- 이로 인해서, 프로세스간의 계층이 생성된다.
▶ exec()
- 기존의 프로세스를 재활용하는 방식으로 새로운 프로세스를 만든다.
- 기존 프로세스의 Text, BSS, Data 메모리를 새로운 코드이미지로 덮어 씌워 새로운 프로세스를 생성한다.
- 여기서 한가지 의문점이 생길 수 있다. 왜 프로세스를 만드는 방식을 fork()와 exec(), 두가지가 필요할까?
1. 리눅스의 프로세스 생성 방식
- fork()를 사용하든, exec()를 사용하든 리눅스에서 프로세스를 생성하는 방식은 2가지이다.
- 왜 2가지가 필요할까?
a) fork()로 프로세스를 생성하는 과정
- fork()는 기존의 프로세스를 복사하여 새로운 프로세스를 생성한다.
- 새롭게 생성된 프로세스는 부모 프로세스와 동일한 데이터를 가지고 있다.
- 그렇다면 새롭게 생성된 프로세스를 사용하기 위해서는 새로운 데이터를 업로드 해야한다.
- 새로운 프로세스의 Text, BSS, Data 메모리에 새로운 데이터를 덮어씌우는 과정을 거치게 된다.
- 익숙하지 않은가? 이 과정에서 exec()를 사용하게 되는 것이다.
2. wait() 시스템 콜
- 이전 포스팅의 fork() 실습에서 동일한 코드에 대해 부모, 자식 프로세스가 각각 실행하는 것을 보았다.
- 자식프로세스가 먼저 실행되고나서 부모프로세스가 실행 되었는데, 이 과정이 어떻게 가능한 것인가 이해가 잘 안갔을 것이다.
- 이 부분에 대한 이해를 하기 위해서는 wait() 시스템 콜을 알아야 한다.
a) wait()의 작동과정
- wait() 시스템 콜을 사용하면 fork()를 호출했을 때, 자식프로세스가 종료될 때까지 부모 프로세스는 대기상태가 된다.
- 자식 프로세스보다 부모 프로세스가 먼저 종료되는 것 또는 죽는 것을 방지하기 위해서 사용하며,
- 자식 프로세스와 부모 프로세스의 동기화(=순차적 실행)를 위해서 사용한다.
- 위의 그림을 참고하자.
- fork()를 호출하면 부모 프로세스와 자식 프로세스가 생성된다.
- 자식 프로세스는 exec()를 호출하여 새로운 데이터를 메모리에 업로드한다.
- 자식프로세스가 exit()을 호출하기 전까지, 부모 프로세스는 wait() 시스템 콜로 인해 대기상태가 된다.
- 자식 프로세스의 작업이 끝나면 exit()을 호출하고, 부모 프로세스는 대기상태에서 풀려나 재시작한다.
b) wait()를 사용하지 않는다면?
- wait()를 사용하지 않으면 부모 프로세스와 자식프로세스는 비동기적(= 순서가 없는) 작업수행을 하게된다.
- 그렇게 되면 부모 프로세스가 자식 프로세스보다 먼저 작업을 끝내는 경우가 발생할 수 있다.
- 이 경우, 문제가 되는 부분이 있다. 바로, 자식프로세스가 종료되지 않고 계속해서 메모리에 남아있다는 점이다.
- 자식 프로세스가 종료된 상태를 부모 프로세스가 확인해야 자식 프로세스는 완전히 종료된다.
- 하지만, 부모 프로세스가 먼저 종료되는 경우에는 자식 프로세스의 종료를 확인해줄 상위 프로세스가 없게된다.
- 이러한 자식 프로세스를 고아 또는 좀비 프로세스라고 하는데, wait()를 사용하지 않고 비동기적으로 처리하게되면 발생할 수 있다.
3. 코드 예시
- fork(), execl(), wait()를 사용한 예시를 이해해보자.
int main()
{
int pid;
int child_pid;
int status;
pid = fork()
switch(pid) {
case -1: //pid가 -1이면 실패한 경우를 의미
perror("fork is failed \n");
break;
case 0: //pid가 0이면 자식 프로세스인 경우
execl("/bin/ls", "ls", "-al", NULL);
perror("execl is failed \n");
break;
default: //pid가 0보다 큰, 부모 프로세스인 경우
child_pid = wait(NULL); //자식프로세스 종료까지 대기
printf("ls is completed\n");
printf("Parent PID is (%d), Child PID is (%d)\n", getpid(), child_pid);
exit(0);
}
}
- 위의 코드를 이해하다보면, 굳이 fork()를 사용해서 자식프로세스를 생성해야 할까? 라는 의문이 들수도 있다.
- 만약 자식프로세스를 사용하지 않는다면, execl()만을 사용해야 한다.
- 그런 경우에는 부모 프로세스의 데이터가 다 사라지고 새로운 데이터가 부모 프로세스를 덮어쓰기 때문에
- 부모 프로세스와 자식 프로세스를 나눠서 각기 다른 작업을 수행하도록 할 수 없다.
4. 쉘 만들기
- 지금까지 배운 내용을 토대로 나만의 쉘을 만들어보자.
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
#define MAXLINE 64
int main(int argc, char **argv) {
char buf[MAXLINE]; // 64 character를 가질 수 있는 buf
pid_t pid;
printf("RaphyShell ver 1.0 \n");
while(1)
{
memset(buf, 0x00, MAXLINE); // buf 값을 0으로 초기화
fgets(buf, MAXLINE - 1, stdin);
// stdin(표준 입력, 키보드 입력)을 63바이트(MAXLINE - 1)만큼 가져온다
// 63 바이트 만큼 stdin 스트림에서 가져오는 이유는 제일 마지막에 엔터가 입력되기 때문이다.
// char *fgets (char *string, int n, FILE *stream)
if(strncmp(buf, "exit\n", 5) == 0) {
break;
}
buf[strlen(buf) - 1] = 0x00; // 키보드의 마지막입력인 엔터를 0으로 초기화
pid = fork();
if(pid == 0) { // 자식 프로세스인 경우
if(execl(buf, buf, NULL) == -1) {
printf("command execution is failed \n");
exit(0);
}
}
if(pid > 0) { // 부모 프로세스인 경우
wait(NULL);
}
}
return 0;
}
a) 실행결과
- 실행파일을 만들어서 해당 파일을 구동시켜보았다.
- 다음과 같은 결과를 볼 수 있다.
- 왜 ls는 안되고 /bin/ls를 써야 명령어가 먹힐까? execl()을 사용했기 때문이다.
- execl() 시스템 콜은 절대주소 형식으로 입력해야 해당 파일의 위치를 찾을 수 있는 형식을 가졌다.
- 만약 ls만 이용해서 동작시키고 싶다면 execlp()를 사용해야 한다.
- 이전 포스팅에서 배운 내용이다. 기억나는가?
- 이 부분만 execl()에서 execlp()로 변경시켜주면 된다.
- 이제 절대주소가 아닌 path를 참조해서 명령어를 사용할 수 있게 되었다.
'컴퓨터공학기초 개념 > 시스템 프로그래밍' 카테고리의 다른 글
23. 프로세스 - 프로세스 종료(exit) (0) | 2021.09.17 |
---|---|
22. 프로세스 - copy on write (0) | 2021.09.16 |
20. 프로세스 - 프로세스 생성(exec) (0) | 2021.09.15 |
19. 프로세스 - 프로세스 생성(fork) (0) | 2021.09.15 |
18. 프로세스 - 시스템 콜 (0) | 2021.09.15 |
댓글