1. Buffer(버퍼)를 사용하는 이유

더보기
  • 버퍼는 여러 입출력 장치간에 데이터를 읽고 쓰는 동안 발생하는 속도 차이를 해결한다.
  • 1,000 바이트를 읽는다고 가정할 때, 1바이트를 여러 번 읽을 경우 CPU 사용횟수와 메모리 접근시 평균 탐색시간이 누적되어 한 번에 1000 바이트를 읽는 것 보다 시간이 오래 걸린다.
    • 1바이트씩 1,000번 읽기
    • 1,000바이트씩 1번 읽기
  • 보통 8KB(8192Byte) 정도의 버퍼 크기를 유지한다.
#include <stdio.h>
#include <time.h> // 시간 측정을 위해 포함

#define FILE_SIZE 1048576// 테스트 파일 크기 (1048576 바이트 = 1MB)
#define BUFFER_SIZE 4096 // 버퍼의     크기 (4096 바이트 = 4KB) // 16384 

// 방법 1: 한 글자씩 읽기 (비효율적인 방법)
void read_one_by_one(const char *filename)
{
    FILE *fp = fopen(filename, "r");
    if (fp == NULL)
    {
        printf("파일 열기 실패!\n");
        return;
    }

    // 파일의 끝(EOF)에 도달할 때까지 한 글자씩, FILE_SIZE 만큼 (10만 번 이상) 실행
    while (fgetc(fp) != EOF)
    {
        // 아무 작업도 하지 않아도, 반복문 자체가 계속 실행되는 비용이 든다.
    }

    fclose(fp);
}

// 방법 2: 버퍼를 사용해 덩어리로 읽기 (효율적인 방법)
void read_with_buffer(const char *filename)
{
    FILE *fp = fopen(filename, "r");
    if (fp == NULL)
    {
        printf("파일 열기 실패!\n");
        return;
    }

    // 8KB 크기의 바구니(버퍼)를 준비한다.
    char buffer[BUFFER_SIZE];

    // 데이터를 읽어 8KB 덩어리로 버퍼에 담아, 더 이상 읽을 데이터가 없을 때까지 실행
    // 이 반복문은 (100KB / 8KB) = 약 13번만 실행된다.
    while (fread(buffer, 1, BUFFER_SIZE, fp) > 0)
    {
        // 바구니를 채우기만 하고 다른 작업은 하지 않는다.
    }

    fclose(fp);
}

int main()
{
    const char* test_file = "my_test_file.txt";

    // --- 준비: 테스트를 위한 더미(dummy) 파일 생성 ---
    printf("테스트를 위해 %dKB (%dMB) 크기의 파일을 생성합니다...\n", FILE_SIZE / 1024, FILE_SIZE / 1024 / 1024);
    FILE* fp = fopen(test_file, "w");
    for (int i = 0; i < FILE_SIZE; i++) {
        fputc('A', fp);
    }
    fclose(fp);
    printf("파일 생성 완료!\n\n");
    // --------------------------------------------------


    printf("--- 파일 읽기 속도 비교 시작 ---\n");

    // --- 테스트 1: 한 글자씩 읽기 ---
    clock_t start = clock();
    read_one_by_one(test_file);
    clock_t end = clock();
    printf("방법 1 (한 글자씩): %.6f 초\n", (double)(end - start) / CLOCKS_PER_SEC);

    // --- 테스트 2: 버퍼로 덩어리째 읽기 ---
    start = clock();
    read_with_buffer(test_file);
    end = clock();
    printf("방법 2 (버퍼 사용): %.6f 초\n", (double)(end - start) / CLOCKS_PER_SEC);

    printf("------------------------------\n\n");
    
    // 테스트가 끝난 파일을 지워줍니다.
    remove(test_file);

    return 0;
}
테스트를 위해 1024KB (1MB) 크기의 파일을 생성합니다...
파일 생성 완료!

--- 파일 읽기 속도 비교 시작 ---
방법 1 (한 글자씩): 0.001468 초
방법 2 (버퍼 사용): 0.000138 초
------------------------------

 

 

방법 비유 반복 횟수 (100KB 파일 기준)
fgetc 맨손으로 물건 1개씩 나르기
진열대와 계산대를 1번씩 계속 왕복
약 102,400 번
fread (8KB 버퍼) 진열대에서 물건을 카트에 가득 채웁니다.
쇼핑 카트에 가득 담아 8천 개씩 나르기
약 13 번

 

 

2. Buffer(버퍼)란?

더보기
  • 영어로 '완충제' 이다.
  • Buffer(버퍼)는 스트림과 같은 데이터의 가상 연결 통로에서
    데이터를 읽고 쓰는 동안 발생하는 속도 차이를 해결하기 위해
    데이터가 임시 저장되는 메모리의 물리적 공간을 의미하는 용어다.

 

3. Buffer 사용 예시

더보기

3.1 버퍼를 사용한 화면 출력

 

입력 버퍼: 사용자와 저장장치의 입력은 프로그램의 연산 속도보다 훨씬 느리다.

프로그램이 이러한 입력의 완료를 대기하는 것은 낭비가 심하다.

 

출력 버퍼: 모니터의 출력은 프로그램의 연산 속도보다 매우 느리다.

프로그램이 모니터 출력 완료를 대기하는 것 또한 엄청난 낭비다.

*예) 컴퓨터 게임에서 화면 주사율 딜레이 및 수직 동기화 백버퍼, 프론트 버퍼 이슈가 발생하는 경우

 

: 프로그램의 연산 속도는 입력과 출력 시간에 비해 빠르다.

그래서 입력 버퍼와 출력 버퍼를 사용해 딜레이 문제를 보완한다.

 

 

3.2 Stream 입출력 기반 버퍼 동작 예시 

 

 

계층 핵심 역할 주요 기술
1. 애플리케이션
(사용자 공간)
고수준의 쉬운 인터페이스 제공
C 표준 라이브러리 (stdio), FILE 구조체,
사용자 버퍼링
2. 운영체제 커널
(커널 공간)
장치 추상화 및 데이터 중개
시스템 콜 (System Call), 파일 서술자 (File Descriptor),
커널 버퍼 캐시
3. 디바이스 드라이버
(커널 공간)
하드웨어 제어 신호로 변환
특정 하드웨어(그래픽카드, 디스크 등)를 위한 소프트웨어 인터페이스
4. 하드웨어 물리적 데이터 입출력 수행
CPU, 메모리, 그래픽 카드, 모니터 등 물리적 장치

 

1단계: 애플리케이션 (사용자 공간) - 버퍼링

  1. 함수 호출: 프로그래머가 printf("hello"); 코드를 실행합니다. printf는 C 표준 라이브러리(stdio.h)에 속한 함수입니다.
  2. 사용자 버퍼(User Buffer)에 데이터 쓰기: printf 함수는 "hello"라는 데이터를 즉시 화면(하드웨어)으로 보내지 않습니다. 대신, C 라이브러리가 관리하는 메모리 공간인 **'사용자 버퍼'**에 데이터를 복사합니다. 이 버퍼는 FILE 구조체 내에 존재하며, stdout 스트림에 할당된 메모리입니다.
    • 왜 버퍼를 사용할까? 시스템 콜(System Call, 커널에 요청하는 것)은 비용이 매우 비싼 작업입니다. 한 글자씩 출력할 때마다 커널을 호출하면 엄청난 성능 저하가 발생합니다. 버퍼에 데이터를 모았다가 한 번에 전달하면 시스템 콜 횟수를 획기적으로 줄여 효율을 극대화할 수 있습니다.

 

2단계: 버퍼 플러시(Flush) 및 시스템 콜 (사용자 공간 → 커널 공간)

  1. 플러시 조건 충족: 사용자 버퍼에 있는 데이터는 특정 조건이 되면 커널로 전달(Flush, 플러시)됩니다.
    • 버퍼가 가득 찼을 때
    • \n (개행 문자)를 만났을 때 (표준 출력의 기본 버퍼링 방식은 라인 버퍼링)
    • 프로그램이 종료될 때
    • fflush() 함수로 강제 플러시를 호출했을 때
  2. 시스템 콜(System Call) 호출: 플러시 조건이 되면, C 표준 라이브러리는 운영체제 커널에게 "이 데이터를 처리해줘"라고 요청하는 시스템 콜을 호출합니다. printf의 경우, 내부적으로 write() 시스템 콜이 사용됩니다.
    • 예: write(1, "hello", 5);
    • 1: 파일 서술자(File Descriptor). 커널이 열려 있는 파일(장치)을 식별하는 정수 값입니다. 0은 표준 입력(stdin), 1은 표준 출력(stdout), 2는 표준 에러(stderr)로 약속되어 있습니다.
    • "hello": 전달할 데이터의 메모리 주소.
    • 5: 데이터의 길이(바이트).

이 시스템 콜이 호출되는 순간, 프로그램의 제어권은 사용자 모드(User Mode)에서 커널 모드(Kernel Mode)로 전환됩니다.

 

3단계: 커널 (커널 공간) - 데이터 처리

  1. 시스템 콜 처리: 커널은 write() 요청을 받고, 파일 서술자 1이 어떤 장치와 연결되어 있는지 확인합니다. 현재는 터미널(가상 터미널, 예: /dev/pts/0)에 연결되어 있습니다.
  2. 커널 버퍼 캐시: 커널은 데이터를 즉시 하드웨어로 보내지 않고, 자체적인 커널 버퍼에 데이터를 복사할 수 있습니다. (특히 디스크 I/O의 경우 성능 향상을 위해 필수적)
  3. 적절한 디바이스 드라이버 호출: 커널은 파일 서술자 1에 연결된 장치가 '터미널'이라는 것을 알고 있으므로, 수많은 하드웨어 제어 프로그램 중 **터미널 디바이스 드라이버(Terminal Device Driver)**를 호출하고 데이터를 전달합니다.

 

4단계: 디바이스 드라이버 및 하드웨어 (커널 공간 → 하드웨어)

  1. 하드웨어 제어: 디바이스 드라이버는 커널로부터 받은 데이터를 실제 물리적 하드웨어가 이해할 수 있는 신호로 변환하여 전달하는 역할을 합니다.
    • 터미널 드라이버는 그래픽 카드 드라이버와 통신합니다.
    • 그래픽 카드 드라이버는 "hello"라는 텍스트에 해당하는 픽셀 정보를 계산합니다.
  2. 물리적 출력: 최종적으로 그래픽 카드는 계산된 픽셀 정보를 비디오 메모리(VRAM)에 쓰고, 모니터 컨트롤러는 이 정보를 읽어 화면에 전기적 신호를 보내 'h', 'e', 'l', 'l', 'o' 라는 글자를 물리적으로 출력합니다.
  3. 작업 완료 및 제어권 반환: write 시스템 콜의 모든 과정이 끝나면, 커널은 작업 결과를 애플리케이션에 반환하고, CPU 제어권은 다시 커널 모드에서 사용자 모드로 전환되어 프로그램의 다음 코드를 실행하게 됩니다.

 

4. Buffer 적용 예시

더보기

개발자가 소스코드 레벨에서 버퍼를 직접 구현하는 것도 한가지 방법이지만, 프로그래밍 언어가 제공하는 '표준 입출력 라이브러리'를 사용하면,  내부에 버퍼링 구현체를 손쉽게 사용 할 수 있다.

 

 

C언어, 표준 입출력 stream

  • C언어, <stdio.h> 라이브러리를 사용해 표준 입출력을 사용한다.
    printf( ) - 스트림과 연결된 버퍼 구현체 - stdout stream  
    scanf( ) - 스트림과 연결된 버퍼 구현체 - stdin stream 

  • printf("Hello") 수행 시
    • stdout 스트림에 "Hello" 전달
    • 버퍼에 저장됨 (즉시 출력되지 않음)
    • 개행(\n), fflush(), 버퍼가 가득 찼을 때 → 화면으로 출력

 

C++, 표준 입출력 stream

  • C++, <iostream> 라이브러리를 사용해표준 입출력을 사용한다.
    cin 객체
    cout 객체

 

C/C++ 언어의 표준 입출력 라이브러기 기반  함수들은 내부적으로 입출력 버퍼를 사용하여 데이터를 처리한다. 

표준 스트림을 사용하는 입출력 버퍼 구현체는 겉으로 드러나지 않는다. 

다음과 같은 buffer 구현체는 표준 입출력 라이브러리가 사용하는 stdin, stdout, stderr 스트림의 버퍼가 아니다.

 

char buffer[100];
gets_s(buffer, sizeof(buffer));

 

5. "buffer 를 비운다"는 의미

더보기

입력 버퍼 사용 관점에서,

사용자가 데이터를 입력하면, 프로그램으로 바로 전달되는 것이 아니라 우선 입력버퍼에 데이터가 저장된다.

그리고 엔터 키를 치거나, 개행문자(\n)가 입력버퍼로 들어오거나, 입력버퍼가 가득차면,

버퍼를 비우면서 데이터를 프로그램으로 전달한다. ​

 

출력 버퍼 관점에서

프로그램에서 출력장치(모니터 등)로 데이터가 바로 전달되지 않고 우선 출력버퍼에 데이터가 저장된다.

그리고 개행문자(\n)가 출력버퍼로 들어오거나, 출력버퍼가 가득차면 버퍼를 비우면서 데이터를 출력장치로 전달한다. ​ ​

 

"buffer 를 비운다" 는 의미는, "버퍼에 저장된 데이터가 버퍼를 떠나 목적지로 전송된다"는 의미에 가깝다.

 

6. 버퍼 사이즈 조절 예시

더보기
setvbuf(파일포인터, 사용자지정입출력버퍼, 모드, 크기);
int setvbuf(FILE *_Stream, char *_Buffer, int _Mode, size_t _Size);
// 설정 변경에 성공하면 0을 반환, 실패하면 0이 아닌 값을 반환

 

예) 문장이 한번에 출력된다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    printf("How");
    sleep(1);
    printf(" are");
    sleep(1);
    printf(" you?");
    sleep(1);
    printf("\n");  // 문장이 쌓인 후 한번에 출력된다.
    exit(0);
}

 

예2) 문장이 버퍼에 쌓이는 경우 비교

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 1024

int main()
{
    // 1. 버퍼 없음 (즉시 출력)
    setbuf(stdout, NULL);
    printf("[무버퍼링] ");
    printf("버퍼에"); sleep(1);
    printf(" 쌓이지"); sleep(1);
    printf(" 않는다."); sleep(1);
    printf("\n");

    // 2. 버퍼 있음 (출력 지연됨)
    char buf[BUFFER_SIZE];
    setbuf(stdout, buf); // 사용자 버퍼 설정

    printf("\n[버퍼링] ");
    printf("문장이!"); sleep(1);
    printf(" 버퍼에"); sleep(1);
    printf(" 쌓인다.!!\n"); sleep(1);

    // 버퍼에 출력이 쌓여 있었기 때문에 아직 아무것도 화면에 보이지 않음
    // 이제 강제로 출력
    fflush(stdout); // 출력 강제

    // 3. 다시 무버퍼링
    setbuf(stdout, NULL);
    printf("\n[다시 무버퍼링] ");
    printf("How"); sleep(1);
    printf(" are"); sleep(1);
    printf(" you?"); sleep(1);
    printf("\n");

    return 0;
}