1. 파일 열기 - open(), creat()
System/System Programming

1. 파일 열기 - open(), creat()


개요


  유닉스 기반 시스템에서는 거의 모든 것을 파일로 표현하다. 그러므로 파일을 읽고, 쓰고, 삭제하고, 생성하고... 하는 모든 파일 다루기는 아주 중요하다. [Linux System Programming] 시리즈의 포스팅의 첫 번째는 파일 입출력에 대해서 써보려 한다.

  파일은 읽거나 쓰기 전이 반드시 열어야 한다. 이 열린 파일들은 파일 테이블이라는 목록이 있는데, 이곳에 프로세스별로 열린 파일들을 기록한다. 파일 테이블은 커널에 의해 관리된다.
file table에 대한 이미지 검색결과

  파일 테이블은 파일 디스크립터(File Descripter, fd)라는 정수 값으로 인덱싱된다. 파일 테이블의 각 항목은 열린 파일에 대한 정보를 담고 있으며, 여기에는 메모리에 복사된 inode를 가리키는 포인터, 파일 위치와 접근 모드 등과 같은 각종 메타데이터가 포함되어 있다. 위의 그림을 참고하자.

  파일 디스크립터는 사용자 영역, 커널 영역 모두에서 프로세스 내의 고유한 식별자로 사용된다. 다시 말해, 파일을 열면 파일 디스크립터가 반환되고, 이 반환된 값을 담당 시스템 콜의 첫 번째 인자로 넘겨서 파일 디스크립터를 보고 읽기, 쓰기 등 여러 다양한 연산을 수행한다. 여기서 파일 디스크립터는 정수(int)형인데, -1을 반환하면 에러가 난 것이다. 반대로 음수가 아닌 반환값은 유효하다고 판단된다.

  파일 디스크립터는 0~1023(최댓값 - 1)까지 증가한다. 기본적으로 최댓값은 1,024이지만, 사용자가 설정한다면 1,048,576까지 설정할 수 있다(!)

  프로세스에서 따로 닫지 않는 이상, 모든 프로세스는 표준 입력, 표준 출력, 표준 에러, 이 세 가지 파일 디스크립터를 열어 두고 있다. 아래 표로 정리해 놓은 것을 참고하자.

fd

설명

C 라이브러리 선행처리기 정의

 0

 표준 입력

 STDIN_FILENO

 1

 표준 출력

 STDOUT_FILENO

 2

 표준 에러

STDERR_FILENO 


  사용자는 이러한 파일 디스크립터를 redirect하거나 파이프(pipe, '|')를 사용해서 프로그램의 출력을 다른 프로그램의 입력으로 redirect할 수도 있다.


  파일 디스크립터는 일반 파일 뿐만 아니라, 장치 파일, 퓨텍스, 디렉터리, FIFO, 소켓 접근에도 사용된다. 유닉스 기반의 운영체제에서는 모든 것이 파일로 이루어져 있는 이상, 모든 것은 파일 디스크립터를 통해 접근할 수 있다.


  이제 적당히 기본 설명을 마치고, 본론으로 들어가 직접 파일 열기 연습을 해 보자. 글쓴이가 공부하는 책이 C언어로 설명하고 있기 때문에, 글쓴이도 마찬가지로 C언어로 설명을 하려 한다.


파일 열기

  모든 파일 작업은 파일을 여는 것으로 시작한다. 이를 돕는 시스템 콜은 open()이나 creat()이다. 또한 파일을 다 사용하고 작업을 종료할 때에는 close() 시스템 콜로 닫아야 한다.


open() 시스템 콜

  파일을 열고 파일 디스크립터를 얻는 시스템 콜이다. 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 == -1printf("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() 시스템 콜을 호출한 프로세스는 호출 시 사용한 플래그에 알맞는 접근 권한을 확보해야 한다.




(Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY)


  그렇다면 읽기, 쓰기만 할 수 있는 걸까? 그렇지 않다. 비트 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() 호출은 실패하게 된다.


새로운 파일의 소유자




Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY

  이건 어떻게 정하지? 간단하다. 파일 소유자의 uid는 그 파일을 생성한 프로세스의 euid[각주:4]이기 때문이다.

  그럼 소유 그룹은? 이건 파일의 소유자를 결정하는 것보다는 조금 더 복잡하다. 기존에 알려진 시스템 V에서의 동작 방식에 의하면 파일 그룹은 파일을 생성한 프로세스의 egid[각주:5]로 설정한다. 리눅스 또한 이런 작업 방식을 표준 작업 방식으로 사용하고 있다.

  하지만 BSD에서는 어떨까? 이 쪽으로 들어가면 더 어려워진다. BSD에서는 파일의 그룹이 상위 디렉터리의 gid로 설정되도록 하는 독자적인 시스템을 사용하기 때문이다. 이러한 동작 방식은 리눅스 mount 옵션[각주:6]으로 변경할 수 있다. 또한 리눅스에서 파일 상위 디렉터리에 SetGID를 설정해놨을 때 적용되는 동작 방식이기도 하다. 다행히 이렇게 파일 소유 그룹을 일일이 따져야 하는 경우는 많지 않다.

새로운 파일의 권한

Icons made by Kiranshastry from www.flaticon.com is licensed by CC 3.0 BY


  앞에서 설명한 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] 시스템 콜


  지금까지는 open() 시스템 콜에 대한 설명이었다. O_WRONLY | O_CREAT | O_TRUNC는 너무 많이 쓰이는 일반적인 조합인데, 아예 이렇게 자주 쓰이는 동작 방식들을 지원하는 시스템 콜이다. creat() 시스템 콜의 원형은 다음과 같다.

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 == -1printf("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 == -1printf("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를 적절한 에러 값으로 설정하는 것은 공통적이다. 파일 열기에서 에러 처리는 사실 그리 복잡하지는 않다. 필요한 선행 작업이 거의 없다시피 하기 때문에 기존에 수행한 작업을 취소하거나 하지 않아도 되기 때문이다. 에러 처리 과정에서 다른 파일 이름을 사용자에게 요청하거나, 그냥 프로그램을 끝내는 등 단순한 조치를 취한다.

  1. (+) 기본적으로 자식 프로세스는 부모 프로세스가 소유한 파일 테이블의 복사본을 상속받는다. [본문으로]
  2. 기본값으로 SIGIO [본문으로]
  3. exec 를 이용해서 프로세스를 생성하기 전, 기존에 열렸던 파일지정자들을 정리할 때 사용되는 플래그 [본문으로]
  4. 유효 uid [본문으로]
  5. 유효 gid [본문으로]
  6. bsdgroups, sysvgroups [본문으로]
  7. 여담이지만, create이 아니라 creat가 맞다. 유닉스를 설계한 켄 톰슨이 빼먹었다고 한다. [본문으로]

'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