파일 입출력 - 동기식 입출력

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

동기식 입출력

커널 버퍼에 담긴 데이터를 디스크에 동기화 하는 것은 매우 중요한 주제이긴 하지만, 쓰기 작업이 지연되는 문제를 너무 확대 해석해서는 안된다. 쓰기 버퍼링은 분명 가시적인 성능 향상을 제공하며 흔히 말하는 최신 운영체제라면 버퍼를 통해서 지연된 쓰기 작업을 구현하고 있다. 그럼에도 애플리케이션에서 직접 데이터가 디스크에 기록되는 시점을 제어하고 싶을 때가 있다. 이런 때를 위해 리눅스 커널에서는 성능을 희생하는 대신 입출력을 동기화하는 몇 가지 옵션을 제공한다.

fsync()와 fdatasync()

데이터가 디스크에 기록되도록 요청할 수 있는 가장 단순한 방법은 POSIX.1b에 정의되어 있는 fsync() 시스템 콜을 사용하는 것이다.

#include <unistd.h>

int fsync(int fd);

fsync()를 호출하면 파일 디스크립터 fd에 맵핑된 파일의 모든 변경점을 디스크에 기록한다. 이때 파일 디스크립터 fd는 반드시 쓰기 모드로 열려야 한다. fsync()는 데이터와 파일 생성 시간 같은 inode에 포함된 메타데이터를 모두 디스크에 기록한다. fsync()는 하드 디스크에 데이터와 메타데이터가 성공적으로 기록을 완료할 때까지 반환하지 않고 기다린다.

하드 디스크에 캐시가 있다면 fsync()를 호출해서 실제로 디스크에 데이터를 기록했는지 확인할 수 있는 방법이 없다. 하드 디스크는 데이터를 제대로 기록했다고 알려줄 태지만, 실제로 데이터가 하드 디스크 캐시에만 기록됐을 수도 있다. 다행히도 하드디스크의 캐시에 저장된 데이터는 매우 짧은 시간 안에 실제 디스크에 기록된다.

리눅스에서는 또한, fdatasync() 시스템 콜도 제공한다.

#include <unistd.h>

int fdatasync(int fd);

fdatasync()는 fsync()와 동일한 기능을 하지만, 데이터만 기록한다는 점에서 차이가 있다. fdatasync()를 호출하면 메타데이터까지 실제 디스크에 기록하도록 보장하지 않으므로 이론상으로는 fsync()보다 더 빠르게 반환된다. 대부분 fdatasync() 정도면 충분하다.

두 함수 모두 변경된 파일이 포함된 디렉터리 엔트리에 대한 디스크 동기화는 보장하지 않는다. 파일의 링크가 최근에 갱신되었고 파일 데이터도 디스크에 제대로 기록되었지만 관련된 디렉터리 엔트리가 디스크에 기록되지 않았을 경우에는 그 파일에 대한 접근이 불가능하다. 디렉터리 엔트리 역시 디스크에 강제로 기록하려면 디렉터리 자체를 대상으로 연 파일 디스크립터를 fsync()에 인자로 넘겨야 한다.

sync()

최적화는 조금 부족하지만 활용 범위가 넓은 sync() 시스템 콜은 모든 버퍼 내용을 디스크에 동기화한다.

#include <unistd.h>

void sync (void);

이 함수는 인자도 반환하는 값도 없다. 호출은 항상 성공하며 버퍼의 모든 내용을 디스크에 강제로 기록한다.

표준에서는 sync()가 버퍼의 모든 내용을 디스크에 기록한 뒤에 반환하도록 강제하지 않는다. 그냥 모든 버퍼를 디스크에 기록하는 과정을 시작하도록 요구할 뿐이다. 리눅스에서 sync()는 모든 데이터를 안전하게 저장하기 위해 기다린다. 따라서 한번의 sync() 호출이면 충분하다.

O_SYNC 플래그

open() 호출 시 O_SYNC 플래그를 사용하면 모든 파일 입출력은 동기화된다.

int fd;

fd = open (file, O_WRONY | O_SYNC);
if (fd == -1){
	perror("open");
    return -1;
}

읽기 요청은 언제나 동기화된다. 그렇지 않다면 읽기 요청을 결과로 저장된 데이터가 유효한지 확인할 수 있는 방법이 없다. 하지만 앞에서 얘기한 것처럼 write() 호출은 보통 동기화되지 않는다. 호출이 반환된느 것과 데이터가 기록되는 것에는 아무런 관련성이 없다. O_SYNC 플래그는 write()가 파일을 기록하는 작업이 동기화되도록 해준다.

O_SYNC는 write()가 작업 후 반환하기 직전에 fsync()를 매번 호출하는 방식이라고 이해해도 좋다. 실제로는 리눅스 커널에서 좀 더 효율적인 방식으로 O_SYNC()를 구현하고 있지만 의미는 동일하다.

O_SYNC()는 사용자 영역과 커널 영역에서 소모되는 시간을 조금씩 늘리는 결과를 가져온다. 게다가 디스크에 쓴 파일 크기에 따라 전체 소요 시간이 수십 배로 늘어나기도 한다. 모든 입출력 레이턴시는 프로세스에 의해 초래되기 때문이다. 이렇게 입출력 동기화는 들어가는 비용이 매우 크기 때문에 가능한 다른 대안을 모두 적용한 다음 최후의 선택으로 사용해야 한다.

0개의 댓글