본문 바로가기

프로그래밍

[42서울] pipex 심플 가이드

서론

프로젝트 실패 이후 멘탈이 깨져서 신나게 놀다가 블랙홀이 가까워지면서 슬슬 공포감이 들어서 다시 과제를 시작했다. 후... 이러면 안되는데... 라는 생각이 들어서 다음 과제까지는 쉬지 않고 달려서 마무리할 예정이다.

 

pipex 는 보너스까지 마무리하고 평가를 받으려고 했는데, 테스터기에서 일부가 해결이 안돼서 우선은 필수 파트만 내고 나머지는 나중에 다시 볼 생각이다.

 

과제 자체는 별로 어렵지 않았는데 리팩토링과 보너스 때문에 일주일 이상 걸렸다. 빠르게 필수 파트만 마무리한다면 3~4일 정도 걸리지 않을까 싶다.

 

서브젝트 정리

./pipex file1 cmd1 cmd2 file2
이렇게 실행했을때의 결과물이
< file1 cmd1 | cmd2 > file2
이렇게 실행했을때의 결과물과 동일해야한다.

예제들
./pipex infile ``ls -l'' ``wc -l'' outfile
< infile ls -l | wc -l > outfile

./pipex infile ``grep a1'' ``wc -w'' outfile
< infile grep a1 | wc -w > outfile

``ls -l'' 의 backtick 과 작은따옴표 두개는 인용 표시일 뿐 따로 신경 쓰지 않아도 된다고 한다

 

허용 함수 정리

사실 허용 함수들을 전부 제대로 이해한다면 과제 자체는 별로 어렵지않다.

 

여기선 내가 사용한 함수들만 정리했지만 다른 함수들도 꼭 확인하고 넘어가길 바란다.

 

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 : 파일을 읽기 전용으로 연다. (Read Only)
  • O_RDWR : 파일을 쓰기와 읽기용으로 연다. (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

 

fork

pid_t fork(void);

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

성공시 pid, 실패시 -1

자식 프로세스는 부모 프로세스의 메모리 상태를 그대로 복사해서 생성하기 때문에 부모 프로세스의 반환 값은 자식 프로세스의 pid지만 자식프로세스에서의 반환값은 0이다.(즉. -1이냐 0이냐 다른 숫자냐를 가지고 분기 가능)

 

waitpid

pid_t waitpid(pid_t pid, int *status, int options);

특정 pid를 지닌 자식 프로세스의 상태를 획득하고 필요시 메모리를 회수한다

pid : 프로세스의 id

  • pid = -1 : 임의의 자식 프로세스를 기다림
  • pid > 0 : 프로세스의 아이디가 pid와 같은 자식 프로세스를 기다림
  • pid == 0 : waitpid를 호출한 프로세스 그룹 pid와 같은 그룹 id를 지닌 프로세스를 기다림
  • pid < 0 : 프로세스 그룹 id가 pid 의 절댓값과 같은 자식 프로세스를 기다림

ststus : 프로세스 종료시의 상태를 보관할 변수의 주소

options

  • WNOHANG : 기다리는 PID가 종료되지 않아서 즉시 종료 상태를 회수할 수 없는 상황에서 호출자는 차단되지 않고 반환 값으로 0을 받음

 

dup2

int dup2(int fd, int fd2);

파일 디스크립터를 복제하는 함수. fd를 복제해서 fd2 로 지정한다. fd2가 사용 중이라면 기존 fd2를 닫은 후 복제를 한다.

fd : 복제할 원본 파일 디스크립터

fd2 : 복제한 fd를 담을 fd

반환값은 성공 시 새로운 fd(fd2), 실패 시 -1

 

pipe

int pipe(int fd[2]);

프로세스 간 통신을 위해 fd 쌍을 생성하는 함수

fd[2] : 파일 디스크립터 배열, fd[0]은 파이프의 출구로 데이터를 입력받는 fd 가 담기고 fd[1]에는 파이프의 입구로 데이터를 출력할 수 있는 fd가 담긴다.

반환값은 성공 시 0, 실패 시 -1

 

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를 함께 출력한다

 

 

메인 함수 파싱

int	main(int ac, char *av[], char *envp[])
{
//fd를 담아둘 구조체를 하나 선언한다.
	t_arg	arg;

//argc는 무조건 5개로 고정되어있으니 해당 부분 에러처리 먼저 해준다.
	if (ac != 5)
		print_std_error("argument error");
	
//infile 과 outfile을 open한다. open이 성공이 아니면(return이 -1 일때) 에러처리 해준다.
	arg.infile = open(av[1], O_RDONLY);
	if (arg.infile == -1)
		print_std_error("infile error");
	arg.outfile = open(av[4], O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (arg.outfile == -1)
		print_std_error("outfile error");
	
// 환경변수에서 PATH를 찾아서 PATH= 이후의 글자를 ft_split 으로 : 로 나눠서 저장한다.
	arg.path = get_path_envp(envp);

// command를 가져온 다음 실행가능한 PATH를 확인한다. 
	arg.cmd_arg1 = ft_split(av[2], ' ');
	arg.cmd_arg2 = ft_split(av[3], ' ');
	if (arg.cmd_arg1 == NULL || arg.cmd_arg2 == NULL)
		print_std_error("cmd missing error");
	arg.cmd1 = get_cmd_argv(arg.path, arg.cmd_arg1[0]);
	arg.cmd2 = get_cmd_argv(arg.path, arg.cmd_arg2[0]);
	if (arg.cmd1 == NULL || arg.cmd2 == NULL)
		print_std_error("path or cmd error");

// 가공한 값들이 정상적으로 들어갔는지 확인
	printf("%s, %s \n", arg.cmd1, arg.cmd2);	
}

 

char	**get_path_envp(char *envp[])
{
	char	*path;

	while (ft_strncmp("PATH", *envp, 4))
		envp++;
	path = *envp + 5;
// 정상적으로 들어왔는지 확인
	printf("%s\n", path);
	return (ft_split(path, ':'));
}

char	*get_cmd_argv(char **path, char *cmd)
{
	int		i;
	int		fd;
	char	*path_cmd;
	char	*tmp;

	path_cmd = ft_strjoin("/", cmd);
	fd = 0;
	i = 0;
	while (path[i])
	{
		tmp = ft_strjoin(path[i], path_cmd);
		fd = access(tmp, X_OK);
		if (fd != -1)
		{
			free(path_cmd);
			return (tmp);
		}
		close(fd);
		free(tmp);
		i++;
	}
	free(path_cmd);
	return (NULL);	
}

 

처음에 헷갈렸던 부분이 envp에서 'PATH=' 을 찾아서 그 뒤를 나눠서 가져온다고 생각해놓고 실제로는 'PATH=' 까지 가져와놓고 못 찾은 게 있었다. 그래서 *envp + 5를 split 해서 가져와야 한다.

 

데이터 흐름 제어

// 파이프를 만든 다음 자식 프로세스를 만든다
	if (pipe(arg.pipe_fds) < 0)
		exit_perror("pipe error");
	arg.pid = fork();
	
	// pid 값을 이용하여 에러와 자식 프로세스와 부모 프로세스를 분기한다
	if (arg.pid == -1)
		exit_perror("fork error");
	else if (arg.pid == 0)
	{
		// 표준 입력을 infile로 바꾸고, 표준 출력을 pipe 로 바꾼 다음 명령어를 실행한다
		close(arg.pipe_fds[0]);
		dup2(arg.infile, STDIN_FILENO);
		dup2(arg.pipe_fds[1], STDOUT_FILENO);
		close(arg.pipe_fds[1]);
		close(arg.infile);
		execve(arg.cmd1, arg.cmd_arg1, envp);
	}
	else
	{
		// 표준 입력을 pipe로 바꾸고, 표준 출력을 outfile 로 바꾼다
		close(arg.pipe_fds[1]);
		dup2(arg.pipe_fds[0], STDIN_FILENO);
		dup2(arg.outfile, STDOUT_FILENO);
		close(arg.pipe_fds[0]);
		close(arg.outfile);
		
		// 자식 프로세스가 끝나기를 기다려서 pipe에 값을 받아온 다음 명령어를 실행한다
		waitpid(arg.pid, NULL, 0);
		execve(arg.cmd2, arg.cmd_arg2, envp);
	}

이후에 중복되는 부분을 함수로 빼줬는데, 가독성이 떨어져서 뭔가 마음에 안들기는 한다. 그래서 여기에는 함수 쓰기 전 버전으로 가져왔다.

 

테스터기 사용 / 평가 후 수정

pipex-tester 를 보고 놓친 부분들을 수정했다. 테스터기가 마음에 안들었던건 완벽한 OK와 완벽하진 않지만 OK를 초록색과 노란색으로 표현해뒀는데, 색약 탓인지 난시 탓인지 모니터 탓인지 구별이 더럽게 힘들어서 색을 바꿔서 다시 봐야 했다

눈이 편하게 파란색으로 개조해서 다시 봤습니다.

 

1. 25번 KO 수정

테스터기에서는 명령어가 없는 상황에 프로그램을 종료할 필요가 없다고 이야기하고 있다. 그래서 테스터기와 동일하게 assets/deepthought.txt < notexisting | wc > test.txt 이렇게 찍어보니

       0       0       0

이라고 하는 wc 기본 값이 나왔다. 그래서 코드를 이렇게 바꿨다

if (arg.cmd1 == NULL || arg.cmd2 == NULL)
{
	result = 127;
	perror("command not found");
}

추가로 result 는 main 함수의 반환값으로 command not found를 뜻한다

 

2. 30번 수정

assets/deepthought.txt < grep Now | /bin/cat > outs/test-xx.txt

이런식으로 경로까지 정확하게 쓰여있는 경우에도 정상 작동해야 했다. KO는 아니지만 테스터기가 다른 색을 내는 게 기분이 나빠서 수정했다.

char	*get_cmd_argv(char **path, char *cmd)
{
	int		i;
	int		fd;
	char	*path_cmd;
	char	*tmp;

	fd = access(cmd, X_OK);
	if (fd != -1)
		return (cmd);
	path_cmd = ft_strjoin("/", cmd);
	i = 0;
	while (path[i])
	{
		tmp = ft_strjoin(path[i], path_cmd);
		fd = access(tmp, X_OK);
		if (fd != -1)
		{
			free(path_cmd);
			return (tmp);
		}
		close(fd);
		free(tmp);
		i++;
	}
	free(path_cmd);
	return (NULL);	
}

get_cmd_argv 함수에 fd = access(cmd, X_OK); if (fd != -1) return (cmd); 를 미리 넣어서 해결하였다.

 

31번 타임 아웃 수정

waitpid에서 무한히 큰 텍스트를 읽어야 할 때 문제가 생겼다.

waitpid(arg.pid, NULL, WNOHANG);

기다리는 PID가 종료되지 않아서 즉시 종료 상태를 회수할 수 없는 상황에서 호출자는 차단되지 않고 반환 값으로 0을 받는 WNOHANG을 옵션으로 줘서 해결했다

 

dup2, execve 에러 핸들링

if (dup2(std_out, STDOUT_FILENO) == -1)
	exit_perror("dup2 fail");
if (execve(arg.cmd2, arg.cmd_arg2, envp) == -1)
	exit_perror("execve fail");

프로그램에서 각각의 함수의 에러 케이스 전체를 커버하지 못하기 때문에 따로 에러 핸들링을 해줘야 한다.

 

 

결론

다른 언어로 프로젝트를 진행하고 그래픽스 과제를 풀다가 시스템 과제로 돌아오니까 적응하는데 좀 시간이 걸렸다. 한번에 여러 언어로 코딩하는 사람들은 어떻게 하는 걸까?

 

에러 처리 때문에 fail을 받고 엄청 디테일하게 했는데 가이드에 쓸만한 내용은 아닌 것 같다. 내가 평가를 받을때면 몰라도 평가를 할 때는 안 물어볼 내용일 것 같아서 그 부분은 생략했다. 혹시 코드가 이 가이드와 다른 부분은 이런 에러 처리라고 생각하면 될 것 같다.

 

fork 함수가 다음 과제들에서 많이 쓰이는 걸로 알고 있어서 좀 더 확실하게 이해하고 넘어가고 싶었는데 잘 쓰려고 노력한다고 잘 쓸 수 있는 함수는 아니었던 게 아쉽다.

 

이제 평가받고 바로 다음 과제를 시작해야겠다

 

 

레퍼런스

https://github.com/vfurmane/pipex-tester

 

GitHub - vfurmane/pipex-tester: Project Tester - Pipex

Project Tester - Pipex. Contribute to vfurmane/pipex-tester development by creating an account on GitHub.

github.com

https://bigpel66.oopy.io/library/42/inner-circle/8

 

pipex

Subjects

bigpel66.oopy.io

https://tall-crustacean-783.notion.site/PipeX-66fed554cb5c4ddabc52cbf8d0286c05

 

PipeX

프로젝트 소개

tall-crustacean-783.notion.site

https://codetravel.tistory.com/42?category=993122 

 

waitpid 함수 사용하기(wait함수와 비교)

waitpid 함수는 wait 함수처럼 자식 프로세스를 기다릴때 사용하는 함수입니다. 즉, 자식 프로세스의 종료상태를 회수할 때 사용합니다. 하지만 waitpid 함수는 자식 프로세스가 종료될 때 까지 차

codetravel.tistory.com

https://nomad-programmer.tistory.com/110

 

[Programming/C] pipe() 함수

자식 프로세스가 생성하는 데이터를 실시간으로 읽는 방법 pipe() 함수는 데이터 스트림 두 개를 연다. # 데이터 스트림 0 표준 입력 1 표준 출력 2 표준 에러 3 파이프의 읽는 쪽 (ex: fd[0]) 4 파이프

nomad-programmer.tistory.com

https://www.it-note.kr/157

 

execve(2) - 프로그램 실행.

execve(2) #include int execve(const char *filename, char *const argv[], char *const envp[]); 실행가능한 파일인 filename의 실행코드를 현재 프로세스에 적재하여 기존의 실행코드와 교체하여 새로운 기능..

www.it-note.kr