본문 바로가기
42seoul/circle02

[Circle02] pipex

by jungcow 2021. 5. 29.

1. 과제에 대한 설명

./pipex file1 cmd1 cmd2 file2

< file1 cmd1 | cmd2 > file2

와 같이 작동시키도록 코드를 짜라.

  • 허용함수
    • open ◦ close ◦ read ◦ write ◦ malloc ◦ waitpid ◦ wait ◦ free ◦ pipe ◦ dup ◦ dup2 ◦ execve ◦ fork ◦ perror ◦ strerror ◦ exit

2. 구현하기 위해 필요한 사전 지식

  • 멀티 프로세스 프로그래밍
  • 파이프 시스템 콜
  • file descriptor 관리 시스템
  • 여러 함수들의 사용 방법
    • fork
    • pipe
    • dup, dup2
    • execve
    • wait, waitpid

3. 핵심 의도

핵심 의도는 멀티프로세싱을 구현하라는 것 같다. 따라서 멀티 프로세싱을 위한 자식 프로세스 생성 및 자식 프로세스간의 통신 방법 등을 알아야 했기에 다음과 같은 코드를 간략히 적어보려 한다.

typedef struct        s_execute
{
    char            *input;
    char            *output;
    char            ***command;
    int                input_fd;
    int                output_fd;
    int                command_num;
}                    t_execute;
// #main.c
int        main(int argc, char *argv[], char *envp[])
{
    t_execute    execute;

    if (!validate(argc, argv))
      return (1);
    init_execution(&execute, argc, argv);
    ft_execute(&execute, envp);
    clear_execution(&execute);
    return (0);
}
  • 인자는 file1과 file2, 그리고 명령어가 최소 두개 이상이 와야 하므로 이러한 경우로 유효한 인자인지를 확인한다.
    • 여기서 file1은 입력으로 들어가는 부분이므로 파일이 사전에 존재해야만 한다.
  • 그 다음 받은 인자를 관리할 execute 구조체를 생성한다음 할당해 준다.
  • 그 다음 멀티 프로세스로 명령어 실행 후 execute 구조체를 clear 까지 해준다.
// #execute.c
int        ft_execute(t_execute *execute, char *envp[])
{
    pid_t    pid;
    int        i;
    int        fd[2][2];
    int        status;

    i = -1;
    while (++i < execute->num)
    {
        if (ft_pipe(fd[NEW], i) < 0)
              exit(EXIT_FAILURE);
        pid = fork();
        if (pid < 0)
              exit(EXIT_FAILURE);
        else if (pid == 0)
              execute_child(execute, envp, fd, i);
        else if (pid > 0 && i > 0)
        {
            waitpid(pid, &status, 0);
            close_fds(fd[OLD]);
        }
        fd[OLD][0] = fd[NEW][0];
        fd[OLD][1] = fd[NEW][1];
      }
      close_fds(fd[OLD]);
      return (1);
}
  • pipe 생성 후 fork로 자식 프로세스를 생성한다.
// #execve 

int        check_path_env(char *filename, char **dir, char *envp[])
{
    char    **path;
    int        i;
    char    *value;

    value = getenv("PATH");
    if (value == NULL)
          return (NO_FILE);
    path = ft_split(value, ':');
    free(value);
    if (path == NULL)
          return (-1);
    i = -1;
    while (path[++i])
        if (has_file(path[i], filename))
              break ;
    if (path[i] == NULL)
    {
        ft_strsfree(path);
        return (0);
    }
    if ((dup_str(dir, path[i]) < 0) || (join_path(dir, filename) < 0))
          return (-1);
    ft_strsfree(path);
    return (1);
}
  • execve는 첫번째 인자로 명령어의 위치(path)를 받는다.

  • 위 코드는 그 첫번 째 인자를 만들어주는 함수이다.

  • 중요

    • 명령어들은 모두 $PATH의 디렉토리들에서 찾을 수 있다.

      • ❯ echo $PATH
        /Users/jungwoo/.rbenv/shims:/Users/jungwoo/.rbenv/bin:/Users/jungwoo/.pyenv/shims:/Users/jungwoo/.pyenv/bin:/Library/Frameworks/Python.framework/Versions/3.7/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin
      • 디렉토리들이 : 으로 구분되어 있다.

    • 만약에 없다면 없는 명령어이므로 에러를 띄워야 한다.

중요 원리(가장 헷갈렸던 부분)

1 2 3
1 2 3

1.

  • fd 3, 4번이 pipe 에 연결된 채로 fork()로 자식프로세스를 생성하였다.
  • 자식프로세스 쪽에서 STDOUT을 4번 fd(write end)에 연결시켰다.
  • 이후 자식프로세스에 연결된 3번과 4번을 닫았다.
  • ⌾ 결론: 자식프로세스의 실행 결과가 1번 fd에 연결된 STDOUT에 출력되게 되는데, 위에서 4번 fd가 가리키고 있는 pipe의 write end에 연결시켰기 때문에 결국 실행 결과는 4번 fd에 연결된 pipe의 write end에 출력되게 된다.

2.

  • 두번 째 자식 프로세스를 만들기 전에 새로운 pipe를 fd 5, 6번에 연결시키고 fork()로 두번째 자식 프로세스를 생성시켰다.
  • ※ 여기서 중요한 점은 부모 프로세스에는 아직 첫번째 pipe가 3, 4번 fd에 연결된 상태임을 명심하는 것이다. 위와 같은 이유로 두번째 자식 프로세스는 3, 4번은 첫번째 pipe에, 5번과 6번 fd는 두번째 pipe에 연결된 상태에 있다. 이 때 첫번째 자식 프로세스의 실행 결과는 write end에 출력되었으므로 두번째 자식 프로세스는 첫번째 pipe의 read end로 두번째 자식 프로세스의 입력이 될 첫번째 자식 프로세스의 결과를 받아와야 한다.(write end에 쓴 것은 read end로 나오는 단방향 스트림이 파이프임을 기억하자)
  • 따라서 두번째 자식 프로세스에서는 0번 fd가 가리키는 stream을 (STDIN -> 첫번째 pipe의 read end)와 같이 변경시켰다.
  • 또한 첫번째 자식 프로세스와 마찬가지로 세번째 자식 프로세스와 통신하기 위해 두번째 자식 프로세스의 실행 결과를 두번째 pipe의 write end로 연결시켰다.
  • ⌾결론: 두번째 자식 프로세스는 명령을 실행할 때 첫번째 pipe의 read end에서 입력받을 데이터를 받아오고, write end로 실행결과를 출력한다.

3.

  • 세번째 자식 프로세스 과정도 이전과 동일한 방식으로 이 전 pipe의 read end에서 입력받을 데이터를 불러오고, 현재 pipe의 write end에 실행 결과를 출력한다.비하인드

pipex 과제에서 첫날과 두번째 날까진 pipe() 시스템 콜을 사용하지 못하도록 되어있었다. 이때까지만 해도 minishell 과제와 다른 점이 pipe를 직접 구현하는 거에 있구나! 하는 생각이었지만 세번 째 날에 pipe가 허용함수로 들어왔다.. 이로 인해 이번 과제의 의도는 아마 pipe 시스템콜을 사용해보아라 인 것 같은 느낌을 받았다. pipe() 시스템 콜 없이 구현한 방법은 여기 에 들어가면 볼 수 있다.