리눅스 시스템 프로그래밍 챕터 2
ch2. 파일 입출력
Overview
유닉스에서는 모든 것을 파일로 표현하기 때문에 파일 입출력은 매우 중요한 부분이다.
- 이 장에서는 파일 입출력의 기본을 알아보고 파일을 다루는 시스템 콜을 알아본다.
int fd = open("/home/amazingguni/asdf.txt", O_RDWR);
unsigned long word;
read(fd, &word, sizeof(unsigned long));
const char *buf = "hi everyone";
write(fd, buf, strlen(buf));
파일은 읽거나 쓰기 전에 반드시 열어야 한다. 이 때 커널은 프로세스별로 파일 테이블을 관리한다.
- 이 파일 테이블은
File discripter
(흔히 fd라고 부르는)로 인덱싱 되어 있음 - 테이블의 각 항목은 열린 파일에 대한 정보를 담고 있음
- 메모리에 복사된 inode를 가리키는 포인터
- 파일 위치와 접근 모드(r/w)와 같은 메타 데이터
fd
는 사용자 영역, 커널 영역 모두에서 프로세스 내에서 고유한 식별자로 쓰임- 파일을 열면
fd
가 반환되고 이걸 통해 읽기 쓰기 등 연산을 수행
프로세스에서 명시적으로 닫지 않는다면 모든 프로세서는 아래 3개의 fd
를 열어두고 있다.
- 0 - 표준 입력(stdin)
- C에서는 STDIN_FILENO로 정의
- 키보드 같은 터미널의 입력장치에 연결
- 1 - 표준 출력(stdout)
- STDOUT_FILENO
- 터미널의 출력 장치에 연결
- 2 - 표준 에러(stderr)
- STDERR_FILENO
사용자는 이런 표준 fd
를 리다이렉트하거나 파이프를 사용해서 한 프로그램의 출력을 다른 프로그램의 입력으로 리다이렉트할 수도 있다.
fd
는 단순히 일반 파일만 나타내지는 않는다.
- 장치 파일(usb, mount된 장비, 등등)
- 디렉터리
- 퓨텍스
- 개발자가 기본적인 잠금을 구현하기 위해 사용되거나 세마포어, POSIX 뮤텍스, 조건 변수와 같은 상위 계층의 잠금 추상화를 위한 빌딩 블록으로서 쓰일 수 있는 리눅스 커널에서 제공하는 시스템 호출이다.
- FIFO
- 소켓 접근
기본적으로 자식 프로세스는 부모 프로세스가 소유한 파일 테이블의 복사본을 상속받는다.
- 이후 자식 프로세스에서 파일 테이블이 추가/삭제 되어도 부모 프로세스는 영향받지 않는다.
2.1 파일 열기
파일을 여는 시스템콜은 open(), creat() => (오타 아님)이며, 다 쓴 다음에는 close() 시스템 콜로 파일을 닫아야 한다.
2.1.1 open()
open() 시스템 콜을 사용해서 파일을 열고 fd
를 얻는다.
#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);
경로 이름이 name인 파일을 fd
에 맵핑하고, 성공하면 이 fd
를 반환한다.
파일 오프셋은 시작 지점인 0으로 설정되며 flags
로 지정된 플래그에 대응하는 접근 모드로 열린다.
- 접근 모드는 소유자, 그룹, 사용자별 읽기 쓰기 실행 권한을 의미
- 644나 755로도 동작하는 시스템은 있다.
open() 플래그
flags 인자는 O_RDONLY
, O_WRONLY
, O_RDWR
중 하나를 포함해야 한다.
아래는 읽기 전용으로 파일을 여는 예시다.
int fd = open("/home/amazingguni/asdf.txt", O_RDONLY);
if(fd == -1)
/* error */
int fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0664);
당연히 읽기 전용에서는 쓰지 못하고 쓰기 전용에서는 읽지 못한다.
flags 매개 변수에 비트 OR 연산으로 다음 값 중 하나 이상을 추가해서 열기 동작을 변경할 수 있다.
- O_APPEND
- 덧붙이기 모드, 파일 오프셋을 끝으로 설정해줌
- O_ASYNC
- 소켓이나 터미널에서 사용하며, 특정 파일에서 읽기나 쓰기가 가능해질 경우 시그널이 발생
- O_CREAT
- 파일이 없으면 새로 만들어 줌
- O_NONBLOCK
- 가능한 경우 파일을 논블록킹 모드로 연다.
- O_TRUNC
- 파일이 존재하고, 일반 파일이고, 쓰기가 가능할 경우 파일 길이를 0으로 잘라버린다.
- FIFO나 터미널 디바이스에서는 무시됨
int fd = open("/home/asdf/notexist.txt", O_WRONLY | O_TRUNC);
if(fd == -1)
/* 파일이 없을 경우 에러가 발생한다. */
2.1.2 새로운 파일의 소유자
파일 소유자의 uid는 파일을 생성한 프로세스의 euid(유효 uid)이다.
소유 그룹은 파일을 생성한 프로세스의 egid(유효 gid)로 설정한다.
- 시스템마다 구현 방법이 다를 수 있기 때문에 gid가 중요한 경우에는
chown()
시스템 콜을 활용해서 직접 설정하는 것이 좋다. - 근데 중요한 경우가 흔치는 않다.
2.1.3 새로운 파일의 권한
파일을 생성하는 경우에는 mode
인자에 있는 권한으로 설정되고, 아닌 경우에는 무시된다.
O_CREAT
로 파일을 생성하는 경우에는mode
를 반드시 주는 것이 좋다.- 없으면 문제가 일어날 여지가 있다.
mode에는 아래 상수 집합이 쓰인다.
- 소유자 권한
S_IRWXU
,S_IRUSR
,S_IWUSR
,S_IXUSR
- 그룹 권한
S_IRWXG
,S_IRGRP
,S_IWGRP
,S_IXGRP
- 그 외 사용자 권한
S_IRWXO
,S_IROTH
,S_IWOTH
,S_IXOTH
int fd = open(file, O_WRONLY | O_CREAT | O_TRUNC,
S_IWUSR | S_IRUSR | S_IWGRP | S_IRGRP | S_IROTH);
if(fd == -1)
/* error */
int fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0664);
2.1.4 creat() 함수
O_WRONLY | O_CREAT | O_TRUNC 조합이 자주 사용되서 이를 지원하는 함수가 따로 있다.
int creat (const char *name, mode_t mode);
왠지 create여야 할 거 같지만 정확한 이름이다. 켄 톰슨(유닉스의 아버지)는 유닉스 설계했을 때 e를 빼먹은 게 가장 후회되는 일이라고 농담했다고 한다.. 뭐라는거람
int fd = creat(filename, 0644);
if(fd == -1)
/* error */
// 위와 동일하다
int fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
2.1.5 반환값과 에러코드
open()
, creat()
는 성공하면 파일 디스크립터(fd)를 반환한다. 에러가 발생하면 둘 다 -1
을 반환한다.
2.1 read()로 읽기
read() 시스템 콜은 파일을 읽을 때 사용한다.
ssize_t read(int fd, void *buf, size_t len);
fd
가 참조하는 파일의 현재 파일 오프셋에서len
바이트만큼buf
로 읽어 들인다.- 성공하면 buf에 쓴 바이트 숫자를 반환하며, 실패하면 -1을 반환한다.
- 연산 이후에 파일 오프셋은
fd
에서 읽은 바이트 크기만큼 전진한다.
기본 사용법은 아래와 같다.
unsigned long word;
ssize_t nr = read(fd, &word, sizeof(unsigned long));
if(nr == -1)
/* error */
이게 가장 단순한 구현인데 이 코드에는 두가지 문제점이 있다.
- len 바이트만큼 모든 데이터를 읽지 못할 경우
- 점검 후 처리 과정이 빠져 있는 것
2.2.1 반환값
- read()는 buf에 쓴 바이트 크기를 반환한다.
- 0을 리턴하는 경우는 read()가 파일 끝(EOF)을 만났을 때이다.
- 에러가 발생하면 -1을 반환한다.
- read()가 len보다 작은 양수값을 반환하는 경우는 아래 이유가 있을 수 있다.
- len 바이트보다 적은 바이트만 사용 가능
- 시그널이 시스템 콜을 중단
- 파이프가 깨지는 경우(fd가 파이프라면)
만약 read()를 했는데도 읽을 데이터가 없을 경우 읽을 바이트가 생길 때까지 블록된다.
- 논블록 모드로 열리지 않았을 경우에 이렇다.(나중에 설명)
- EOF를 만나지 않았다는 말은, 사용가능한 데이터가 없다는 말이다.
- 즉, 소켓이나 디바이스 파일을 읽는 경우에 이에 해당될 수 있다.
정리하자면 read() 호출은 다음과 같은 가능성을 가지고 있다.
- 호출이 len과 같은 값을 반환
- len 바이트 전체를 읽어서 buf에 저장했다.
- 의도한 결과
- 호출이 len보다 작지만 0보다는 큰 값을 반환, 읽은 바이트는 buf에 저장됨
- 시그널이 중간에 읽기를 중단시켰거나, 에러가 발생해서 데이터를 가져오지 못했을 수 있음
- 혹은, len바이트를 읽기 전에 EOF에 도달
- 적절하게 buf와 len 값을 고친 다음에 다시 read()를 호출한다.
- 0을 반환
- 더이상 읽을 데이터가 없음을 의미
- 사용가능한 데이터가 없기 때문에 호출이 블록
- (논블록모드에서는 이런 상황이 발생하지 않는다)
- 호출이 -1을 반환하고 errno을 EINTR로 설정
- EINTR은 현재 읽을 데이터가 없기 때문에 블록된 상태이며 나중에 요청하라는 뜻
- 논블록 모드에서만 일어나는 상황
- 다시 읽기를 요청하면 된다
- 호출이 -1을 반환하고 errno을 EINTR이나 EAGAIN이 아닌 다른 값으로 설정
- 심각한 에러
2.2.2 전체 바이트 읽기
앞서 말한 상황에 대처하는 read() 코드는 아래와 같이 작성해야 한다.
- 에러를 처리하면서 len만큼 바이트를 읽거나 EOF까지 읽는 코드
ssize_t ret;
// 읽고 ret에는 읽은 길이가 저장된다
while (len != 0 && (ret = read(fd, buf, len)) != 0) {
if (ret == -1) {
// 에러가 발생하고 EINTR이 아닌 다른 에러가 발생했다면
// perror를 출력하고 루프을 빠져나간다.
if (errno == EINTR)
continue;
perror("read");
break;
}
// 만약 len만큼 읽었다면 len이 0이 된다.
// 아닐 경우 남은 byte만큼 len에 남음
len -= ret;
buf += ret;
}
2.2.3 논블록 읽기
읽을 데이터가 없을 때 read() 호출이 블록되지 않기를 바라는 경우에는 읽을 데이터가 없다는 사실을 알려주기 위해 호출이 즉시 반환되는 편이 좋다.
이를 논블록 입출력
이라고 한다.
- 논블록 입출력은 애플리케이션이 다중 파일 입출력을 수행하도록 만든다.
- 즉, 특정 파일에서 블록되어 다른 파일에서 사용 가능한 데이터를 놓치는 현상이 벌어지지 않는다.
여기서 반환되는 값은 -1이고 errno는 EAGAIN
이다. 그래서 논블록인 경우 이 errno를 반드시 점검해야 한다.
char buf[BUFSIZ];
ssize_t nr;
start:
nr = read(fd, buf, BUFSIZ);
if(nr == -1) {
if (errno == EINTR)
goto start; /* 시그널로 인해 중단되어 에러, 다시 시도한다 */
if (errno == EAGAIN)
/* 읽을 데이터가 없다. 나중에 다시 시도 */
else
/* 에러 */
}
2.2.4 그 외 에러 값
read() 호출 시에 발생할 수 있는 다른 에러들
- EBADF - 파일 디스크립터가 유효하지 않거나 읽지 못하는 경우
- EFAULT - buf가 프로세스의 주소 공간 밖에 존재
- EINVAL - 파일 디스크립터가 읽기를 허용하지 않는 객체에 맵핑
- EIO - 저수준 입출력 에러
2.2.5 read() 크기 제약
POSIX에서는 size_t
와 ssize_t
타입을 지원한다.
size_t
는 바이트 단위로 크기를 측정하기 위해 사용됨- 읽을 바이트 개수를 표현
- 일반적으로
signed int
ssize_t
는 부호가 있는size_t
타입이다.- -1로 에러를 나타내기 위해 사용됨
- 일반적으로
int
두 유형은 종종 함께 사용되기 때문에 범위가 좀 더 작은 ssize_t
가 size_t
의 범위를 제한한다.
그래서 SSIZE_MAX
보다 len
이 큰 경우에는 SSIZE_MAX
만큼 읽는다. 하지만 오류가 있는 시스템이 있을 수 있기 때문에 이런 처리를 하는 것이 좀 더 범용적이다.
if (len > SSIZE_MAX)
len = SSIZE_MAX;
len이 0일 경우에는 read() 호출시 즉시 0을 반환한다.
2.3 write()로 쓰기
파일에 데이터를 기록하기 위해 사용하는 가장 기본적인 콜은 write()이다.
ssize_t write(int fd, const void *buf, size_t count);
count
바이트만큼 fd
가 참조하는 파일의 현재 파일 위치에 시작 지점이 buf
인 내용을 기록한다.
성공하면 쓰기에 성공한 바이트 수를 반환하며 파일 오프셋도 그만큼 전진한다.
- 에러가 발생하면 -1을 반환하며
errno
를 적절한 값으로 설정한다. - write() 호출은 0을 반환할 수 있지만, 단순히 0바이트를 썻다는 의미이다.
기본 사용법은 간단하다.
- read와 비슷하게 부분 쓰기가 발생했을 경우도 염두해야 한다.
const char *buf = "My ship is solid!";
ssize_t nr;
size_t len = strlen(buf);
nr = write(fd, buf, len);
if (nr == -1)
/* error errno을 확인 */
else if(nr != count)
/* 부분쓰기.. 에러일 가능성이 있지만, errorno는 없다. */
2.3.1 부분 쓰기
write()는 read()의 부분 읽기에 비해 부분 쓰기를 일으킬 가능성이 더 적다.
- 일반 파일에 대한 write()는 에러가 발생하지 않을 경우 요청받은 쓰기 작업 수행을 보장함
- 따라서 일반 파일을 대상으로할 때에는 루프를 돌릴 필요가 없다.
하지만 (소켓과 같은) 다른 파일 유형을 대상으로 할 경우에는 루프가 필요하다.
ssize_t ret, nr;
white (len != 0 && (ret = write(fd, buf, len)) != 0) {
if (ret == -1) {
if (errno == EINTR) /* 시그널로 인해 중단 다시 시도 */
continue;
perror("write");
break;
}
len -= ret;
buf += ret;
}
2.3.2 덧붙이기 모드
O_APPEND
옵션을 이용해 fd를 덧붙이기 모드로 열면 fd의 파일 오프셋이 파일 끝으로 지정된다.
- 즉 파일 끝에서부터 쓰기 연산이 일어난다.
Helloworld
# Append일 때
write("!!!")
Helloworld!!!
Helloworld
# Append가 아닐 때
write("!!!")
!!!loworld
이는 여러 프로세스가 동일한 파일에 쓰기 작업을 할 때 효과적이다.
덧붙이기 모드가 아니라면 같은 파일에 여러 프로세스가 붙었을 때 아래와 같은 일이 일어난다.
A process
write("Helloworld")
Helloworld
^ A process file offset
B process
write("!!!!") // append mode
Helloworld!!!
A process
Helloworld!!!
^ A process file offset
write("Why?")
HelloworldWhy
^ file offset
그래서 Append를 사용한다.
- 덧붙이기 모드는 파일 오프셋이 항상 파일 끝에 위치하도록 설정
- 쓰기 작업을 수행하는 프로세스가 여럿이라도 항상 덧붙임
- 매번 쓰기 작업마다 파일 오프셋을 원자적으로 갱신한다고 생각해도 좋다.
- 로그 파일 갱신과 같은 특정 작업에서 뛰어난 효과를 발휘
2.3.3 논블록 쓰기
O_NONBLOCK
옵션을 지정해서 fd
가 논블록 모드로 열린 상태에서 쓰기 작업을 하는 경우이다.
- 블록될 경우
write()
는 -1을 반환하고errno
을EAGAIN
으로 설정 - 나중에 다시 쓰기 요청을 해야 한다.
- 일반 파일에서는 블록이 잘 일어나지 않음
2.3.4 그 외 에러 코드
- EBADF - fd가 유효하지 않거나 쓰기 모드가 아님
- EFAULT - buf의 포인터가 호출하는 프로세스 주소 공간 안에 없음
- EFBIG - 너무 큰 양을 쓸 경우
- EINVAL - fd가 쓰기에 적합하지 않은 객체에 맵핑
- EIO - 저수준의 입출력 에러
- ENOSPC - 파일 시스템에 충분한 공간이 없음
- EPIPE - fd가 파이프와 소켓에 연결되어 있는데 반대쪽 읽기 단이 닫혀버린 경우
2.3.5 write() 크기 제약
딴건 없고 count가 0이면 호출 즉시 바로 0을 반환한다.
2.3.6 write() 동작 방식
write() 호출이 반환될 때, 사용자 영역에서 커널에 넘긴 버퍼에서 커널 버퍼로 데이터가 복사된다. - 즉, 반환했을 때 의도한 목적지에 데이터를 썻다는 보장이 되지 않는다.
- 리눅스 커널은 모든 변경된 버퍼를 수집해 배경 작업으로 디스크에 쓴다
- 이런 과정을 쓰기 저장(writeback)이라 함
- 프로세서와 하드 디스크 사이의 성능 격차로 인해서 이렇게 함
- 이런 방식 덕분에 쓰기 호출은 바로 수행되서 반환되고, 커널은 여유가 생길 때 쓰기 작업을 한꺼번에 배치로 수행한다.
쓴 이후에 바로 읽는 경우에 문제가 발생할 것 같지만 이런 경우에는 디스크로 가지 않고 메모리 내부의 캐시를 참조하기 때문에 문제가 되지 않는다.
앱에서는 쓰기/읽기 동작이 정상적으로 잘 수행되서 모르지만 실제로는 디스크에 저장되지 않았을 수 있다.(속도상에서 성능이 개선되면서)
지연된 연산중에 커널이 쓰기에 용이한 순서로 쓰기 요청을 바꿀 수 있기 때문에, 중간에 시스템이 비정상으로 종료되면 문제가 될 수 있다.
DB는 이런 경우에도 부적합한 상태가 되는 것을 방지해준다.
커널은 지연된 쓰기에 따른 위험을 최소화하기 위해 노력한다.
- 최대 버퍼 나이를 만들어서 나이가 꽉 차면 빠짐없이 기록
/proc/sys/vm/dirty_expire_centiseconds
에서 설정
- 버퍼를 이용하지 않고 동기적으로 쓰기를 할 수 있는데 이는 다음 장을 참고
2.4 동기식 입출력
쓰기 버퍼링은 대부분의 최신 운영체제에서 제공하며, 주목할만한 성능 향상을 제공한다.
- 앞에서 말한 단점은 사실 크지 않고 자주 발생하지 않는다.
그래도 어플리케이션이 디스크에 기록되는 시점을 제어하고 싶을 때는 몇가지 옵션을 사용하면 좋다.
2.4.1 fsync()와 fdatasync()
버퍼의 데이터를 디스크에 기록되도록 할 수 있는 단순한 방법은 fsync()
를 이용하는 것이다.
#include<unistd.h>
int fsync (int fd);
fsync()
를 호출하면 fd
에 맵핑된 파일의 모든 변경점을 디스크에 기록된다.
- 이때
fd
는 쓰기 모드로 열려야 한다. - fsync()는 데이터와 inode의 메타데이터(파일 생성 시간 같은)를 모두 디스크에 기록하고 반환된다.
만약 하드 디스크 자체에 캐시가 있다면 fsync나 fdatasync()로 보장이 되지 않지만 캐시에서 실제 디스크에 저장되기까지의 시간이 매우 짧기 때문에 큰 의미는 없다
리눅스는 또한 fdatasync()
를 제공한다.
- 데이터만 쓰고 메타데이터를 쓰지 않고 반환한다.
- 이론 상으로는
fsync()
보다 빠르다. - 일반적으로
fdatasync()
정도면 충분하다.
int ret;
ret = fsync(fd);
if (ret == -1)
/* error */
int ret;
ret = fdatasync(fd);
if (ret == -1)
/* error */
fsync()
, fdatasync()
모두 변경된 파일이 포함된 디렉토리 엔트리에 대한 디스크 동기화는 보장하지 않는다.
디렉터리 엔트리
가 디스크에 기록되지 않았을 경우 파일에 대한 접근이 불가능하다.- 파일의 링크가 최근에 갱신되었고 파일 데이터도 디스크에 제대로 기록되었다고 해도
- 디렉터리 엔트리를 강제로 기록하려면 이를 대상으로 한
fd
를fsync()
의 인자로 넘겨야 함
디렉토리 엔트리
란 디렉토리를 표현하는 데에 쓰이는 자료구조이다. 유닉스 계열에서는 파일이름과 inode 번호만 저장된다.[http://maj3sty.tistory.com/928%5D%28http://maj3sty.tistory.com/928%29
** 반환값과 에러 코드 **
호출이 성공하면 0을 반환하고 실패하면 fsync()
, fdatasync()
모두 -1을 반환하고 errno을 설정
- EBADF - 주어진 fd가 유효하지 않거나 쓰기 모드가 아님
- EINVAL - fd가 동기화가 지원되지 않는 객체에 맵핑
- EIO - 저수준 입출력 에러, 이는 쓰기 과정 중에 입출력 에러가 발생했을을 나타냄
몇몇 리눅스 베포판에서는 fdatasync()
는 구현되었지만 fsync()
는 구현되지 않은 경우가 있음
- 동기화할 메타데이터가 없는 파일 유형이나 특별한 파일시스템에서는
fdatasync()
만 구현하기도 하기 때문
if(fsync(fd) == -1){
// fsync()를 선호해서 fsync()를 먼저 호출해보고 실패하면 fdatasync()를 호출
if(errno == EINVAL) {
if(fdatasync(fd) == -1)
perror("fdatasync");
} else {
perror("fsync");
}
}
2.4.2 sync()
sync()
시스템 콜은 모든 버퍼 내용을 디스크에 동기화 한다.
- 최적화는 조금 부족하지만 활용범위가 넓음
void sync(void);
이 호출은 인자도 없고 반환하는 값도 없으며 항상 성공한다.
호출 이후에는 버퍼의 모든 내용(데이터와 메타데이터 모두)을 디스크에 강제로 기록한다.
- 표준에서는 디스크에 기록하는 과정을 시작하도록 구현되라고 되어 있다.
- 즉 반환 당시에 기록이 완료되어 있지 않을 수 있다.
- 하지만 리눅스에서는 버퍼를 모두 기록할 때까지 기다리기 때문에 한번만 호출해도 좋다
- 다른 시스템에선 그렇지 않을 수 있어서 여러번 호출하기를 권하기도 한다.
실제로 sync()
를 사용하는 곳은 sync(8)
유틸리티이다.
- shell 에서
sync
를 입력
$ sync
fd를 가지고 있다면 fsync()
나 fdatasync()
를 사용하는 것이 좋다. 작업량이 많은 시스템에서는 sync()
호출이 반환되기따지 수분이 걸릴 수 있기 때문이다.
2.4.3 O_SYNC 플래그
open()
호출 시 O_SYNC
를 사용하면 모든 파일 입출력은 동기화된다.
write()
호출은 동기화되지 않기 때문에O_SYNC
플래그를 하면 동기화가 보장된다.read()
호출은 원래 언제나 동기화 된다.- 그렇지 않다면 읽기 요청의 결과로 저장된 데이터가 유효한지 확인할 수 있기 때문이다
int fd = open(file, O_WRONLY | O_SYNC);
if (fd == -1) {
perror ("open");
return -1;
}
O_SYNC
플래그는 write()
후 반환하기 직전에 fsync()
를 매번 호출하는 방식이라고 보면 좋다.
- 실제로는 좀 더 효율적인 방식이지만 의미는 동일
- 그 때문에 사용자 영역과 커널 영역에서 소모되는 시간을 조금씩 늘림
- 디스크에 쓴 파일 크기에 따라 전체 소요시간이 늘어남
- 모든 입출력 레이턴시는 프로세스에 의해 초래되기 때문(?)
- 내 생각에는 프로세스에서 동기화를 요청했기 때문이라는 느낌인듯한 느낌임
- 입출력 동기화는 비용이 많이 들기 때문에 최후의 선택으로 사용
가급적이면 fsync()
나 fdatasync()
를 꼭 동기화가 필요할 때 하고 이 플래그를 쓰지 않는 것이 좋다.
2.4.4 O_DSYNC와 O_RSYNC
O_DSYNC
와 O_RSYNC
라는 입출력 동기화 관련 플래그도 open()
에서 쓸 수 있다.
O_SYNC
와 동일하며 동작 방식도 같다.
O_DSYNC
는 쓰기 작업 직후에 메타데이터를 제외한 일반 데이터만 동기화 한다.
- 쓰기 요청 직후에
fdatasync()
를 호출 O_SYNC
가 더 확실한 동기화를 보장하므로 O_DSYNC를 O_SYNC의 별칭으로 두면 실수 방지가 가능typedef O_DSYNC O_SYNC
- 이로 인한 성능 하락은 trade-off
O_RSYNC
는 쓰기 뿐만 아니라 읽기까지도 동기화한다.
- 이 옵션은 쓰기 동기화 옵션(
O_SYNC
,O_DSYNC
)과 함께 사용해야 한다.
이 옵션은 읽기로 인해 변경되는 메타데이터도 반환되기 전에 디스크에 기록한다.
- 파일 접근 시간 갱신
- 읽기 자체는 이미 동기화가 되어 있다.
- 리눅스는
O_RSYNC
를O_SYNC
와 동일하게 정의한다.- 리눅스 구현상 이런 동작을 구현하기가 쉽지 않다고 한다.
2.5 직접 입출력
리눅스 커널은 디바이스와 애플리케이션 사이에 캐시, 버퍼링, 입출력 관리 같은 복잡한 계층을 구현하고 있다.
- 직접 이런 계층을 만들어서 성능 향상을 노력을 하는 것은 효과가 미미하니 미추
- 일반적으로 운영체제 수준에서 제공하는 도구가 훨씬 뛰어남
- 데이터베이스 시스템 정도에서는 독자적인 캐시를 선호하고 운영체제의 개입을 최소한으로 줄이곤함
뭐 여튼 하고 싶다면 open()
호출에 O_DIRECT
플래그를 사용하면 커널의 입출력 관리를 최소화할 수 있다.
- 패이지 캐시를 우회해서 사용자 영역 버퍼에서 직접 디바이스로 입출력 작업 시작
- 모든 입출력은 동기화 방식으로 동작하게 되며 호출은 입출력 작업이 완료된 후에야 반환됨
직접 입출력을 수행할 때 요청하는 크기, 버퍼 정렬, 파일 오프셋은 모두 디바이스의 섹터 크기(512바이트)의 정수배가 되어야 한다.
2.6 파일 닫기
파일 디스크립터로 읽고 쓰는 작업을 마치고 나면 close()
시스템 콜을 이용해 파일 맵핑을 끊어야 한다.
#include <unistd.h>
int close(int fd);
close()
를 호출하면 열려있는 fd에 연관된 파일과의 맵핑을 해제하며 프로세스에서 파일을 떼어낸다.
해제된 파일 디스크립터는 유효하지 않으며 커널이 다음 open()
이나 creat()
호출에서 그 fd
를 다시 사용할 수 있게 된다.
close()
호출은 성공하면 0을 반환하고 실패하면 -1을 반환하고 errno을 설정한다.
if (close(fd) == -1)
perror("close");
파일을 닫더라도 파일을 디스크에 강제로 쓰지 않는다.
- 즉 동기화하지 않는다.
- 필요하다면 앞서 설명한
fsync()
,fdatasync()
를 하고close()
해야 한다.
2.6.1 에러 값
close()
의 반환값을 검사하는 것은 중요하다. 지연된 연산에 의한 에러는 한참 후에 나타나기 때문에 반환값으로 미연에 조치해야 하기 때문이다.
- EBADF - 파일 디스크립터가 유효하지 않음
- EIO - 저수준의 입출력에러
2.7 lseek()로 탐색하기
입출력은 파일 전체에 걸쳐 선형적으로 발생한다.
- 그렇기 때문에 읽기와 쓰기 중에 자연스롭게 발생하는 파일 위치 갱신만으로 충분하다.
- 하지만 어떤 경우에는 파일의 특정 위치로 직접 이용해야 할 필요가 있다
lseek()
시스템콜을 사용해 파일 디스크립터에 연결된 파일의 오프셋을 특정 값으로 지정할 수 있다.
lseek()
은 파일 오프셋만 갱신하며 다른 동작은 하지 않는다.- 입출력 에러도 발생시키지 않는다.
off_t lseek(int fd, off_t pos, int origin);
lseek()
은 origin
인자에 따라 다음과 같이 동작한다.
- SEEK_CUR
- fd의 파일 오프셋을 현재 오프셋에서 pos를 더한 값으로 설정, pos는 음수, 0, 양수 다 가능
- SEEK_END
- fd의 파일 오프셋을 현재 파일 크기에서 pos를 더한 값으로 설정, pos는 음수, 0, 양수 다 가능
- SEEK_SET
- fd의 파일 오프셋을 pos값으로 설정, pos가 0이면 파일의 처음으로 설정됨
성공하면 새로운 파일 오프셋을 반환하며 에러 발생시 -1을 반환한다.
// fd의 파일 오프셋을 1825로 설정
off_t ret = lseek(fd, (off_t) 1825, SEEK_SET);
if(ret == (off_t) - 1)
/* 에러 */
// fd의 파일 오프셋을 현재 파일의 끝으로 설정
off_t ret = lseek(fd, 0, SEEK_END);
if (ret == (off_t) -1)
/* 에러 */
// lseek()는 갱신된 파일 오프셋을 반환하므로
// 아래처럼 하면 현재 파일 오프셋을 얻을 수 있다.
int pos = lseek(fd, 0, SEEK_CUR);
if(pos == (off_t) -1)
/* 에러 */
else
/* 'pos'는 fd의 현재 위치 */
lseek()
은 파일의 시작, 혹은 끝 지점으로 오프셋을 이동시키거나, 현재 파일의 오프셋을 알아내는데 가장 많이 사용된다.
2.7.1 파일 끝을 넘어서 탐색하기
lseek()
는 파일 끝을 넘어서도록 지정할 수도 있다.
int ret = lseek(fd, (off_t) 1688, SEEK_END);
if (ret = (off_t) -1)
// error
지정할 때는 아무일도 발생하지 않지만 읽거나 쓸때 아래와 같은 일이 일어난다.
- 읽기 - EOF 반환
- 쓰기 - 마지막 오프셋과 새로운 오프셋 사이에 새로운 공간이 만들어지며 0으로 채워짐
2.7.2 에러 값
에러 == -1
- EBADF - 열리지 않은 fd
- EINVAL - origin 값이 잘못되있을 경우, 새로운 오프셋이 음수
- EOVERFLOW - 오프셋이 오버플로 나는 경우, 32바이트 아키텍처에서만 나타남, 반환만 못하는거지 갱신은 된다
- ESPIPE - fd가 잘못된 객체에 연결되어있음(파이프, FIFO, 소켓)
2.7.3 제약 사항
파일 오프셋의 최댓값은 off_t의 크기에 제한됨