blog.stackframe.dev

fallocate 함수 사용법과 예제

fallocate()는 리눅스 전용의 파일 공간 할당에 관련된 작업을 하는 함수이다. 블록 할당, 해제, 초기화 등 여러가지 작업을 할 수 있지만 그만큼 사용하는 방법도 복잡해지기 때문에 정리한다.

먼저 어떤 결과가 나오는지 확인하기 쉽게 파일의 크기와 실제 할당된 블록 수, 크기를 출력하는 프로그램을 작성했다(stat.c):

#include <stdio.h>
#include <sys/stat.h>

int main(int argc,char *argv[])
{
        int ret = 0;
        long wasted_size = 0;
        struct stat buf;

        if(argc != 2)
                return -1;

        ret = stat(argv[1],&buf);
        if(ret < 0)
                return -1;
        printf("file size: %ld\n",buf.st_size);
        printf("file system IO blocksize: %ld\n",buf.st_blksize);
        printf("512B block count: %ld\n",buf.st_blocks);
        printf("block total size: %ld\n",512 * buf.st_blocks);
        printf("4096B block count: %ld\n",512 * buf.st_blocks / 4096);

        wasted_size = 512 * buf.st_blocks - buf.st_size;
        if(wasted_size < 0)
                wasted_size = 0;
        printf("wasted size: %ld\n",wasted_size);

        return 0;
}

디스크 공간 할당

fallocate 함수의 가장 주된 용도는 디스크에 공간을 할당하는 것이다. 미리 공간을 확보해두는 것으로 추후 데이터를 저장할 때 공간 부족으로 생기는 에러를 예방할 수 있다.

fallocate(int fd, int mode, off_t offset, off_t len);

할당을 위해서는 mode가 0이 되어야 한다. offset은 할당을 어디부터 할 지, len은 얼마큼 할당을 할 지를 정한다. 예시로 아래의 코드를 실행해보자:

#define _GNU_SOURCE

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

int main()
{
        int fd = open("test.file",O_RDWR|O_CREAT,0644);
        if(fd == -1)
                return -1;

        fallocate(fd,0,5000,1234);

        close(fd);
        return 0;
}

위의 코드를 컴파일하고 실행하면 test.file이 생성된다. 이 파일의 속성을 확인하면 아래와 같다:

$ ./stat test.file
file size: 6234
file system IO blocksize: 4096
512B block count: 8
block total size: 4096
4096B block count: 1
wasted size: 0

offset을 5000으로 하고 len을 1234로 하였으니 당연히 파일 크기는 6234가 된다. 그런데 블록 할당을 보면 4096 바이트 크기의 블록 1개만 되어있다는 것을 알 수 있다. 참고로 파일시스템에서는 파일 크기가 얼마나 작든지 상관없이 일정한 크기의 블록이 사용된다. 현재 나의 컴퓨터에서는 그 크기가 4096이다. 그러므로 offset으로 넘어간 앞의 4096 만큼의 크기에는 블록이 할당되지 않았고, 뒤의 1234 크기의 공간을 위해 한 블록인 4096이 할당되었다는 뜻이다.

여기서 이 파일의 맨 앞 1바이트의 값을 변경한다면 추가로 한 블럭이 더 할당 될 것이다:

$ echo -ne \\x6A | dd conv=notrunc bs=1 count=1 of=test.file
1+0 records in
1+0 records out
1 byte copied, 6.3958e-05 s, 15.6 kB/s
$ ./stat test.file
file size: 6234
file system IO blocksize: 4096
512B block count: 16
block total size: 8192
4096B block count: 2
wasted size: 1958

mode 0에 FALLOC_FL_KEEP_SIZE 플래그를 추가로 사용하게 된다면 할당 결과가 파일 크기를 증가시켜야 할 경우에도 파일 크기를 기존의 값으로 계속 유지시킨다. 위의 소스를 조금 수정해서 실행해보자:

...
fallocate(fd,FALLOC_FL_KEEP_SIZE,9000,1234);
...

위에서 봤던 동작대로라면 파일 크기는 10234가 되어야 할 것이다. 하지만 파일을 확인하면 전혀 변하지 않았다:

$ ./stat test.file
file size: 6234
file system IO blocksize: 4096
512B block count: 24
block total size: 12288
4096B block count: 3
wasted size: 6054

파일 크기는 6234로 그대로지만 블록은 하나 더 할당되었다. offset 9000에 len 1234로 할당을 요청했으므로 뒤에 한 블록이 추가된 것이다.

FALLOC_FL_UNSHARE_RANGE라는 플래그도 존재한다. 만약 동일한 블록을 가리키는 동일한 내용의 파일들이 있을 때, 그 중 한 파일에 플래그를 사용하여 fallocate를 수행하면 그 파일은 offsetlen 범위의 공유되는 블록이 다른 블록에 복사되고 새로 만들어진 블록은 그 파일이 독점적으로 사용하게 된다. 현재는 XFS 파일 시스템만 이 플래그를 사용할 수 있으므로 실제로 사용할 일은 딱히 없을 것 같다.

파일 공간 할당 해제

할당이 가능하므로 해제도 가능하다. 할당 해제는 FALLOC_FL_PUNCH_HOLE 플래그를 사용한다. 이 플래그는 무조건 FALLOC_FL_KEEP_SIZE 플래그와 함께 사용해야 한다. 그러므로 어떤 위치의 블록을 해제하더라도 파일 크기는 변하지 않는다.

만약 해제하려는 공간이 블록 단위에 정렬되지 않는다면 그냥 0으로 초기화가 된다:

#define _GNU_SOURCE

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

int main()
{
        int fd = open("test2.file",O_RDWR|O_CREAT|O_TRUNC,0644);
        if(fd == -1)
                return -1;

        for(int i = 0;i < 6000;i++)
                write(fd,"a",1);

        fallocate(fd,FALLOC_FL_PUNCH_HOLE|FALLOC_FL_KEEP_SIZE,4095,10);

        close(fd);
        return 0;
}

6000바이트만큼 a 문자를 저장하고 두 블록이 겹치는 4095 부터 10 바이트만큼만 해제를 요청했다. 그 결과 파일의 내용은 아래와 같이 되었다:

00000fe0: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
00000ff0: 6161 6161 6161 6161 6161 6161 6161 6100  aaaaaaaaaaaaaaa.
00001000: 0000 0000 0000 0000 0061 6161 6161 6161  .........aaaaaaa
00001010: 6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa

또한 어떤 블록도 해제되지 않았다:

$ ./stat test2.file
file size: 6000
file system IO blocksize: 4096
512B block count: 16
block total size: 8192
4096B block count: 2
wasted size: 2192

만약 범위에 완전히 포함되는 블록이 있다면 그 블록은 할당이 해제될 것이다:

#define _GNU_SOURCE

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

int main()
{
        int fd = open("test2.file",O_RDWR|O_CREAT|O_TRUNC,0644);
        if(fd == -1)
                return -1;

        for(int i = 0;i < 10000;i++)
                write(fd,"a",1);

        fallocate(fd,FALLOC_FL_PUNCH_HOLE|FALLOC_FL_KEEP_SIZE,3500,5000);

        close(fd);
        return 0;
}
$ ./stat test2.file
file size: 10000
file system IO blocksize: 4096
512B block count: 16
block total size: 8192
4096B block count: 2
wasted size: 0

3개의 블록 중 중간에 완전히 포함된 블록이 할당 해제되었다.

파일 공간 잘라내기

man 페이지에는 Collapsing file space라고 되어있는데 한국어로 어떻게 적는게 괜찮을지 고민을 좀 했다. 고민 끝에 잘라내기로 정했는데, 이 모드는 우리가 흔히 아는 Ctrl + X처럼 그 범위에 있는 부분을 빼버리고 뒤에 남은 부분을 이어 붙인다. 대신 Ctrl + X와는 달리 임시로 저장되지 않고 날려버린다는 차이점이 있다.

이 모드는 사용하는 파일 시스템에 따라 offsetlen에 들어가는 값이 파일 시스템의 블록 크기의 배수가 되어야 한다는 약간 까다로운 조건이 있을 수 있다. 적어도 ext4를 사용한다면 해당 조건을 지켜줘야 한다. 만약 이것에 맞지 않게 인자를 넘기면 -1이 리턴되고 errnoEINVAL 값이 설정된다.

또한 offset + len이 파일의 끝에 딱 맞거나 넘어가는 경우에도 EINVAL이 돌아온다. man 페이지에는 이런 경우엔 ftruncate()를 대신 사용하라고 한다. 실제로 저 경우는 그냥 파일 크기를 줄이는 것이니 ftruncate()가 더 알맞다.

#define _GNU_SOURCE

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main()
{
        int fd = open("test3.file",O_RDWR|O_CREAT|O_TRUNC,0644);
        if(fd == -1)
                return -1;

        for(int i = 0;i < 4096 * 3 + 1234;i++)
                write(fd,"a",1);

        int ret = fallocate(fd,FALLOC_FL_COLLAPSE_RANGE,4096,4096);
        if(ret == -1)
        {
                if(errno == EINVAL)
                        printf("errno: EINVAL\n");
                else
                        printf("errno: %d\n",errno);
        }

        close(fd);
        return 0;
}
$ ./stat test3.file
file size: 9426
file system IO blocksize: 4096
512B block count: 24
block total size: 12288
4096B block count: 3
wasted size: 2862

두번째 블록이 할당 해제되었다.

파일 공간 0으로 초기화

이 모드는 FALLOC_FL_ZERO_RANGE 플래그를 사용하며 지정한 범위를 0으로 초기화 시킨다. 지정한 범위에 완전히 포함되는 블록은 파일 시스템 상에 unwritten 상태로 변경되기 때문에 직접 0을 기록하는 것이 아니라서 빠르다.

여기서도 FALLOC_FL_KEEP_SIZE 플래그를 사용할 수 있다. 이 플래그를 함께 사용하면 위에서 본 다른 모드와 같이 파일 크기를 변경시키지 않는다.

#define _GNU_SOURCE

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main()
{
        int fd = open("test4.file",O_RDWR|O_CREAT|O_TRUNC,0644);
        if(fd == -1)
                return -1;

        for(int i = 0;i < 4096 * 3 + 1234;i++)
                write(fd,"a",1);

        int ret = fallocate(fd,FALLOC_FL_ZERO_RANGE,2048,8192);
        if(ret == -1)
        {
                if(errno == EINVAL)
                        printf("errno: EINVAL\n");
                else
                        printf("errno: %d\n",errno);
        }

        close(fd);
        return 0;
}
debugfs:  dump_extents test4.file
Level Entries       Logical            Physical Length Flags
 0/ 0   1/  4     0 -     0 52097924 - 52097924      1
 0/ 0   2/  4     1 -     1 52097925 - 52097925      1 Uninit
 0/ 0   3/  4     2 -     2 52097926 - 52097926      1
 0/ 0   4/  4     3 -     3 52068452 - 52068452      1

debugfs로 확인해보면 초기화 범위에 완전히 포함된 두번째 블록이 Uninit으로 된 것을 볼 수 있다.

파일 공간 늘리기

위의 파일 공간 잘라내기와 정반대로 offset 위치에 len만큼의 빈 공간을 만들고 원래 그 곳에 있던 데이터는 뒤에 이어 붙이는 모드이다. 중요한 점은 할당은 하지 않는다. FALLOC_FL_INSERT_RANGE 플래그를 사용하며 제약사항도 잘라내기와 동일하게 offsetlen이 블록 크기의 배수가 되어야 하고, offset + len이 해당 파일의 크기보다 작아야 한다. 이 제약사항이 지켜지지 않는다면 EINVAL이 돌아온다.

아래는 알파벳 소문자가 반복되는 13522 바이트 크기의 파일을 만들고 두번째 블록 부분에 한 블록 크기의 공간을 만드는 예제이다:

#define _GNU_SOURCE

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main()
{
        int fd = open("test4.file",O_RDWR|O_CREAT|O_TRUNC,0644);
        if(fd == -1)
                return -1;

        for(int i = 0;i < 4096 * 3 + 1234;i++)
        {
                char c = 0x61 + i % 26;
                write(fd,&c,1);
        }

        int ret = fallocate(fd,FALLOC_FL_INSERT_RANGE,4096,4096);
        if(ret == -1)
        {
                if(errno == EINVAL)
                        printf("errno: EINVAL\n");
                else
                        printf("errno: %d\n",errno);
        }

        close(fd);
        return 0;
}
...
00000ff0: 797a 6162 6364 6566 6768 696a 6b6c 6d6e  yzabcdefghijklmn
00001000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
...
00001ff0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00002000: 6f70 7172 7374 7576 7778 797a 6162 6364  opqrstuvwxyzabcd
...

두번째 블록의 내용이 0으로 나오며 첫번째 블록과 세번째 블록의 알파벳이 이어진다.

debugfs 1.46.2 (28-Feb-2021)
Level Entries       Logical            Physical Length Flags
 0/ 0   1/  2     0 -     0 22038132 - 22038132      1
 0/ 0   2/  2     2 -     4 22038129 - 22038131      3

debugfs로 보면 두번째 블록이 할당되어 있지 않다.

댓글