[42seoul] pipex

ppparkta·2023년 2월 13일
1

42Seoul

목록 보기
5/7

pipex

pipex는 유닉스를 이해하는 프로젝트이다. 이번 프로젝트에서 새롭게 배우게 될 외장함수와 프로세스, 표준 스트림에 대해 정리하고 pipex 프로젝트를 완성한다.

프로세스

프로세스는 실행 중인 프로그램을 의미한다.

프로세스는 생성(new)되고 실행(running)될 때까지 준비(ready)상태에 놓이며 실행 중 I/O나 이벤트가 발생할 때 대기(waiting)한다. 대기 없이 프로그램이 실행되면 종료(terminated)된다.

대기상태에 놓인 프로그램은 일반적으로 block되고 새로운 이벤트와 swap되기 때문에 메모리 할당

이와 같은 프로세스의 주기를 프로세스 상태(process state)라고 한다.

이 과제는 파이프를 사용해서 다중 프로세스를 처리한다.

표준 스트림

STDnumber
STDIN0
STDOUT1
STDERR2

표준 스트림이란 데이터 요소의 입출력 통로이다. 입출력은 보통 물리적으로 구현되지만 이것을 시각적으로 구현했다. 일반적으로 명령의 스트림은 셸이 실행중인 텍스트 터미널에 연결된다. 다만 파이프나 리다이렉션을 통해서 전환할 수 있다.

리다이렉션

리다이렉션은 사용자가 원하는 방향으로 스트림을 바꾸는 것이다.

1) 표준 출력 (덮어쓰기)

cmd > filename

>를 사용하면 cmd의 출력 결과를 filename에 덮어쓰기할 수 있다. filename이 존재하지 않을 때 새로운 파일을 생성하고, 존재할 때 덮어쓴다.

2) 표준 출력 (이어쓰기)

cmd >> filename

>>를 사용하면 cmd의 출력 결과를 filename에 이어쓰기할 수 있다. filename이 존재하지 않을 때 새로운 파일을 생성하고, 존재할 때 기존 파일의 다음으로 이어쓴다.

3) 표준 입력

cmd < filename

<를 사용하면 filename의 내용을 표준 입력으로 사용한다. cmd를 사용하되 filename이 표준 입력 대상이 된다.

ls 와 같이 표준 입력을 필요로 하지 않는 경우 filename을 무시하고 cmd만 작동하는 것으로 확인된다.

echo는 표준 입력을 필요로 한다고 생각했는데 filename 무시하고 냅다 공백을 출력했다.

이건 표준 스트림에 대한 정의를 잘못 이해하고 있어서 이해하지 못했던 부분인데, fd를 필요로 하는 명령어의 경우 표준 입력을 필요로 한다. 그런데 echo는 파일을 여는 것이 아니라 문자열을 그대로 출력할 뿐이므로 fd를 필요로 하지 않는다.

실제로 echo a.txt를 실행하면 a.txt라고 그대로 출력 될 뿐이다. 따라서 표준 입력을 필요로 하지 않는다.

⭐️ 어떤 파일을 열 필요가 있는 명령어는 표준 입력을 필요로 한다. 
echo는 파일을 열 필요 없이 argument를 그대로 출력하는 방식이다.
따라서 표준 입력을 필요로 하지 않는다.

4) 응용

cmd < infile > outfile

infile을 표준 입력으로 받는 명령어 cmd를 실행시켜 그 출력결과를 outfile에 덮어쓰기로 저장한다.

5) 표준 파일 핸들

cmd 2> filename

일반적으로 stderr로 처리되는 구문은 리다이렉션으로 표준 출력해도 저장되지 않는다. 이 때 리다이렉션에 fd핸들러(이 경우는 표준에러에 해당하는 2)를 추가해서 stderr 출력도 저장할 수 있다.

파이프

cmd1 | cmd2

파이프는 cmd1을 읽어서 그 출력 결과를 cmd2에 대한 표준 입력으로 사용한다.

외장함수

이번 과제에서 새롭게 다루게 될 함수는 access, dup, dup2, execve, fork, pipe, unlink, wait, waitpid 가 있다. 내가 사용하면서 특히 주의하고 더 열심히 공부했던 함수들만 따로 정리했다.

fork()

fork는 함수를 호출한 process를 복제하는 함수이다.

부모 / 자식 프로세스가 나뉘어 복제된다. fork함수에 반환된 값은 부모 프로세스에는 자식의 pid를 반환하고, 자식 프로세스에는 0을 반환한다. fork에 실패한 경우 -1을 반환한다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main(){
    
    printf("L0\n");
    fork();
    printf("L1\n");
    fork();
    printf("Bye\n");
}

위의 코드와 같은 경우 main 안에서 아래의 그림처럼 다중 프로세스가 되어 같은 작업을 여러 프로세스에서 실행한다. 따라서 "Bye"가 네번 출력된다.

(그림에 오타 있음! 'L2' -> 'L1')

L0
L1
L1
Bye
Bye
Bye
Bye

다만 실행순서를 보장하진 않는다. 각각의 프로세스 중 어느 것이 먼저 실행될지는 알 수 없기 때문이다. 따라서 아래의 순서대로 출력될 수 있다.

프로세스의 실행 순서가 달라지면 출력 결과는 얼마든지 바뀔 수 있다. 다만 코드의 흐름을 거스르는 출력 결과가 나올 수는 없다.

L0
L1
Bye
L1
Bye
Bye
Bye

pipe()

pipe함수는 자식프로세스와 부모프로세스 간 통신을 위해 통로를 열어주는 함수이다.

int	pipe(int fd[2])

기본적으로 파이프를 생성했을 때 다음과 같은 상태가 된다.

fd[0]과 fd[1]을 이용해서 단방향으로 통신한다. 지금은 프로세스가 하나인 상황이고 fork로 인해 프로세스가 두개가 된다면 아래와 같이 통신할 수 있게 된다.

위의 경우에는 부모프로세스가 자식에게 데이터를 보내기 위해서 fd[1]이 STDOUT을, 자식프로세스가 부모에게서 데이터를 받기 위해서 fd[0]이 STDIN을 담고 있다.

pipex에서 부모-자식 프로세스 간 통신을 위해 파이프를 사용하는데, 이 때 dup2를 통해서 표준 스트림을 부모프로세스의 fd[1]과 자식프로세스의 fd[0]에 연결한다.

$ < infile cat | grep a > outfile

이 명령어를 그림으로 표현하면 다음과 같다.

process1

  • infile을 STDIN(0)으로 설정한다
  • fd[1]을 STDOUT(1)으로 설정한다
  • cat을 실행한다

process2

  • fd[0]을 STDIN(0)으로 설정한다
  • outfile을 STDOUT(1)으로 설정한다
  • grep a를 실행한다

pipe함수는 그저 프로세스 간 통신을 위한 통로를 연결시키는 것 뿐이다. 그 통로인 fd[0]과 fd[1]에 표준스트림을 넣고 싶다면 그 때는 dup2를 이용하면 된다.

따라서 pipex 과제 내에서 pipe와 dup2는 항상 붙어 사용하게 된다.

dup2() 중요!!!

dup2함수는 열린 fd를 다른 fd로 복사하는 함수이다.

dup2함수에 대한 설명
이 과제의 핵심인 함수라고 생각한다. 표준 스트림을 fd에 연결할 때, 파이프로 연 임시fd를 다른 fd와 연결할 때 사용한다.

  • fd2가 fd1을 가리키게 된다.
  • fd2가 기존에 가리키던 file은 close 된다.

wait(), waitpid()

wait함수는 부모 process가 자식 process의 종료신호를 받을 때까지 종료되지 않게 하는 함수이다.

wait함수는 fork와 pipe에 대한 이해를 끝내고 공부하는게 좋을 것 같다.

초반에 wait을 공부할 때 단순히 좀비프로세스를 방지하기 위해 사용하는 함수라고 생각했으나 pipe로 연결된 여러 프로세스들이 내 의도대로 종료되게 하기 위해 반드시 사용해야 하는 함수이다.

void	main(int argc, char **argv, char **envp)
{
	pid_t	pid;
    
    pid = fork();
    if (pid == 0)
    {
    	execute(argc, argv, envp);
	}
    wait(0);
}

적절한 예시는 아니지만 위와같이 fork로 프로세스가 복제된 상황에서 사용할 수 있다.

코드를 짜다보면 무한대로 들어오는 입력파일을 중단하고 outfile로 내보내기 위해서 (예를 들면 < /dev/random "cat" | "head -1" > outfile) waitpid의 WNOHANG옵션을 사용하게 될 수 있다. 그런데 waitpid의 WNOHANG옵션을 사용한 경우 sleep과 같은 명령어를 사용했을 때 정상적인 실행이 안 될 수 있다.

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

이 옵션은 자식 프로세스를 정상적으로 종료하는 것이 아니라 무시하고 호출자를 실행시켜버리는 것이기 때문이다. pipex의 경우 호출자는 부모프로세스를 의미한다.

이 옵션을 쓰지 않고 일반적인 wait과 같이 사용한다면 정상 종료 될 때까지 호출자가 차단되겠지만 옵션을 쓴 경우 조금만 입력시간이 길어져도 호출자는 자식프로세스를 무시하고 종료시키게 된다. 정상적인 종료로 볼 수 없다.

execve()

execve함수는 현재 프로세스에서 실행파일을 실행시키는 함수이다.

기본적으로 exec이며 ve는 기본 함수에서 추가적인 기능을 수행한다.

만약 execve함수의 마지막 인자를 0으로 넣으면 가장 최근에 넣었던 값으로 실행된다.

pipex과제는 환경변수를 변경하는 상황까지 고려하지 않으므로 이 값을 비워도 되지만 이후에 minishell을 진행할 때는 unset을 직접 제작하므로 꼭 세번째 인자에 넣을 값을 고려해야 함.

profile
겉촉속촉

0개의 댓글