0, 1, 2

  • /dev/fd/0 : stdin
  • /dev/fd/1 : stdout
  • /dev/fd/2 : stderr

/dev/fd/0은 표준 입력이므로, 다음과 같이 cat을 실행해 놓고, 키보드로 입력을 해보면 입력한 내용이 그대로 출력되는 것을 볼 수 있다.

cat /dev/fd/0

/dev/fd/1은 표준 출력이므로, 다음과 같이 출력을 관찰할 수 있다.

echo "hello" > /dev/fd/1

한편, /dev/stdin, /dev/stdout, /dev/stderr는 각각 /dev/fd/0, /dev/fd/1, /dev/fd/2에 대한 심볼릭 링크이다.

$ ls -al /dev/std*
lr-xr-xr-x 1 root wheel 0 2023-08-17 Thu 22:59:57 /dev/stderr -> fd/2
lr-xr-xr-x 1 root wheel 0 2023-08-17 Thu 22:59:57 /dev/stdin -> fd/0
lr-xr-xr-x 1 root wheel 0 2023-08-17 Thu 22:59:57 /dev/stdout -> fd/1

man

/dev/fd는 special file에 해당하므로 [[/cmd/man]]로는 4를 지정해야 볼 수 있다.

man 4 fd

From: UNIX 고급 프로그래밍

최근 시스템들은 /dev/fd라는 이름의 디렉터리를 제공한다. 이 디렉터리에는 이름이 0, 1, 2 등인 파일들이 있다. 파일 서술자 n이 이미 열려 있다고 가정할 때, /dev/fd/n 이라는 파일을 여는 것은 서술자 n을 복제하는 것과 동등한 일이다.

/dev/fd 기능은 [[/duff-s-device]]{톰 더프Tom Duff}가 개발했으며 Research UNIX System 제8판에 등장했다. 이 책에서 설명하는 모든 시스템(FreeBSD 8.0, Linux 3.2.0, Max OS X 10.6.8, Solaris 10)이 이 기능을 지원한다. 이 기능이 POSIX.1의 일부는 아니다.

(중략)

/dev/stdin이나 /dev/stdout, /dev/stderr 같은 경로이름들을 제공하는 시스템들도 있다. 이들은 각각 /dev/fd/0, /dev/fd/1, /dev/fd/2와 동등하다.

/dev/fd 파일들은 주로 셸에 쓰인다. 이들을 이용하면 경로이름 인수들을 사용하는 프로그램들이 표준 입력과 표준 출력을 다른 경로이름들과 동일한 방식으로 취급할 수 있다. 예를 들어 cat(1) 프로그램은 일부러 - 이라는 입력 파일이름을 찾아보고, 만일 그런 이름이 있으면 그것을 표준 입력으로 간주한다. 다음이 그러한 예이다.

filter file2 | cat file1 - file3 | lpr

이 경우 cat은 먼저 file1을 읽고, 그 다음 표준 입력(file2에 대한 filter 프로그램의 출력)을 읽고, 그런 다음 file3을 읽는다. /dev/fd가 지원되는 경우에는 cat에서 -에 대한 특별한 처리를 제거할 수 있다. 대신 다음과 같은 명령을 사용하면 된다.

filter file2 | cat filel /dev/fd/O files | lpr

명령줄 인수에서 -가 표준 입력이나 표준 출력을 뜻하도록 특별하게 취급하는 방식을 따르는 프로그램들이 많이 생겼는데, 사실 그러한 처리는 바람직하지 않은 군더더기이다. 또한 -를 첫 번째 파일로 지정하는 경우에는 그것이 다른 명령줄 옵션의 시작으로 보인다는 문제도 있다. /dev/fd를 사용하는 것은 균일함과 깔끔함으로 나아가는 한 걸음이다.

1

cat 명령을 통해 살펴보는 파일 디스크립터 0, 1, 2, 3

다음의 예를 통해 strace를 사용하여 cat이 어떤 동작을 하는지 알아보자(가독성을 위해서 몇몇 호출들은 삭제함).

prompt> strace cat foo
...
open("foo", O_RDONLY|O_LARGEFILE) = 3
read(3, "hello\n", 4096) = 6
write(1, "hello\n", 6) = 6
hello
read(3, "", 4096) = 0
close(3) = 0
...
prompt>

cat이 가장 먼저 하는 것은 파일을 읽기 위해서 여는 것이다. 몇 가지 짚고 넘어갈 사항이 있다. 파일은 O_RDONLY라는 플래그가 나타내는 것처럼 읽기만 가능한 상태로 열렸다(쓰기는 안됨). 두 번째는 O_LARGEFILE 플래그를 사용하여 64 bit 오프셋이 사용되도록 설정하였다. 세 번째는 open()이 성공한 후에 3이라는 값을 파일 디스크립터로 리턴하였다.

어째서 첫 번째 open() 임에도 불구하고 예상과 달리 0 또는 1이 아닌 3을 리턴하였을까? 프로세스가 이미 세 개의 파일을 열어 놓았기 때문이다. 이미 열려진 세 개의 파일은 표준 입력과 표준 출력, 그리고 오류 메시지를 기록할 수 있는 표준 에러이다. 각각의 파일 디스크립터는 0, 1 그리고 2로 표현된다. 다른 파일을 처음으로 열게 되면 (cat이 하는 듯이), 거의 확실하게 파일 디스크립터는 3일 것이다.

파일 열기가 성공하면 catread()시스템 콜을 사용하여 파일에서 몇 바이트씩 반복적으로 읽는다. read()의 첫 번째 인자는 파일 디스크립터이다. 파일 디스크립터는 읽고자 하는 파일을 파일 시스템에 알려준다. 프로세스는 동시에 여러 파일을 열 수 있다. 디스크립터는 운영체제가 read 명령이 읽어야 할 파일을 명시한다. 두 번째 인자는 read() 결과를 저장할 버퍼를 가리킨다. 위의 시스템 콜 추적 예제에서 strace는 읽은 결과인 "hello"를 두 번째 인자 위치에 표시하였다. 세 번째 인자는 버퍼의 크기로서 여기서는 4 KB이다. read()가 성공적으로 리턴하며 읽은 바이트 수를 반환한다("hello" 의 5개의 문자와 줄의 끝을 표시하는 문자 하나가 있기 때문에 6을 반환함).

이 시점에서 strace의 결과에 또 다른 흥미로운 점이 있다. write()시스템 콜이 결과를 쓰는 대상 파일로 파일 디스크립터 1번을 사용하는 것이다. 앞서 설명했듯이 이 디스크립터는 표준 출력 (STDOUT)으로서 "hello”라는 단어를 화면에 나타내기 위해 사용되고, 이는 cat이 하기로 되어 있는 작업이다. cat 프로그램이 write()를 직접 호출하는 것일까? (만약 상당히 최적화가 되었다면) 그럴지도 모른다. 그렇지 않다면 cat은 라이브러리 루틴인 printf()를 호출했을 것이다. 내부적으로 printf()는 전달 받은 문자열에 적절한 포멧을 적용한 후, 결과를 표준 출력에 써서 화면에 출력한다.

출력한 이후 cat 프로그램은 파일의 내용을 더 읽으려고 시도하고, 파일에 남은 바이트가 없기 때문에 read()0을 리턴한다. 프로그램은 리턴 값으로 파일을 끝까지 다 읽었음을 알게 된다. 그런 후 프로그램은 해당 파일 디스크립터를 인자로 close()를 호출하여 “foo”라는 파일에서 할 일이 끝났음을 표시한다. 이제 파일은 닫혔으며 읽기 작업은 완료된다. 2

참고문헌

  • UNIX 고급 프로그래밍 [제3판] / 리처드 스티븐스, 스티븐 레이고 공저 / 류광 역 / 퍼스트북 / 인쇄일: 2014년 08월 28일 / 원제: Advanced Programming in the UNIX Environment
  • 운영체제 아주 쉬운 세 가지 이야기 [제2판] / Remzi H. Arpaci-Dusseau, Andrea C. Arpaci-dusseau 공저 / 원유집, 박민규, 이성진 공역 / 홍릉 / 제2판 발행: 2020년 09월 10일 / 원제: Operating Systems: Three Easy Pieces

주석

  1. UNIX 고급 프로그래밍. 3.16장. 110쪽. 

  2. 운영체제 아주 쉬운 세 가지 이야기. 39장. 514쪽.