Redirection & Pipe

Violet_Evgadn·2023년 4월 26일
0

Linux

목록 보기
13/34

들어가기 앞서

Redirection과 Pipe는 Linux 명령어를 더욱 강력하고 편리하게 해주는 핵심 역할을 하는 기능이다.

이 2가지를 제대로 활용하지 못한다면 Linux 자체를 잘 활용하기가 어려워진다.

이번 Section은 계속해서 활용할 기술이기 때문에 잘 알아둬야 한다.


Pipe

Linux Pipe란?

Linux를 활용하다 보면 이전 명령어의 결과를 다음 명령어의 입력값으로 활용하는 방식으로 여러 개의 명령을 동시에 사용하고 싶을 때가 있다.

예를 들어 현재 디렉터리에 있는 모든 파일명 중 "sample"이라는 문자열을 포함하는 파일명만 찾고 싶은 경우가 있을 것이다.

파이프란 이를 가능하게 해주는 도구로써 이전 명령어의 결과물을 다음 명령(주로 필터링)에서 사용할 수 있게 도와준다.

Pipe의 원리

Pipe의 원리를 알기 위해선 Linux에서 명령어를 어떻게 실행하는지를 먼저 알아야 한다.

Linux는 Terminal에서 명령어를 실행할 때 "fork" 명령어를 통해 백그라운드에 자식 프로세스를 만든 뒤 자식 프로세스에서 명령어를 실행한다.

백그라운드에서는 부모 프로세스(Linux Terminal)의 상태 및 정보를 기준으로 명령어를 stdin(표준 입력)으로 입력받는다. 그리고 명령어를 처리한 뒤 결과물을 stdout(표준 출력)으로 부모 프로세스로 보내면 Linux Terminal에서 결과물이 반환되는 방식으로 동작한다.

그렇다면 Pipe는 어떻게 이전 명령어의 실행 결과를 사용할 수 있는 것일까?

Pipe를 통해 다수의 명령어가 입력된다면 Linux는 먼저 각 명령어에 대한 자식 프로세스를 미리 만들어둔다.

그리고 이 자식 프로세스들을 병렬로 처리하여 이전 명령어의 stdout이 다음 명령어의 stdin으로 들어가도록 만들어준다.

이때 모든 자식 프로세스는 "exec"라는 System Call 함수를 통해 fork가 발생되며 이 때문에 PID가 모두 동일하다.

PID가 같다는 것은 1개의 Process라는 의미이므로 Pipe를 통한 여러 명령들이 1개 Process에서 모두 동작한다는 것이다.

즉, 이전 명령어의 결과물을 현재 명령어의 입력값으로 사용함으로써 1개 Process 내에서 여러 개의 명령을 같이 쓸 수 있는 것이다.

위 사진과 같은 상황에서 명령어 1이 종료될 때까지 명령어 2,3은 wait상태가 되고 명령어 1이 stdout을 발생시키면 명령어 2의 stdin으로 들어가 명령어 2를 실행할 자식 프로세스가 활성화되는 것이다.

Pipe 기본 사용

명령어1|명령어|명령어...

즉 파이프 기호인 |를 사용해 여러 개의 명령어를 이어 붙이는 것이다.

Pipe를 사용할 때 자주 활용되는 것은 필터 명령어이다.

표준 입력을 받아들여 필요한 부분만 거른 후 결과를 표준 출력으로 내보내는 명령어들을 필터 명령어라 하는데 이 명령어들은 Pipe와 함께 활용되며 더욱 강력해진다.

특히 "표준 입력"을 받는다는 것과 "표준 출력"을 내보내므로 결과물을 계속해서 다음 Pipe 명령어의 stdin으로 활용할 수 있다는 점이 필터 명령어를 파이프와 잘 맞는다.

대표적인 필터 명령어로 grep, tail, sort 등이 존재한다.

Pipe 심화 과정

ls | sort | cd /

Pipe를 통해 여러 개의 명령을 실행시켰지만 결과물로써 아무것도 출력되지 않았으며 Root Directory로 이동하지도 않았다.

그렇다면 이 때 Pipe를 통해 기입한 여러 명령어들은 모두 실행되지 않은 것일까?

정답은 아니다. 이를 이해하기 위해선 Linux 명령어 및 Pipe를 통해 연결된 명령어들이 모두 자식 프로세스에서 동작하며 Pipe의 자식 프로세스는 결과를 stdout으로 내보낸다는 사실을 알아야 한다.

먼저 자식 프로세스 1이 ls를 통해 파일 리스트를 stdout으로 출력한 뒤 sort를 실행시키는 자식 프로세스로 보낼 것이다.

자식 프로세스 2는 stdin으로 파일 리스트를 받았으며 이 리스트에 대하여 sort 명령어를 실행하고 stdout으로 출력할 것이다.

문제는 cd /를 실행하는 자식 프로세스 3에 있다. 자식 프로세스 3은 "ls | sort"의 결과물을 stdin으로 받았다.

그런데 cd 명령어는 딱히 stdin이 필요한 명령어는 아니다. 즉, "ls | sort"를 통해 도출된 결과물들이 무시되는 것이다.

대신 자식 프로세스 3의 Working Directory를 "/"(Root)로 변경시킬 것이다.

이후 cd 명령어는 stdout을 내보내지 않으므로 종료될 것이다.

정리하자면 cd /를 실행시킬 자식 프로세스 3에서 중간 결과물을 가지고 있다 자식 프로세스 3의 Working Directory를 Root로 변경시킨 뒤 종료되는 것이다.

당연히 cd 명령어는 stdout을 내보내는 명령어 아니므로 중간 결과물은 없어질 것이고 Pipe는 "stdout"을 내보내는 명령어이므로 자식 프로세스의 상태가 바뀐 것을 부모 프로세스(Linux Terminal)에 적용시킬 수 없다.

따라서 아무 명령어도 동작하지 않는 것처럼 보이는 것이다.

만약 "ls | sort"를 실행시키고 "cd /"를 연이어 실행시키고 싶다면 아래와 같이 사용해야 한다.

ls | sort && cd /

출력문이 생기고 Working Directory가 ~에서 /로 바뀜


Redirection

표준 스트림

Redirection을 알기 위해선 먼저 Linux의 표준 스트림에 대해 공부할 필요가 있다.

위키피디아에서는 표준 스트림을 아래와 같이 정의하고 있다.

표준 스트림(standard streams)은 특정한 프로그래밍 언어 인터페이스뿐 아니라 유닉스 및 유닉스 계열 운영 체제(어느 정도까지는 윈도우에도 해당함)에서 컴퓨터 프로그램과 그 환경(일반적으로 단말기) 사이에 미리 연결된 입출력 통로를 가리킨다.

뭔가 조금 개념이 어려워 보이는 데 간단히 말하자면 "Process와 주변 기기(주로 Terminal) 사이에 미리 연결된 입출력 통로"라고 말하면 될 것이다.

표준 스트림(IO Stream)이라고 불리는 입출력 통로를 통해서만 입력값과 출력값이 이동할 수 있다.

Linux에서는 Process가 실행될 때 기본적으로 3개의 Stream이 자동적으로 연결된다.

입력을 위한 Stream인 stdin(Standard input), 출력을 위한 Stream인 stdout(Standard output), 오류 메시지 출력을 위한 스트림 stderr(Standard error) 이 3개를 주로 Linux 표준 스트림이라 부른다.

기본적으로 stdin은 키보드와 연결되어 있으며 stdout 및 stderr는 Terminal과 연결되어 있다.

모든 프로세스는 실행될 때 자신을 실행시킨 부모 프로세스 Stream을 상속받는다.

예를 들어 Terminal을 실행시킬 경우 Terminal의 기본 Stream을 상속받으며 Shell을 실행시킬 경우 Shell의 기본 Stream을 상속받는 것이다.

stdout vs stderr

위 설명을 읽으면 stdout과 stderr는 목적만 다를 뿐 나머지는 모두 동일한 것같이 보인다.

그렇다면 왜 2개로 굳이 나눠놨을까?

이 둘의 차이는 Buffering 여부이다.

stdout은 Buffering을 하지만 stderr는 Buffering 없이 동작한다.

그렇다면 Buffering이 무엇일까?

Buffering이란 Buffer를 사용한다는 의미로 함수를 호출하는 시점에서 즉시 입출력이 일어나지 않고 Buffer라는 중간 저장 공간에 잠시 입/출력값을 저장한 후 Buffer에 저장된 데이터를 읽어 입출력을 처리하는 방식을 의미한다.

stderr는 Buffering이 없어 Process에서 에러 문구를 발생시키는 순간 바로 Linux Terminal에 에러 구문을 띄운다.

하지만 stdout은 Buffering이 있는 Stream이다. stdout은 줄 단위로 Buffering을 수행하므로 구분자(주로 \n)를 통해 1줄이 완성되었다고 판단되는 시점에 Buffer에 저장된 출력문구를 Terminal에서 띄우는 것이다.

아래 코드를 통해 확인해 보자.

#include <stdio.h>

int main(){
	char str1[] = "stdout";
    char str2[] = "stderr";
    
    printf("%s\n", str1);
    fprintf(stderr, "%s\n", str2);
}

예상하듯, 또 사진에서 볼 수 있듯 printf 명령문이 출력하는 문구가 먼저 출력되었다.

이 때 printf의 \n 문구를 없애보자.

이제 stderr와 stdout의 차이를 알겠는가?

stdout은 "\n"이 없을 경우 출력문구를 Buffer에 저장시켰다 프로그램이 끝나기 전 Buffer에 남아있는 데이터를 출력하는 형식이지만 stderr는 실행된 직후 바로 문구를 출력하므로 stderr 문자열이 더 빠르게 출력되었음을 볼 수 있다.

파일 디스크립터(FD; File Descriptor)

파일 디스크립터란 Process가 파일에 접근하기 위해 제공되는 고유 식별자이다.

FD는 0부터 순차적으로 번호가 부여되는 Non-negative Integer로써 사용자가 추가적으로 File Descriptor를 추가 및 사용할 수도 있으나 이것까지는 아직 알 필요가 없다.

Inode와 헷갈릴 수 있는데 이 둘의 차이점은 "File System 안에 저장되어 있는지 여부"이다.

Inode는 File System 안에 저장된 숫자로써 "ls" 명령어 등을 통해 Inode 번호를 확인할 수 있다.

하지만 File Descriptor는 파일 시스템 안에 저장되어 있지 않다. 단지 open()과 같은 System Call 함수가 실행될 때마다 동적으로 생성되는 "열린 파일에 대한 고윳값"인 것이다.

FD는 open() 같은 System call이 실행되며 특정 파일이 열릴 때 Open File의 고유값으로써 생성된다.

이 FD는 여러 가지 데이터를 가지고 있는데 특히 Index값을 저장하고 있다.

해당 Index는 dentry 객체를 가리키고 있으며 이 dentry 객체는 Inode 객체를 가리키고 있다.

즉, FD의 Index가 가리키는 dnetry 객체가 가리키는 Inode를 통해 파일에 접근할 수 있게 되는 것이다.

FD는 그 자체만으로는 아무런 기능을 하지 못하며 Process ID인 PID 값과 같이 활용되어야지만 제 기능을 할 수 있다.

Redirection

Redirection을 제대로 알아보기 전 Stream File Description들은 외워둘 필요가 있다.

Stream File Descriptor

  • 표준 입력 Stream : 0
  • 표준 출력 Stream : 1
  • 표준 오류 Stream : 2

출력되는 데이터를 임의로 다른 장치에 보내는 것을 Redirection이라고 말한다.

즉, 명령어나 실행 파일 등이 출력하는 문구를 다른 실행 파일의 표준 입력으로 보내는 기능이다.

Redirection 문법

  • > [파일명]
    • 출력 Redirection
    • 파일이 없을 경우 표준 출력으로 파일을 새로 생성하고 파일이 있을 경우 덮어씀
  • >> [파일명]
    • 출력 Redirection
    • 파일이 없을 경우 표준 출력으로 파일을 새로 생성하고 파일이 있을 경우 존재하는 파일에 덧붙임
  • < [파일명]
    • 입력 Redirection
    • 파일의 내용을 표준 입력으로 받도록 함
  • &1
    • stderr를 stdout으로 바꿈
  • /dev/null
    • 특수 파일로써 이 파일로 출력되는 데이터는 버려짐

이 때 재밌는 점이 Redirection을 사용할 때 "명령어 + Stream FD" 형식으로 사용하여 IF문과 같은 효과를 줄 수 있다는 것이다.

예를 들어 ls -y라는 명령을 내리고 싶다 가정하자.

그런데 ls는 y라는 Option을 가지지 않으므로 에러 문구(stderr)를 반환할 것이다.

이때 에러 문구가 발생할 경우에도 표준 출력으로 출력시키고 싶다 가정하자.

정말 Pseudo Code 적으로 생각하면 if(명령어가 에러 반환) frpintf(stdout, "%s\n", 에러문구)처럼 될 것이다.

그리고 Linux에서는 이를 "명령어 + Stream FD" 형식으로 구현할 수 있다.

우리는 에러 문구(stderr FD : 2)를 stdout으로 바꾸고 싶기(&1) 때문에 ls -y 2>&1을 통해 이를 구현할 수 있다.

만약 ls -y가 에러(2)를 발생한다면 "2>&1"이 실행되어 stdout으로 에러 문구를 출력할 것이며 에러를 발생시키지 않을 경우 "2>&1"은 아무런 역할을 하지 못할 것이므로 일반적은 ls -y 명령을 출력할 것이다.

(이때 중요한 점은 2>&1에 띄어쓰기가 들어가면 안 된다)

"2>&1"과 비슷하게 많이 활용되는 것이 있는데 바로 2>/dev/null이다.

2>&1은 stderr를 stdout으로 바꾸는 것이었지만 2>/dev/null은 에러가 발생할 경우 에러 문구를 없애는 것이다.

(에러 문구가 /dev/null의 stdin으로 들어가는데 /dev/null을 통해 출력되는 데이터는 버려지므로 결과적으로 에러 문구가 버려지는 것이다)

profile
혹시 틀린 내용이 있다면 언제든 말씀해주세요!

0개의 댓글