/dev/fd
file descriptor file들이 있는 디렉토리
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
를 사용하는 것은 균일함과 깔끔함으로 나아가는 한 걸음이다.
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
일 것이다.파일 열기가 성공하면
cat
은read()
시스템 콜을 사용하여 파일에서 몇 바이트씩 반복적으로 읽는다.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