process를 다루기 위한 함수들을 알아보자
execve 함수를 공부하기 전 exec 계열 함수에 대해서 먼저 이해할 필요가 있다
exec 계열의 함수란?
기존 실행 process 대신 executable file을 수행
즉, 현재 프로세스에서 이 함수를 호출하면
이 함수 호출의 아래에 있는 코드들은 이 함수에서 비정상적인 에러가 발생되지 않는한 수행되지 않는다.
이미 다른 프로그램이 현 프로세스에 대체되어 올라오기 때문!
exec 계열 함수는 다음과 같은 함수들이 있다.
함수명 뒤에 붙은 e, l, p, v는 다음과 같은 의미를 가지고 있다.
e : 새 process image에 환경 변수 배열에 대한 포인터를 넘겨준다.
예를 들어 exec 계열 함수에서 실행하려고 하는 script에 echo "$PATH" 이런 line이 들어있다면 이 프로그램을 실행되는 과정에서 PATH라는 환경변수를 필요로 하게 된다. 이런 프로그램들을 실행시킬 때는 e 키워드가 붙어있는 함수를 활용하여 환경 변수 정보를 인자로 넘겨주어야 한다.
l : command-line 인자를 독립적인 list 형태로 넘겨준다.
ls라는 command에 a옵션과 l옵션을 추가하여 실행시킨다고 하면
"ls -al" 혹은 "ls -a -l" 등과 같이 실행시킬 수 있다. 이를 l 키워드가 붙은 함수를 활용하면 execlp("/bin/ls", "/bin/ls", "-al") 또는 execlp("/bin/ls", "/bin/ls", "-a", "-l")처럼 인자를 나열해야 한다.
p : file 인자의 위치를 환경변수 PATH에서 찾아준다.
p 키워드가 붙은 함수들은 어떤 command에 대해 실행 경로를 알아서 내부적으로 PATH 환경변수에서 찾아준다 ! 그렇기 때문에 PATH 환경변수에서 경로를 찾을 때만 필요하다면 e가 아닌 p 계열의 함수를 활용하면 좋을 것 같다. 다만 실행 파일의 경로가 아니라 파일을 실행할 때 환경변수가 필요한 케이스라면 e계열을 써야할 것이다 !
v : command-line 인자를 array(vector) 포인터로 넘긴다.
이를 바탕으로 우리가 사용할 수 있는 execve 함수를 이해하면 된다
# include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[])
pathname에 해당하는 program을 현재 process를 대체하여 실행시킨다.
pathname : binary executable이거나 script여야 함
argv: new program에 command-line arguments로 넘겨줄 문자열들의 array pointer
envp : new program에 환경변수로 넘겨줄 문자열들의 array pointer
이런 인자를 왜 넘겨주는 지에 대해 이해없이 그냥 필요하다니까 넣고 진행을 하기 쉬운데,
우리가 shell에서 어떤 program을 실행시킬 때 main에 필요한 인자들을 넘겨주는 것과 같다고 이해하고 사용하면 쉽게 와닿을 것 같다.
./a.out arg1 arg2 arg3 ... 로 입력 시
program이 실행되고 main에 argument로 파일명을 포함한 인자를 받을 수 있는데, 다른 프로그램을 현재 프로그램 내에서 호출할 때에도 이처럼 프로그램을 실행시키기 위한 정보를 넘겨준다고 이해했다.
#include <unistd.h>
pid_t fork(void)
process를 복제하여 new process를 생성한다.
성공 시,
parent process는 child process의 pid를 return
child process는 0을 return
실패 시, -1을 return
for는 process를 복제하여 새 process를 생성하기 때문에 code, data 모두 동일한 process가 생성된다.
따라서 fork로 부터 받은 pid에 따라서 child process인지 parent process인지 판단하고 process 별 코드 진행이 일어날 수 있도록 구현을 해주어야 한다.
#include <unistd.h>
int pipe(int pipefd[2])
IPC(interprocess communication)에 사용되는 unidirectional data channel인 pipe를 생성한다.
각 프로세스는 코드, data, stack, heap을 모두 별개로 갖기 때문에 기본적으로는 자원을 공유하지 않는다. 하지만 때에 따라 process간에 정보 공유가 필요한 경우 IPC를 활용하여 통신해야 한다.
pipe는 IPC 기술 중 하나로,
어떤 process의 output이 다른 process의 input으로 들어가는 구조이다.
단방향 통신이며 system은 이를 FIFO queue로 관리한다.
pipe함수를 통해 pipe를 생성하면 인자로 넘겨준 pipefd의
pipefd[0]은 pipe의 read end,
pipefd[1]은 pipe의 write end가 된다.
함수가 성공적으로 pipe를 생성하면 0을,
실패하면 -1을 return 한다.
pipe를 먼저 생성하고 fork를 해줌으로써 parent process와 child process 모두 해당 파이프를 사용할 수 있도록 해주고,
단방향 통신이기 때문에 parent와 child 중 pipe에 write을 할 process와 read할 process를 정해 사용하지 않는 end 단은 닫아준다.
사용하는 end도 read나 write이 모두 끝나면 닫아주어야 한다.
위 코드에서는 child가 파이프에서 write을 하고 부모가 read를 하도록 작성을 했기 때문에 child는 read 단을 닫고 시작한 뒤, write 작업만을 진행한다. write 작업이 끝나면 write end도 닫아준다
parent에 대한 처리는 wait_child_and_execute_cmd 함수 내에서 비슷하게 처리하였다.
#include <sys/wait.h>
pid_t wait(int *stat_loc)
pid_t waitpid(pid_t pid, int *stat_loc, int options)
wait 계열 함수는 child process가 종료되거나 siganl에 의해 stat_loc 정보가 available해질 때 까지 현 process의 실행을 중단시킨다.
wait 함수가 성공적으로 return 되면 stat_loc은 기다렸던 process의 종료 정보를 포함하게 된다.
child process의 상태를 수거하면 그 정보가 stat_loc에 저장이 된다.
이 stat_loc에 적절한 연산을 취하여 원하는 정보를 추출할 수 있다.
이에 대해서는 이미 다양한 macro가 존재한다.
- WIEXITED(status)
process가 정상종료되었다면 True를 반환- WIFSIGNALED(status)
signal을 받아 종료되었다면 True를 반환- WIFSTOPPED(status)
process가 종료되지는 않았지만, stop되어 재시작 될 수 있는 상태이면 True를 반환
이 macro는 WUNTRACED option에 의해 child가 호출되었을 때나 child process가 trace될 때 사용해야 정상적으로 처리된다.- WEIXTSTATUS(status)
WIFEXITED(status)가 true이면, child의 exit함수로 부터 받은 status의 low-order 8 bit- WTERSIG(status)
WIFSIGNALED(status)가 true이면, process를 termination시킨 signal의 번호- WCOREDUMP(status)
WIFSIGNALED(status)가 true이면, process가 종료할 때 signal을 받았을 때의 image를 포함하는 코어 파일을 생성하는지 여부- WSTOPSIG(status)
WIFSTOPPED(status)가 true이면, process를 중단시킨 signal의 번호
함수 macro의 경우 과제에서 사용해도 되는가에 대한 여부에 대해 논란이 있기 때문에, 해당 함수 매크로의 로직을 이해하고 직접 작성하여 사용하는 것이 좋다.
나는 위와 같이 함수를 작성해서 사용했는데, wifexited 함수의 경우 '!'로 인해 실제와 반대로 작동한다.. 🥲
wifexited는 status의 하위 2byte(종료, 시그널 정보를 담는 2byte, 상위 2byte는 사용하지 않는다)가 0인지를 확인한다. 0이라면 정상종료, 0이 아니라면 정상종료가 아니다 !
wexitstatus는 하위 2byte 중에서도 종료 정보를 담는 byte의 정보를 추출한다. exit를 통해 정상종료를 하면 exit(종료 정보)에서 종료 정보의 하위 8bit이 status의 하위 2byte중 상위 byte, 즉 3번째 byte에 저장된다. 따라서 불필요한 8bit을 shift로 밀어내면 정보를 추출할 수 있다.
wait 함수는 아무 child process만을 기다리는데 반해 waitpid는 인자에 넘겨준 pid를 통해 기다리고자 하는 process를 정할 수 있다.
- pid == -1
아무 child process를 기다린다
wait()과 동일한 처리라고 볼 수 있다.- pid == 0
현 process와 동일한 process group에 있는 아무 child process를 기다린다.- pid > 0
pid에 해당하는 process id를 가진 process를 기다린다.- pid < -1
process group id가 pid의 절댓값과 동일한 아무 프로세스를 기다린다.
waitpid는 option 또한 다음과 같이 추가적인 인자를 넣어줄 수 있다.
- WNOHANG
status를 report하려고 하는 process가 없을 때 block하지 않는다.- WUNTRACED
SIGINT, SIGTTOU, SIGTSTP, SIGTOP signal들에 의해 stop된 자식 프로레스도 status를 report하도록 한다.
이 option들은 bitwise OR하여 넣어줄 수 있다.
pipex 프로젝트에서 나는 wait이 아닌 waitpid만을 사용했다.
부모-자식-손자-증손자 .... 의 아키텍쳐를 선택했기 때문에 각 부모가 하나의 자식두고 있고, 해당 자식만을 기다리는 방식을 채택했다.
그리고 WNOHANG 옵션을 사용하여 자식 process가 아직 종료되지 않아도 부모 process가 block되지 않고 나머지 command를 실행할 수 있도록 하였다.
이는 < /dev/random cat | head -1 > outfile 과 상황을 처리하기 위함인데,
cat /dev/random을 하게 되면
아래와 같이 무한히 random하게 문자가 출력되는 것을 볼 수 있는데
파이프 뒤의 명령어 head 가 1줄만 필요로 하기 때문에 위 같은 상황에서는 무한히 출력되지 않고 프로그램이 종료된다.
만약 직렬적으로 코드를 작성했을 경우에는 파이프 뒤의 head가 cat /dev/random의 종료를 계속 기다려서 결국 원하는 결과를 내지 못하게 된다.
따라서 이를 고려하여 코드를 작성해야 한다.
🦋 pipex repo address