pipex(1) 내용인 프로그램과 프로세스의 차이 프로세스 간 통신인 IPC
에 대해 개념 파악
처음 접해보는 다양한 내장 함수 공부 및 활용
인자는 총 5개를 받습니다. 실행파일인 argv[0]을 제외하면 각 인자들은 아래와 같습니다.
< infile "order 1" | "order 2" > outfile
먼저 pipe
를 만들기 위해 필요한 내장함수 먼저 설명하겠습니다.
int dup2(int fd, int fd2);
dup2
는 새 서술자의 값을 fd2로 지정합니다.
만일 fd2가 이미 열려있으면 fd2를 닫은 후 복제가 됩니다.
역시 성공 시 새 파일 서술자, 오류 시 -1을 반환합니다.
pid_t fork(void);
자식 프로세스를 생성하는 함수
성공 시 pid, 실패 시 -1
포크를 한 오리지널 프로세스를 부모 프로세스
/ 새로 만들어지게 된 프로세스를 자식 프로세스
자식 프로세스
는 고유한 프로세스 id를 가집니다 (pid == 0)자식 프로세스
는 고유의 메모리 공간을 가집니다. (두 프로세스는 각각 독립된 별도의 가상 메모리 공간을 가지게 된다.)자식 프로세스
는 부모 프로세스
의 파일 디스크립터의 복사본을 가집니다. 부모와 자식 프로세스의 파일 디스크립터는 같은 파일을 가리킵니다.int pipe(int fd[2]);
프로세스 간 통신을 위해 fd 쌍을 생성하는 함수
fd[2] : 파일 디스크립터 배열
fd[0]은 파이프의 출구로 데이터를 입력받는 fd 가 담기고, fd[1]에는 파이프의 입구로 데이터를 출력할 수 있는 fd가 담깁니다.
반환값은 성공 시 0, 실패 시 -1
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
int main(void)
{
pid_t pid;
int fd[2];
pipe(fd); //fd[0]과 fd[1]로 지정된 파이프를 생성합니다.
pid = fork(); //fork함수를 통해 정보를 주고받을 2개의 프로세스 객체를 생성합니다.
if(pid==0)
{
//자식 프로세스의 경우//
}
else
{
//부모 프로세스의 경우//
}
exit(0);
}
pipe
는 2개의 파일 서술자를 묶는 역할을 합니다.
pipe
를 통해 묶여진 2개의 서술자 중 하나(fd[1])는 write 하고, 다른 하나(fd[0])는 read 합니다.
즉 1에 쓰고 0으로 읽습니다.
int access(const char *path, int mode);
파일의 권한을 체크하는 함수
path : 파일 명
mode : mask 값(비트 연산을 이용해서 여러 개 확인 가능)
성공 시 0, 실패 시 -1 / set errno
int open(const char *filename, int flag, [mode_t mode]);
파일을 여는 함수
filename : 파일명
flag
mode : O_CREAT 옵션 사용에 의해 파일이 생성될 때 지정되는 파일 접근 권한
420을 하려면 8진수 0644를 입력해야 합니다.
int close(int fildes);
open으로 연 파일의 사용을 종료하는 함수
fildes : 파일 디스크립터(fd)
성공 시 0, 실패 시 -1 / set errno
int execve(const char *file, char * const *argv, char * const *envp);
파일을 실행하는 함수
exec 계열 함수들은 기본적으로 파일의 경로를 첫 번째 인자로 받아와서 실행하는 함수입니다.
v는 vector, e는 environment의 매개변수를 의미합니다.
file : 디렉터리 포함 전체 파일 이름
argv : 인수 목록
envp : 환경설정 목록
실패 시 -1 성공 시에는 return을 받을 수 없음
void perror(const char *s);
시스템 에러 메시지 출력 함수
s : 출력할 문구
s를 표준 에러로 출력하게 되는데, s 뒤에 에러와 errno를 함께 출력합니다.
pid_t waitpid(pid_t pid, int *statloc, int options);
두번째 인자 statloc에 대해서 알아 보겠습니다.
waitpid 함수 반환 값 | 두 번째 인자 인 statloc 값 | |
---|---|---|
자식 프로세스가 정상적으로 종료 | 프로세스 ID | - WIFEXITED(statloc) 매크로가 true를 반환 |
- 하위 8비트를 참조하여 자식 프로세스가 exit, _exit, _Exit에 넘겨준 인자값을 얻을 수 있음, WEXITSTATUS(statloc) | ||
자식 프로세스가 비정상적으로 종료 | 프로세스 ID | - WIFSIGNALED(statloc) 매크로가 true를 반환 |
- 비정상 종료 이유를 WTERMSIG(statloc) 매크로를 사용하여 구할 수 있음 | ||
waitpid 함수 오류 | -1 | - ECHILD : 호출자의 자식 프로세스가 없는 경우 |
- EINTR : 시스템 콜이 인터럽트 되었을 때 |
성공을 하면, 프로세스 ID를, 오류가 발생하면 -1을, 그 외의 경우에는 0을 리턴합니다. 그런데, 이 함수가 wait와 다른 점은, 특정한 자식 프로세스를 기다리게 할 수 있다는 것입니다.
3번째 인자의 옵션에는 WNOHANG
, WCONTINUED
, WUNTRACED
등이 있습니다.
int main(int argc, char *argv[], char *envp[])
{
t_data all;
if (argc != 5)
just_error("input count"); // 인자 오류 확인
all.cmd1 = ft_split(argv[2], ' '); // 2번째 인자인 명령어를 원하는 형식으로 변환
all.cmd2 = ft_split(argv[3], ' '); // 3번째 인자인 명령어를 원하는 형식으로 변환
all.path = path_maker(envp); // 환경변수 $PATH를 활용하여 access 여부 확인
pipe_maker(&all, argv, envp); // 파이프를 만들고 동작
allfree(all.path);
allfree(all.cmd1);
allfree(all.cmd2); // 메모리 누수를 방지하기 위한 메모리 해제 함수
return (0);
}
큰 동작 방식은
인자의 개수가 5개가 맞는지 확인
명령어 인자를 원하는 형태로 파싱 ex) ls -al 과 같은 문자열을 char ** 형식으로 변환
envp에 있는 환경 변수에서 명령어가 위치한 파일 여부 확인
파이프 통신 진행
void pipe_maker(t_data *all, char *argv[], char *envp[])
{
pid_t pid1;
pid_t pid2;
int fd[2];
pipe(fd); // 파이프 생성 fd[0], fd[1]
pid1 = fork(); // 자식 프로세스1 생성
if (pid1 == -1)
just_error("pid error");
else if (pid1 == 0)
first_child(all, fd, argv, envp); // 자식 프로세스에서 명령어 실행 및 fd값 변경
else
{
pid2 = fork(); // 자식 프로세스2 생성
if (pid2 == -1)
just_error("pid error");
else if (pid2 == 0)
last_child(all, fd, argv, envp); // 자식 프로세스에서 명령어 실행 및 fd값 변경
else
{
close(fd[0]);
close(fd[1]); // 자식간의 통신을 위해 부모 프로세스는 파이프의 fd를 닫아줍니다.
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0); // 부모 프로세스는 fd값을 닫은 상태에서 자식 프로세스가 끝날 때 까지 대기
}
}
}
파이프를 생성한 상태에서 부모 프로세스
가 자식 프로세스
2개를 생성해 줍니다.
fd[0]
은 다른 프로세스에서 전달받을 데이터를read
하는 파이프 입구가 되고,
fd[1]
은 다른 프로세스로 전달할 데이터를write
하는 파이프 출구가 됩니다.
부모 프로세스
는 자식 프로세스
가 끝날 때까지 wait 해주는 역할을 합니다.
char *check_order(char **path, char *cmd)
{
int i;
char *find;
if (access(cmd, X_OK) == 0) // 명령어가 절대경로로 들어올 경우 체크 ex) /bin/ls
return (cmd);
i = 0;
while (path[i])
{
find = ft_strjoin(path[i], cmd);
if (access(find, X_OK) == 0) // 절대경로가 아니라면 $PATH의 값이랑 strjoin을 하여 여부 확인
return (find);
free(find);
i++;
}
return (0);
}
void first_child(t_data *all, int *fd, char *argv[], char *envp[])
{
all->infile = open(argv[1], O_RDONLY, 0644); // infile을 옵션에 맞게 open
if (all->infile == -1)
perror("file open error"); // open에 실패 시 perror
all->order1 = check_order(all->path, all->cmd1[0]); // 1번 char **의 명령어 중 0번 인덱스 값이 path에 있는지 여부 확인
close(fd[0]); // 안쓰는 파이프의 fd[0]을 닫기
if (dup2(all->infile, 0) == -1) // 표준입력인 '0'을 open한 fd를 가리키게 변경하여 파일의 내용을 입력으로 사용
just_error("dup error"); // perror를 출력 후 exit하는 함수
if (dup2(fd[1], 1) == -1) // 표준출력인 '1'을 파이프의 fd[1]을 가리키게 변경하여 파일의 내용을 파이프에 담아넣음
just_error("dup error");
close(fd[1]);
close(all->infile); // dup2로 표준입력과 표준출력이 가르키는 값을 변경했으니 안쓰게 되어 닫아줍니다.
if (execve(all->order1, all->cmd1, envp) == -1) // exec 함수를 활용하여 실행
just_error("exec error");
}
void last_child(t_data *all, int *fd, char *argv[], char *envp[])
{
all->outfile = open(argv[4], O_RDWR | O_CREAT | O_TRUNC, 0644); // outfile을 옵션에 맞게 open
if (all->outfile == -1)
just_error("file open error"); // open에 실패 시 perror 후 exit
all->order2 = check_order(all->path, all->cmd2[0]); // 2번 char **의 명령어 중 0번 인덱스 값이 path에 있는지 여부 확인
close(fd[1]); // 안쓰는 파이프의 fd[1]을 닫기
if (dup2(all->outfile, 1) == -1) // 표준출력인 '1'을 open한 fd를 가르키게 변경하여 파이프의 내용을 파일에 출력
just_error("dup error");
if (dup2(fd[0], 0) == -1) // 표준입력인 '0'을 파이프의 fd[0]를 가리키게 변경하여 파이프의 내용을 입력으로 받음
just_error("dup error");
close(fd[0]);
close(all->outfile); // dup2로 표준입력과 표준출력이 가리키는 값을 변경했으니 안쓰게 되어 닫아줍니다.
if (execve(all->order2, all->cmd2, envp) == -1) // exec 함수를 활용하여 실행
just_error("exec error");
}
첫 번째 자식 프로세스
argv[1]인 infile을 열고 open fd 값을 all->infile에 저장합니다.
파이프의 안 쓰는 fd[0]
을 닫아줍니다.
기존에 지정되어 있는 파일디스크립터(fd)의 0, 1, 2중 표준 입력인 0
을 infile의 fd
값을 가르키게 바꿔줍니다. 즉, 표준 입력값 대신 파일의 내용을 읽게 됩니다.
표준 출력인 1
을 파이프의 fd[1]
을 가리키게 변경하여 출력 결과를 파이프에 들어가게 설정해 줍니다.
fd를 설정했으면 execve 함수를 활용하여 실행시킵니다.
두 번째 자식 프로세스
argv[4]인 outfile을 열고 open fd 값을 all->outfile에 저장합니다.
파이프의 안 쓰는 fd[1]
을 닫아줍니다.
기존에 지정되어 있는 파일디스크립터(fd)의 0, 1, 2중 표준 출력인 1
을 outfile의 fd
값을 가리키게 바꿔줍니다. 즉, 표준 출력 대신 파일에 출력하게 됩니다.
표준 출력인 0
을 파이프의 fd[0]
을 가리키게 변경하여 파이프에 담긴 내용을 입력으로 받습니다.
fd를 설정했으면 execve 함수를 활용하여 실행시킵니다.
cat
명령어의 경우 close를 잘 해주지 않으면 프로세스가 종료되지 않는 무한 루프에 빠질 수 있습니다.
cat 명령어는 파일의 내용을 보여주는 명령어인데 파이프의 read가 열려있으면 파일의 eof를 찾지 못해 계속 읽는 상태가 됩니다.
이러한 이유 때문에 부모 프로세스와 자식 프로세스에서는 자기 쪽에서 안쓰는 fd를 닫아주어야 합니다.
ex) ./pipex /dev/urandom/ "cat" "head -1" outfile
fd가 열려있는 상태에서 cat 명령어를 실행하면, 첫 번째 자식 프로세스는 죽지 않고 계속 살아있는좀비 프로세스
가 됩니다.
파이프는 병렬로 실행되어야 합니다.
ex) ./pipex infile "sleep 5" "sleep 5" outfile
명령어를 실행시켰을 때, 10초가 아닌 5초에 프로세스가 종료되어야 합니다.
첫 번째 자식 프로세스가 다 종료되고 나서 두번 째 자식 프로세스를 실행시키면 안 됩니다.
부모 프로세스에서 waitpid의 옵션 및 설정에 주의해야 합니다.
"./pipex infile "sleep 20" "ls" outfile
wait을 사용하거나 설정을 잘못할 경우 바로 종료되어 버리는 현상이 나타납니다.
shell에서는 종료가 된 것처럼 보이지만, ps -al 명령어를 활용해서 실행 중인 프로세스를 확인하면 sleep 20프로세스가 존재하는 걸 확인 할 수 있습니다.위와 같은 경우를
고아 프로세스
라 하며 주의해야 합니다.
운영체제의 기본인 프로그램
과 프로세스
, 프로세스 간 통신인 IPC
에 대해 이론으로만 알고 있던 내용들을 직접 코드를 짜보며 실행시키는 과정에서 더 많은 것을 배우고 자세하게 알 수 있었습니다.
이전 포스트 get_next_line에서 파일디스크립터(fd)
에 대해 개념적인 내용을 이해했다면, 이번 프로젝트에서는 dup2를 활용해서 fd가 가르키는 값들을 변경도 해보고 자유자재로 활용할 수 있어 많은 공부가 되었습니다.
단순히 암기해서 지식을 습득하는 것보단 자전거를 타는 것 마냥 넘어져가며 배우는 것이 결국엔 더 빠르게 성장할 거라 생각합니다.
하나하나 코딩해보고, 다양한 함수들을 직접 사용해 보며 테스트해보고 하는 과정에서 여러 번 오류를 익히는 게 더 오래 기억에 남는다는 걸 크게 느끼고 있습니다.