README.md
1 # pipex 2 3 UNIX의 파이프 기능을 구현하는 과제이다. 4 5 ## 평가 후 수정 사항 6 7 - [x] here_doc 처리 8 - [x] find_path envp NULL 처리 9 - [ ] 환경변수 unset 처리 고민... (미니쉘을 위해!) 10 - [ ] 의문의 cat 프로세스? git이 들어있는 폴더는 기본적으로 실행되는건가... 11 12 ## Some Concepts 13 14 ### IPC 15 16 Inter Process Communication의 약자로, 프로세스 간 데이터를 주고 받는 방식을 의미한다. 운영체제에서 제공하는 주요 IPC 방식으로 파일 시스템, 시그널, 소켓, 메세지 큐, 파이프, 공유 메모리 ... 등이 있다. 17 18 ### 파일(FILE) 19 20 파일은 UNIX에서 사용하는 입출력 방식이자 데이터를 저장하는 형식이다. UNIX는 모든 정보와 장치를 파일로 조작하기 때문에, UNIX 시스템 내의 자원도 파일로서 접근할 수 있다. 21 22 ### 파일 디스크립터(FD, File Descriptor) 23 24 UNIX에서 파일 또는 입출력 장치에 접근하는데 사용하는 식별자(identifier). 일반적으로 양의 정수값을 가지며, 0번은 표준 입력, 1번은 표준 출력, 2번은 표준 에러에 할당되어 있다. 25 26 각 프로세스는 파일 디스크립터 테이블을 가지며, 이를 바탕으로 사용중인 파일을 관리한다. 27 28 UNIX는 사용자가 파일을 열거나 소켓을 생성하는 경우, 파일 디스크립터 테이블을 탐색하여, 사용하지 않는 FD의 가장 작은 값을 할당한다. 사용자는 해당 FD에 시스템 콜을 사용함으로써 해당 파일에 접근할 수 있다. 29 - 사용자는 시스템 콜을 사용하지 않고서는 파일에 접근할 수 없다. 30 31 ### 파일 테이블(File Table) 32 33 파일 디스크립터 테이블이 프로세스 내부에서 파일을 관리하는 데 사용된다면, 파일 테이블은 시스템에서 파일을 관리하는데 사용된다. 파일 디스크립터 테이블과 유사하게, 프로세스가 `open()`과 같은 시스템 콜을 사용하여 파일을 열 때마다 항목이 하나씩 할당되고, `close()` 시스템 콜을 사용하면 항목을 해제한다. 34 35 ### 아이노드(i-node) 36 37 UNIX에서 파일 시스템을 관리하기 위해 사용되는 자료구조. 파일의 속성을 가리키며, 모든 파일은 각자 1개의 아이노드를 가지고 있다. 하나의 파일은 데이터 영역과 아이노드 영역으로 구분된다. 프로세스는 파일의 아이노드 정보를 공유한다. 파일의 아이노드를 모아놓은 아이노드 테이블이 존재한다. 38 39 ### 프로세스의 파일 접근 40 41 <img src = "./IMG_README/filesystem.png" width="60%" height="60%"> 42 43 여러 개의 파일 디스크립터가 동일한 파일 테이블 엔트리를 가리킬 수 있고, 여러 개의 파일 테이블 엔트리가 동일한 아이노드를 가리킬 수 있다. 그림에서 3번 파일 디스크립터는 close 되었기에, 아무것도 가리키지 않는다. 44 45 1) 하나의 프로세스가 하나의 파일을 열었을 경우 46 47 <img src = "./IMG_README/1.png" width="60%" height="60%"> 48 49 2) 서로 다른 프로세스가 동일한 하나의 파일을 열었을 경우 50 51 <img src = "./IMG_README/2.png" width="60%" height="60%"> 52 53 프로세스마다 파일을 읽고 쓰는 위치가 다르므로, 파일 테이블 엔트리의 offset은 서로 다르지만, 아이노드 테이블 엔트리는 같다. 54 55 3) 하나의 프로세스가 하나의 파일에 대해 두 개의 파일 디스크립터를 가지는 경우 56 57 <img src = "./IMG_README/3.png" width="60%" height="60%"> 58 59 `dup()` 시스템 콜은 위와 같이 파일 디스크립터를 복사한다. 가령 `P > A`와 같이, 프로그램 P의 결과값을 FD 1번 표준 출력에서 리다이렉션(redirection) 하여 프로그램 A에 출력을 하도록 설정하는 것처럼, `A = dup(1)`을 사용하면 FD 1번이 가리키는 파일을 (사용되지 않은 가장 낮은 값인) FD 3번도 같이 가리키게 되어, 프로그램 A에 P의 표준 출력을 리다이렉션 할 수 있다. (뒤에서 좀 더 자세하게 후술할 예정.) 60 61 ### 파이프(PIPE) 62 63 프로세스 간 통신을 위한 메모리 공간(버퍼)를 생성하여, 프로세스간 데이터 송수신을 처리한다. 64 65 과제에서는 부모-자식 프로세스 간 통신을 위한 **익명 파이프(Anonymous PIPE)**를 사용한다. 익명 파이프는 한쪽 방향으로만 통신이 가능하기에, 반이중(Half-Duplex) 통신의 특성을 가진다. 66 67 몇 가지 쉘 표현들 : 68 - `|` : 이전 명령어의 실행 결과를 다음 명령어로 전달한다. 69 - `&&` : 이전 명령어가 정상적으로 실행되었을 경우에만 결과를 다음 명령어로 전달한다. 70 - `<` : 저장소에 위치한 파일의 내용을 쉘에 연결한다. 71 - `>` : 프로그램의 실행 결과를 저장소의 파일에 저장한다. 72 - `>>` : 위 명령어와 다르게 저장소의 파일의 내용을 지우지 않고, 프로그램의 실행 결과를 저장소의 파일에 덧붙인다. 73 = `<<` : here document, 이 형식 이후의 입력은 모두 저장이 되다가, 표현과 함께 쓴 단어가 나오는 경우 지금까지 저장된 모든 입력들이 출력된다. 74 75 ### `pipe()` 76 77 <img src = "./IMG_README/4.png" width="60%" height="60%"> 78 79 ```c 80 #include <unistd.h> 81 82 int pipe(int fd[2]); 83 ``` 84 85 `pipe()` 사용 시, 배열로 두 개의 파일 디스크립터를 할당받는다. `fd[0]`은 input stream, `fd[1]`은 output stream으로 86 작동한다. 부모 프로세스가 `fd[1]`에 write 한 데이터를, 자식 프로세스가 `fd[0]`으로 read 할 수 있다. 87 88 ``` 89 기억하자, 1에 쓰고 0으로 읽는다! 90 ``` 91 92 주의할 점으로, **각 프로세스에서 사용하지 않는 FD는 닫아야 한다**. write end가 닫히면, 이후 read는 EOF를 나타내는 0을 반환한다. read end가 닫히면, 이후 write는 SIGPIPE를 발생시킨다. 만약 write end가 닫히지 않는다면, read는 write가 EOF를 줄 때까지 계속 기다리게 된다. 반대로 read end가 닫히지 않는다면, write는 read가 write를 완료하기 위한 공간을 만들어 줄 때까지 계속 기다리게 된다(혹은 문제가 생기지 않는 것처럼 보이지만, read pipe가 닫힌 이후, write를 하는 일은 오류임에도, read end가 열려있다면 이를 오류로 처리할 수 없다). 93 94 ### `dup(), dup2()` 95 96 `dup()`과 `dup2()`는 파일 디스크립터를 복제한다. 97 98 ```c 99 #include <unistd.h> 100 101 int dup(int fd); 102 ``` 103 `dup()`은 전달받은 파일 디스크립터를 복제한다. 성공 시 새로운 파일 디스크립터를, 오류 시 -1을 반환한다. 104 105 예제 코드: 106 ```c 107 #include <unistd.h> 108 #include <fcntl.h> 109 #include <stdio.h> 110 111 int main(void) 112 { 113 int int fd1, fd2; 114 char message[6] = "Hello\n"; 115 116 fd1 = open("made_by_fd1", O_RDWR|O_CREAT); 117 if (fd1 < 0) 118 { 119 close(fd1); 120 return (1); 121 } 122 fd2 = dup(fd1); 123 write(fd2, message, 6); 124 printf("fd1: %d, fd2: %d\n", fd1, fd2); 125 close(fd1); 126 close(fd2); 127 return (0); 128 } 129 ``` 130 131 ``` 132 // output 133 > gcc main.c 134 > ./a.out 135 fd1: 3, fd2: 4 136 > cat made_by_fd1 137 Hello 138 ``` 139 <img src = "./IMG_README/5.png" width="60%" height="60%"> 140 141 ```c 142 #include <unistd.h> 143 144 int dup2(int fd, int fd2); 145 ``` 146 `dup2()`는 복제된 파일 디스크립터를 fd2로 지정한다. 만약 fd2가 이미 할당되어 있다면, 해당 파일 디스크립터를 닫은 후, 전달받은 파일 디스크립터를 복제하여 할당한다. 성공 시 새로운 파일 디스크립터를, 오류 시 -1을 반환한다. 147 148 예제 코드: 149 ```c 150 #include <unistd.h> 151 #include <fcntl.h> 152 #include <stdio.h> 153 154 int main(void) 155 { 156 int fd1, ret; 157 char message[7] = "STDERR\n"; 158 159 // 1 160 fd1 = open("made_by_fd1", O_RDWR|O_CREAT); 161 if (fd1 < 0) 162 { 163 close(fd1); 164 return (1); 165 } 166 printf("THIS SHOULD PRINT OUT\n"); 167 // 2 168 ret = dup2(fd1, STDOUT_FILENO); 169 printf("fd1: %d, ret: %d\n", 1, ret); 170 // 3 171 ret = dup2(STDERR_FILENO, fd1); 172 write(fd1, message, 7); 173 printf("THIS SHOULDN'T PRINT OUT\n"); 174 close(fd1); 175 close(ret); 176 return (0); 177 } 178 ``` 179 180 ``` 181 // output 182 > gcc main.c 183 > ./a.out 184 THIS SHOULD PRINT OUT 185 STDERR 186 > cat made_by_fd1 187 fd1: 3, ret: 1 188 THIS SHOULD'T PRINT OUT 189 ``` 190 <img src = "./IMG_README/6.png" width="60%" height="60%"> 191 192 1. fd1이 3번 FD를 가진 상태로, 파일을 연다. 193 2. `dup2()`로 1번 FD(표준 출력 FD)가 표준 출력이 아닌 파일을 가리키도록 변경한다. ret의 값은 1을 가진다. 194 - `printf()`는 표준 출력, 즉 FD 1번으로 출력을 하는데, 방금 `dup2()`로 FD 1이 표준 출력이 아닌 파일을 가리키도록 변경하였기 때문에, 터미널이 아닌 파일로 출력이 된다. 195 3. `dup2()`로 fd1이 파일이 아닌 표준 에러를 가리키도록 한다. 따라서 fd1에 메세지를 출력하면 파일이 아닌 터미널에 표준 에러로 작성된다. 196 197 ### `fork()` 198 199 ```c 200 #include <unistd.h> 201 202 pid_t fork(void); 203 ``` 204 `fork()` 시스템 콜은 프로세스를 생성하는 데 사용한다. `fork()`를 호출하는 쪽이 부모 프로세스, 새롭게 생성되는 프로세스가 자식 프로세스가 된다. 시스템 콜 성공 시 **부모 프로세스에게는 자식 프로세스의 PID 값을 반환하고, 자식 프로세스에게는 0을 반환한다**. 실패 시 부모 프로세스에게 -1을 반환한다. 205 206 `fork()` 시스템 콜이 실행되고 프로세스가 생성되면, 부모 프로세스와 자식 프로세스가 동일한 주소 공간의 복사본을 가진다. 즉, 자식 프로세스와 부모 프로세스의 메모리 공간이 별도로 구성된다. 207 208 부모와 자식 프로세스는 별도로 동작하기 때문에, 안정적인 프로그램을 만들기 위해선 `wait()` 혹은 `waitpid()` 시스템 콜을 잘 사용해야 한다. 209 210 ### `wait()`, `waitpid()` 211 212 ```c 213 #include <sys/wait.h> 214 215 pid_t wait(int *status); 216 ``` 217 `wait()` 함수는 자식 프로세스가 종료될 때까지 부모 프로세스를 `sleep()` 모드로 대기시킨다. 만약 자식 프로세스가 종료되었다면, 함수는 즉시 리턴하여 자식이 사용한 모든 시스템 자원을 해제한다. 218 219 이로써 부모 프로세스보다 자식 프로세스가 먼저 종료되어, *고아 프로세스*(PPID 1)가 생기는 것을 방지한다. 하지만 어떤 이유로 부모가 `wait()`를 호출하기 전에, 자식 프로세스가 종료되는 경우가 발생할 수 있는데, 이 경우 부모 프로세스는 *좀비 프로세스*가 된다. 이 경우, `wait()` 함수는 즉시 리턴하도록 되어있다. 220 221 `wait()`의 인자 status를 통해 자식 프로세스의 종료 상태를 받아올 수 있다. 호출 성공 시 종료된 자식 프로세스의 PID를 반환하고, 실패할 경우 -1을 반환한다. 가령, 자식 프로세스가 `return` 혹은 `exit`으로 종료되지 않고, 시그널에 의해 종료되는 경우, `wait()`은 -1을 반환한다. 222 223 `wait()` 함수는 자식 프로세스의 종료 말고도 부모 프로세스가 자식 프로세스를 수거하는 역할을 하기도 한다. 따라서 `fork()`로 자식 프로세스를 생성하였다면, 부모 프로세스에서 `wait()`을 호출하여 자식 프로세스가 좀비 프로세스가 되지 않도록 꼭 수거를 하도록 하자. 224 225 ```c 226 #include <sys/wait.h> 227 228 pid_t waitpid(pid_t pid, int *status, int options); 229 ``` 230 `waitpid()` 시스템 콜은 `wait()`처럼 자식 프로세스가 종료될 때까지 부모 프로세스를 대기시키지만, `wait()`은 자식 프로세스 중 어느 하나라도 종료되면 부모 프로세스로 바로 복귀하지만, `waitpid()`는 특정 자식 프로세스가 종료될 때까지 대기한다. 231 232 또한 `wait()`는 자식 프로세스가 종료될 때까지 block되지만, `waitpid()`에 WNOHANG 옵션을 사용하면 부모 프로세스가 block 상태가 되지 않고 작업을 병행할 수 있다. 233 234 일반적으로 pid에는 특정한 자식 프로세스의 PID가 들어가고, status에는 자식 프로세스의 종료 상태를 담는다. options에는 부모 프로세스의 대기 상태를 설정한다. 시스템 콜 성공 시 종료된 자식 프로세스의 PID를 반환하고, 실패 시 -1을 반환한다. WNOHANG을 사용하였는데 자식 프로세스가 종료되지 않았다면 0을 반환한다. 235 236 ``` 237 pid 238 -1 : 여러 자식 중 하나라도 종료되면 부모 프로세스로 복귀한다. wait()와 동일한 처리. 239 0 : 현재 프로세스 그룹 ID와 같은 그룹의 자식 프로세스가 종료되면 복귀한다. 240 양수 : pid에 해당하는 자식 프로세스가 종료되면 복귀한다. 241 242 options 243 WNOHANG : 자식 프로세스가 종료되었는지, 실행 중인지 확인하고, 바로 부모 프로세스로 복귀한다. 부모 프로세스가 block되지 않는다. 244 WUNTRACED : STOP 시그널을 통해 멈춘 자식 프로세스의 status에 대해 반환한다. 245 WCONTINUED : CONT 시그널을 통해 멈춘 자식 프로세스의 status에 대해 반환한다. 246 0 : 자식 프로세스가 종료될 때까지 부모 프로세스가 block된다. wait()와 동일한 처리. 247 ``` 248 249 ### `access()` 250 251 ```c 252 #include <unistd.h> 253 254 int access(const char *path, int mode); 255 ``` 256 path에는 확인하고자 하는 디렉토리 및 파일명이 들어가며, mode에는 파일 존재 여부 및 권한을 확인한다. `access()` 함수는 디렉토리 및 파일이 인자로 받은 mode를 만족하면 0을 반환하고, 그렇지 않으면 -1을 반환한다. 257 258 ``` 259 mode 260 F_OK : 파일 존재 여부 261 R_OK : 파일 존재 여부, 읽기 권한 여부 262 W_OK : 파일 존재 여부, 쓰기 권한 여부 263 X_OK : 파일 존재 여부, 실행 권한 여부 264 ``` 265 266 ### `unlink()` 267 268 ```c 269 #include <unistd.h> 270 271 int unlink(const char *path); 272 ``` 273 `unlink()` 함수는 시스템 콜을 사용하여, 파일을 삭제한다. 정확하게는 파일의 하드 링크를 끊는다. 만약 inode에 접근할 수 있는 하드 링크가 하나도 없다면, 곧 파일의 삭제로 이어지게 된다. 함수의 반환 값이 0이면 정상적으로 하드 링크를 끊은 것이고, 실패 시 -1을 반환한다. 274 275 ``` 276 [Hard link] -> [inode] -> [data / file] 277 ``` 278 279 ### `execve()` 280 281 ```c 282 #include <unistd.h> 283 284 int execve(const char *filename, char *const argv[], char *const envp[]); 285 ``` 286 `execve()` 함수는 실행가능한 파일의 실행 코드를 현재 프로세스에 적재하여, 기존의 실행코드를 실행파일로 교체하고 새로운 기능으로 실행한다. (즉, **새로운 프로세스를 만들어, 현재 프로세스의 흐름을 새로운 프로세스로 교체**한다. 따라서 `fork()` 함수와는 다른 맥락을 가진다.) 함수의 v와 e의 의미는 각각 실행파일에서 vector(배열)로 받을 파라미터와 실행파일에서 사용할 environment(환경 변수)를 파라미터로 받는다는 의미이다. 함수 성공 시, 이미 지정된 프로그램으로 현재 프로세스의 프로그램이 전환되었으므로, 반환값을 받아서 확인할 수 없다. 실패 시, -1을 리턴한다. (벡터와 환경 변수는 항상 NULL로 끝내야 한다는 점을 기억하자.) 287 288 참고 자료: 289 1. IPC: 290 - [https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4_%EA%B0%84_%ED%86%B5%EC%8B%A0](https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4_%EA%B0%84_%ED%86%B5%EC%8B%A0) 291 - [https://dar0m.tistory.com/233](https://dar0m.tistory.com/233) 292 2. 파일 처리: [https://ddongwon.tistory.com/16](https://ddongwon.tistory.com/16) 293 3. 파일 디스크립터 : [https://en.wikipedia.org/wiki/File_descriptor](https://en.wikipedia.org/wiki/File_descriptor) 294 4. 파이프: 295 - [https://hyeonski.tistory.com/8](https://hyeonski.tistory.com/8) 296 - [https://sosal.kr/83](https://sosal.kr/83) 297 - [https://m.blog.naver.com/nywoo19/221708412078](https://m.blog.naver.com/nywoo19/221708412078) 298 - [https://stackoverflow.com/questions/11599462/what-happens-if-a-child-process-wont-close-the-pipe-from-writing-while-reading](https://stackoverflow.com/questions/11599462/what-happens-if-a-child-process-wont-close-the-pipe-from-writing-while-reading) 299 5. `dup()`, `dup2()`: 300 - [https://en.wikipedia.org/wiki/Dup_(system_call)](https://en.wikipedia.org/wiki/Dup_(system_call)) 301 - [https://reakwon.tistory.com/104](https://reakwon.tistory.com/104) 302 6. `fork()`: 303 - [https://codetravel.tistory.com/23](https://codetravel.tistory.com/23) 304 - [https://bigpel66.oopy.io/library/42/inner-circle/8](https://bigpel66.oopy.io/library/42/inner-circle/8) 305 7. `wait()`, `waitpid()`: 306 - [https://reakwon.tistory.com/45](https://reakwon.tistory.com/45) 307 - [https://www.joinc.co.kr/w/man/2/wait](https://www.joinc.co.kr/w/man/2/wait) 308 - [https://badayak.com/entry/C%EC%96%B8%EC%96%B4-%EC%9E%90%EC%8B%9D-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EC%A2%85%EB%A3%8C-%ED%99%95%EC%9D%B8-%ED%95%A8%EC%88%98-waitpid](https://badayak.com/entry/C%EC%96%B8%EC%96%B4-%EC%9E%90%EC%8B%9D-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EC%A2%85%EB%A3%8C-%ED%99%95%EC%9D%B8-%ED%95%A8%EC%88%98-waitpid) 309 8. `access()`, `unlink()`: [https://jybaek.tistory.com/578](https://jybaek.tistory.com/578) 310 9. `execve()`: 311 - [https://www.it-note.kr/157](https://www.it-note.kr/157) 312 - [https://80000coding.oopy.io/0c3a00e2-178c-46cc-8c8a-ceb8ea2f4dbe#0c3a00e2-178c-46cc-8c8a-ceb8ea2f4dbe](https://80000coding.oopy.io/0c3a00e2-178c-46cc-8c8a-ceb8ea2f4dbe#0c3a00e2-178c-46cc-8c8a-ceb8ea2f4dbe) 313 314 ## Note 315 316 처음에는 하나의 파이프로 여러 자식 프로세스의 명령어 실행을 담으려 했는데, 권장되지 않는 방법인 것 같다. 317 - [https://stackoverflow.com/questions/63967015/why-cant-i-reuse-a-pipe-to-communicate-with-multiple-child-processes](https://stackoverflow.com/questions/63967015/why-cant-i-reuse-a-pipe-to-communicate-with-multiple-child-processes) 318 319 파이프는 sequential하지 않다는 사실을 꼭 기억하자. 그저 하나의 프로세스의 stdout을 다음 프로세스의 stdin으로 보낸다. bash는 모든 프로그램을 병렬로 실행한다. 320 321 헷갈리는 표현: 322 - `>` : 표준출력을 파일로 보낸다. 323 - `<` : 파일을 표준입력으로 보낸다. 324 325 파이프의 명령어는 마지막 파이프 명령어의 상태를 가진다. (If you would like to get an intermediate return code you have to set the pipefail or get it from the PIPESTATUS) 326 - [https://stackoverflow.com/questions/9834086/what-is-a-simple-explanation-for-how-pipes-work-in-bash](https://stackoverflow.com/questions/9834086/what-is-a-simple-explanation-for-how-pipes-work-in-bash) 327 328 부모 프로세스는 파이프 READ END의 내용을 STDIN으로 삼는다. 자식 프로세스는 매번 새로 생성되어 (복사된) 부모의 STDIN을 읽어 명령어를 수행하고, 결과값을 파이프 WRITE END에 보낸다. 파이프에 저장된 내용을 가지고 STDIN 한다!!! 329 330 자식의 명령어 수행을 기다린 후, 자식에게서 읽어온 값을 STDIN으로 삼고 다음 프로세스로 진행.