더블 버퍼링으로 CPU와 I/O 대기 시간을 겹치는 방법

더블 버퍼링의 동작 방식과 단일 버퍼 대비 차이, 파일 시스템·그래픽스·네트워크·오디오 적용 시 고려할 점을 정리한다.

2026-08-14 · 최초 발행 2026-01-02

CPU와 I/O가 같은 버퍼를 기다리지 않게 하려면

CPU가 데이터를 준비한 뒤 I/O 장치가 전송을 끝낼 때까지 기다리고, 다시 I/O 장치가 비워질 때까지 CPU가 멈추는 구조는 유휴 시간을 만든다. 더블 버퍼링은 독립된 두 버퍼를 번갈아 쓰는 방식으로 이 대기를 겹친다.

CPU는 쓰기 버퍼를 채우고, 채널 또는 I/O 장치는 읽기 버퍼의 데이터를 전송한다. 한쪽 버퍼가 가득 차고 다른 쪽이 비워지면 역할만 바꾼다. 계산과 I/O가 동시에 진행되므로 전체 처리 시간을 줄일 수 있다.

버퍼의 역할은 교환된다

초기에는 버퍼 A를 CPU의 쓰기 공간으로, 버퍼 B를 I/O 장치의 읽기 공간으로 둘 수 있다. 두 버퍼는 같은 크기의 독립된 메모리 영역이며 포인터로 접근한다.

처리 사이클에서는 CPU가 버퍼 A에 데이터를 기록하는 동안 I/O 장치가 버퍼 B를 읽어 전송한다. 버퍼 A가 가득 차고 버퍼 B가 비워진 시점을 확인한 뒤, A는 읽기용으로 B는 쓰기용으로 전환한다. 이후 CPU는 버퍼 B를 채우고 I/O 장치는 버퍼 A를 처리한다.

이 전환은 실제 데이터를 복사하기보다 포인터를 교환하는 방식으로 구현할 수 있다. 접근 경쟁을 제어하려면 쓰기 가능한 버퍼 수를 나타내는 Empty 세마포어, 읽기 가능한 버퍼 수를 나타내는 Full 세마포어, 그리고 버퍼 접근을 보호하는 Mutex를 사용할 수 있다. 버퍼가 가득 찼거나 I/O가 완료됐다는 인터럽트도 교환 시점을 알리는 계기가 된다.

단일 버퍼와 달라지는 시간 구조

단일 버퍼에서는 CPU가 쓴 뒤 I/O가 읽는 순서가 이어진다. 따라서 총 시간은 CPU 시간과 I/O 시간의 합으로 나타나며, 각 작업은 상대 작업의 완료를 기다린다.

더블 버퍼링은 CPU 쓰기와 I/O 읽기를 동시에 수행한다. 총 시간은 CPU 시간과 I/O 시간 중 큰 값에 맞춰지고, 대기 시간을 최소화한다. 처리량은 최대 2배 향상될 수 있다.

단일 버퍼의 실행은 다음처럼 순차적으로 이어진다.

CPU: [====]     [====]     [====]
I/O:      [====]     [====]     [====]
시간: 0---1---2---3---4---5---6

버퍼를 둘로 나누면 두 작업이 겹친다.

CPU: [====][====][====]
I/O: [====][====][====]
시간: 0---1---2---3

I/O 경로부터 화면과 오디오까지

파일 읽기와 쓰기에서는 애플리케이션이 한 버퍼에 데이터를 기록하는 동안 운영체제가 다른 버퍼를 디스크에 기록할 수 있다. 로그 기록에서도 데이터 생성 속도와 디스크 쓰기 속도 차이를 완충해 연속적인 기록을 지원하고 시스템 성능에 미치는 영향을 줄인다.

그래픽스에서는 화면에 표시 중인 프레임을 Front Buffer에 두고, 다음 프레임은 Back Buffer에 렌더링한다. 수직 동기화(VSync) 시점에 버퍼를 교환하면 화면 찢어짐(Tearing)을 막고 애니메이션을 부드럽게 만들 수 있다. 게임과 비디오에서 널리 쓰이는 이유다.

네트워크 통신에서는 네트워크 카드가 데이터를 받는 수신 버퍼와 애플리케이션이 처리하는 버퍼를 분리한다. 역할을 바꾸며 연속 수신을 이어 갈 수 있고, 비디오·오디오 스트림에서는 수신, 디코딩, 재생을 병렬로 진행해 끊김 없는 재생을 목표로 한다.

오디오 처리도 같은 구조를 사용한다. CPU가 오디오 샘플을 만들고 사운드 카드로는 DMA를 통해 전송한다. 버퍼 크기를 조정해 지연 시간을 제어하고 언더런(Underrun)을 막아 안정적인 실시간 출력을 유지한다.

구현에서 확인할 지점

소프트웨어 구현은 쓰기 포인터와 읽기 포인터를 분리하고, 처리 완료 시 두 포인터를 바꾸는 흐름으로 시작할 수 있다.

char buffer_a[BUFFER_SIZE];
char buffer_b[BUFFER_SIZE];
char *write_buffer = buffer_a;
char *read_buffer = buffer_b;

// CPU: write_buffer에 데이터 쓰기
fill_buffer(write_buffer);

// I/O: read_buffer에서 데이터 읽기 (병렬)
flush_buffer(read_buffer);

// 버퍼 교환
swap_buffers(&write_buffer, &read_buffer);

DMA(Direct Memory Access)를 사용하면 CPU가 개입하지 않고 메모리와 I/O 장치 사이에서 전송할 수 있어 CPU 부하를 낮춘다. 전용 I/O 프로세서가 있는 환경에서는 버퍼 관리 자체를 하드웨어에 맡길 수도 있다.

버퍼 크기는 I/O 블록 크기, 가용 메모리, 레이턴시와 처리량의 트레이드오프를 함께 고려해 정한다. CPU가 너무 빠르면 대기하거나 버퍼를 확장해야 하고, I/O가 너무 느리면 버퍼 크기를 늘려야 한다.

메모리와 복잡도를 감수할 이유

더블 버퍼링은 CPU와 I/O 장치를 동시에 활용해 유휴 시간을 줄이고, 응답성과 실시간 처리에 유리한 조건을 만든다. 사용자 입력을 빠르게 처리하고 출력 지연을 낮춰야 하는 경로에서 효과가 크다.

반면 버퍼 메모리는 2배 필요하다. 메모리가 제한된 환경, 특히 임베디드 시스템에서는 부담이 될 수 있다. 버퍼 교환 로직과 동기화가 추가되므로 디버깅도 어려워진다. 두 버퍼를 오가는 과정에서 캐시 미스와 메모리 대역폭 사용이 늘어 일부 환경에서는 성능이 저하될 가능성도 있다.

세 번째 버퍼가 필요한 환경

트리플 버퍼링은 쓰기, 대기, 읽기 역할을 위해 세 개의 버퍼를 사용한다. 버퍼 대기 시간을 더 줄일 수 있으며, 그래픽스에서는 VSync와 성능의 균형 및 최고 프레임률을 위한 선택지가 된다.

대신 메모리 사용량은 3배가 되고, 추가 버퍼 때문에 레이턴시가 증가할 수 있다. 관리 복잡도도 더 높다. 일반적인 I/O 처리, 메모리 제약, 레이턴시가 중요한 상황에는 더블 버퍼링이 맞고, 고성능 그래픽스에서 최대 프레임률이 필요하며 메모리에 여유가 있을 때는 트리플 버퍼링을 검토할 수 있다.

더블 버퍼링운영체제I/O병렬 처리DMA