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를 수행하면 그 파일은 offset과 len 범위의 공유되는 블록이 다른 블록에 복사되고 새로 만들어진 블록은 그 파일이 독점적으로 사용하게 된다. 현재는 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와는 달리 임시로 저장되지 않고 날려버린다는 차이점이 있다.
이 모드는 사용하는 파일 시스템에 따라 offset과 len에 들어가는 값이 파일 시스템의 블록 크기의 배수가 되어야 한다는 약간 까다로운 조건이 있을 수 있다. 적어도 ext4를 사용한다면 해당 조건을 지켜줘야 한다. 만약 이것에 맞지 않게 인자를 넘기면 -1이 리턴되고 errno에 EINVAL 값이 설정된다.
또한 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 플래그를 사용하며 제약사항도 잘라내기와 동일하게 offset과 len이 블록 크기의 배수가 되어야 하고, 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로 보면 두번째 블록이 할당되어 있지 않다.