pipex 프로젝트를 진행하기 전 아키텍쳐를 먼저 구상하는 것이 좋다.
어떤 아키텍쳐로 프로그램을 작성할 지 정하지 않으면 구조를 바꾸느라 코드를 거의 전부 새로 짜야 하는 경우가 종종 생긴다.
나는 크게 3가지 아키텍쳐를 구상했고, 그 중 하나를 선택하여 과제를 진행했다.
부모 프로세스에서 자식 프로세스를 fork하고, 그 자식 프로세스에서 자식 프로세스를 fork하는 방식의 아키텍쳐를 먼저 구상했다.
이런 방식에는 또 두가지 구현 방식이 있을 수 있다.
가장 마지막에 생성된 자식 process(가장 baby인 process)에서 cmd1을 먼저 실행시키는 방식이다.
내가 선택한 방식은 이 형태인데, 그 때 당시 이 아키텍쳐를 선택한 이유는
라는 생각 때문이었는데,
실제로 pipe를 기준으로 각 명령어들은 병렬적으로 실행된다.
예시를 살펴보자.
cat 이라는 명령어는 뒤에 파일명이 같이 입력으로 들어오면 파일을 화면에 출력해주지만 만약 별도의 인자가 없으면 표준 입력으로 입력을 기다린다. 사용자가 ctrl+D 를 통해 eof를 넣어주기 전까지 대기를 하는데, 그렇다면 cat | ls 를 하면 어떻게 될까?
실제로 해당 command line을 입력해보면 ls의 결과가 먼저 출력됨을 확인할 수 있다.
실제 bash처럼 병렬적으로 pipex가 동작하도록 하기 위해서 현재 아키텍쳐에서 waitpid의 옵션을 변경해주면 일부 문제를 해결할 수가 있다.
waitpid에 WNOHANG 옵션을 넣어줌으로써 해결할 수 있는데, WNOHANG 옵션은 부모가 자식이 끝날 때까지 block 되지 않도록 해준다.
따라서 위와 같이 WNOHANG으로 waitpid를 걸어두고 비정상 종료가 아니면 아직 자식이 끝나지 않아도 다음 command를 일단 실행한다. 이 옵션을 사용하면 자식이 종료되지 않아도 다음 line이 실행되기 때문에 return 값과 status 값을 통해 자식이 어떤 상태인지를 check 해주어야 한다.
나는 이 방식으로 구현했지만 minishell 과제를 하면서 내가 짠 코드의 허점을 많이 발견했다.
실제 bash도 이러한 아키텍쳐로 구현되어 있다고 해서 어떤 방식에서 이점이 있을까 다른 카뎃분들과 얘기를 많이 나누었지만,, 명확한 idea를 얻지는 못했다.
다만 내가 생각하는 이 방식의 장점은 직관적이라는 것이다.
이후 기술할 부모 - 자식, 자식, 자식 ...과 비교해보면 좋은데, a | b 와 같이 입력이 들어오면 a와 b 사이에 pipe가 생기고 해당 pipe가 a와 b에 대한 프로세스를 연결해주는 역할을 하는 형태이다.
참고로 최상단의 부모의 경우는 execve로 덮이면 안되기 때문에 command를 실행시키지 않는다. 그림에서는 STDOUT이라고 되어 있지만 pipex의 경우 항상 output이 redirection 처리가 된다고 가정하므로 해당 파일로 cmd3의 결과물을 출력하면 된다.
이 방식은 cmd1을 먼저 부모 쪽에서 실행하고 순차적으로 자식 프로세스에서 command를 실행하는 방식이다. 이 케이스는 아키텍쳐를 떠올리고 얼마안되어 바로 사용하지 않기로 결정했다.
a | b 와 같은 command가 들어왔을 때,
일반적으로 b가 a로부터의 결과(출력물)을 기반으로 실행되기 때문에 a의 실행 status를 알 수 있는 아키텍쳐가 적합하다고 생각하는데
자식에서는 부모의 status를 알 수 없기 때문에 이 방식으로 코드를 작성하면 exit status를 관리하기 힘들다고 판단했기 때문이다.
물론 pipex 자체에서는 exit status를 관리하는 것 까지 요구하지는 않지만 minishell 과제까지 생각한다면 해당 아키텍쳐를 선택하는 것은 별로 추천하지 않는다.
참고로 bash에서는 어떤 command-line이 있으면 가장 마지막으로 수행된 명령어의 exit status를 echo $? 명령어로 확인할 수 있도록 되어 있다.
이는 minishell에서 구현해야 하는 사항에 포함되어 있고, 따라서 아키텍쳐도 마지막 수행 command의 상태를 관리하기 쉽게 구성하는 것이 좋다.
만약 가장 마지막 command를 가장 깊은 자식 process에서 실행한다면 이를 최상단 부모에서 상태를 가지고 있기 쉽지 않을 것이다.
모든 command에 대해서 한 부모에서 자식을 생성하는 방식으로 아키텍쳐를 구현할 수도 있다. 이 방식으로 하게 되면 부모에서 모든 명령어의 종료 status를 알 수 있기 때문에 exit status를 관리하기가 용이하다.
또한 부모에서 우선 fork를 명령어 수만큼 다 실행시켜놓고 while문으로 waitpid를 하면 병렬적으로 command를 처리하기도 쉽다.
다만 pipe 자체의 생성이 부모-자식 간에 생성되기 때문에 이전에 살펴봤던 아키텍쳐처럼 pipe의 연결이 직관적이지 않다.
최상단 부모가 pipe의 READ_END에 대한 file descriptor number를 다음 command가 알 수 있도록 해주어야 처리가 가능하다.
나는 minishell에서는 해당 아키텍쳐를 선택했다. 이 방식에 대해서는 minishell 포스트에 더 자세히 기술할 예정이다.
아까 언급한대로 bash는 command-line에서 가장 마지막 실행한 명령어의 exit status를 echo $?를 통해 알 수 있도록 관리하고 있다.
그래서 부모에서 자식 프로세스를 하나 생성하고 해당 자식에서 나머지 command에 대해 모두 fork하여 손자 process에서 처리한 뒤, 마지막 command만 최상단 부모의 자식 프로세스에서 실행하고 부모에서 exit status를 회수하는 방식으로 구현하는 방법도 있다.
개인적으로 pipex에서 시행착오를 많이 겪으면서 과제를 진행하여 minishell은 비교적 (다른 분들에 비해) 쉽게 진행한 것 같다.
minishell은 42 공통과정에서는 꽤나 큰 프로그램에 속하기 때문에 minishell에서 고민하면 돼 ~~ 하고 넘기면 그 과제가 너무 막막하게 다가올 수 있다.
지금 과제를 시작하는 사람들은 miniminishell인 pipex에서 최대한 많은 고민을 하는 것을 추천한다.
그리고 나는 이미 과제를 하면서 고민을 많이 했기 때문에 추천하지 않는다고 써놓은 아키텍쳐가 있지만 직접 구현을 해보면서 이를 체감하는 것과 글로만 읽는 것은 또 다르다고 생각한다.
남들이 추천하지 않는 방식으로 짜보다 보면 직접 문제를 겪으면서 왜 추천하지 않는 방식이었는지도 느낄 수 있고, 문제를 해결하려고 애쓰다보면 보다 많은 함수의 기능들을 알아낼 수 있는 기회가 되기도 한다.
pipex와 minishell을 같은 아키텍쳐로 만들 수도 있지만 비슷한 형태의 과제가 공통과정에 2개나 있는 만큼 pipex는 힘든 길을 선택해보는 것도 추천한다 ❗️
🦋 pipex repo address