File Descriptors

프로그램이 실행 중에 사용하는 파일이나 pipe, socket, 외부 장치 같은 리소스들은 처음 사용될때 OS 에의해 리소스 번호가 할당됩니다. 이 번호는 양의 정수로 프로세스마다 가지고 있는 PCB 의 file descriptor table 에 등록되고 이후부터는 이 FD 번호를 이용해 리소스를 사용하게 됩니다. shell 에서는 FD 를 파일에 연결하면 파일 포지션을 이용할 수 있고 pipe 나 socket 의 경우에는 읽어들일 데이터가 없어도 연결을 유지할 수 있으며 버퍼를 사용할 수 있습니다.

parent process 에 설정되어있는 FD 들은 child process 에게 상속됩니다.

File descriptor 의 생성, 복사, 삭제

터미널을 열면 자동으로 생성되는 FD 0, 1, 2 외에 필요하면 다른 번호의 FD 를 생성, 복사, 삭제 할수 있습니다. 이와같은 작업은 exec builtin 명령으로 하며 사용 가능한 번호의 범위는 ulimit -n 과 같습니다. ( sh 에서는 9번 까지만 사용할 수 있습니다. )

FD 를 설정할때 사용하는 메타문자

  • 입력      : <
  • 출력      : >   (이 기호를 파일로 연결하면 먼저 기존 파일 내용이 삭제됩니다)
  • append : >> (기존 파일 내용이 삭제되지 않고 이후부터 append 됩니다)
  • 입, 출력 : <> (이 기호는 < + > 와 같지만 기존 파일 내용이 삭제되지는 않고 출력시 첫라인 부터 overwrite 하게 됩니다.)

이 메타문자의 방향성은 실제 리소스에 연결할 때만 의미를 갖습니다. 이후에 FD 끼리 서로 복사하거나 삭제할 때는 무시할 수 있습니다 ( 방향에 상관없이 원본 FD 의 설정이 그대로 복사되기 때문에 ). FD 설정을 할때 FD 와 리소스를 위치시키는 방법은 다음과 같습니다.

새로 생성, 복사, 삭제되는 FD 는 왼쪽에, 실제 리소스, 원본 FD, - 는 오른쪽에

그러므로 FD 3 번을 생성하면서 infile 에 입력으로 연결할 경우

# 왼쪽 : 새로 생성되는 FD, 오른쪽 : 리소스
# 실제 리소스에 연결하므로 방향을 올바르게 설정해야 합니다.

$ exec 3< infile

FD 4 번을 생성하고 FD 0 번에 연결하기

# 왼쪽 : 복사되는 FD, 오른쪽 : 원본 FD
# 실제 리소스와의 설정이 아니므로 방향성을 무시할수 있습니다.

$ exec 4<&0  또는  exec 4>&0

FD 3 번을 삭제하기

# 왼쪽 : 삭제되는 FD, 오른쪽 : '-'
# 실제 리소스와의 설정이 아니므로 방향성을 무시할수 있습니다.

$ exec 3<&-  또는  exec 3>&-

FD 3 번을 만들고 outfile 파일을 출력으로 연결

출력이므로 이때 outfile 파일에 내용이 있다면 삭제됩니다. 삭제를 방지하고 append 되게 하려면 >> 을 사용합니다. > 는 데이터를 파일의 처음부터 쓰기 시작하며, >> 는 기존의 내용에 뒤이어서 쓰게 됩니다.

$ exec 3> outfile

# write 으로 연결 하였으므로 퍼미션은 `l-wx------` 가 됩니다.
$ ls -l /proc/$$/fd/3
l-wx------ 1 mug896 mug896 64 04.07.2015 10:56 /proc/9363/fd/3 -> /home/mug896/outfile

$ echo 111 >&3
$ cat outfile 
111

$ echo 111 >&3
$ cat outfile        # 파일 포지션이 이동하여 다음 라인에 추가됩니다.
111
111

$ echo 111 >&3
$ cat outfile
111
111
111

FD 가 아니고 파일로 출력할 경우는 파일 포지션이 사용되지 않으므로 첫 라인에만 쓰여집니다.

$ echo 111 > outfile
$ cat outfile
111

$ echo 111 > outfile
$ cat outfile               # 라인이 추가되지 않고 첫 라인에만 쓰여진다.
111

$ echo 111 >> outfile       # '>>' 를 사용하면 append 됩니다.
$ cat outfile
111
111
--------------------------------------

$ exec 3> outfile

$ echo 111 > /dev/fd/3
$ cat outfile
111

$ echo 111 > /dev/fd/3
$ cat outfile
111

$ echo 111 >> /dev/fd/3      # '>>' 사용
$ cat outfile
111
111

FD 3 번을 사용후 삭제하기

$ exec 3>&-

$ ls -l /proc/$$/fd/3
ls: cannot access /proc/9363/fd/3: No such file or directory

FD 4 번을 만들고 infile 파일을 입력으로 연결

$ exec 4< infile

# read 로 연결 하였으므로 퍼미션은 `lr-x------` 가 됩니다.
$ ls -l /proc/$$/fd/4
lr-x------ 1 mug896 mug896 64 04.07.2015 10:56 /proc/9363/fd/4 -> /home/mug896/infile

파일에서 라인을 읽어 들일때 FD 를 사용하는것과 단순히 < 를 사용하는것은 차이가 있습니다.

$ cat infile
111
222
333

$ read var < infile; echo $var  
111
$ read var < infile; echo $var    # 읽을때 마다 새로 실행되는 명령과 같아 동일한 값이 출력된다  
111

$ exec 4< infile                  # FD 4 번을 infile 파일에 입력으로 연결.

$ read var <&4; echo $var         
111
$ read var <&4; echo $var         # FD 이므로 한번 읽으면 안의 파일 포지션이 이동한다.
222
$ read var <&4; echo $var
333
-------------------------------------

$ exec 4<<END
> 111
> 222
> 333
> END

$ read var < /dev/fd/4; echo $var
111
$ read var < /dev/fd/4; echo $var
111

$ read var <&4; echo $var 
111
$ read var <&4; echo $var   
222
$ read var <&4; echo $var
333

FD 4 번을 사용후 삭제하기

$ exec 4<&-  또는  exec 4>&-

FD 5 번을 만들고 outfile 파일을 입, 출력 으로 연결

$ cat iofile
111

$ exec 5<> iofile

# read, write 로 연결 하였으므로 퍼미션은 `lrwx------` 가 됩니다.
$ ls -l /proc/$$/fd/5
lrwx------ 1 mug896 mug896 64 04.07.2015 10:56 /proc/9363/fd/5 -> /home/mug896/iofile

$ cat <&5
111

$ echo 222 >&5

$ cat iofile
111
222

$ exec 5>&-

<> 로 파일을 연결하게 되면 기존의 데이터는 삭제되지 않습니다. 하지만 쓰기작업은 기존의 데이터를 overwrite 하면서 첫라인 부터 쓰게됩니다. 또한 읽기와 쓰기시에 파일 포지션 값을 공유하므로 유의해야 합니다.

$ cat iofile
111
222
333
444
555

$ exec 3<> iofile          # 기존의 데이터는 삭제되지 않는다.

$ echo XXX >&3             # 파일의 첫 라인부터 overwrite 된다.
$ cat iofile     
XXX
222
333
444
555

$ read var <&3; echo $var  # 쓰고난후 파일 포지션이 이동하여 222 값이 출력된다.
222
$ read var <&3; echo $var   
333

$ echo YYY >&3    # 읽고난후 파일 포지션이 이동하여 444 자리에 overwrite 된다.
$ cat iofile
XXX
222
333
YYY
555

<> 는 읽거나 쓰게되면 계속해서 파일 포지션이 아래로 이동합니다. 그러므로 윗부분의 데이터를 다시 읽으려면 exec 으로 다시 FD 를 연결해야 합니다. (이때 먼저 FD 를 삭제할 필요는 없습니다.) 하나의 파일에 두개의 FD 를 연결하면 각각 독립적으로 파일 포지션을 사용할 수 있습니다.

pipe 나 socket 의 연결을 유지할 수 있다.

Shell 에서 FD 를 pipe 나 socket 에 연결하면 읽어들일 데이터가 없어도 연결을 유지할 수 있습니다.

바로 socket 에 메시지를 보내면 연결이 유지되지 않는다.

$ echo hello > /dev/tcp/www.google.com/80

$ ls -l /proc/$$/fd
total 0
lrwx------ 1 mug896 mug896 64 08.19.2015 18:37 0 -> /dev/pts/18
lrwx------ 1 mug896 mug896 64 08.19.2015 18:37 1 -> /dev/pts/18
lrwx------ 1 mug896 mug896 64 08.19.2015 18:37 2 -> /dev/pts/18

FD 를 사용하면 socket 의 연결이 유지된다.

$ exec 3<> /dev/tcp/www.google.com/80

$ ls -l /proc/$$/fd
total 0
lrwx------ 1 mug896 mug896 64 08.19.2015 18:37 0 -> /dev/pts/18
lrwx------ 1 mug896 mug896 64 08.19.2015 18:37 1 -> /dev/pts/18
lrwx------ 1 mug896 mug896 64 08.19.2015 18:37 2 -> /dev/pts/18
lrwx------ 1 mug896 mug896 64 08.19.2015 18:37 3 -> socket:[5641570]

$ echo hello >&3

$ cat <&3
HTTP/1.0 400 Bad Request
Content-Type: text/html; charset=UTF-8
Content-Length: 1419
Date: Wed, 19 Aug 2015 09:36:39 GMT
Server: GFE/2.0
...

버퍼를 사용할 수 있다.

$ mkfifo pipe

$ echo hello > pipe     # reader 가 없으므로 block 된다.
^C                      # Ctrl-c 종료

$ exec 3<> pipe         # FD 를 연결하면 버퍼가 사용되므로 block 되지 않는다.
$ echo hello > pipe     # ( 버퍼가 차기 전까지 )
$

Named file descriptor

FD 번호를 생성할 때 현재 어떤 번호가 사용되고 있는지 체크할 필요 없이 자동으로 FD 번호를 생성해서 변수에 할당해 주는 기능입니다. 생성할 때와 삭제할 때 변수 이름에 { } 를 사용해야 합니다.

sh 에서는 사용할 수 없습니다.

$ exec {myFD}> outfile

$ echo $myFD
10

$ ls -l /proc/$$/fd/$myFD
l-wx------ 1 mug896 mug896 64 04.07.2015 10:56 /proc/9363/fd/10 -> /home/mug896/outfile

# 삭제할 때는 다음과 같이 합니다. ($myFD 를 사용하면 안 됩니다.)
$ exec {myFD}>&-

$ ls -l /proc/$$/fd/$myFD
ls: cannot access /proc/9363/fd/10: No such file or directory

$ ( echo 111 >& $fd1 ) {fd1}> >( cat  )
111

exec 로 FD 를 생성할 때 변수를 사용하려면

command ... >& $var 와 같이 redirection 기호의 오른쪽에 위치한 값은 변수를 사용하는데 문제가 없지만 기호 왼쪽에 공백 없이 붙여 사용하는 FD 값에는 변수를 사용할 수 없습니다. 따라서 exec 으로 FD 를 생성할 때 변수를 사용하려면 eval 명령을 사용하고 quote 을해야 합니다.

# 다음과 같이 하면 some/file 이 생성됨과 동시에 먼저 stdout 이 some/file 로 연결되고
# 이후 변수값 3 을 exec 이 실행할 명령으로 인식합니다.
$ AA=3
$ exec $AA> some/file
bash: exec: 3: not found

# 따라서 다음과 같이 eval 명령과 quotes 을 함께 사용해야 합니다.
$ eval exec "$AA>" some/file
OK

$ exec {AA}>&-    
# sh 에서는 
$ eval exec "$AA>&-"

FD 는 child process 에게 상속된다.

shell script 가 실행되어 child process 가 생성될 때 parent 에 설정되어있는 FD 를 물려받습니다. 그러므로 parent 와 동일하게 터미널로부터 입력을 받고, 출력을 할 수 있습니다.

$ exec 3> outfile

$ ls -l /proc/$$/fd
total 0
lrwx------ 1 mug896 mug896 64 Jul  5 09:45 0 -> /dev/pts/14   
lrwx------ 1 mug896 mug896 64 Jul  5 09:45 1 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 Jul  5 09:45 2 -> /dev/pts/14
l-wx------ 1 mug896 mug896 64 Jul  5 09:45 3 -> /home/mug896/outfile

$ bash -c 'ls -l /proc/$$/fd'   # child process 생성
total 0
lrwx------ 1 mug896 mug896 64 Jul  5 09:45 0 -> /dev/pts/14   
lrwx------ 1 mug896 mug896 64 Jul  5 09:45 1 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 Jul  5 09:45 2 -> /dev/pts/14
l-wx------ 1 mug896 mug896 64 Jul  5 09:45 3 -> /home/mug896/outfile
------------------------------------------------------------------------

$ cat test.sh
#!/bin/bash

echo hello >&3
........................

$ ./test.sh 
./test.sh: line 3: 3: Bad file descriptor

$ 3>&1 ./test.sh
hello

File Truncation

로그파일에 로그가 많이 쌓여서 디스크 공간을 확보하기 위해 파일을 rm -f 했는데 디스크 공간이 반환되지 않는 경우를 경험해 보셨을 겁니다. 프로그램이 실행 중에 파일을 open 하면 이후부터는 파일명이 아니라 inode 번호를 통해 관리가 됩니다. 따라서 파일로부터 입,출력을 하는 도중에 파일을 rename 하거나 다른 디렉토리로( 같은 파티션 내의 ) 옮겨도 정상적으로 입,출력이 되고 또한 삭제를 하더라도 디렉토리에만 보이지 않을 뿐이지 디스크는 반환되지 않고 지속적으로 쓰기를 하게 됩니다.

삭제된 파일이 차지하고 있던 디스크 공간이 반환되기 위해서는 프로그램 내에서 직접 파일을 close 하거나 ( 쉘의 경우 exec 3>&- ) 프로그램이 종료돼야 합니다. 하지만 쉘에서 간단히 파일 사이즈를 0 으로 만들고 디스크 공간을 반환받을 수 있는 방법이 있습니다.

# 파일 사이즈가 0 으로 truncate 되고 디스크가 반환된다.

$ > filename