[42Seoul] Pipex

jiyseo·2022년 8월 12일
0

42 Seoul

목록 보기
8/9

Pipex

Pipex는 ./pipex infile cmd1 cmd2 outfile 로 입력받아 infile을 읽어 cmd1, cmd2를 실행한 후 그 결과를 outfile에 출력하는 과제이다.

명령어는 2개 이상이어야 하며 infile 대신 here_doc이 들어오는 경우에는 문장들을 입력받은 후 동일하게 진행해야한다.

과제 시작 전

1. pipe()

#include <unistd.h>
int pipe(int fd[2]);
  • pipe()함수는 말그대로 파이프를 생성해준다고 생각하면 될 것같습니다. 이 파이프에 데이터를 넣고 뺄 수 있습니다.
  • 단, FIFO (First In First Out, 선입선출)의 방식으로 데이터를 주고받을 수 있습니다.
  • 인자로 fd(파일디스크립터)변수를 넣어주어야합니다. 변수명은 마음대로지만 int형의 크기가 2인 배열을 넣어주어야합니다. pipe()함수는 이 fd[0]을 입력fd로 fd[1]을 출력fd로 만들어 줍니다.
int main(void)
{
    int fd1[2];
    int fd2[2];
    int fd3[2];

    pipe(fd1);
    pipe(fd2);
    pipe(fd3);
    printf("%d %d\n", fd1[0], fd1[1]);
    printf("%d %d\n", fd2[0], fd2[1]);
    printf("%d %d\n", fd3[0], fd3[1]);
}
//출력값
// 34
// 57
// 89
  • 위의 예시는 파이프를 3개만들어준 뒤 각각의 fd요소를 확인해 보는 코드입니다.
  • 출력값을 보면 알 수 있듯이 3부터 겹치지 않게 배정되어 졌음을 알 수 있습니다.
  • 기본적으로 fd(파일디스크립터)에서 0, 1, 2는 다음으로 배정되어 있습니다.
    • 0: 표준입력
    • 1: 표준출력(콘솔출력)
    • 2: 에러출력
  • open()함수를 이용했을때도 fd를 배정받는데 사용한 후에 close()함수를 이용하여 닫아줬습니다. pipe()함수 역시 fd를 배정받기에 사용 후 혹은 사용하지않는 fd(파일디스크립터)는 close()함수를 이용하여 닫아주어야 합니다.
  • pipe()함수가 실패했을 경우 1을 반환합니다.

2. execve()

  • 헤더: unistd.h
  • 형태: int execve(const char filename, char const argv[], char *const envp[]);
  • 인수: char char 디레토리 포함 전체 파일 명const char argv[] 인수 목록char * const envp[] 환경 변수 목록
  • 반환: 실패일 때만 -1
#include <unistd.h>
#include <stdio.h>

int main(int argc, char * const *argv, char **envp)
{
	char *arr[] = {"ls", "-al", NULL};
	int returnv = execve("/bin/ls", arr, envp);
	printf("value = %d\n", returnv);
}

3. fork()

https://reakwon.tistory.com/104

#include <unistd.h>
pid_t fork(void);
  • fork()함수는 프로세스를 복사해주는 함수입니다. 이렇게 복사된 프로세스를 "자식 프로세스" 기존의 프로세스를 "부모 프로세스"라고 부릅니다.
  • 반환값으로 자식프로세스의 pid값을 반환합니다. 여기서 pid란 Process IDentifier의 약자로, 프로세스의 고유 ID입니다. 자료형타입으로 pid_t를 사용하는데 int형으로 선언해도 잘 동작합니다. (자세한 이유는 모르겠지만 pid_t는 0 ~ 32767의 범위를 갖는다고 합니다. -1 == 32768경우도 포함)
  • fork()함수가 실패할 경우 1을 반환합니다.
  • 자식프로세스는 fork()함수가 호출된 이후부터 진행합니다. 그렇기 때문에 자식프로세스에서는 fork()반환값을 저장한 변수의 값이 0입니다. 그렇기 때문에 다음의 코드예시와 같이 fork()함수가 반환한 자식함수pid를 이용하여 부모프로세스 혹은 자식프로세스에서만 동작하는 함수를 만들 수 있습니다.
int main(void)
{
    pid_t pid;

    pid = fork();

    if (pid == -1)
    {
        printf("fork() error");
        exit(1);
    }
    if (pid == 0)
    {
        printf("\n****자식프로세스****\n");
        printf("변수pid값: %d\n", pid);
        printf("자식피드: %d\n", getpid());
    }
    else
    {
        printf("\n****부모프로세스****\n");
        printf("변수pid값: %d\n", pid);
        printf("부모피드: %d\n", getpid());
    }
    return (0);
}

/* 출력 */

****부모프로세스****
변수pid값: 6239
부모피드: 6238

****자식프로세스****
변수pid값: 0
자식피드: 6239
  • getpid() 함수를 이용하면 현재 프로세스의 pid값을 얻을 수 있다.
  • fork() → 프로세스(메모리에서 실행 중인 하나의 프로그램) 복제
  • 부모 프로세스에는 자식 process id를 return 하고 자식 프로세스에는 0을 return
  • fork() 실행 → 자식 프로세스, 부모 프로세스에서 각각 다음 실행할 문장부터 실행(pid에 fork()결과 넣는거부터) → pid = 0 자식프로세스

4. dup2()

  • 헤더 : unistd.h
  • 매개변수 :
    • int fd : fd로 전달받은 파일디스크립트를 복제하여 반환
    • int fd2 : 새 디스크립트의 값을 fd2로 지정하고, 만약 fd2가 열려있다면, 닫은 후 복제가 됨.
  • 반환값 : 성공 시 새 파일 디스크립트, 실패 시 1

pipe 관련 내용 참고 https://iworldt.tistory.com/18

pipe 관련 연습 코드

<단방향 연결>

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(int argc, char *argv[]) {
    //pipe 생성 후 디스크립터를 fd[0], fd[1]에 저장
    int fd[2];
    if (pipe(fd) == -1) {
        fprintf(stderr, "pipe error: %s\n", strerror(errno));
        return 1;
    }
    //printf("fd[0] = %d fd[1] = %d\n",fd[0], fd[1]);
    pid_t pid = fork(); //pid는 자식프로세스
    if (pid == -1) {
        fprintf(stderr, "fork error : %s\n", strerror(errno));
        return 1;
    }
    // 자식 프로세서가 처리하는 코드
    if (pid == 0) {
        // 표준 출력을 파이프를 쓰는 쪽으로 설정
        dup2(fd[1], 1); // 복제 1 = fd[1] 
        //0
        //1 = fd[1] -> 1 = pipe 출력
        //2
        //3(pipe 입력)ㅇ이 = fd[0]
        //4(pipe 출력) = fd[1]
        // 자식 프로세스는 파이프에서 읽지 않으므로 읽는 쪽을 닫는다
        close(fd[0]);
        int ste = execlp("ls", "ls", "grep a", NULL);
        if (ste == -1) {
            fprintf(stderr, "run error: %s\n", strerror(errno));
            return 1;
        }
    }
    // 부모 프로세스만 여기에 도달
    // 표준 입력을 파이프의 읽는 쪽으로 리다이렉션한다.
    dup2(fd[0], 0);
    // 0 = fd[0] -> 0 = pipe 입력
    // 부모 프로세스는 파이프에 쓰지 않으므로 파이프의 쓰는 쪽을 닫는다
    close(fd[1]);
    char line[255];
    // 표준 입력이 파이프에 연결되어 있으므로 표준 입력에서 읽는다.
    // fd[0]을 사용해도 된다.
    while (fgets(line, sizeof(line), stdin) != 0) {
        printf("%s", line);
    }
    return 0;
    // 결과로 aux 명령의 결과가 출력됨
}

<양방향 연결>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    pid_t pid;
    int testVal = 5;
    int fdf, fd1[2], fd2[2];
    char fixed_str[] = " - test"; // char *fixed_str = " - test"; 와 같음
    char input_str[100], concat_str[256];

    fdf = open("aaa.txt", O_RDWR | O_CREAT, 0644);
    if (fdf == -1)
    {
        perror("Open Failed");
        return 1;
    }
    if (pipe(fd1) == -1) {
        perror("Pipe Failed");
        return 1;
    }
    if (pipe(fd2) == -1) {
        perror("Pipe Failed");
        return 1;
    }

    scanf("%s", input_str); // 문자열 입력(99바이트 초과하면 buffer overflow 발생)

    pid = fork();
    if (pid < 0) {
        perror("fork Failed");
        return 1;
    }

/*
※ fd1, fd2, fdf를 비롯한 모든 변수는 자식이 부모것을 복제한 것이기 때문에
   부모에서 close(fd1[1])을 한다고 하여 자식의 것도 close()되는 것은 아님.
   부모와 자식은 완전 별개의 공간에 각각의 변수와 file descriptor를 가지고 있음
※ concat_str은 부모와 자식이 공유할 수 있는 것이 아니고 완전 별개 공간임,
   프로세서간에 메모리 공유는 그냥은 할 수 없고 공유메모리(Shared Memory)라는
   개념을 이용하고 shmget()등의 함수를 이용해야 함
*/

    if (pid > 0) { // Parent process (요기는 부모)

        int status;
        char concat_str[100];

        testVal = 10; // 부모와 자식이 공유하는지 확인하고자 다른 값으로 변경해 봄

        // dprintf() 함수는 fd에 출력하는 printf() 함수
        dprintf(fdf, "parent process testVal=%d\n", testVal); // aaa.txt에 쓰기
        close(fdf);

        close(fd1[0]); // 사용하지 않는 첫 번째 pipe의 읽기 fd 닫기
        close(fd2[1]); // 사용하지 않는 두 번째 pipe의 쓰기 fd 닫기

        // 첫 번째 pipe에 입력 받은 문자열 input_str을 출력함
        write(fd1[1], input_str, strlen(input_str) + 1);
        close(fd1[1]); // 첫 번째 pipe의 쓰기 fd 닫기

        // pid의 프로세서가 종료되길 대기함(=자식 프로세서 종료 대기)
        waitpid(pid, &status, 0); // wait(&status)는 waitpid(-1, &wstatus, 0)과 같음

        read(fd2[0], concat_str, 100 + sizeof(fixed_str)); // 자식이 보내온 것 읽기
        close(fd2[0]); // 두 번째 pipe의 읽기 fd 닫기

        printf("Concat string = %s\n", concat_str);

    } else { // child process (요기는 자식)

        char *src, *dest;

        dprintf(fdf, "clild process testVal=%d\n", testVal); // aaa.txt에 쓰기
        close(fdf);

        close(fd1[1]); // 사용하지 않는 첫 번째 pipe의 쓰기 fd 닫기
        close(fd2[0]); // 사용하지 않는 두 번째 pipe의 읽기 fd 닫기

        read(fd1[0], concat_str, 100); // 부모 프로세서가 pipe로 보내준 것 읽기
        close(fd1[0]); // 첫 번째 pipe의 읽기 fd 닫기

        src = fixed_str;
        dest = concat_str + strlen(concat_str); // concat_str에는 부모가 보내준 input_str의 값이 있음
        while (*src) *dest++ = *src++;
        *dest = '\0';

        write(fd2[1], concat_str, strlen(concat_str) + 1); // 부모에게 pipe로 보내줌
        close(fd2[1]); // 두 번째 pipe의 쓰기 fd 닫기

        exit(0); // 요거 실행되면 부모의 waitpid() 함수가 대기를 종료함

    }
}

ft_printf 수정한 부분

pipex 과제를 하며 어떤 명령이 잘못되었는지, 어떤 fd를 dup2하는데 실패하였는지 등을 알고 싶어서 출력을 하려다가 ft_printf()로만 출력을 하려니까 자식 프로세스들이 동시에 시작되면서 문자들이 겹쳐 나오는 현상을 발견했다. 따라서 snprintf 및 vsnprintf를 구현하여 buf에 저장하고 이를 한 번에 write하는 방식으로 진행하려한다.

  • int snprintf(char buffer, int buf_size, const char format, ...)
  • int vsnprintf(char str, size_t size, const char format, va_list ap);

이를 다음과 같이 구현하였다.

다른 함수들은 ft_printf 참고

int	ft_vsnprintf(char *buf, int buf_size, const char *format, va_list ap)
{
	t_info	inf;

	if (!format || !buf || buf_size <= 0)
		return (0);
	inf.buf_size = buf_size;
	inf.buf = buf;
	va_copy(inf.ap, ap);
	print_format(&inf, format);
	if (inf.count >= inf.buf_size)
		inf.count--;
	buf[inf.count] = '\0';
	return (inf.count);
}

int	ft_snprintf(char *buf, int buf_size, const char *format, ...)
{
	va_list	ap;

	va_start(ap, format);
	return (ft_vsnprintf(buf, buf_size, format, ap));
}

Logic

  1. 다음과 같이 선언하여 **envp 변수에 환경변수를 받아올 수 있도록 한다.

    int	main(int argc, char *argv[], char *envp[])
  2. heredoc인 경우와 heredoc이 아닌 경우를 분리하여 heredoc인 경우에는 heredoc_fd 파이프를 생성하고 get_next_line을 통해 표준입력으로 들어오는 문장을 읽어옴. heredoc_fd의 입력 fd를 prev_in_fd에 대입하고, heredoc이 아닌 경우는 입력받은 fd를 대입해준다. + 나머지 변수들 초기화

  3. 마지막 자식이 아닐 때까지 파이프를 생성하고 fork()를 통해 자식 프로세스를 생성한다.

  4. 부모 프로세스는 사용하지 않는 prev_in_fd 를 close해주고 파이프의 입력을 prev_in_fd에 대입해준다.

  5. 자식 프로세스에서는 dup2를 통해 표준 입력과 prev_in_fd를 연결하여 prev_in_fd를 표준 입력으로 받고, 마지막이 아닌 경우에는 표준 출력을 pipe의 출력으로, 마지막인 경우에는 main에서 입력받았던 out_fd를 넣어주어 out_fd로 표준 출력을 하도록 한다.

  6. 자식 프로세스에서 명령어를 실행하기 위해 execve()함수를 사용하는데, 이는 아래와 같으며 경로 및 명령어의 인수 목록이 함께 주어져야한다.

    int execve( const char *path, char *const argv[], char *const envp[]);
  7. 환경변수에서 PATH=가 있는 배열을 찾는다면 : 으로 구분되어 있다 :를 구분자로 하여 split 하고 이와 명령어를 join하여 실행가능한 경로인지 확인하고 execve의 첫 번째 인자로 넣어준다.

    PATH=/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin
  8. 입력받은 명령어를 **argv에 띄어쓰기를 구분자로 구분한 배열을 대입하고 명령어를 담은 배열로 사용한다. 이를 execve의 두 번째 인자로 준다. 마지막 배열의 값은 NULL어야한다.

에러처리

  1. argv 개수 에러, fork, pipe 실패 에러, 메모리 에러, file open error, dup2 error
  2. 명령어 인자로 예를들어 /bin/ls 와 같이 입력되는 경우에도 실행이 되어야한다. /경로/명령어 이런 경우

  1. 안쓰는 pipe close 잘해주기 잘 안해주면 좀비프로세서, 고아 프로세서가 됨 ㅜㅜ

코드

#include "pipex.h"

static void	pipex_child(t_pipex *px, int idx)
{
	char	**argv;
	char	*pathfile;

	if (px->heredoc_limiter)
		close(px->heredoc_fd[1]);
	pipex_dup2(px->prev_in_fd, STDIN_FILENO);
	if (idx >= px->lastchildidx)
		pipex_dup2(px->out_fd, STDOUT_FILENO);
	else
	{
		close(px->out_fd);
		close(px->pipe_fd[0]);
		pipex_dup2(px->pipe_fd[1], STDOUT_FILENO);
	}
	argv = ft_split(px->cmdv[idx], ' ');
	if (!argv)
		pipex_memory_error();
	pathfile = find_path(argv[0], px->envp);
	if (!pathfile)
		pipex_error("%s: command not found", argv[0]);
	execve(pathfile, argv, px->envp);
	free(pathfile);
	ft_free_array(argv);
	pipex_perror("execve failed");
}

static void	pipex_parent(t_pipex *px, int idx)
{
	if (idx < px->lastchildidx)
	{
		close(px->prev_in_fd);
		px->prev_in_fd = px->pipe_fd[0];
		close(px->pipe_fd[1]);
	}
}

static int	pipex_process(t_pipex *px, int idx)
{
	int	pid;

	if ((unsigned int)idx > (unsigned int)px->lastchildidx)
		return (1);
	if (idx < px->lastchildidx && pipe(px->pipe_fd) == -1)
		return (pipex_perror("pipe failed"));
	pid = fork();
	if (pid == -1)
		return (pipex_perror("fork failed"));
	if (pid > 0)
	{
		pipex_parent(px, idx);
	}
	else
	{
		pipex_child(px, idx);
		exit(0);
	}
	return (pipex_process(px, idx + 1));
}

static int	pipex_init(t_pipex *px, int argc, char *argv[], char *envp[])
{
	int	firstcmdidx;

	if (argc < 5)
		return (pipex_error("argument error"));
	firstcmdidx = 2;
	if (ft_strcmp(argv[1], "here_doc") != 0)
	{
		px->heredoc_limiter = NULL;
		px->in_fd = open(argv[1], O_RDONLY);
		if (px->in_fd < 0)
			return (pipex_perror("%s open failed", argv[1]));
	}
	else
	{
		firstcmdidx++;
		px->heredoc_limiter = argv[2];
		if (pipe(px->heredoc_fd) == -1)
			return (pipex_perror("here_doc pipe failed"));
	}
	px->lastchildidx = argc - firstcmdidx - 2;
	px->cmdv = argv + firstcmdidx;
	px->envp = envp;
	return (1);
}

int	main(int argc, char *argv[], char *envp[])
{
	t_pipex	px;
	int		flag;

	pipex_init(&px, argc, argv, envp);
	if (px.heredoc_limiter)
	{
		px.prev_in_fd = px.heredoc_fd[0];
		flag = O_APPEND;
	}
	else
	{
		px.prev_in_fd = px.in_fd;
		flag = O_TRUNC;
	}
	px.out_fd = open(argv[argc - 1], O_WRONLY | O_CREAT | flag, 0644);
	if (px.out_fd < 0)
		return (pipex_perror("%s open failed", argv[argc - 1]));
	if (pipex_process(&px, 0) > 0)
	{
		if (px.heredoc_limiter)
			pipex_heredoc(&px);
		wait(NULL);
	}
	close(px.out_fd);
	return (0);
}
#include "pipex.h"

static char	*join_path(char *path, char *file)
{
	char	*mem;
	char	*p;

	mem = (char *)malloc(sizeof(char) * (ft_strlen(path)
				+ ft_strlen(file) + 2));
	if (!mem)
		return (pipex_memory_error());
	p = mem;
	while (*path)
		*p++ = *path++;
	if (p > mem && p[-1] != '/')
		*p++ = '/';
	while (*file)
		*p++ = *file++;
	*p = '\0';
	return (mem);
}

static char	*find_file(char *pathlist, char *file)
{
	int		i;
	char	**patharr;
	char	*pathfile;

	patharr = ft_split(pathlist, ':');
	if (!patharr)
		return (pipex_memory_error());
	i = 0;
	while (patharr[i])
	{
		pathfile = join_path(patharr[i], file);
		if (!pathfile || access(pathfile, X_OK) == 0)
			break ;
		free(pathfile);
		i++;
	}
	if (!patharr[i])
		pathfile = NULL;
	ft_free_array(patharr);
	return (pathfile);
}

char	*find_path(char *cmd, char *envp[])
{
	int	i;

	if (ft_strchr(cmd, '/'))
	{
		if (access(cmd, X_OK) != 0)
			return (NULL);
		cmd = ft_strdup(cmd);
		if (!cmd)
			return (pipex_memory_error());
		return (cmd);
	}
	i = 0;
	while (envp[i])
	{
		if (ft_strncmp(envp[i], "PATH=", 5) == 0)
			return (find_file(envp[i] + 5, cmd));
		i++;
	}
	return (NULL);
}
#include "pipex.h"
#include <stdarg.h>
#include <errno.h>

int	pipex_error(char *format, ...)
{
	va_list	ap;
	int		len;
	char	buf[128];

	va_start(ap, format);
	len = ft_snprintf(buf, 128, "pipex: ");
	len += ft_vsnprintf(buf + len, 128 - len, format, ap);
	buf[len++] = '\n';
	buf[len] = '\0';
	write(STDERR_FILENO, buf, len);
	exit(1);
	return (-1);
}

int	pipex_perror(char *format, ...)
{
	va_list	ap;
	int		len;
	char	buf[128];

	va_start(ap, format); 
	len = ft_snprintf(buf, 128, "pipex: ");
	len += ft_vsnprintf(buf + len, 128 - len, format, ap);
	len += ft_snprintf(buf + len, 128 - len, ": %s\n", strerror(errno));
	write(STDERR_FILENO, buf, len);
	exit(1);
	return (-1);
}

void	*pipex_memory_error(void)
{
	pipex_perror("memory error");
	return (NULL);
}

int	pipex_dup2(int fd1, int fd2)
{
	if (dup2(fd1, fd2) != -1)
		return (0);
	return (pipex_perror("dup2(%d,%d) failed", fd1, fd2));
}

int	pipex_heredoc(t_pipex *px)
{
	char	*line;
	size_t	len;

	len = ft_strlen(px->heredoc_limiter);
	while (1)
	{
		ft_printf("pipex here_doc> ");
		line = get_next_line(STDIN_FILENO);
		if (!line)
			return (pipex_perror("get_next_line error"));
		if (ft_strlen(line) - 1 == len
			&& !ft_strncmp(line, px->heredoc_limiter, len))
			break ;
		ft_putstr_fd(line, px->heredoc_fd[1]);
		free(line);
	}
	close(px->heredoc_fd[1]);
	return (1);
}

근데 평가를 받는 중 오류를 발견했다 ㅜㅜ

바로 heredoc에서 한 줄만 받고 중지된다는 것이다 …….. ㅜㅜ 머가 문제일까효

profile
코딩뿡뿡이

0개의 댓글