본문 바로가기
컴퓨터공학기초 개념/시스템 프로그래밍

21. 프로세스 - wait & 쉘 만들기

by devraphy 2021. 9. 16.

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를 참조해서 명령어를 사용할 수 있게 되었다. 

댓글