2. 파일 읽기 - read()
System/System Programming

2. 파일 읽기 - read()

개요

  저번 포스팅에서는 open(), creat() 시스템 콜을 이용해 파일을 열어보는 것 까지 했다. 파일을 열었으니 이제 읽는 방법에 대해 알아볼 차례이다. 

파일 읽기


read() 시스템 콜

  가장 잘 알려진 파일 읽는 방법은 read() 시스템 콜을 사용하는 것이다. 이는 POSIX.1에 정의되어 있는 메커니즘이기도 하다. read()의 기본적인 형태는 다음과 같다.

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

  read()는 해당 함수가 호출될 때마다 파일 디스크립터(이하 fd)가 참조하는 파일의 현재 file offset에서 len 바이트만큼 buf로 읽어들인다. 읽어들이는 작업에 성공하면 buf에 쓴 바이트 숫자를 반환하고, 실패하면 이전 게시글에서 다루었던 open()처럼 -1을 반환하면서 errno를 설정한다. file offset은 fd에서 읽은 바이트 크기(len)만큼 앞으로 나아간다. 파일 디스크립터가 표현하는 객체에 탐색 기능이 없다면, 읽기 작업(read)는 항상 현재 위치에서 일어난다.

  예제를 통해 기본적인 사용법을 알아보자. read()를 사용하기 전에는 반드시 open()을 통해 파일을 열어주는 선행작업이 필요로 한다는 것을 잊지 말자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int main() {
    int fd;
    fd = open("TEXT", O_RDONLY);
 
    if(fd == -1) {
        fprintf(stderr, "Failed to open file.\n");
        exit(0);
    }
    else printf("Successfully opened file.\n");
 
    unsigned long word;
    ssize_t nr;
 
    /* fd에서 몇 바이트를 읽어 'word'에 저장한다. */
    nr = read(fd, &word, sizeof(unsigned long));
    if(nr == -1) {
            fprintf(stderr, "Failed to read file.\n");
            exit(0);
    }
    else printf("Successed reading file.\n");
 
    return 0;
}
cs

  18행 전까지는 이전에 배웠던 내용과 다를 게 없다. 그 다음부터가 read() 시스템 콜을 사용하는 부분이다. fd는 앞에서 파일을 열면서 가져왔다. fd에서 sizeof(unsigned long)만큼의 바이트를 읽어 word에 저장한다. read()의 반환값을 저장한 ssize_t 변수 nr에 저장된 값이 -1이면 read()가 실패했다는 뜻이므로, stderr를 출력하면서 exit한다. 아니라면 성공했다는 뜻이기 때문에 성공했다는 간단한 메시지를 출력한다.

  이렇게 아주 간단한 구현 방법에는 문제점이 존재한다. 첫 번째는 파일에 저장된 데이터가 len바이트보다 클 때, 모든 데이터를 읽지 못할 가능성이 있다는 것이다. 그 다음은 점검 후 처리 과정이 빠져 있기 때문에 에러가 발생할 가능성이 있다. 이렇게 어딘가 부실하게 설계된 코드를 개선해서 사용할 줄 알아야 한다.

반환값


  read()가 len보다 작은 양수값을 반환하는 일은 파일에서 len보다 적은 바이트만 사용 가능하거나, Signal이 시스템 콜을 중단시키거나, 파일 디스크립터가 파이프라면 파이프에 문제가 생기는 등 다양한 원인이 있다. 그렇다면 반환값이 0일 때는 어떤 경우일까? read() 시스템 콜이 EOF를 알려줄 때이다. 즉, 읽을 바이트가 남아 있지 않다는 뜻과 같다. -1을 반환할 때와 다른 점이 있다면 EOF는 에러로 취급되지 않고, 단순히 file offset 이 파일에서 마지막으로 유효한 offset 을 넘어갔다는 상태를 나타내기 때문에 그저 '더 이상 읽을 게 없다'는 정보만을 알려줄 뿐이다.


  EOF와는 달리, len바이트만큼 읽으라고 요청했지만 더 읽을 게 없다면 그건 에러로 취급된다. 이 때 read()는 읽을 바이트가 생길 때까지 block된다. 다시 말해 사용 가능한 데이터가 없는 것과 파일 끝에 도착했다는 것에는 차이가 있다.



  자신이 설계한 코드에서 에러나 버그가 발생한다면 프로그래머 입장에서는 굉장히 착잡할 것이다. 하지면 몇몇 에러는 복구가 가능하니 너무 상심하지는 말자. 예를 들어 읽기도 전에 시그널이 read()를 중단시켰을 경우에도 read()는 -1을 반환하고, errno를 EINTR[각주:1]로 설정하는데, 이 경우에는 한 번 더 읽기 요청을 하는 것이 가능하다. read()는 다양한 가능성을 가지고 있는 시스템 콜이니, 다양한 경우를 고려하여 에러의 원인을 유추할 수 있다.


전체 바이트 읽기

  최소한 EOF까지, 실제로 모든 len 바이트를 읽어야 하는 경우에는 어울리지 않는 방법이 지금까지 다루었던 read()의 사용 방법이다. 그 이유는 앞에서 방금 말했지만 너무 많은 가능성 때문이다. loop와 몇 가지 조건문을 이용해서 코드를 개선해보자.

1
2
3
4
5
6
7
8
9
10
11
12
ssize_t ret;
 
while(len != 0 && (ret = read(fd, buf, len)) != 0) {
    if(ret == -1) {
        if(errno == EINTR) continue;
        perror("Reading Error");
        break;
    }
 
    len -= ret;
    buf += ret;
}
cs

  while문 (loop)에서는 현재 file offset에서 len바이트만큼을 fd에서 읽어 buf에 저장하는 작업을 한다. 모든 len 바이트를 읽거나 파일 끝(EOF)에 도달할  때까지 반복적으로 읽어들이는 것이다. 0보다 크지만 len바이트보다는 적게 읽었다면(아직 다 읽은 상태가 아니라면) len에서 읽은 만큼 바이트 수를 감산한다. 그 다음 buf에 읽은 만큼 바이트 수를 더해서 다시 호출하는 것을 반복한다.

  read()가 -1을 반환하고 errno가 EINTR이라면 인자를 변경하지 않고, 대신 다시 한 번 호출하는 것을 위 코드의 5번째 줄에서 알 수 있다. 반환값이 -1이지만, errno 설정이 EINTR가 아니라면 perror()를 호출해서 루프를 종료한다.

  일부만 읽어들이는 것이 조금 불완전해 보일 수도 있겠지만, 흔히 사용되는 방법이다. 예외 처리는 언제나 중요하니 꼭 잊지 않고 코드를 보완해주자.

논블록 읽기



read() 호출이 블록되지 않았으면 좋겠는데, 어떻게 하지?


  블록되는 대신 읽을 데이터가 없으면 알려주기 위해 호출이 그 즉시 반환되는 상황을 원할 때도 있다. 이러한 것을 논블록 입출력이라고 한다. 이러한 입출력 방식은 애플리케이션이 잠재적으로 다중 파일 입출력을 수행하도록 한다. 따라서 어떤 특정한 파일에서 read() 호출이 블록되면서 다른 파일에서 사용할 수도 있는 데이터를 못 쓰게 되는 현상을 방지해준다는 장점이 있다.


  이 때에는 errno값 EAGAIN을 추가로 점검할 필요가 있다. 파일 디스크립터를 논블록 모드로 열었는데, 읽을 데이터가 없을 때 read() 호출은 블록되는 대신 -1을 반환하며 errno를 EAGAIN으로 설정한다. 논블록 읽기에서 EAGAIN을 점검하지 않는다면 데이터 부족이라는 그냥 간단한 오류가 심각한 에러로 처리되어 알려질 수도 있다. 다음 예제를 살펴보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
char buf[255];
ssize_t nr;
 
start:
    nr = read(fd, buf, 255);
    if(nr == -1) {
        if(errno = EINTR) goto start;
        else if(errno = EAGAIN)
        else {
            fprintf("error");
            exit(0);
        }
    }
cs


  위의 코드는 goto문을 사용해서 상당히 지저분한 편이다. 예제에서 goto start으로 EINTR를 처리하는 방법은 실제로 거의 아무 의미가 없다. 사실 아예 논블록 입출력을 사용하지 않는 편이 낫다고 할 수 있다. 논블록 입출력은 시간을 줄여줄 수는 있지만, 이러한 알고리즘적인 특성으로 인해 루프로 인한 부하는 어쩔 수 없다.

그 외 에러 값

  EINTR, EAGAIN 외에도 에러 값은 굉장히 많다. 그 외의 다른 에러 코드들은 프로그래밍 상의 에러나 EIO[각주:2] 등의 저수준 에러를 표현하는 코드들이다. read() 호출 시 발생할 수 있는 에러 값들은 다음과 같다.

에러 코드

설명

EBADF

 주어진 파일 디스크립터가 유효하지 않거나 읽기 가능한 모드로 열리지 않았다.

EFAULT

 buf로 전달된 포인터가 호출하는 프로세스의 주소 공간 밖에 존재한다.

EINVAL

 파일 디스크립터가 읽기를 허용하지 않는 객체에 맵핑되어 있다.

EIO

 저수준 입출력 에러 발생


read() 크기 제약

  앞에서 몇 번 코드를 짚어보다 보면, 궁금증이 생겼을 것이다. 도대체 size_t는 무엇이고, ssize_t는 뭐지? 자료형인 것 같은데... 이들은 POSIX에서 지원하는 자료형(타입)이다. size_t 타입은 바이트 단위로 크기를 측정하기 위해 사용되는 값을 저장한다. ssize_t는 signed size_t를 뜻하며, 부호가 있는 size_t 값이다. ssize_t에서 음수 값은 에러를 표현하기 위해 사용된다. size_t와 ssize_t는 종종 둘이 함께 사용되기 때문에 잠재적으로 범위가 좀 더 작은 ssize_t가 size_t의 범위를 제한한다.


  size_t의 최댓값은 SIZE_MAX, ssize_t의 최댓값은 SSIZE_MAX라는 상수로 정의되어 있다. len이 SSIZE_MAX보다 큰 경우에서 read() 시스템 콜 호출 결과는 정의되어 있지 않다. 대부분의 리눅스 시스템에서 SSIZE_MAX = LONG_MAXd이며, 32bit에서는 0x7ffffffff이다. 한 번 읽기에는 비교적 큰 값이다... 


  앞에서 언급했던 읽기 루프를 조금 더 개선해서 범용 읽기 루틴으로 만들고 싶다면 다음과 같이 수정해 보자. len을 0으로 둔 상태에서 read()를 호출하면 즉시 0을 반환한다.


1
if(len > SSIZE_MAX) len = SIZE_MAX
cs


  1. 커널 소스에 define되어 있는 상수. 시스템 콜 수행중 인터럽트가 걸려 수행이 중단된 경우를 말한다. (Error + INTeRupt) [본문으로]
  2. Error Handling is Occasionally Correct. [본문으로]

'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
1. 파일 열기 - open(), creat()  (0) 2019.04.11