본문 바로가기
【Fundamental Tech】/→ 🐧Kernel

블럭 디바이스(Block device) I/O - 3

반응형
* 출처: http://superkkt.com/374


저번 글에 이어서 이번에는 한번에 읽고 쓰는 블럭의 크기에 따라 성능이 어떻게 차이가 나는지를 살펴보겠다. 실험용 코드는 저번 글에서 사용한것을 조금 수정해서 O_SYNC, O_DIRECT, mmap 모드에서 각각 5MB의 데이터를 정해진 블럭크기 단위로 디스크에 쓰기 연산을 하는 코드이다.

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

#define SECTOR_SIZE 512
#define BUF_SIZE (SECTOR_SIZE * 32)
#define DATA_SIZE (1048576UL * 5)

int main(int argc, char **argv)
{
   int i, fd, rv;
   char *addr = NULL;
   void *buf = NULL;
   off_t offset;

   if (argc != 2) {
       fprintf(stderr, "Usage: %s filename\n", argv[0]);
       exit(EXIT_FAILURE);
   }

   /*
    * O_DIRECT로 열린 파일은 기록하려는 메모리 버퍼, 파일의 오프셋, 버퍼의 크기가
    * 모두 디스크 섹터 크기의 배수로 정렬되어 있어야 한다. 따라서 메모리는
    * posix_memalign을 사용해서 섹터 크기로 정렬되도록 해야 한다.
    */
   rv = posix_memalign(&buf, SECTOR_SIZE, BUF_SIZE);
   if (rv) {
       fprintf(stderr, "Failed to allocate memory: %s\n", strerror(rv));
       exit(EXIT_FAILURE);
   }

   fd = open(argv[1], O_RDWR | O_SYNC);
   if (fd == -1) {
       fprintf(stderr, "Failed to open %s: %s\n", argv[1], strerror(errno));
       exit(EXIT_FAILURE);
   }
   for (i = DATA_SIZE; i > 0; i -= BUF_SIZE) {
           if (write(fd, buf, BUF_SIZE) == -1) {
                   fprintf(stderr, "Failed to write %s: %s\n", argv[1], strerror(errno));
                   exit(EXIT_FAILURE);
           }
   }

   printf("O_SYNC..\n");

   fd = open(argv[1], O_RDWR | O_DIRECT);
   if (fd == -1) {
       fprintf(stderr, "Failed to open %s: %s\n", argv[1], strerror(errno));
       exit(EXIT_FAILURE);
   }
   for (i = DATA_SIZE; i > 0; i -= BUF_SIZE) {
           if (write(fd, buf, BUF_SIZE) == -1) {
                   fprintf(stderr, "Failed to write %s: %s\n", argv[1], strerror(errno));
                   exit(EXIT_FAILURE);
           }
   }

   printf("O_DIRECT..\n");

   /*
    * O_DIRECT로 열린 파일을 mmap으로 매핑하는 경우에는 read/write처럼 섹터크기의
    * 배수로 맞춰서 접근하지 않아도 된다.
    */
   addr = mmap(NULL, BUF_SIZE * DATA_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
   if (!addr) {
       fprintf(stderr, "Failed to mmap %s: %s\n", argv[1], strerror(errno));
       exit(EXIT_FAILURE);
   }
   offset = 0;
   for (i = DATA_SIZE; i > 0; i -= BUF_SIZE) {
           memcpy(addr + offset, buf, BUF_SIZE);
           offset += BUF_SIZE;
   }

   printf("mmap..\n");

   munmap(addr, BUF_SIZE);
   free(buf);
   return 0;
}

BUF_SIZE를 512, 4096, 16384바이트로 다르게 하면서 수행시간을 측정해봤다.
512 - 1m22.170s
4096 - 0m10.486s
16384 - 0m2.805s

한번에 기록하는 블럭의 크기가 커질수록 수행속도가 상당히 빨라졌다. 블럭의 크기가 작은 경우 상대적으로 더 많은 시스템콜을 호출해야 하기 때문에, 즉 더욱 많은 I/O 요청이 발생하기 때문에 속도가 느린것이다.

그리고 실험용 코드를 동일한 횟수의 루프를 돌면서 일정한 크기의 데이터를 기록하도록 바꾸고 블럭 사이즈를 바꿔가면서 실험을 해보면 수행 시간에 큰 차이가 없다. 이는 동일한 횟수의 시스템콜이 발생하는 상황에서 한번에 기록하는 데이터의 크기가 커지더라도 큰 성능 저하가 없다는 뜻이다. 하드디스크의 특성상 데이터를 기록할 섹터를 찾은 후에는 전자기적으로 기록을 하기 때문에 한번에 많은 양을 기록할 수 있다.

그러나 무작정 블럭의 크기를 키운다고 끊임없이 성능이 좋아지는건 아니다. 최상의 성능을 내는 임계값은 블럭 디바이스의 종류와 OS에 따라 조금씩 다르겠지만 내가 실험해본 환경에서는 64KB가 최대값으로 측정되었다.


지금까지 3편의 글을 통해 살펴본 블럭 디바이스 I/O의 성능을 높이는 방법을 정리하자면,

1. 캐쉬나 버퍼를 사용해서 최대한 디스크 접근 횟수를 줄이고,
2. I/O 요청의 크기는 블럭 디바이스 섹터 크기의 배수(파일시스템을 사용한다면 페이지 사이즈)로 맞추고,
3. 한번에 읽고 쓰는 블럭의 크기는 임계값 내에서 최대한 크게 수행한다.

반응형