개요
fd |
설명 |
C 라이브러리 선행처리기 정의 |
0 |
표준 입력 |
STDIN_FILENO |
1 |
표준 출력 |
STDOUT_FILENO |
2 |
표준 에러 |
STDERR_FILENO |
사용자는 이러한 파일 디스크립터를 redirect하거나 파이프(pipe, '|')를 사용해서 프로그램의 출력을 다른 프로그램의 입력으로 redirect할 수도 있다.
파일 디스크립터는 일반 파일 뿐만 아니라, 장치 파일, 퓨텍스, 디렉터리, FIFO, 소켓 접근에도 사용된다. 유닉스 기반의 운영체제에서는 모든 것이 파일로 이루어져 있는 이상, 모든 것은 파일 디스크립터를 통해 접근할 수 있다.
이제 적당히 기본 설명을 마치고, 본론으로 들어가 직접 파일 열기 연습을 해 보자. 글쓴이가 공부하는 책이 C언어로 설명하고 있기 때문에, 글쓴이도 마찬가지로 C언어로 설명을 하려 한다.
파일 열기
open() 시스템 콜
1 2 3 4 5 6 7 | // open() 시스템 콜의 #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); | cs |
이름이 'const char*name'은 파일 디스크립터에 매핑할 파일 경로 이름이다. 매핑이 성공하면 이 파일 디스크립터를 반환한다. 파일 오프셋은 파일의 시작 지점(0)으로 설정되고, flags는 지정된 플래그에 대응하는 접근 모드로 열리게 하는 역할을 수행한다.
◆ open() 플래그
flags 인자에 대한 것을 잠깐 짚어보고 가자. flags는 O_RDONLY, O_WRONLY, O_RDWR 중 적어도 하나는 포함해야 한다.
flags 인자 |
설명 |
O_RDONLY |
읽기 모드 |
O_WRONLY |
쓰기 모드 |
O_RDWR |
읽기 + 쓰기 모드 |
예제 코드를 살펴보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main() { int fd = open("TEXT", O_RDONLY); if(fd == -1) printf("Failed to open file.\n"); else printf("Successfully opened file!\n"); return 0; } | cs |
7행에서 open() 시스템 콜을 사용한 것을 볼 수 있다. 같은 디렉터리 내에 TEXT라는 이름의 파일을 읽기 모드로 열려고 할 때 반환되는 파일 디스크립터를 변수 fd에 저장한다. 이때 TEXT 파일을 찾을 수 없거나 기타 다른 문제가 발생한다면 fd는 -1이 될 것이다. 9~10행의 if~else 문으로 에러 발생 여부를 확인하는 것을 볼 수 있다.
쓰기 전용 모드(O_WRONLY)로 열린 파일은 읽기 작업이 불가능하며, 반대로 읽기 전용 모드(O_RDONLY)로 열린 파일은 쓰기 작업을 할 수 없다. open() 시스템 콜을 호출한 프로세스는 호출 시 사용한 플래그에 알맞는 접근 권한을 확보해야 한다.
그렇다면 읽기, 쓰기만 할 수 있는 걸까? 그렇지 않다. 비트 OR 연산(|)을 통해 다음 값 중 원하는 것들을 추가해서 열기 동작을 더 자유롭게 즐길 수 있다. 아래 추가 플래그들에 대해 살펴보자.
▶ O_APPEND
덧붙이기 모드(Append Mode)로 파일을 연다. 쓰기 작업을 할 때에 항상 파일 오프셋이 파일 끝을 가리키도록 한다. 덧붙이기 모드로 파일을 열어서 쓰고 난 후, 다른 프로세스에서 이 파일을 또 써도 파일 오프셋은 항상 파일 끝을 가리킨다.
▶ O_ASYNC
특정 파일에서 읽기/쓰기가 가능해질 때(파일 디스크립터가 음수가 아닐 때) 시그널 2이 발생한다. O_ASYNC 플래그는 일반 파일에는 사용할 수 없고, 터미널과 소켓 파일에만 사용할 수 있다.
▶ O_CLOEXEC
열린 파일에 close-on-exec 플래그 3를 설정한다. 새 프로세스를 실행하면 자동으로 파일이 닫힌다. O_CLOEXEC 플래그를 설정하면 fcntl() 함수를 이용해서 따로 FD_CLOSEXEC 플래그를 설정할 필요가 없다. 리눅스 커널 2.6.23 버전 이상에서만 사용할 수 있다.
▶ O_CREAT
열려는 파일이 없으면 새로 만든다. 파일이 이미 있는 경우, O_EXCL플래그를 같이 붙이지 않는다면 아무 효과가 없다.
▶ O_DIRECT
직접 입출력 작업 수행을 위해 파일을 연다.
▶ O_DIRECTORY
name에 넣은 경로가 디렉터리가 아니면 open() 호출을 실패하게 한다. opendir() 라이브러리 호출을 내부적으로 사용하는 플래그.
▶ O_EXCL
O_CREAT와 함께 사용하면 name으로 지정한 파일이 이미 있을 때 open() 호출이 실패한다. 파일 생성 과정에서 중복, 즉 경쟁 상대를 피하기 위해 자주 사용된다.
▶ O_LARGEFILE
용량이 2GB를 초과하는 파일을 연다. 이를 위해 64bit offset을 사용하며, 64bit 아키텍처를 가진다.
▶ O_NOATIME+
O_CREAT와 함께 사용하면 name으로 지정한 파일이 이미 있을 때 open() 호출이 실패한다. 파일 생성 과정에서 중복, 즉 경쟁 상대를 피하기 위해 자주 사용된다.
▶ O_NOCTTY
name이 /dev/tty등과 같은 터미널 device를 가리키면, 프로세스에 현재 제어하고 있는 터미널이 없어도 프로세스의 제어 터미널이 되게 하지 않는다. 자주 사용하지는 않는다.
▶ O_NONBLOCK
파일을 가능한 nonblocking 모드로 연다. open() 시스템 콜이나 다른 연산은 입출력 과정에서 프로세스를 block하지 않는다. FIFO에서만 이러한 동작 방식이 정의된다.
▶ O_SYNC
파일을 동기식 입출력으로 연다. 데이터를 물리적으로 디스크에 쓰기 전까지는 쓰기 연산이 완료되지 않는다. 일반적으로 읽기 연산은 이미 동기식인데, 그렇기 때문에 이 플래그는 전혀 영향이 없다. POSIX에서는 O_DSYNC와 O_RSYNC라는 플래그 두 개를 더 정의하고 있다. 하지만 리눅스에서 O_DSYNC와 O_RSYNC는 O_SYNC와 동일한 기능을 하기 때문에, 이 둘을 딱히 정의할 필요가 없다.
▶ O_TRUNC
파일이 존재하고, 열려고 하는 파일이 일반 파일이고, 또 flags 인자에 쓰기가 가능하도록 되어 있으면(O_WRONLY 또는 O_RDWR이면) 파일 길이를 0으로 잘라버린다. FIFO, 터미널 디바이스에서는 무시되는 플래그이다. 다른 파일 유형에 대한 O_TRUNC의 동작은 정의되지 않았다. 파일을 자르려면 쓰기 권한이 있어야 하므로, O_RDONLY와 함께 쓰이는 경우의 동작도 딱히 정의되지 않았다.
이런 플래그들을 응용하여 다양한 결과를 볼 수 있다. 예시를 들기 위해 조금 전 위에서 제시했던 소스코드의 7행을 다음과 같이 조금 변형해 보았다.
1 | int fd = open("TEXT", O_WRONLY | O_TRUNC); | cs |
우선 같은 디렉터리 내에 있는 'TEXT'라는 이름의 파일을 여는 것은 동일하다. 하지만 이번에는 파일이 이미 존재하면 길이를 0으로 잘라버리는 O_TRUNC가 함께 새용되었다. 또한 O_CREAT 플래그를 붙이지 않았기 때문에 파일이 없다면 open() 호출은 실패하게 된다.
새로운 파일의 소유자
새로운 파일의 권한
앞에서 설명한 open() 시스템 콜에서는 mode 인자가 붙은 형식, 그리고 붙지 않은 형식 모두 유효하다. 파일을 생성하지 않으면 mode 인자는 무시되어버린다. 반대로 mode 인자 없이 O_CREAT로 파일을 생성한다면, 파일 권한이 정의되지 않아서 만들어놓은 파일을 가지고 이러지도, 저러지도 못하는 상황이 벌어질 수도 있다. 그렇기 때문에 O_CREAT를 붙일 때에는 반드시 mode를 확인해야 한다.
리눅스 시스템에 대한 기본 지식을 가지고 있다면 mode 인자에 대해서는 충분히 알고 있으리라 생각한다. mode는 유닉스 접근 권한 비트 집합으로, 4자리 8진수로 이루어져 있다. POSIX에서는 비트 위치에 따른 비호환성을 보완하기 위해 이진 OR 연산이 가능한 여러 상수 집합들을 제공하고 있다. 아래 표에 나온 상수들을 사용하여 mode 인자에 사용할 수 있다.
상수 |
권한 |
S_IRWXU |
u+rwx |
S_IRUSR |
u+r |
S_IWUSR |
u+w |
S_IXUSR |
u+x |
S_IRWXG |
g+rwx |
S_IRGRP |
g+r |
S_IWGRP |
g+w |
S_IXGRP |
g+x |
S_IRWXO | o+rwx |
S_IROTH | o+r |
S_IWOTH | o+w |
S_IXOTH |
o+x |
디스크에 기록될 실제 접근 권한 비트는 umask의 보수와 mode 인자를 이진 AND 연산한 값으로 결정된다. 다시 말해 uamsk 비트는 open()에 넘긴 mode 인자에 들어 있는 비트를 꺼버린다는 뜻이다.
앞에서 작성한 소스코드를 다시 한 번 조금 수정해 보았다. (파일 디스크립터를 정의하는 부분만)
1 | int fd = open("TEXT", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); // 0644 | cs |
뭔가 더 길어졌지만 사실 뜯어보면 해석하기 쉬운 코드이다.
방금 수정한 코드는 쓰기 모드(O_WRONLY)로 'TEXT' 파일을 연다. 파일이 존재하지 않고, umask 값이 022라면 접근 권한이 0644인 파일이 만들어진다. 또한 'TEXT' 파일이 이미 존재한다면 길이를 0으로 잘라버린다.(O_TRUNC)
호환성은 맞지 않지만, 이 코드도 방금 설명한 코드와 동일하게 동작한다.
1 | int fd = open("TEXT", O_WRONLY | O_CREAT | O_TRUNC, 0644); | cs |
creat() 시스템 콜 7
1 2 3 4 5 | #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int creat(const char *name, mode_t mode); | cs |
그러면 open()과 creat()가 다른 게 뭐지?
간편한 설명을 위해 두 가지 예제 코드를 사용하기로 했다. 두 예제 모두 같은 기능을 수행하지만, 하나는 open()을 사용했고 하나는 creat()을 사용했다. 먼저 open()을 사용한 예제이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main() { int fd = open("TEXT", O_WRONLY | O_CREAT | O_TRUNC, 0644); if(fd == -1) printf("Failed to open file.\n"); else printf("Successfully opened file!\n"); return 0; } | cs |
두 번째는 creat()을 사용한 코드이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main() { int fd = creat("TEXT", 0644); if(fd == -1) printf("Failed to open file.\n"); else printf("Successfully opened file!\n"); return 0; } | cs |
정말 간단하지 않은가? 하지만 creat보다는 open이 더 많이 쓰인다. creat은 간결하지만, 기능이 적기 때문이다.
파일을 여는 시스템 콜이 2개인 이유는 open()에 인자가 두 개만 있던 시절의 부산물이 creat()이기 때문이다. 지금의 creat()는 하위 호환성만을 위해 남아 있는 것이다. 최신 아키텍처에서는 glibc에서 앞서 보여준 방식처럼 creat()를 구현하고 있다.
반환값과 에러코드
open(), creat()는 파일 열기 작업이 성공적으로 이루어지면 파일 디스크립터를 반환한다. 에러가 발생하면 -1을 반환하고 errno를 적절한 에러 값으로 설정하는 것은 공통적이다. 파일 열기에서 에러 처리는 사실 그리 복잡하지는 않다. 필요한 선행 작업이 거의 없다시피 하기 때문에 기존에 수행한 작업을 취소하거나 하지 않아도 되기 때문이다. 에러 처리 과정에서 다른 파일 이름을 사용자에게 요청하거나, 그냥 프로그램을 끝내는 등 단순한 조치를 취한다.
'System > System Programming' 카테고리의 다른 글
6. 파일 닫기 - close() (0) | 2019.04.24 |
---|---|
5. 직접 입출력 (0) | 2019.04.24 |
4. 동기식 입출력 - fsync(), fdatasync() (2) | 2019.04.22 |
3. 파일 쓰기 - write() (0) | 2019.04.16 |
2. 파일 읽기 - read() (0) | 2019.04.15 |