pipex(2) - 파이프 구현

yeham·2023년 4월 2일
0

42Seoul

목록 보기
11/18
post-thumbnail

들어가기에 앞서

  • pipex(1) 내용인 프로그램과 프로세스의 차이 프로세스 간 통신인 IPC에 대해 개념 파악

  • 처음 접해보는 다양한 내장 함수 공부 및 활용

  • 인자는 총 5개를 받습니다. 실행파일인 argv[0]을 제외하면 각 인자들은 아래와 같습니다.
    < infile "order 1" | "order 2" > outfile

    • infile = argv[1]
    • order 1 = argv[2]
    • order 2 = argv[3]
    • outfile = argv[4]
      형식으로 인자를 받습니다.

🖥️ Mandatory

먼저 pipe를 만들기 위해 필요한 내장함수 먼저 설명하겠습니다.

사용할 함수

dup2

int dup2(int fd, int fd2);

dup2는 새 서술자의 값을 fd2로 지정합니다.
만일 fd2가 이미 열려있으면 fd2를 닫은 후 복제가 됩니다.
역시 성공 시 새 파일 서술자, 오류 시 -1을 반환합니다.

fork

pid_t fork(void);

자식 프로세스를 생성하는 함수

성공 시 pid, 실패 시 -1

포크를 한 오리지널 프로세스를 부모 프로세스 / 새로 만들어지게 된 프로세스를 자식 프로세스

  • 자식 프로세스는 고유한 프로세스 id를 가집니다 (pid == 0)
  • 자식 프로세스는 고유의 메모리 공간을 가집니다. (두 프로세스는 각각 독립된 별도의 가상 메모리 공간을 가지게 된다.)
  • 자식 프로세스부모 프로세스의 파일 디스크립터의 복사본을 가집니다. 부모와 자식 프로세스의 파일 디스크립터는 같은 파일을 가리킵니다.

pipe

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으로 읽습니다.

access

int access(const char *path, int mode);

파일의 권한을 체크하는 함수

path : 파일 명

mode : mask 값(비트 연산을 이용해서 여러 개 확인 가능)

  • R_OK : 파일 존재 여부, 읽기 권한 여부
  • W_OK : 파일 존재 여부, 쓰기 권한 여부
  • X_OK : 파일 존재 여부, 실행/검색 권한 여부
  • F_OK : 파일 존재 여부

성공 시 0, 실패 시 -1 / set errno

open

int open(const char *filename, int flag, [mode_t mode]);

파일을 여는 함수

filename : 파일명

flag

  • O_RDONLY : 파일을 읽기 전용으로 open (Read Only)
  • O_RDWR : 파일을 쓰기와 읽기용으로 open (Read & Write)
  • O_CREAT : 파일이 없으면 생성한다. 이 플래그를 명시하면, open 함수에 Permission 정보를 추가로 더 받아야 하고 파일이 존재하면 해당 파일을 열어줍니다.
  • O_TRUNC : 파일이 이미 존재하고 write-only, read-write 모드로 열 수 있는 경우, 파일 사이즈를 0으로 초기화 시킵니다.

mode : O_CREAT 옵션 사용에 의해 파일이 생성될 때 지정되는 파일 접근 권한

  • 읽기 권한 : 4
  • 쓰기 권한 : 2
  • 실행 권한 : 1

420을 하려면 8진수 0644를 입력해야 합니다.

close

int close(int fildes);

open으로 연 파일의 사용을 종료하는 함수

fildes : 파일 디스크립터(fd)

성공 시 0, 실패 시 -1 / set errno

execve

int execve(const char *file, char * const *argv, char * const *envp);

파일을 실행하는 함수

exec 계열 함수들은 기본적으로 파일의 경로를 첫 번째 인자로 받아와서 실행하는 함수입니다.

v는 vector, e는 environment의 매개변수를 의미합니다.

file : 디렉터리 포함 전체 파일 이름
argv : 인수 목록
envp : 환경설정 목록

실패 시 -1 성공 시에는 return을 받을 수 없음

perror()

void perror(const char *s);

시스템 에러 메시지 출력 함수

s : 출력할 문구

s를 표준 에러로 출력하게 되는데, s 뒤에 에러와 errno를 함께 출력합니다.

waitpid()

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와 다른 점은, 특정한 자식 프로세스를 기다리게 할 수 있다는 것입니다.

https://blog.kakaocdn.net/dn/bsVaVI/btqy4d54JXQ/BxyyIBoOcv054rCUixfap1/img.png

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);
}

큰 동작 방식은

  1. 인자의 개수가 5개가 맞는지 확인

  2. 명령어 인자를 원하는 형태로 파싱 ex) ls -al 과 같은 문자열을 char ** 형식으로 변환

  3. envp에 있는 환경 변수에서 명령어가 위치한 파일 여부 확인

  4. 파이프 통신 진행

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");
}

첫 번째 자식 프로세스

  1. argv[1]인 infile을 열고 open fd 값을 all->infile에 저장합니다.

  2. 파이프의 안 쓰는 fd[0]을 닫아줍니다.

  3. 기존에 지정되어 있는 파일디스크립터(fd)의 0, 1, 2중 표준 입력인 0infile의 fd값을 가르키게 바꿔줍니다. 즉, 표준 입력값 대신 파일의 내용을 읽게 됩니다.

  4. 표준 출력인 1을 파이프의 fd[1]을 가리키게 변경하여 출력 결과를 파이프에 들어가게 설정해 줍니다.

  5. fd를 설정했으면 execve 함수를 활용하여 실행시킵니다.

두 번째 자식 프로세스

  1. argv[4]인 outfile을 열고 open fd 값을 all->outfile에 저장합니다.

  2. 파이프의 안 쓰는 fd[1]을 닫아줍니다.

  3. 기존에 지정되어 있는 파일디스크립터(fd)의 0, 1, 2중 표준 출력인 1outfile의 fd값을 가리키게 바꿔줍니다. 즉, 표준 출력 대신 파일에 출력하게 됩니다.

  4. 표준 출력인 0을 파이프의 fd[0]을 가리키게 변경하여 파이프에 담긴 내용을 입력으로 받습니다.

  5. fd를 설정했으면 execve 함수를 활용하여 실행시킵니다.

❗️ 주의해야 할 점

  1. cat 명령어의 경우 close를 잘 해주지 않으면 프로세스가 종료되지 않는 무한 루프에 빠질 수 있습니다.
    cat 명령어는 파일의 내용을 보여주는 명령어인데 파이프의 read가 열려있으면 파일의 eof를 찾지 못해 계속 읽는 상태가 됩니다.
    이러한 이유 때문에 부모 프로세스와 자식 프로세스에서는 자기 쪽에서 안쓰는 fd를 닫아주어야 합니다.

    ex) ./pipex /dev/urandom/ "cat" "head -1" outfile
    fd가 열려있는 상태에서 cat 명령어를 실행하면, 첫 번째 자식 프로세스는 죽지 않고 계속 살아있는 좀비 프로세스가 됩니다.

  2. 파이프는 병렬로 실행되어야 합니다.

    ex) ./pipex infile "sleep 5" "sleep 5" outfile
    명령어를 실행시켰을 때, 10초가 아닌 5초에 프로세스가 종료되어야 합니다.
    첫 번째 자식 프로세스가 다 종료되고 나서 두번 째 자식 프로세스를 실행시키면 안 됩니다.

  3. 부모 프로세스에서 waitpid의 옵션 및 설정에 주의해야 합니다.

    "./pipex infile "sleep 20" "ls" outfile
    wait을 사용하거나 설정을 잘못할 경우 바로 종료되어 버리는 현상이 나타납니다.

    shell에서는 종료가 된 것처럼 보이지만, ps -al 명령어를 활용해서 실행 중인 프로세스를 확인하면 sleep 20프로세스가 존재하는 걸 확인 할 수 있습니다.위와 같은 경우를 고아 프로세스라 하며 주의해야 합니다.

✅ 배운점

운영체제의 기본인 프로그램프로세스, 프로세스 간 통신인 IPC에 대해 이론으로만 알고 있던 내용들을 직접 코드를 짜보며 실행시키는 과정에서 더 많은 것을 배우고 자세하게 알 수 있었습니다.

이전 포스트 get_next_line에서 파일디스크립터(fd)에 대해 개념적인 내용을 이해했다면, 이번 프로젝트에서는 dup2를 활용해서 fd가 가르키는 값들을 변경도 해보고 자유자재로 활용할 수 있어 많은 공부가 되었습니다.

단순히 암기해서 지식을 습득하는 것보단 자전거를 타는 것 마냥 넘어져가며 배우는 것이 결국엔 더 빠르게 성장할 거라 생각합니다.
하나하나 코딩해보고, 다양한 함수들을 직접 사용해 보며 테스트해보고 하는 과정에서 여러 번 오류를 익히는 게 더 오래 기억에 남는다는 걸 크게 느끼고 있습니다.

https://github.com/zerowin96/my_pipe_line

profile
정통과 / 정처기 & 정통기 / 42seoul 7기 Cardet / 임베디드 SW 개발자

0개의 댓글