파일 입출력 - read()

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

read()로 읽기

가장 대표적인 읽기 메커니즘은 POSIX.1에 정의된 read() 시스템 콜을 사용하는 것입니다.

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t len);

read 시스템 콜을 호출할 때마다 fd가 참조하는 파일의 현재 파일 오프셋에서 len 바이트만큼 buf로 읽어 들입니다. 성공하면 buf에 쓴 바이트 만큼 숫자를 반환합니다. 실패하면 -1을 반환하며 errno를 설정합니다. 파일 오프셋은 fd에서 읽은 바이트 크기만큼 전진합니다.

unsigned long word;
ssize_t nr;

nr = read(fd, &word, (unsigned long));
if (nr == -1)
	// 에러

기본 사용법은 간단합니다. 위 예제는 파일 디스크립터로부터 읽어서 word에 저장합니다. 읽은 바이트 수는 unsigned long 타입 크기와 동일합니다. 반환값 nr은 읽은 바이트 숫자이며 오류가 발생했을 경우에는 -1을 반환합니다.

이런 단순한 구현 방법에는 두 가지 문제점이 있습니다. len 바이트만큼 모든 데이터를 읽지 못할 가능성과 점검 후 처리 과정이 빠져 있기 때문에 에러가 발생할 가능성이 있습니다.

반환값

read()가 len보다 작은 양수값을 반환하는 경우도 있습니다. 이런 현상에는 몇 가지 이유가 있는데, len 바이트보다 적은 바이트만 사용 가능하거나, 시그널이 시스템 콜을 중단시키거나, 파이프가 깨지는 등 이유는 다양합니다.

read()를 사용할 때 반환값이 0이 될 또 다른 가능성을 고려해야 합니다. read() 시스템 호출은 파일 끝을 알려주기 위해 0을 반환합니다. 이 경우 당연히 읽을 바이트가 남아 있지 않습니다. EOF는 에러로 취급되지 않으며 단순히 파일 오프셋이 파일에서 마지막으로 유효한 오프셋을 넘어갔기 때문에 더 이상 읽을 데이터가 없다는 사실을 알려줄 뿐입니다.

하지만 파일 끝에 도달한 경우와는 달리, len 바이트만큼 읽으라고 요청했지만 읽을 데이터가 없다면 read() 호출은 읽을 바이트가 생길 때까지 블록(잠자기)됩니다. 이는 EOF를 반환하는 앞선 동작 방식과 다릅니다. 다시 말해 '사용 가능한 데이터가 없음'과 '파일 끝' 사이에는 차이점이 존재합니다. EOF는 파일 끝에 도달했다는 의미가 있습니다.

어떤 에러는 복구가 가능합니다. 예를 들어 한 바이트도 읽기 전에 시그널이 read()를 중단시켰을 경우, read()는 -1을 반환하며 errno를 EINTR로 설정합니다. 이 경우 다시 한 번 읽기 요청이 가능합니다.

사실상 read() 호출은 다음과 같은 다양한 가능성을 가지고 있습니다.

  • 호출이 len과 같은 값을 반환한다.
  • 호출이 len보다 작지만 0보다는 크다. 읽은 바이트는 buf에 저장된다. 시그널이 중간에 읽기를 중단시켰거나, 읽는 도중에 에러가 발생해서 1바이트 이상이지만 len 길이만큼 데이터를 가져오지는 못했거나, len 바이트를 읽기 전에 EOF에 도달한 경우에 발생한다. 다시 read()를 호출하면 남은 바이트를 남은 버퍼 공간에 읽거나 에러 원인을 확인할 수 있다.
  • 0을 반환한다. 이는 EOF를 나타낸다. 더 이상 읽을 데이터가 없다.
  • 현재 사용 가능한 데이터가 없기 때문에 호출이 블록된다. 논블록 모드에서는 이런 상황이 발생하지 않는다.
  • 호출이 -1을 반환하고 errno를 EINTR로 설정한다. EINTR은 바이트를 읽기 전에 시그널이 도착했음을 알려준다. 이럴 때는 다시 호출하면 된다.
  • 호출이 -1을 반환하고 errno를 EAGAIN으로 설정한다. EAGAIN은 현재 읽을 데이터가 없기 때문에 블록된 상태이며 나중에 다시 읽기를 요청을 해야 한다고 알려준다. 논블록 모드일 때만 일어나는 상황이다.
  • 호출이 -1을 반환하고 errno를 EINTR이나 EAGAIN이 아닌 다른 값으로 설정한다. 이는 심각한 에러가 발생했음을 알려준다.

전체 바이트 읽기

앞서 소개한 가능성 때문에 직전에 소개한 간단하면서도 전형적인 read()용법은 에러를 처리하면서 실제로 모든 len 바이트를 읽어야 하는 경우에는 적합하지 않습니다. 이를 가능하게 하려면 루프와 몇 가지 조건문이 필요합니다.

ssize_t ret;

while (len != 0 && (ret = read (fd, buf, len)) != 0) {
	if (ret == -1) {
    	if (errno = EINTR)
        	continue;
        perror("read");
        break;
   }
   
   len -= ret;
   buf += ret;

여기서 소개하는 코드 조각은 다섯 가지 조건 모두를 처리합니다. 루프는 현재 파일 오프셋에서 len 바이트 만큼을 fd에서 읽어서 buf에 기록합니다. 모든 len 바이트를 읽거나 EOF에 도달할 때까지 반복해서 읽습니다. 0보다 크지만 len 바이트보다 적게 읽었다면 len에서 읽은 만큼 바이트 숫자를 빼고 buf에 읽은 만큼 바이트 숫자를 더하여 다시 호출합니다. 호출이 -1을 반환하고 errno가 EINTR이 아니라면 perror()를 호출해서 표준 에러로 설명을 출력한 다음에 루프를 종료합니다.

일부만 읽는 방식은 합법적일 뿐만 아니라 흔히 사용되기도 합니다. 하지만 일부만 읽은 결과를 제대로 점검해서 예외 처리를 잊어버린 프로그래머가 수많은 버그를 만들어냅니다.

논블록 읽기

때때로 프로그래머 입장에서 읽을 데이터가 없을 때 read() 호출이 블록(잠자기)되지 않기를 바라는 경우가 있습니다. 이 경우, 블록되는 대신 읽을 데이터가 없다는 사실을 알려주기 위해 호출이 즉시 반환되는 편을 선호합니다. 이를 논블록 입출력이라고 합니다. 논블록 입출력은 애플리케이션이 잠재적으로 다중 파일 입출력을 수행하도록 만듭니다. 따라서 특정 파일에서 블록되는 바람에 다른 파일에서 사용 가능한 데이터를 놓치는 현상이 벌어지지는 않습니다.

따라서 추가적인 errno 값인 EAGAIN을 점검할 필요가 있다. 파일 디스크립터를 논블록 모드로 열었지만 읽을 데이터가 없다면 read() 호출은 블록되는 대신 -1을 반환하며 errno를 EAGAIN으로 설정합니다. 논블록 읽기를 수행할 때는 EAGAIN을 반드시 점검해야 합니다. 그렇지 않으면 단순히 데이터 부족인 상황에서 심각한 에러가 발생한 듯이 보이는 위험을 감수해야 합니다.

그 외 에러 값

다른 에러 코드는 프로그래밍 에러나 EIO 같은 저수준 문제를 나타냅니다. read() 호출 시에 발생할 수 있는 에러 값은 다음과 같습니다.

  • EBADF: 주어진 파일 디스크립터가 유효하지 않거나 읽기 가능한 모드로 열리지 않았다.
  • EFAULT: buf로 전달된 포인터가 호출하는 프로세스의 주소 공간 밖에 존재한다.
  • EINVAL: 파일 디스크립터가 읽기를 허용하지 않는 객체에 맵핑되어 있다.
  • EIO: 저수준 입출력 에러가 발생했다.

read()크기 제약

POSIX에서는 size_t와 ssize_t 타입을 지원하는데, size_t 타입은 바이트 단위로 크기를 측정하기 위해 사용되는 값을 저장합니다. ssize_t 타입은 부호가 있는(signed) size_t 타입입니다. 32비트 시스템에서 기본 C 타입은 일반적으로 각각 unsigned int와 int입니다. 두 유형은 종종 함께 사용되므로 잠재적으로 범위가 좀 더 작은 ssize_t가 size_t의 범위를 제한합니다.

size_t의 최대값은 SIZE_MAX이며 ssize_t의 최댓값은 SSIZE_MAX입니다. len이 SSIZE_MAX보다 큰 경우 read() 호출 결과는 정의되어 있지 않습니다. 대다수 리눅스 시스템에서 SSIZE_MAX는 LONG_MAX이며 32비트 기계에서는 0x7ffffffff입니다.

len을 0으로 둔 상태에서 read()를 호출하면 즉시 0을 반환합니다.

0개의 댓글