파일 입출력 - open()

김신·2023년 1월 1일
0
post-thumbnail

0. 파일 입출력

유닉스 시스템에서는 거의 모든 것을 파일로 표현하므로 파일 입출력은 정말 중요한 부분입니다.

파일은 읽거나 쓰기 전에 반드시 열어야 합니다. 커널은 파일 테이블이라고 하는 프로세스 별로 열린 파일 목록을 관리합니다. 이 테이블은 음이 아닌 정수 값인, 파일 디스크립터로 인덱싱되어 있습니다. 이 테이블의 각 항목은 열린 파일에 대한 정보를 담고 있으며 여기에는 메모리에 복사된 inode를 가리키는 포인터와 각종 메타데이터가 포함되어 있습니다. 파일 디스크립터는 사용자 영역과 커널 영역 모두에서 프로세스 내의 고유한 식별자로 사용됩니다. 파일을 열면 파일 디스크립터가 반환되고 이 파일 디스크립터를 관련 시스템 콜의 첫 번째 인자로 넘겨 다양한 연산을 수행합니다.

프로세스에서 명시적으로 닫지 않는 이상 모든 프로세스는 최소한 0, 1, 2라는 세 가지 파일 디스크립터를 열어 두고 있습니다. 파일 디스크립터 0번은 표준 입력(stdin), 1번은 표준 출력(stdout), 파일 디스크립터 2번은 표준 에러(stderr)입니다.

파일 디스크립터는 단순히 일반 파일만 나타내는 것이 아닙니다. 파일 디스크립터는 장치 파일, 파이프, 디렉터리, 퓨텍스 ,FIFO, 소켓 접근에도 사용되며 모든 것이 파일이라는 유닉스 철학에 따라 읽고 쓸 수 있는 모든 것은 파일 디스크립터를 통해 접근할 수 있습니다.

기본적으로 자식 프로세스는 부모 프로세스가 소유한 파일 테이블의 복사본을 상속받습니다. 열린 파일, 접근 모드, 현재 파일의 오프셋 등을 담고 있는 목록은 동일하지만, 예를 들어 자식 프로세스가 파일을 닫는 특정 프로세스에서 일어난 변화는 다른 프로세스의 파일 테이블에 영향을 미치지 않습니다.

1. 파일 열기

파일에 접근하는 가장 기본적인 방법은 read()와 write() 시스템 콜입니다. 하지만 파일에 접근하려면 open()이나 creat() 시스템 콜을 이용해서 파일을 열어야 합니다. 그리고 다 쓴 뒤에는 close() 시스템 콜로 파일을 닫아야 합니다.

1.1 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);

open() 시스템 콜은 경로 이름이 name인 파일을 파일 디스크립터에 맵핑하고, 성공하면 파일 디스크립터를 반환한다. 파일 오프셋은 파일의 시작 지점인 0으로 설정되며 파일은 flags로 지정한 플래그에 대응하는 접근 모드로 열리게 됩니다.

open() 플래그

flags 인자는 O_RDONLY, O_WRONLY, O_RDWR 중 하나를 포함해야 합니다. 읽기, 쓰기, 읽기와 쓰기 모드를 나타냅니다. 각 모드에 맞는 접근을 하지 않으면 그 접근은 실패합니다. flags 매개 변수에 비트 OR 연산으로 다음 값 중 하나 이상을 추가해서 열기 동작을 변경할 수 있습니다.

  • O_RDONLY Necessary
    파일을 읽기 전용으로 연다. (Read Only)
  • O_WRONLY Necessary
    파일을 쓰기 전용으로 연다. (Write Only)
  • O_RDWR Necessary
    파일을 쓰기와 읽기용으로 연다. (Read & Write)
  • O_EXEC Necessary
    파일을 실행 전용으로 연다. (Execute Only)
  • O_SEARCH Necessary
    디렉토리 파일을 탐색 전용으로 연다. (Search Only)
  • O_APPEND Optional
    파일의 끝부분 (EOF)에 write하도록 설정한다.
  • O_CLOEXEC Optional
    FD_CLOEXEC 플래그를 설정한 채 파일을 연다. (exec류의 함수를 수행하고 나면 fd가 닫긴다.)
  • O_CREAT Optional
    파일이 없으면 생성한다. 이 플래그를 명시하면, open 함수에 Permission 정보를 추가로 더 받아야 한다. 파일이 존재하면 연다.
  • O_DIRECTORY Optional
    path에 해당하는 파일이 디렉토리가 아니면 에러를 발생한다.
  • O_EXCL Optional
    O_CREAT 플래그와 같이 사용한다 파일이 이미 존재하면 에러를 발생한다.
  • O_NOCTTY Optional
    path에 해당하는 파일이 터미널 장치인 경우, 해당 장치를 현재 프로세스의 컨트롤링 터미널로 할당하지 않는다.
  • O_NOFOLLOW Optional
    path에 해당하는 파일이 심볼릭 링크면 에러를 발생한다.
  • O_NONBLOCK Optional
    FIFO, Block Device, Charactoer Device에 대해 논 블록킹 방식으로 read 함수와 write 함수를 수행하도록 기본 설정을 세팅한다.
  • O_SYNC Optional
    path에 해당하는 파일에 write 함수를 사용할 경우 실제 물리적인 I/O가 끝날 때까지 기다리도록 설정한다
  • O_TRUNC Optional
    파일이 이미 존재하고 write-only, read-write모드로 열 수 있는 경우, 파일 사이즈를 0으로 초기화시킨다
  • O_DSYNC Optional
    write 함수 수행시 파일의 데이터 부분에 실제 물리적인 I/O가 끝나기를 기다린다. 파일의 설정이나 Attribute부분에 대한 업데이트는 기다리지 않는다.
  • O_RSYNC Optional
    read 함수 수행시 커널에 해당 파일의 offset에 대한 write 함수의 pending이 있으면 그 write 함수의 수행이 끝나기를 기다린다.

1.2 새로운 파일의 소유자

새로 생긴 파일의 소유자는 파일을 생성한 프로세스의 euid(유효 uid)입니다.

소유 그룹은 파일을 생성한 프로세스의 egid(유호 gid)로 설정합니다. 이는 시스템 V 동작 방식이며 리눅스가 따른 표준 작업 방식입니다.

1.3 새로운 파일의 권한

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)
	/* 에러 */

1.4 creat() 함수

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);

1.5 반한값과 에러 코드

open()과 creat()는 성공하면 파일 디스크립터를 반환합니다. 에러가 발생하면 둘 다 -1을 반환하고 errno를 적절한 에러 값으로 설정합니다. 파일을 여는 과정에서의 에러 처리는 복잡하지 않습니다. 일반적으로 파일을 열기에 앞서 수행하는 단계가 거의 없으므로 기존에 수행한 작업을 취소할 필요가 없기 때문입니다. 에러 처리 과정에서 다른 파일 이름을 사용자에게 요청하거나 단순히 프로그램을 끝내는 전형적인 대응 방법을 사용합니다.

0개의 댓글