유닉스 시스템에서는 거의 모든 것을 파일로 표현하므로 파일 입출력은 정말 중요한 부분입니다.
파일은 읽거나 쓰기 전에 반드시 열어야 합니다. 커널은 파일 테이블이라고 하는 프로세스 별로 열린 파일 목록을 관리합니다. 이 테이블은 음이 아닌 정수 값인, 파일 디스크립터로 인덱싱되어 있습니다. 이 테이블의 각 항목은 열린 파일에 대한 정보를 담고 있으며 여기에는 메모리에 복사된 inode를 가리키는 포인터와 각종 메타데이터가 포함되어 있습니다. 파일 디스크립터는 사용자 영역과 커널 영역 모두에서 프로세스 내의 고유한 식별자로 사용됩니다. 파일을 열면 파일 디스크립터가 반환되고 이 파일 디스크립터를 관련 시스템 콜의 첫 번째 인자로 넘겨 다양한 연산을 수행합니다.
프로세스에서 명시적으로 닫지 않는 이상 모든 프로세스는 최소한 0, 1, 2라는 세 가지 파일 디스크립터를 열어 두고 있습니다. 파일 디스크립터 0번은 표준 입력(stdin), 1번은 표준 출력(stdout), 파일 디스크립터 2번은 표준 에러(stderr)입니다.
파일 디스크립터는 단순히 일반 파일만 나타내는 것이 아닙니다. 파일 디스크립터는 장치 파일, 파이프, 디렉터리, 퓨텍스 ,FIFO, 소켓 접근에도 사용되며 모든 것이 파일이라는 유닉스 철학에 따라 읽고 쓸 수 있는 모든 것은 파일 디스크립터를 통해 접근할 수 있습니다.
기본적으로 자식 프로세스는 부모 프로세스가 소유한 파일 테이블의 복사본을 상속받습니다. 열린 파일, 접근 모드, 현재 파일의 오프셋 등을 담고 있는 목록은 동일하지만, 예를 들어 자식 프로세스가 파일을 닫는 특정 프로세스에서 일어난 변화는 다른 프로세스의 파일 테이블에 영향을 미치지 않습니다.
파일에 접근하는 가장 기본적인 방법은 read()와 write() 시스템 콜입니다. 하지만 파일에 접근하려면 open()이나 creat() 시스템 콜을 이용해서 파일을 열어야 합니다. 그리고 다 쓴 뒤에는 close() 시스템 콜로 파일을 닫아야 합니다.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *name, int flags);
int open(const char *name, int flags, mode_t mode);
open() 시스템 콜은 경로 이름이 name인 파일을 파일 디스크립터에 맵핑하고, 성공하면 파일 디스크립터를 반환한다. 파일 오프셋은 파일의 시작 지점인 0으로 설정되며 파일은 flags로 지정한 플래그에 대응하는 접근 모드로 열리게 됩니다.
flags 인자는 O_RDONLY, O_WRONLY, O_RDWR 중 하나를 포함해야 합니다. 읽기, 쓰기, 읽기와 쓰기 모드를 나타냅니다. 각 모드에 맞는 접근을 하지 않으면 그 접근은 실패합니다. flags 매개 변수에 비트 OR 연산으로 다음 값 중 하나 이상을 추가해서 열기 동작을 변경할 수 있습니다.
새로 생긴 파일의 소유자는 파일을 생성한 프로세스의 euid(유효 uid)입니다.
소유 그룹은 파일을 생성한 프로세스의 egid(유호 gid)로 설정합니다. 이는 시스템 V 동작 방식이며 리눅스가 따른 표준 작업 방식입니다.
open() 시스템 콜에서는 mode 인자가 붙은 형식과 붙지 않은 형식 둘 다 유효합니다. mode 인자는 파일의 생성과 관련이 있습니다. 파일을 생성하지 않는다면 mode 인자는 무시됩니다. 하지만 mode 인자 없이 O_CREAT로 파일을 생성하면 파일의 권한이 정의되지 않아 종종 골치 아픈 일을 겪습니다.
파일이 생성되면 새로 만들어진 파일의 접근 권한은 mode 인자에 따라 설정됩니다. mode 인자가 없으면 파일을 생성할 때 mode를 점검하지 않으므로 파일을 쓰기 상태로 열었지만, 파일의 접근 권한이 읽기 전용인 경우처럼 모순되는 결과를 초래할 수도 있습니다.
mode 인자는 시스템 관리자에게는 낯익은 유닉스 접근 권한 비트 집합이며 8진수 0644와 같이 표현합니다. 모든 유닉스 시스템에서 원하는 방식대로 접근 권한 비트 패턴을 설계하도록 허용합니다. 하지만 모든 유닉스 시스템은 접근 권한 비트를 동일한 방식으로 구현하고 있습니다. 따라서 기술적으로는 이식성이 없는 방식이지만 mode 값으로 0644나 0700처럼 직접 적용해도 모든 시스템에서 동일하게 동작합니다.
디스크에 기록될 실제 접근 권한 비트는(0666) 사용자 파일 생성 마스크(umask)의 보수와 mode 인자를 이진 AND로 계산한 값으로 결정합니다. 알기 쉽게 설명하자면 umask에 들어 있는 비트는 open()에 넘긴 mode 인자에 들어 있는 비트를 꺼버립니다. 따라서 보통 022로 설정한 umask 값은 0666으로 설정한 mode 인자를 0644로 만듭니다.(0644 & ~022). 시스템 프로그래머 입장에서는 일반적으로 접근 권한을 설정할 때 umask를 고려하지 않는다. umask는 프로그램이 새로운 파일에 설정하는 접근 권한을 사용자가 제한하는 메커니즘이기 때문입니다.
예를 들어 다음 코드는 쓰기 모드로 file을 엽니다. 파일이 존재하지 않고 umask 값이 022라면 mode 인자를 0644로 지정했음에도 불구하고 접근 권한이 0644인 파일이 만들어집니다. 그리고 파일이 존재하면 길이를 0으로 잘라버립니다.
int fd;
fd = open (file, O_WRONLY | O_CREAT | O_TRUNC, 066);
if (fd == -1)
/* 에러 */
O_WRONLY | O_CREAT | O_TRUNC 조합은 너무나도 일반적이라 아예 이런 동작 방식을 지원하는 시스템 콜이 아래처럼 존재합니다.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat (const char *name, mode_t mode);
open()과 creat()는 성공하면 파일 디스크립터를 반환합니다. 에러가 발생하면 둘 다 -1을 반환하고 errno를 적절한 에러 값으로 설정합니다. 파일을 여는 과정에서의 에러 처리는 복잡하지 않습니다. 일반적으로 파일을 열기에 앞서 수행하는 단계가 거의 없으므로 기존에 수행한 작업을 취소할 필요가 없기 때문입니다. 에러 처리 과정에서 다른 파일 이름을 사용자에게 요청하거나 단순히 프로그램을 끝내는 전형적인 대응 방법을 사용합니다.