Skip to content

Instantly share code, notes, and snippets.

@maczniak
Created July 14, 2023 01:32
Show Gist options
  • Save maczniak/66c44ceb4c6a5f03bb2b0b14c9f76fdc to your computer and use it in GitHub Desktop.
Save maczniak/66c44ceb4c6a5f03bb2b0b14c9f76fdc to your computer and use it in GitHub Desktop.
Linux User Guide

standard input/output/error

유닉스는 모든 입출력을 파일로 봅니다. 파일을 읽고쓰는 것은 물론이고 하드웨어 센서와 네트워크 등도 마치 파일을 읽고쓰는 것처럼 처리합니다. 프로세스가 파일/네트워크/하드웨어 입출력을 하려고 대상을 열면(유닉스 시스템호출 open()) (정수값인) file descriptor가 생깁니다. 줄여서 fd라고 쓰고, /proc/PID/fd에 보이는 그 fd입니다. 우리가 /etc/security/limits.conf 파일에 nofile을 설정하면, 프로그램의 최대 fd 개수가 늘어나고 ulimit -a 출력의 open files 줄에서 확인할 수 있습니다.

프로그램을 실행하면, 쉘은 세가지 fd를 만들어서 프로그램에 붙입니다.

  • 0번 fd - standard input (stdin, 표준 입력), 우리가 키보드로 입력하는 내용, 프로그램이 0번 fd를 읽으면 우리가 키보드로 입력한 내용이 읽힘
  • 1번 fd - standard output (stdout, 표준 출력), 프로그램의 기본 출력, 프로그램이 1번 fd에 쓰면 화면에 출력됨, 예) ls -a
  • 2번 fd - standard error (stderr, 표준 오류), 프로그램의 오류 출력, 프로그램이 2번 fd에 쓰면 화면에 출력됨, 예) ls 존재하지않는파일명

여기서 standard output과 standard error는 (정확히는 버퍼링 차이가 있지만) 똑같이 화면에 출력됩니다.

                       +---------+
(0) standard input --> | program | --> standard output (1)
                       |         | --> standard error  (2)
                       +---------+

redirection

쉘은 프로그램의 standard input/output/error를 키보드와 화면이 아니라 파일로 연결할 수 있습니다. 이를 redirection이라고 부릅니다.

프로그램 ... < 파일은 키보드 대신 파일을 standard input으로 만듭니다. 파일 내용을 키보드를 입력한 것처럼 동작합니다.

프로그램 ... > 파일은 화면 대신 파일을 standard output으로 만듭니다. 화면에 결과가 보이지 않고 파일에 기록됩니다. ls > file_list는 현재 디렉토리의 파일 목록을 파일에 기록합니다. (파일을 보면 그냥 터미널에서 ls한 결과와 달리 알록달록한 여러 열이 아니라 파일이 한줄씩 차지합니다. ls 프로그램이 화면에 출력할 때와 파일로 출력할 때 다르게 동작하기 때문입니다. C 함수 isatty() 참고) 그러나 standard error는 여전히 화면을 지칭하므로 ls 존재하지않는파일 > file_list는 (파일은 비어있고) 오류문이 화면에 보입니다. 여기서 파일이 없으면 파일이 새로 생깁니다. 프로그램이 아무것도 standard output으로 안써도 빈 파일이 만들어집니다. 파일이 이미 존재하면 기존 내용을 지우고 시작합니다. 프로그램을 미처 실행하기 전에 파일 내용을 지우기 때문에 프로그램이 제대로 실행되지 않아도 파일 내용이 사라집니다. 그래서 프로그램으로 파일을 편집하려고 프로그램 same_file > same_file같이 실행하면 파일 내용이 날아가는 사고가 터집니다.

프로그램 ... >> 파일은 standard output을 출력할 때 기존 파일 내용을 덮어쓰지 않고, 파일 뒤에 추가합니다(append mode). (물론 파일이 없으면 파일이 새로 생깁니다.) 파일에 쓸 때마다 파일 끝으로 이동해서 내용을 덧붙입니다. 그래서 여러 프로그램이 >> 파일하면 모든 프로그램이 출력한 내용이 시간 순서대로 섞어서 파일에 기록됩니다.

standard error를 파일로 변경하고 싶다면, 프로그램 ... 2> 파일합니다. 여기서 2는 2번 file descriptor인 standard error를 말합니다. 이제 ls ... > file_list 2> error_here는 오류가 있던 없던 화면에 아무것도 출력하지 않습니다. 두 파일 중 한 곳에 (혹은 두 파일 모두) 프로그램 결과(혹은 오류문)를 기록합니다.

만약 standard output과 standard error를 동일한 파일에 기록하고 싶다면 어떻게 할까요? 프로그램 > same_file 2> same_file은 아닙니다. 이유는? 정답은 프로그램 > same_file 2>&1입니다. 2>&1이 처음 나왔습니다. standard output을 파일에 연결한 후 standard error(2)도 standard output(1)으로 돌립니다.

                       +---------+
                       |         | standard output (1) -+-> 파일
(0) standard input --> | program |                      |
                       |         | standard error  (2) -+ 
                       +---------+

> /dev/null이나 2> /dev/null을 자주 보게됩니다. /dev/null은 출력을 먹는 블랙홀 역할을 하는 (소프트웨어로 만든) 가상의 장치(device) 파일입니다. 여기로 간 내용은 화면에 출력되거나 파일에 기록되지 않고 버립니다. 반대로 0 비트를 무한히 뿜어내는 /dev/zero와 난수를 내뱉는 /dev/random/dev/urandom 가상 장치파일도 있습니다.

pipe

한 명령어가 여러가지 일을 하는 대신 한가지 일을 잘하는 명령어를 모아서 원하는 작업을 하는 것이 유닉스의 철학입니다. 문서에 있는 단어의 빈도를 구하고 싶다면, 단어를 추출하는 프로그램 + 단어를 사전 순서대로 정렬하는 프로그램 + 동일한 단어가 몇개 있는지 세는 프로그램 + 개수 순서대로 정렬하는 프로그램을 결합합니다. 기본 명령어들을 조합하여 예상하지 못한 수많은 작업을 할 수 있습니다.

명령어는 파이프(|)로 조합합니다. 명령어1 | 명령어2를 입력하면 쉘은 명령어1의 standard output을 명령어2의 standard input으로 연결하여 명령어를 실행합니다.

          +----------+                  +----------+                  +----------+
stdin --> | program1 | stdout --> stdin | program2 | stdout --> stdin | program3 | --> stdout
          |          | --> stderr       |          | --> stderr       |          | --> stderr
          +----------+                  +----------+                  +----------+

이렇게 파이프로 연결해서 사용하는, 한가지 일을 잘하는 명령어들은 다음 시간에 소개하겠습니다. (위에서 말한 단어 빈도 세기는 cat file | tr ' ' '\n' | sort | uniq -c | sort -nk 2처럼 할 수 있습니다. 지금은 의미를 몰라도 됩니다.)

일부 프로그램에서 파일명 아규먼트 자리에 -를 적으면, 문맥에 따라 (파일에서 읽는 대신) standard input에서 입력을 읽거나 (파일에 기록하는 대신) standard output으로 출력을 보냅니다. -는 유닉스가 관습적으로 사용합니다.

  • curl -o file_name https://google.com/ vs curl -o - https://google.com/ | ...
  • tar czf foo.tar.gz directorytar xzf foo.tar.gz vs tar czf - directory | tar xzf - -C Downloads

argument

프로그램 동작에 영향을 주는 입력에는 크게 1) stdin/stdout/stderr 같은 (키보드, 화면, 디스크, 네트워크 등) fd와 2) 아규먼트가 있습니다. find ~ -name "*.py"에서 명령어 find를 제외한 ~, -name (옵션), "*.py" (옵션의 아규먼트)는 모두 아규먼트입니다.

파이프는 fd를 연결합니다. 가끔 fd와 아규먼트를 서로 연결하고 싶을 때가 있습니다. echo Hello처럼 아규먼트는 fd로 (아래 그림으로 비유하면 세로를 가로로) 쉽게 변환할 (즉, stdout으로 출력) 수 있습니다. 반대로 fd를 아규먼트로 (가로를 세로로) 변환하려면 xargs 프로그램이 필요합니다. xargs는 stdin 내용을 아규먼트 삼아 다른 프로그램을 실행합니다.

find . -name "*.tar.gz" | xargs -n 1 tar -xf.tar.gz로 끝나는 파일을 찾아서 하나씩(-n 1) 압축을 풉니다(tar -xf). 만약 chaindata.tar.gzlogs/proxy5-20230628.log.tar.gz 파일이 있다면, find가 파일명을 stdout으로 넘기고 xargstar -xf chaindata.tar.gztar -xf logs/proxy5-20230628.log.tar.gz를 실행합니다. (사실 find는 이런 작업을 많이 하기 때문에 아예 -exec 옵션을 제공합니다. find . -name "*.tar.gz" -exec tar -xf {} \;는 위 명령어와 같은 기능을 합니다.)

          +----------+                  +----------+
stdin --> | program1 | stdout --> stdin |   xargs  |
          |          | --> stderr       |          |
          +----------+                  +----------+
                                             ||
                                             || arguments
                                             vv
                                        +----------+
                                        | program2 |+ --> stdout
                                        |          || --> stderr
                                        +----------+|  --> stdout
                                         +----------+  --> stderr

environment variable

환경변수(environment variable, envvar)도 프로그램 동작에 영향을 줍니다. 쉘에서 환경변수를 설정하면, 쉘이 실행하는 모든 프로그램에 환경변수가 전달됩니다. 환경변수 목록은 set이나 env로 확인할 수 있습니다. (단, set은 환경변수가 아닌 쉘변수도 함께 보여줍니다.) 환경변수 이름은 관습상 모두 대문자입니다. 환경변수를 만드려면, 쉘변수를 먼저 만들고 export하거나 한번에 export할 수 있습니다. FOO=BAR 프로그램 식으로 해당 프로그램을 실행할 때만 환경변수를 적용할 수도 있습니다.

$ FOO=BAR
$ export FOO # 뒤로 계속 환경변수 적용

$ export FOO=BAR # 뒤로 계속 환경변수 적용

$ FOO=BAR 프로그램 # 이 프로그램을 실행할 때만 환경변수 적용

중요한 환경변수는 다음과 같습니다.

  • LANG - 출력 언어를 정합니다. 예, LANG=ko. 글꼴이 없어서 한글이 깨져보일 때 (역사적인 이유로 LANG=en 보다 많이 쓰이는) LANG=C하면 기본 언어인 영어로 변경합니다.
  • PATH - 프로그램을 실행하면, 쉘이 프로그램을 찾는 경로 목록입니다. :로 연결된 경로에서 순서대로 프로그램을 찾습니다. 현재 디렉토리(.)를 넣으면 현재 디렉토리에서도 프로그램을 찾지만, 보통 보안상 이유로 현재 디렉토리를 PATH에 포함하지 않습니다. 대신 ./프로그램처럼 현재 디렉토리에 있는 프로그램을 실행합니다. 새로운 경로에 프로그램을 설치하면, PATH=$PATH:/새로운/프로그램/경로처럼 기존 PATH 앞 혹은 뒤에 경로를 추가합니다.
  • HOME - 사용자 로그인 디렉토리입니다. 쉘스크립트에서 $HOME/상대/경로 식으로 파일을 지칭하려고 사용합니다.
  • TERM - 과거에 사용하던 단말기(terminal) 종류를 지칭합니다. 단말기마다 커서를 움직이거나 색을 칠하는 명령이 다르기 때문에 vi같은 프로그램은 TERM 환경변수를 참고하여 화면을 그립니다. 요즘은 단말기 기계 대신 단말기 기능을 흉내내는 iTerm 같은 터미널 에뮬레이터를 사용합니다. 색깔이 안나올 때 이 환경변수를 수정하곤 합니다.

매번 여러 환경변수를 설정하는 경우 스크립트를 만들어서 활용하는 경우가 많습니다. 이때 . "$HOME/.cargo/env"처럼 .를 사용합니다. 그냥 스크립트 이름만으로 실행하면, 스크립트가 끝날 때 환경변수도 함께 사라지기 때문입니다. (정확히는 스크립트를 실행할 때 새로운 쉘이 시작하고, 새로운 쉘 안에 환경변수를 설정합니다. 스크립트가 끝날 때 새로운 쉘도 종료하기 때문에 쉘 안에 있는 환경변수 정보가 사라집니다.) . (혹은 csh 계열에서 source)는 스크립트를 현재 쉘 안에서 실행합니다. 그래서 스크립트가 설정한 환경변수가 남아 있습니다.

shell

사용자가 로그인할 때 쉘이 반겨줍니다. 쉘은 사용자의 입력을 받아서 프로그램을 실행합니다. (/etc/passwd 파일의 마지막 필드가 사용자가 로그인하면 실행되는 로그인 쉘입니다. 흔하지 않지만 Python이나 텍스트 게임을 로그인 쉘로 지정하는 경우도 있습니다.)

앞에서 본 redirection과 pipe는 쉘이 구성합니다. 프로그램은 자신이 출력한 메시지가 화면에 출력되는지 파일로 redirection되는지 아니면 pipe로 다른 프로그램에게 보내는지를 보통 모르고 신경쓰지 않습니다. 사용자가 redirection과 pipe를 지시하면, 쉘이 알맞게 fd를 구성한 후 실제 프로그램을 실행합니다. 그래서 프로그램 > 파일하면 심지어 프로그램이 존재하지 않아도 쉘은 일단 파일을 만들기 (파일이 존재했다면 파일 내용을 지우고) 시작합니다. > 파일은 빈 파일을 만듭니다.

쉘은 glob pattern이라고 부르는 *?도 처리합니다. ?는 파일명에서 아무 글자 하나에 해당하고, *는 파일명에서 (0개 이상) 여러 글자에 해당합니다. 20230706.log20230707.log 파일이 있는 디렉토리에서 사용자가 rm *.log를 실행하면, 쉘이 .log로 끝나는 파일을 찾아서 rm 20230706.log 20230707.log를 실행합니다. rm 프로그램은 사용자가 원래 *.log를 입력했는지 모릅니다. rm 프로그램 입장에서 20230706.log20230707.log 아규먼트만 보게 됩니다.

  • *.log - access.log, error.log, .log, ...
  • syslog.log.? - syslog.log.1, syslog.log.7, syslog.log, syslog.log.10, ...
  • /dev/sd[abcd], /dev/sd[a-d] - /dev/sda, /dev/sdb, /dev/sdc, /dev/sdd, /dev/sde
  • /dev/sd[a-d][0-3] - /dev/sda0, /dev/sdc2, /dev/sdb4, ...
  • *.{jpg,gif} - sticker.jpg, cat.gif, ...
  • *.{jp*g,png} - portrait.jpg, landscape.jpeg, mavel-logo.png, ...

faucet-202307[0-3][0-9].log라고 가능한 모든 조합으로 확장하지 않습니다. 실제 존재하는 파일명으로만 확장합니다. 만약 패턴에 해당하는 파일이 하나도 존재하지 않는 경우 1) 빈칸으로 확장하거나 2) 에러가 발생합니다. 쉘 마다 둘 중 어떤 행동이 기본값인지는 ls blahblah* 같은 명령으로 확인할 수 있고, 설정으로 기본 행동을 변경할 수 있습니다.

파일이 매우 많은 디렉토리에서 파일명을 확장하면 (예, rm *), 파일 목록이 너무 길어지기 때문에 쉘이 프로그램을 실행하지 않고 오류를 냅니다. 이 경우 find 같은 명령어를 가지고 파일을 찾을 때마다 명령어를 실행하는 식으로 해결할 수 있습니다.

tips

remove file

프로그램이 (로그파일 용도 등으로) 파일을 열고 있다면, 파일을 지워도 디스크 여유공간이 즉시 늘어나지 않습니다. ls로는 파일이 안보이지만, 프로그램이 끝날 때까지 운영체제가 파일 내용을 지우지 않기 때문입니다. 그래서 프로그램이 프로그램 동작 중에만 필요한 임시 파일을 만들 때 mktemp() 등을 사용하여 파일을 만들어서 열고 바로 삭제하곤 합니다. 프로그램이 비정상 종료하더라도 프로그램이 끝날 때 파일이 자동으로 정리됩니다.

the content of directory files

ls -l 결과에서 d로 시작하는 줄은 디렉토리입니다. 디렉토리 파일는 디렉토리 안에 있는 파일명과 inode 목록을 저장합니다. cat 디렉토리 같이 일반 명령어로 디렉토리 파일 내용을 직접 확인할 수 없지만, 파일이 매우 많은 디렉토리를 보면 디렉토리 파일 크기가 큰 것을 확인할 수 있습니다.

drwxr-xr-x 3 root root 280489984 Jul  7 03:36 chaindata 파일이 700백만개가 넘는 디렉토리
drwxr-xr-x 2 root root      4096 Mar  9 15:05 ethash    파일이 3개 들어있는 디렉토리

여담으로 inode는 파일의 메타정보로, 파일마다 inode가 한개씩 존재합니다. inode에는 파일 소유자, 실행 권한, 파일 크기, 마지막 변경시간, 디스크에서 파일 내용이 저장된 위치 등이 담겨있습니다. 파일시스템을 만들 때 최대 inode 개수를 정합니다. 그래서 디스크 공간이 다차면 새로 파일을 만들 수 없듯이 inode 최대 개수(즉, 파일 최대 개수)를 채우면 새로 파일을 만들 수 없습니다. 현재 inode 사용률은 df -i로 확인합니다. 또 재미있는 점은 파일명은 inode에 저장되지 않고, 파일을 포함한 디렉토리 파일에 저장됩니다. 그래서 /a/b/c/d 파일에 접근할 때 / 디렉토리에서 이름이 a인 파일의 inode를 찾고, 그 inode로 디렉토리 파일을 찾아서 이름이 b인 파일의 inode를 찾는 식으로 반복해서 찾아들어갑니다.

mount & filesystem

파일시스템(filesystem, fs)은 파일 내용과 inode 같은 정보를 관리하는 단위입니다. ext3, ext4, swap, ntfs 같은 종류가 있으며 proc, tmpfs 처럼 실제 디스크와 무관한 가상 파일시스템도 있습니다.

우리는 물리 디스크를 파티션으로 나누고, 단일 파티션 혹은 여러 파티션을 묶어서 (RAID) 공간을 만듭니다. 이 공간에 밭을 갈아 (mkfs) 우리가 원하는 파일시스템을 만들 수 있습니다.

$ sudo mount -t 타일시스템종류(예,ext4) 장치(예,/dev/sdd1) 마운트포인트(예,/mount/backup)
$ sudo mount 마운트포인트(예,/mount/backup) # /etc/fstab 파일에 정보가 들어있다면 이렇게 마운트포인트만으로 가능

mount 명령어는 한 파일시스템의 특정 디렉토리에 다른 파일시스템의 최상위 경로를 연결합니다. 마운트한 파일시스템 안에 또 다른 파일시스템을 마운트할 수 있습니다. 그 결과 유닉스는 한 뿌리에서 나온 나무처럼 모든 파일시스템이 루트 디렉토리부터(/) 연결됩니다. 아래 그림에서 (네트워크 파일시스템) /home, /mount/backup, /mount/usb, (가상 파일시스템) /proc이 마운트한 파일시스템입니다. 과거에는 네트워크 파일시스템으로 /home을 공유하여 어떤 컴퓨터에 로그인해도 동일한 홈 디렉토리를 사용할 수 있게 구성하곤 했습니다.

              /
              |
  +--------+--+------+--------+
  |        |         |        |
/home    /mount    /proc    /etc
           |
     +-----+----------+
     |                |
/mount/backup    /mount/usb

동일한 파일시스템 안에서 mv는 파일 경로와 파일명만 변경하므로 빠릅니다. 다른 파일시스템으로 mv하면, 먼저 파일 내용을 cp하고 원본 파일을 rm하는 행동을 합니다. 그래서 파일 크기가 크면, (윈도우즈 C: 드라이브에서 D: 드라이브로 파일을 이동하는 것처럼) 파일시스템간 mv가 시간이 많이 걸립니다.

symbolic link

심볼링크는 윈도우즈 바로가기와 유사하게 다른 파일이나 디렉토리를 지칭하는 파일입니다. ln -s 원본파일 별칭으로 만들며 ln (link) -s (symbolic)을 뜻합니다.

lrwxrwxrwx 1 root root           20 Jun 14  2022 cc -> /etc/alternatives/cc
lrwxrwxrwx 1 root root            5 Mar 20  2020 gcc -> gcc-9
lrwxrwxrwx 1 root root           22 Mar  9  2022 gcc-9 -> x86_64-linux-gnu-gcc-9
lrwxrwxrwx 1 root root            6 Feb 17  2020 latex -> pdftex
lrwxrwxrwx 1 root root            6 Feb 17  2020 pdflatex -> pdftex
lrwxrwxrwx 1 root root           54 Feb 18  2020 perltex -> ../share/texlive/texmf-dist/scripts/perltex/perltex.pl
lrwxrwxrwx 1 root root           22 May 23  2020 pico -> /etc/alternatives/pico
lrwxrwxrwx 1 root root            7 Apr  3  2020 ruby -> ruby2.7
lrwxrwxrwx 1 root root            2 Apr  8  2022 unxz -> xz

ls -l 목록에서 심볼링크는 l (link)로 시작합니다. 파일권한이 다 열려있지만 원본파일의 권한을 따르므로 괜찮습니다. 원본파일은 절대경로(/etc/...)와 상대경로(gcc-9) 모두 가능하고, 심지어 ../share/...같이 지정할 수도 있습니다. (재미있는 점은 파일 크기는 원본파일 이름 길이입니다.)

심볼링크는 ruby처럼 여러 버전 중 하나를 편하게 지칭하거나 unxz처럼 명령어에 다른 이름을 부여합니다. gcc -> gcc-9 -> x86_64-linux-gnu-gcc-9처럼 여러 단계를 거쳐서 최종 원본파일을 찾기도 합니다. 관습적으로 사용하는 /etc/alternatives/...도 여러 단계를 거칩니다. 이 경우 alternatives 명령어로 심볼링크를 관리(추가/변경/삭제)해도 됩니다.

rm 별칭하면 심볼링크 파일만 지워지고 원본파일은 그대로입니다.

사용자 입장에서 중요하지 않지만 하드링크(hard link)란게 있습니다. -s 옵션 없이 ln 원본파일 별칭하면 하드링크를 만듭니다. 하드링크로 연결한 파일끼리 inode를 공유합니다. 그래서 동일한 파일시스템 안에서만 만들 수 있고, (빠져나올 수 없는 순환 구조를 만들 수 있는 위험 때문에) 사용자는 디렉토리를 하드링크할 수 없습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment