버퍼 입출력 - 표준 입출력 라이브러리

파일 시스템의 최소 저장 단위는 '블록'이라는 추상 개념입니다. 따라서, 모든 입출력 연산은 블록 크기의 정수배에 맞춰서 일어납니다. 단지 1Byte를 읽고싶다하더라도, 512Byte만큼(1블록 = 512Byte라 가정) 읽어와야한다는 얘기입니다. 또는 내가 단지 2.5블록만큼(대략 1250Byte) '쓰기' 연산을 하고싶다하더라도 3블록에 대해 '쓰기' 연산을 해야한다는 얘기입니다.

 

 

그런데 잘 생각해보면 사용자 애플리케이션에서는 512Byte 단위로 입출력 연산이 이루어지지 않는 경우가 대부분입니다. 보통 CSV 파일을 다루기 위해 '필드' 단위의 입출력 연산이 필요하다거나, JSON 파일을 다루기 위해 단순히 '문자열' 단위의 입출력 연산이 필요한 경우가 대부분입니다. 그렇기 때문에 사용자 애플리케이션 코드 레벨에서 인위적으로 버퍼링을 구현하여 사용해야합니다. 그렇다면 항상 사용자 버퍼링을 구현해서 사용해야되냐? 직접 구현하는 것도 좋은 방법이지만 그것보다 더 좋은 해법은 견고하고 뛰어난 사용자 버퍼링 구현체를 가져다 사용하는 것입니다. 가장 인기있고 대중적인 구현체가 바로 '표준 입출력 라이브러리(stdio)'와 '표준 C++ iostream'입니다.

 


표준 입출력 라이브러리

표준 입출력 라이브러리는 플랫폼 독립적인 사용자 버퍼링 해법을 제공합니다. 이는 사용하기 쉬우면서도 강력합니다. 당연한 말이겠지만 애플리케이션이 표준 입출력 라이브러리를 사용할지 시스템콜을 통해 더 저수준으로 작업할지는 애플리케이션의 요구사항과 동작 방식에 따른 개발자의 선택입니다. 먼저 표준 입출력 라이브러리에서 사용되는 용어를 짚고 넘어가겠습니다. 

 

파일 포인터: 표준 입출력은 파일 디스크립터를 직접 다루지 않고 '파일 포인터'라는 개념을 통해 파일에 접근합니다. '파일 포인터'는 파일 디스크립터를 래핑한 개념입니다.

스트림(stream): 표준 입출력에선 열린 파일을 '스트림(stream)'이라고 부릅니다. '읽기' 모드로 파일이 열렸다면 '입력 스트림', '쓰기' 모드로 파일이 열렸다면 '출력 스트림', '읽기/쓰기' 모드로 파일이 열렸다면 '입출력 스트림'입니다.

 

 

표준 입출력 라이브러리의 인터페이스를 살펴보겠습니다

#include <stdio.h>

FILE* fopen(const char* path, const char* mode);

fopen() 함수는 path의 파일을 mode에 따라 원하는 용도로 새로운 스트림을 생성합니다. mode 매개변수로 넘길 수 있는 값은 다음과 같습니다.

  • r     '읽기' 모드로 파일을 오픈합니다. 즉, '읽기 스트림'을 생성합니다.
  • r+   '읽기/쓰기' 모드로 파일을 오픈합니다. 
  • w    '쓰기' 모드로 파일을 오픈합니다. 파일이 이미 존재하면 길이를 0으로 잘라버립니다. 파일이 없다면 새로 생성합니다.
  • w+  '읽기/쓰기' 모드로 파일을 오픈합니다. 파일이 이미 존재하면 길이를 0으로 잘라버립니다. 파일이 없다면 새로 생성합니다. 
  • a    '쓰기' 모드로 파일을 오픈합니다. 파일이 이미 존재한다면 맨 끝에서부터 append합니다. 파일이 없다면 새로 생성합니다.
  • a+  '읽기 쓰기' 모드로 파일을 오픈합니다. 파일이 이미 존재한다면 맨 끝에서부터 append합니다. 파일이 없다면 새로 생성합니다.

 

파일의 스트림을 닫고 싶다면 fclose() 함수를 사용합니다.

#include <stdio.h>

int fclose(FILE* stream);

 

만약, 하나의 스트림이 아닌 현재 프로세스의 모든 스트림을 닫고 싶다면 fcloseall() 함수를 사용합니다. 리눅스에서만 사용이 가능합니다.

#define _GNU_SOURCE
#include <stdio.h>

int fcloseall(void);

 

표준 C 라이브러리는 일반적인 형태부터 흔히 접하기 어려운 형태에 이르기까지 열린 스트림에서 데이터를 읽기 위한 다양한 함수를 구현하고 있습니다. 가장 많이 사용되는 3가지 함수를 살펴보겠습니다.

#include <stdio.h>

int fgetc(FILE* stream);
char* fgets(char* str, int size, FILE* stream);
size_t fread(void* buf, size_t size, size_t nr, FILE* stream);

 

fgetc(): 스트림에서 문자 하나를 읽어들입니다.

fgets(): 스트림에서 하나의 문자열을 읽어들여 str에 저장합니다. 즉, 문자를 읽어들이다가 EOF나 개행문자('\n')를 만나면 읽기를 종료합니다. 이 함수의 특징 중 하나가 개행문자를 만나 읽기가 종료되었다면 str의 마지막에 개행문자를 추가로 저장한다는 것입니다. 그리고 스트림의 위치가 개행문자 다음으로 가기 때문에 다시 호출되면 다음 문자열을 읽어들일 수 있습니다.

fread(): size 크기의 '블록'을 nr개 읽어들여 buf에 저장합니다. 구조체 단위로 데이터를 읽어들이고 싶다면 이 함수를 사용하면 됩니다.

 

여담으로 표준 입출력 함수를 살펴보면 공통된 특징이 있습니다. 스트림을 넘겨주는 매개변수는 가장 마지막에 위치하는 것인데요, 리눅스 시스템콜이 파일 디스크립터를 넘겨주는 매개변수는 항상 첫 번째에 위치해 있는 것과 반대다는걸 알 수 있습니다.

 

위의 읽기 함수와 대응되는 '쓰기' 함수는 다음과 같습니다.

#include <stdio.h>

int fputc(int c, FILE* stream);
int fputs(const char* str, FILE* stream);
size_t fwrite(void* buf, size_t size, size_t nr, FILE* stream);

 

함수의 사용법은 '읽기' 함수와 비슷하니 바로 예제를 살펴보겠습니다.

#include <stdio.h>

int main() {
    FILE* in;
    FILE* out;
    struct meminfo {
        char name[20];
        uint8_t age;
        bool sex;
    } tmp, bob = { "Hong Gil Dong", 100, 1 };
    
    out = fopen("data", "w");
    if (!out) {
        perror("fopen");
        return 1;
    }
    
    if (!fwrite(&bob, sizeof(struct meminfo), 1, out)) {
        perror("fwrite");
        return 1;
    }
    
    if (fclose(out)) {
        perror("fclose");
        return 1;
    }
    
    in = fopen("data", "r");
    if (!in) {
        perror("fopen");
        return 1;
    }
    
    if (!fread(&tmp, sizeof(struct meminfo), 1, in)) {
        perror("fread");
        return 1;
    }
    
    if (fclose(in)) {
        perror("fclose");
        return 1;
    }
    
    printf("name: %s  age: %d  sex: %d", tmp.name, tmp.age, tmp.sex);
    
    return 0;
}

 

표준 입출력에서 스트림의 위치를 조작하고 싶다면 fseek() 함수를 사용합니다. 스트림의 위치는 메모장에서 커서의 위치같은 개념입니다.

#include <stdio.h>

int fseek(FILE* stream, long offset, int whence);

whence는 스트림의 기준 위치를 정하는 매개변수로 3가지 값이 올 수 있습니다.

   1. SEEK_SET: 맨 처음을 기준으로 offset 값을 더해 스트림의 위치를 결정합니다.

   2. SEEK_CUR: 현재 커서의 위치를 기준으로 offset 값을 더해 스트림의 위치를 결정합니다.

   3. SEEK_END: 맨 끝을 기준으로 offset 값을 더해 스트림의 위치를 결정합니다.

 

현재 스트림의 위치를 확인하고 싶다면 ftell() 함수를 사용합니다.

#include <stdio.h>

long ftell(FILE* stream);

 

'Operating System > Linux' 카테고리의 다른 글

[Linux/C] 프로세스 관리  (0) 2019.06.06
[Linux/C] 메모리 맵 파일  (0) 2019.06.05
[Linux/OS] 가상 파일 시스템  (0) 2019.06.04
[Linux/C] 다중 입출력 - poll()  (0) 2019.06.03
[Linux/C] 다중 입출력 - select()  (1) 2019.05.23