SquashFS 파일시스템
SquashFS와 EROFS는 이미지 기반 배포에 최적화된 읽기 전용 압축 파일시스템입니다. 본 문서에서는 inode/디렉터리/데이터 블록의 온디스크 배치, 압축 단위와 랜덤 읽기 지연의 상관관계, gzip/lzo/xz/zstd 및 lz4 선택 기준, `mksquashfs`/`mkfs.erofs` 이미지 생성 전략, dm-verity 결합 무결성 모델, Live 시스템·컨테이너·임베디드 펌웨어에서의 성능·용량 절충점을 상세히 설명합니다.
핵심 요약
- 읽기 전용 — 런타임 변경 불가, 무결성/재현성 확보
- 고압축 이미지 — 저장 공간/배포 크기 절감
- 블록 단위 압축 — 필요한 블록만 해제해 랜덤 접근 성능 확보
- 대표 조합 — SquashFS(RO) + OverlayFS(RW)
- 주요 활용 — Live OS, 임베디드 펌웨어, 앱 패키징
단계별 이해
- 읽기 전용 모델 파악
이미지 생성 후 수정하지 않는 배포 모델을 먼저 이해합니다. - 압축 알고리즘 선택
부팅 시간/CPU/용량 제약을 기준으로 gzip/lzo/xz/zstd를 선택합니다. - 실서비스 레이어링
쓰기 요구가 있으면 OverlayFS upperdir를 결합해 운영 모델을 구성합니다. - 복구/업데이트 전략 수립
파일 단위 수정 대신 이미지 교체 방식으로 롤백과 업데이트를 단순화합니다.
개요
SquashFS는 1990년대 후반 Phillip Lougher가 개발한 읽기 전용 압축 파일시스템으로, 높은 압축률과 빠른 랜덤 액세스를 제공합니다.
주요 특징
- 고압축 — gzip, lzo, xz, zstd 압축 알고리즘 지원 (압축률 60~90%)
- 읽기 전용 — 불변성 보장, 무결성 검증 용이
- 블록 단위 압축 — 블록별 압축으로 빠른 랜덤 액세스
- 대용량 지원 — 최대 2^64 bytes 파일시스템 크기
- 커널 내장 — Linux 2.6.29부터 메인라인 커널에 포함
활용 사례
- Live CD/USB — Ubuntu, Fedora 등 대부분의 Live OS
- 임베디드 시스템 — OpenWrt, Buildroot 펌웨어
- 컨테이너 — Docker 이미지 레이어, Snap 패키지
- 게임 콘솔 — PlayStation, Xbox 펌웨어
- 소프트웨어 배포 — AppImage, Flatpak
디스크 레이아웃
파일시스템 구조
Superblock 구조체
struct squashfs_super_block {
__le32 s_magic; /* 0x73717368 "hsqs" */
__le32 inodes; /* inode 개수 */
__le32 mkfs_time; /* 파일시스템 생성 시간 (Unix epoch) */
__le32 block_size; /* 데이터 블록 크기 (기본 128KB) */
__le32 fragments; /* fragment 개수 */
__le16 compression; /* 압축 알고리즘 ID */
__le16 block_log; /* log2(block_size) */
__le16 flags; /* 플래그 (압축 여부, xattr 등) */
__le16 no_ids; /* UID/GID 개수 */
__le16 s_major; /* 버전 major */
__le16 s_minor; /* 버전 minor */
__le64 root_inode; /* 루트 inode 위치 */
__le64 bytes_used; /* 파일시스템 사용 바이트 */
__le64 id_table_start; /* UID/GID 테이블 오프셋 */
__le64 xattr_id_table_start; /* Xattr 테이블 오프셋 */
__le64 inode_table_start; /* Inode 테이블 오프셋 */
__le64 directory_table_start;/* Directory 테이블 오프셋 */
__le64 fragment_table_start; /* Fragment 테이블 오프셋 */
__le64 lookup_table_start; /* Export 테이블 오프셋 */
};
#define SQUASHFS_MAGIC 0x73717368
#define SQUASHFS_MAJOR 4
#define SQUASHFS_MINOR 0
압축 알고리즘
지원 압축 방식
| 알고리즘 | 압축률 | 압축 속도 | 압축 해제 속도 | CPU 사용 | 권장 용도 |
|---|---|---|---|---|---|
| gzip | 중간 (60%) | 빠름 | 빠름 | 낮음 | 범용 |
| lzo | 낮음 (50%) | 매우 빠름 | 매우 빠름 | 매우 낮음 | 임베디드, 실시간 |
| xz | 매우 높음 (80%) | 느림 | 중간 | 높음 | Live CD, 저장 공간 최소화 |
| zstd | 높음 (75%) | 빠름 | 빠름 | 중간 | 최신 권장 (균형잡힌 성능) |
압축 구현
struct squashfs_comp_ops {
void *(*init)(struct squashfs_sb_info *, void *);
void (*free)(void *);
int (*decompress)(struct squashfs_sb_info *, void *,
struct buffer_head **, int, int,
int, int, int);
int id;
char *name;
int supported;
};
/* 압축 알고리즘 등록 */
static const struct squashfs_comp_ops squashfs_comp[] = {
{ zlib_comp_ops }, /* gzip */
{ lzo_comp_ops },
{ xz_comp_ops },
{ lz4_comp_ops },
{ zstd_comp_ops },
};
블록 단위 압축
SquashFS는 파일을 블록(기본 128KB)으로 분할하여 개별 압축합니다. 이를 통해 랜덤 액세스 시 전체 파일을 압축 해제할 필요가 없습니다.
/* 압축 블록 구조 */
┌──────────────┬─────────────────────┐
│ Header (2B) │ Compressed Data │
└──────────────┴─────────────────────┘
↑
Bit 15: Uncompressed flag (1 = 압축 안 됨)
Bit 0-14: Data size
mksquashfs 사용법
기본 생성
# 디렉토리를 SquashFS 이미지로 변환
$ mksquashfs /path/to/source image.sqsh
# 압축 알고리즘 지정
$ mksquashfs source/ image.sqsh -comp zstd
# 블록 크기 지정 (기본 128K)
$ mksquashfs source/ image.sqsh -b 256K
# 압축률 조정 (zstd 레벨 1~22)
$ mksquashfs source/ image.sqsh -comp zstd -Xcompression-level 19
고급 옵션
# 특정 파일/디렉토리 제외
$ mksquashfs source/ image.sqsh -e source/tmp source/*.log
# 소유자 정보 제거 (모든 파일을 root:root로)
$ mksquashfs source/ image.sqsh -all-root
# Fragment 비활성화 (작은 파일 많을 때 압축률 향상)
$ mksquashfs source/ image.sqsh -no-fragments
# Xattr 보존
$ mksquashfs source/ image.sqsh -xattrs
# 멀티코어 활용 (병렬 압축)
$ mksquashfs source/ image.sqsh -processors 8
# 진행 상황 표시 제거
$ mksquashfs source/ image.sqsh -no-progress -quiet
Append 모드 (Incremental)
# 기존 이미지에 추가
$ mksquashfs newfiles/ existing.sqsh
# 기존 파일 삭제 후 추가
$ mksquashfs newfiles/ existing.sqsh -e oldfile.txt
마운트
기본 마운트
# 마운트
$ sudo mount -t squashfs image.sqsh /mnt
# loop 장치 사용
$ sudo mount -o loop image.sqsh /mnt
# 읽기 전용 (명시적)
$ sudo mount -o ro image.sqsh /mnt
# /etc/fstab 항목
/path/to/image.sqsh /mnt/sqsh squashfs ro,loop,defaults 0 0
마운트 옵션
# 압축 해제 스레드 개수 지정
$ sudo mount -o loop,threads=4 image.sqsh /mnt
# 파일 시스템 체크 비활성화 (부팅 속도 향상)
$ sudo mount -o loop,nodev,nosuid image.sqsh /mnt
OverlayFS 통합
SquashFS를 하위 레이어로 사용하고 OverlayFS로 읽기/쓰기 레이어를 추가할 수 있습니다.
# SquashFS (읽기 전용 하위 레이어)
$ sudo mount -t squashfs image.sqsh /mnt/lower
# OverlayFS 마운트
$ sudo mount -t overlay overlay \
-o lowerdir=/mnt/lower,upperdir=/mnt/upper,workdir=/mnt/work \
/mnt/merged
# 이제 /mnt/merged에서 읽기/쓰기 가능
# 변경사항은 /mnt/upper에 저장됨
커널 구현
Inode 캐싱
struct squashfs_inode_info {
u64 start; /* 데이터 블록 시작 오프셋 */
int offset; /* 블록 내 오프셋 */
union {
struct {
u64 fragment_block;
int fragment_size;
int fragment_offset;
u64 block_list_start;
} s1;
struct {
u64 directory_idx_start;
int directory_idx_offset;
int directory_idx_count;
int parent;
} s2;
} u;
struct inode vfs_inode;
};
읽기 경로
/* 파일 읽기 플로우 */
squashfs_readpage()
→ squashfs_read_data()
→ squashfs_decompress()
→ zstd_decompress() / xz_decompress() / ...
→ copy_to_user()
Page Cache 통합
static int squashfs_readpage(struct file *file, struct page *page)
{
struct inode *inode = page->mapping->host;
struct squashfs_sb_info *msblk = inode->i_sb->s_fs_info;
int bytes, i, offset = 0;
u64 block;
/* 블록 위치 계산 */
block = squashfs_read_blocklist(inode, page->index, &offset);
/* 압축 해제 후 페이지 캐시에 저장 */
bytes = squashfs_read_data(inode->i_sb, block, offset,
PAGE_SIZE, NULL, page_address(page));
SetPageUptodate(page);
unlock_page(page);
return 0;
}
성능 최적화
압축 레벨 선택
| 시나리오 | 권장 알고리즘 | 이유 |
|---|---|---|
| Live CD/USB | xz -Xcompression-level 9 | 저장 공간 최소화 (배포 크기 중요) |
| 임베디드 (저사양 CPU) | lzo | 빠른 압축 해제, 낮은 CPU 사용 |
| 컨테이너 이미지 | zstd -Xcompression-level 15 | 압축률과 속도 균형 |
| 게임 에셋 | gzip | 범용성, 안정성 |
블록 크기 최적화
- 큰 블록 (256K~1M) — 압축률 향상, 메모리 사용 증가
- 작은 블록 (64K~128K) — 랜덤 액세스 빠름, 압축률 감소
# 대용량 순차 읽기 (비디오 스트리밍)
$ mksquashfs videos/ image.sqsh -b 1M -comp zstd
# 소규모 랜덤 액세스 (소스 코드)
$ mksquashfs src/ image.sqsh -b 64K -comp lzo
관련 도구
unsquashfs (압축 해제)
# SquashFS 이미지 압축 해제
$ unsquashfs image.sqsh
# 특정 디렉토리만 추출
$ unsquashfs -d output/ image.sqsh /usr/bin
# 파일 목록만 출력
$ unsquashfs -ll image.sqsh
sqfstar (tar 아카이브 변환)
# tar.gz를 SquashFS로 변환
$ sqfstar image.sqsh < archive.tar.gz
# stdin에서 tar 스트림 직접 변환
$ tar -cf - source/ | sqfstar image.sqsh
실전: Docker 레이어
Docker는 내부적으로 SquashFS를 사용하지 않지만, SquashFS로 변환하면 이미지 크기를 크게 줄일 수 있습니다.
# Docker 이미지를 SquashFS로 변환
$ docker export mycontainer | sqfstar myimage.sqsh
# SquashFS 마운트 후 chroot
$ sudo mount myimage.sqsh /mnt
$ sudo chroot /mnt /bin/bash
커널 설정
CONFIG_SQUASHFS=y # SquashFS 파일시스템
CONFIG_SQUASHFS_FILE_DIRECT=y # Direct I/O 지원
CONFIG_SQUASHFS_DECOMP_SINGLE=n # 단일 스레드 압축 해제 (비권장)
CONFIG_SQUASHFS_DECOMP_MULTI=n # 멀티 스레드 (권장 아님, percpu 사용)
CONFIG_SQUASHFS_DECOMP_MULTI_PERCPU=y # Per-CPU 압축 해제 (최적)
# 압축 알고리즘 선택
CONFIG_SQUASHFS_ZLIB=y # gzip
CONFIG_SQUASHFS_LZO=y # LZO
CONFIG_SQUASHFS_XZ=y # XZ (LZMA2)
CONFIG_SQUASHFS_ZSTD=y # Zstandard (권장)
# 추가 기능
CONFIG_SQUASHFS_XATTR=y # Extended Attributes
CONFIG_SQUASHFS_FRAGMENT_CACHE_SIZE=3 # Fragment 캐시 (개수)
문제 해결
마운트 실패
# 오류: unknown filesystem type 'squashfs'
$ sudo modprobe squashfs
# 압축 알고리즘 지원 확인
$ cat /proc/filesystems | grep squashfs
nodev squashfs
$ dmesg | grep -i squashfs
[ 2.345] squashfs: version 4.0 (2009/01/31) Phillip Lougher
손상된 이미지
# Superblock 확인
$ dd if=image.sqsh bs=1 count=4 skip=0 | xxd
00000000: 6873 7173 hsqs
# unsquashfs로 무결성 검증
$ unsquashfs -s image.sqsh
Found a valid SQUASHFS 4:0 superblock on image.sqsh.
Inodes are compressed
Data is compressed
...
EROFS (Enhanced Read-Only File System)
EROFS는 Huawei가 주도해 Linux 메인라인에 병합된 read-only 파일시스템으로, Android 시스템 파티션과 컨테이너 이미지 배포에서 빠른 cold-start와 안정적인 압축 해제를 목표로 설계되었습니다. SquashFS와 같은 읽기 전용 압축 파일시스템이지만, pcluster 기반 압축으로 random read 손실을 최소화합니다.
EROFS vs SquashFS 비교
| 항목 | EROFS | SquashFS |
|---|---|---|
| 주요 타깃 | 모바일/시스템 이미지 | 범용 RO 압축 이미지 |
| random read | pcluster 중심 최적화 | 블록 단위 압축 |
| 운영 패턴 | Android dynamic partition에 적합 | Live CD/임베디드 배포에 강점 |
| 압축 알고리즘 | LZ4/LZMA/Zstd | gzip/lzo/xz/zstd |
| 읽기 지연 | in-place decompress로 최소화 | 블록 캐시로 접근 |
압축 모델과 pcluster
EROFS의 핵심은 logical block과 physical compressed extent를 분리하는 pcluster 모델입니다. 작은 read에도 필요한 압축 덩어리만 해제하도록 메타데이터를 구성합니다.
- LZ4: 빠른 해제 속도 중심 배포에 적합
- LZMA: 공간 절감 우선 시나리오
- Zstd: 균형형 선택지
온디스크 포맷 (Superblock)
struct erofs_super_block {
__le32 magic; /* EROFS_SUPER_MAGIC_V1 */
__le32 checksum;
__le32 feature_compat;
__le32 feature_incompat;
__le64 blocks;
__le32 blkszbits;
__le16 root_nid;
};
읽기 경로
generic_file_read_iter()
-> erofs_read_folio()
-> z_erofs_map_blocks_iter() /* logical -> pcluster 매핑 */
-> z_erofs_decompressqueue_work()
-> z_erofs_lz4_decompress() / z_erofs_zstd_decompress()
커널 설정
CONFIG_EROFS_FS=y
CONFIG_EROFS_FS_ZIP=y
CONFIG_EROFS_FS_ZIP_LZ4=y
CONFIG_EROFS_FS_ZIP_ZSTD=y
CONFIG_EROFS_FS_PCPU_KTHREAD=y
운영 도구 (mkfs.erofs)
# 이미지 생성
$ mkfs.erofs system.erofs ./rootfs
# 압축 알고리즘 지정
$ mkfs.erofs -z lz4hc system.erofs ./rootfs
# 마운트
$ sudo mount -t erofs system.erofs /mnt/erofs
성능 튜닝
| 튜닝 항목 | 선택지 | 권장 시나리오 |
|---|---|---|
| 압축 알고리즘 | LZ4 / LZ4HC / Zstd | 부팅 속도 우선은 LZ4, 용량 우선은 Zstd |
| 클러스터링 정책 | random-friendly / compact | 앱 cold-start가 중요하면 random-friendly |
| CPU/IO 균형 | 압축률 vs 해제비용 | 저사양 CPU는 과도한 고압축 피하기 |
Android/임베디드 배포 파이프라인
# 1) rootfs 준비 및 EROFS 이미지 생성
$ mkfs.erofs -z lz4hc system.erofs ./out/rootfs
# 2) dm-verity 서명 단계 결합
$ veritysetup format system.erofs system.hash
# 3) 부팅 시 read-only 마운트 + OverlayFS 결합
$ sudo mount -t erofs /dev/block/system /mnt/lower
$ sudo mount -t overlay overlay \
-o lowerdir=/mnt/lower,upperdir=/data/upper,workdir=/data/work \
/mnt/rootfs
트러블슈팅
unknown filesystem type 'erofs'가 나오면 커널 설정(CONFIG_EROFS_FS)과 모듈 로드 여부를 먼저 확인하세요.
$ grep EROFS /boot/config-$(uname -r)
$ sudo modprobe erofs
$ dmesg | grep -i erofs
참고자료
- Linux SquashFS Documentation
- SquashFS Official Website
fs/squashfs/— 커널 SquashFS 구현squashfs-tools— mksquashfs/unsquashfs 소스- SquashFS Tools GitHub
- Kernel Docs: EROFS
- Linux Kernel Source: fs/erofs
블록/프래그먼트 레이아웃
SquashFS 이미지는 크게 superblock → compression options → data blocks → metadata blocks → lookup tables의 순서로 배치됩니다. 데이터 블록은 파일 내용을 압축 단위(기본 128KB)로 분할한 것이고, 메타데이터 블록은 inode·디렉터리 엔트리·프래그먼트 정보를 8KB 단위로 압축합니다.
-b 옵션으로 4KB~1MB 범위에서 조정할 수 있습니다. 메타데이터 블록은 항상 8KB 단위입니다.
압축 블록 헤더 포맷
각 데이터 블록은 2바이트 헤더로 시작합니다. 비트 24(MSB)가 설정되면 해당 블록은 비압축 상태이고, 나머지 비트는 블록의 실제 바이트 수를 나타냅니다.
/* 블록 크기 디코딩 매크로 — fs/squashfs/squashfs_fs.h */
#define SQUASHFS_COMPRESSED_SIZE(B) ((B) & ~SQUASHFS_COMPRESSED_BIT)
#define SQUASHFS_COMPRESSED_BIT (1 << 24)
#define SQUASHFS_COMPRESSED_BIT_BLOCK (1 << 15)
/* 메타데이터 블록 헤더 (2 bytes) */
#define SQUASHFS_COMPRESSED_SIZE_BLOCK(B) ((B) & 0x7FFF)
#define SQUASHFS_COMPRESSED_BLOCK(B) (!((B) & SQUASHFS_COMPRESSED_BIT_BLOCK))
/* 데이터 블록 크기 제한 */
#define SQUASHFS_METADATA_SIZE 8192 /* 메타데이터 블록 = 8KB */
#define SQUASHFS_FILE_MAX_SIZE 1048576 /* 데이터 블록 최대 = 1MB */
inode 타입과 온디스크 구조
SquashFS는 파일 유형에 따라 서로 다른 inode 구조체를 사용합니다. 일반 파일은 블록 목록과 프래그먼트 참조를 포함하고, 디렉터리 inode는 디렉터리 테이블의 오프셋을 가리킵니다.
/* inode 타입 열거형 — fs/squashfs/squashfs_fs.h */
enum squashfs_inode_type {
SQUASHFS_DIR_TYPE = 1, /* 기본 디렉터리 */
SQUASHFS_REG_TYPE = 2, /* 일반 파일 */
SQUASHFS_SYMLINK_TYPE = 3, /* 심볼릭 링크 */
SQUASHFS_BLKDEV_TYPE = 4, /* 블록 디바이스 */
SQUASHFS_CHRDEV_TYPE = 5, /* 캐릭터 디바이스 */
SQUASHFS_FIFO_TYPE = 6, /* FIFO */
SQUASHFS_SOCKET_TYPE = 7, /* 유닉스 소켓 */
SQUASHFS_LDIR_TYPE = 8, /* 확장 디렉터리 (인덱스 포함) */
SQUASHFS_LREG_TYPE = 9, /* 확장 일반 파일 (>4GB) */
SQUASHFS_LSYMLINK_TYPE = 10, /* 확장 심볼릭 링크 (xattr) */
};
/* 기본 일반 파일 inode */
struct squashfs_reg_inode {
__le16 inode_type; /* SQUASHFS_REG_TYPE */
__le16 mode; /* 권한 모드 */
__le16 uid; /* UID 인덱스 */
__le16 guid; /* GID 인덱스 */
__le32 mtime; /* 수정 시간 */
__le32 inode_number; /* inode 번호 */
__le32 start_block; /* 첫 데이터 블록 오프셋 */
__le32 fragment; /* 프래그먼트 인덱스 */
__le32 offset; /* 프래그먼트 내 오프셋 */
__le32 file_size; /* 파일 크기 (비압축) */
__le32 block_list[]; /* 가변 길이 블록 크기 배열 */
};
/* 확장 일반 파일 inode (4GB 초과 파일) */
struct squashfs_lreg_inode {
__le16 inode_type; /* SQUASHFS_LREG_TYPE */
__le16 mode;
__le16 uid;
__le16 guid;
__le32 mtime;
__le32 inode_number;
__le64 start_block; /* 64비트 오프셋 */
__le64 file_size; /* 64비트 파일 크기 */
__le64 sparse; /* sparse 블록 수 */
__le32 nlink; /* 하드 링크 수 */
__le32 fragment;
__le32 offset;
__le32 xattr; /* xattr 인덱스 */
__le32 block_list[];
};
디렉터리 엔트리 구조
디렉터리 테이블은 디렉터리 헤더와 디렉터리 엔트리의 반복으로 구성됩니다. 헤더는 같은 메타데이터 블록에 있는 inode 그룹을 묶어 공간을 절약합니다.
/* 디렉터리 헤더 */
struct squashfs_dir_header {
__le32 count; /* 이 그룹의 엔트리 수 - 1 */
__le32 start_block; /* inode 테이블 블록 시작 */
__le32 inode_number; /* 기준 inode 번호 */
};
/* 디렉터리 엔트리 */
struct squashfs_dir_entry {
__le16 offset; /* 메타데이터 블록 내 inode 오프셋 */
__le16 inode_number; /* 기준 대비 inode 번호 차이 */
__le16 type; /* inode 타입 */
__le16 size; /* 이름 길이 - 1 */
char name[]; /* 파일명 (NULL 종료 아님) */
};
프래그먼트 처리
SquashFS의 프래그먼트는 블록 크기보다 작은 파일 데이터를 효율적으로 저장하는 핵심 메커니즘입니다. 파일의 마지막 부분이 블록 크기 미만이면 독립 블록 대신 다른 파일의 잔여 데이터와 함께 하나의 프래그먼트 블록에 묶여 저장됩니다.
/etc 아래 설정 파일이나 소규모 스크립트가 많은 시스템에서 극적인 공간 절감 효과를 보입니다.
/* 프래그먼트 테이블 엔트리 — fs/squashfs/squashfs_fs.h */
struct squashfs_fragment_entry {
__le64 start_block; /* 프래그먼트 블록의 디스크 오프셋 */
__le32 size; /* 압축 크기 (bit 24 = 비압축 플래그) */
__le32 unused; /* 예약 */
};
/* 커널에서 프래그먼트 읽기 — fs/squashfs/fragment.c */
int squashfs_frag_lookup(struct super_block *sb, unsigned int fragment,
u64 *fragment_block)
{
struct squashfs_sb_info *msblk = sb->s_fs_info;
int block, offset, size;
struct squashfs_fragment_entry fragment_entry;
/* 프래그먼트 테이블에서 해당 인덱스의 엔트리를 읽음 */
block = SQUASHFS_FRAGMENT_INDEX(fragment);
offset = SQUASHFS_FRAGMENT_INDEX_OFFSET(fragment);
size = squashfs_read_metadata(sb, &fragment_entry,
&msblk->fragment_index[block], &offset,
sizeof(fragment_entry));
*fragment_block = le64_to_cpu(fragment_entry.start_block);
return le32_to_cpu(fragment_entry.size);
}
프래그먼트 캐시
커널은 자주 접근하는 프래그먼트 블록을 캐싱하여 반복적인 압축 해제를 피합니다. 캐시 크기는 CONFIG_SQUASHFS_FRAGMENT_CACHE_SIZE(기본 3)로 제어됩니다.
/* 프래그먼트 캐시 엔트리 — fs/squashfs/cache.c */
struct squashfs_cache_entry {
u64 block; /* 캐시된 블록의 디스크 오프셋 */
int length; /* 압축 해제 후 데이터 길이 */
int error; /* 오류 코드 */
int pending; /* I/O 진행 중 여부 */
struct squashfs_cache *cache;
void **data; /* 압축 해제된 데이터 페이지 배열 */
struct completion input; /* I/O 완료 대기 */
int num_pages; /* 할당된 페이지 수 */
int refcount; /* 참조 카운트 */
};
/* 캐시 조회 — squashfs_cache_get() 핵심 로직 */
struct squashfs_cache_entry *squashfs_cache_get(
struct super_block *sb,
struct squashfs_cache *cache,
u64 block, int length)
{
/* 1. 캐시에서 동일 block 검색 (히트 시 refcount++ 후 반환) */
/* 2. 미스 시 LRU 엔트리 교체, 새 블록 읽기 + 압축 해제 */
/* 3. completion으로 동시 접근 대기 처리 */
}
| 프래그먼트 관련 설정 | 기본값 | 영향 |
|---|---|---|
CONFIG_SQUASHFS_FRAGMENT_CACHE_SIZE | 3 | 캐시 엔트리 수 (클수록 히트율 향상, 메모리 사용 증가) |
-no-fragments (mksquashfs) | OFF | 프래그먼트 비활성화 → 작은 파일도 독립 블록 할당 |
-always-use-fragments | OFF | 블록 크기 초과 파일의 마지막 블록도 프래그먼트로 처리 |
-no-tailends | OFF | -no-fragments와 동일 효과 (하위 호환) |
압축 알고리즘 비교 심화
SquashFS는 5가지 압축 알고리즘을 지원하며, 각 알고리즘은 압축률·속도·메모리 사용량에서 뚜렷한 특성 차이를 보입니다. 이 절에서는 커널 내부 압축 인터페이스와 각 알고리즘의 정량적 성능 차이를 분석합니다.
/* 압축 해제기 인터페이스 — fs/squashfs/decompressor.h */
struct squashfs_decompressor {
void *(*init)(struct squashfs_sb_info *, void *);
void *(*comp_opts)(struct squashfs_sb_info *, void *, int);
void (*free)(void *);
int (*decompress)(struct squashfs_sb_info *, void *,
struct bio *, int, int,
struct squashfs_page_actor *);
int id;
char *name;
int alloc_buffer; /* 해제 시 버퍼 사전 할당 여부 */
int supported; /* 커널 빌드 시 활성화 여부 */
};
/* 압축 알고리즘 ID — superblock.compression 필드 */
#define ZLIB_COMPRESSION 1
#define LZMA_COMPRESSION 2 /* deprecated, SquashFS 4.0 이전 */
#define LZO_COMPRESSION 3
#define XZ_COMPRESSION 4
#define LZ4_COMPRESSION 5
#define ZSTD_COMPRESSION 6
압축 옵션 구조체
각 압축 알고리즘은 고유한 옵션을 superblock 뒤에 저장할 수 있습니다. Zstd의 경우 압축 레벨을, XZ의 경우 딕셔너리 크기와 필터를 지정합니다.
/* Zstd 압축 옵션 */
struct squashfs_zstd_opts {
__le32 compression_level; /* 1~22 (기본 15) */
};
/* XZ 압축 옵션 */
struct squashfs_xz_opts {
__le32 dictionary_size; /* 딕셔너리 크기 (8KB~1MB) */
__le32 flags; /* BCJ 필터 플래그 (아키텍처별) */
};
/* LZ4 압축 옵션 */
struct squashfs_lz4_opts {
__le32 version; /* LZ4 포맷 버전 */
__le32 flags; /* LZ4 HC 모드 플래그 */
};
압축 알고리즘 정량 비교
아래 벤치마크는 Linux 커널 소스 트리(약 1.2GB)를 128KB 블록 크기로 SquashFS 이미지를 만들었을 때의 결과입니다.
| 알고리즘 | 압축 레벨 | 이미지 크기 | 압축률 | 생성 시간 | 해제 속도 | 메모리 (해제) |
|---|---|---|---|---|---|---|
| LZ4 | 기본 | 658 MB | 45% | 4.2초 | 4,500 MB/s | ~64 KB |
| LZO | 기본 | 598 MB | 50% | 5.8초 | 2,000 MB/s | ~64 KB |
| gzip | -Xcompression-level 9 | 478 MB | 60% | 42초 | 800 MB/s | ~256 KB |
| Zstd | -Xcompression-level 15 | 362 MB | 70% | 28초 | 1,400 MB/s | ~128 KB |
| Zstd | -Xcompression-level 22 | 338 MB | 72% | 185초 | 1,400 MB/s | ~128 KB |
| XZ | -Xcompression-level 9 | 240 MB | 80% | 320초 | 300 MB/s | ~40 MB |
-Xdict-size를 제한하여 메모리 사용을 조절해야 합니다. 예: mksquashfs src/ img.sqsh -comp xz -Xdict-size 32K
OverlayFS 연동 심화
SquashFS의 읽기 전용 특성은 OverlayFS와 결합하여 실용적인 읽기/쓰기 파일시스템으로 확장됩니다. 이 패턴은 Live OS, 임베디드 펌웨어, 컨테이너 이미지 등에서 핵심 배포 모델로 사용됩니다.
OverlayFS 구성 실전
# 기본 OverlayFS + SquashFS 구성
$ sudo mkdir -p /mnt/{lower,upper,work,merged}
# 1. SquashFS 이미지를 읽기 전용으로 마운트
$ sudo mount -t squashfs -o loop,ro rootfs.sqsh /mnt/lower
# 2. OverlayFS로 통합 마운트
$ sudo mount -t overlay overlay \
-o lowerdir=/mnt/lower,upperdir=/mnt/upper,workdir=/mnt/work \
/mnt/merged
# 3. 이제 /mnt/merged에서 읽기/쓰기 모두 가능
$ echo "test" > /mnt/merged/newfile.txt
# → /mnt/upper/newfile.txt에 실제 저장됨
# → /mnt/lower는 변경 없음 (불변)
Copy-up 메커니즘
OverlayFS에서 하위 레이어(SquashFS)의 파일을 수정하면 copy-up이 발생합니다. 파일 전체가 상위 레이어로 복사된 후 수정이 적용됩니다.
/* copy-up 과정 (fs/overlayfs/copy_up.c) */
int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry,
struct path *lowerpath, struct kstat *stat)
{
/* 1. upper에 임시 파일 생성 */
/* 2. lower에서 데이터 복사 (sendfile 사용) */
/* 3. 메타데이터(권한, xattr, 타임스탬프) 복사 */
/* 4. rename으로 원자적 교체 */
/* 주의: 대용량 파일 copy-up은 지연 유발 */
}
Whiteout과 파일 삭제
OverlayFS에서 하위 레이어의 파일을 "삭제"하면 실제로는 상위 레이어에 whiteout 파일이 생성됩니다. 이 특수 파일이 하위 레이어의 해당 파일을 가립니다.
# 삭제 시 whiteout 확인
$ rm /mnt/merged/usr/bin/old-tool
$ ls -la /mnt/upper/usr/bin/
c--------- 1 root root 0, 0 ... old-tool
# 캐릭터 디바이스 0,0 = whiteout 마커
# 디렉터리 삭제 시 opaque 디렉터리
$ rm -rf /mnt/merged/usr/share/old-data/
$ getfattr -d /mnt/upper/usr/share/old-data/
# trusted.overlay.opaque="y" 속성이 설정됨
dm-verity와 무결성 검증
SquashFS + OverlayFS + dm-verity 조합은 읽기 전용 하위 레이어의 무결성을 블록 단위로 검증하는 가장 강력한 보안 모델입니다. Android Verified Boot과 ChromeOS가 이 패턴을 사용합니다.
# === dm-verity + SquashFS 완전 설정 ===
# 1. SquashFS 이미지 생성
$ mksquashfs rootfs/ rootfs.sqsh -comp zstd -Xcompression-level 15
# 2. dm-verity 해시 트리 생성
$ veritysetup format rootfs.sqsh rootfs.verity
VERITY header information for rootfs.sqsh
UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Hash type: 1
Data blocks: 12345
Data block size: 4096
Hash block size: 4096
Hash algorithm: sha256
Salt: abc123...
Root hash: deadbeef0123456789abcdef...
# 3. 부팅 시 dm-verity 장치 생성
$ veritysetup open rootfs.sqsh rootfs-verified rootfs.verity \
deadbeef0123456789abcdef...
# → /dev/mapper/rootfs-verified 장치 생성
# 4. 검증된 장치를 SquashFS로 마운트
$ mount -t squashfs /dev/mapper/rootfs-verified /mnt/lower
# 5. OverlayFS 결합
$ mount -t overlay overlay \
-o lowerdir=/mnt/lower,upperdir=/mnt/upper,workdir=/mnt/work \
/mnt/merged
# 이제 하위 레이어의 모든 블록 읽기는 자동으로
# SHA-256 해시 검증을 거침 (위변조 감지)
/* dm-verity 검증 흐름 */
/*
* 사용자 read() → VFS → OverlayFS → SquashFS
* → squashfs_read_data() → bio submit
* → dm-verity 인터셉트
* → 블록 해시 계산 + 해시 트리 검증
* → 일치: 데이터 전달
* → 불일치: -EIO 반환 (커널 패닉 옵션 가능)
*/
다중 하위 레이어
OverlayFS는 여러 SquashFS 이미지를 하위 레이어로 겹쳐 사용할 수 있습니다. 이는 Docker의 다중 레이어 구조와 동일한 원리입니다.
# 다중 SquashFS 하위 레이어 구성
$ sudo mount -t squashfs base.sqsh /mnt/layer0
$ sudo mount -t squashfs libs.sqsh /mnt/layer1
$ sudo mount -t squashfs app.sqsh /mnt/layer2
# 하위 레이어 순서: 오른쪽이 최상위 (우선순위 높음)
$ sudo mount -t overlay overlay \
-o lowerdir=/mnt/layer2:/mnt/layer1:/mnt/layer0,\
upperdir=/mnt/upper,workdir=/mnt/work \
/mnt/merged
# 파일 탐색 우선순위:
# 1. upperdir (쓰기 레이어)
# 2. layer2 (app.sqsh)
# 3. layer1 (libs.sqsh)
# 4. layer0 (base.sqsh)
Docker overlay2 드라이버와 SquashFS
| 항목 | overlay2 기본 | SquashFS 변환 시 |
|---|---|---|
| 저장 형식 | tar 레이어 → 디렉터리 풀기 | SquashFS 이미지 마운트 |
| 디스크 사용 | 비압축 (원본 크기) | 50~80% 절감 |
| 읽기 성능 | 네이티브 FS 속도 | 압축 해제 오버헤드 |
| 레이어 수 | 최대 128개 | SquashFS 1개로 병합 가능 |
| 무결성 | 별도 검증 필요 | dm-verity 결합 용이 |
SquashFS vs EROFS 비교 심화
SquashFS와 EROFS는 모두 읽기 전용 압축 파일시스템이지만, 설계 철학과 최적화 대상이 다릅니다. SquashFS는 범용 읽기 전용 압축 이미지에 초점을 맞추고, EROFS는 모바일/시스템 이미지에서의 cold-start 지연 최소화에 집중합니다.
상세 기능 비교
| 비교 항목 | SquashFS | EROFS |
|---|---|---|
| 메인라인 병합 | Linux 2.6.29 (2009) | Linux 4.19 (2018) |
| 압축 단위 | 고정 블록 (4KB~1MB) | 가변 pcluster |
| 작은 파일 최적화 | 프래그먼트 패킹 | 인라인 데이터 (tail-packing) |
| 메타데이터 압축 | 예 (8KB 블록) | 아니오 (고정 크기, 비압축) |
| 압축 알고리즘 | gzip, lzo, xz, lz4, zstd | lz4, lzma, zstd, deflate |
| inode 조회 복잡도 | O(log n) — 메타데이터 블록 탐색 | O(1) — NID 기반 직접 접근 |
| NFS 지원 | 예 (Export Table) | 예 (NID 기반) |
| xattr 지원 | 예 | 예 (인라인 + 공유 모두) |
| sparse 파일 | 예 (v4.0+) | 예 |
| FSDAX 지원 | 아니오 | 예 (v5.15+) |
| 멀티 디바이스 | 아니오 | 예 (blob 디바이스) |
| 주요 사용처 | Live OS, 임베디드, Snap, AppImage | Android system, 컨테이너 (containerd) |
| 커널 소스 위치 | fs/squashfs/ | fs/erofs/ |
성능 벤치마크 비교
동일한 루트 파일시스템(Ubuntu 22.04, 약 2GB)을 SquashFS와 EROFS로 각각 만들었을 때의 비교입니다.
| 측정 항목 | SquashFS (zstd) | EROFS (lz4hc) | EROFS (zstd) |
|---|---|---|---|
| 이미지 크기 | 620 MB | 810 MB | 650 MB |
| 마운트 시간 | 12 ms | 4 ms | 5 ms |
| 순차 읽기 (4KB) | 420 MB/s | 680 MB/s | 520 MB/s |
| 랜덤 읽기 (4KB) | 180 MB/s | 450 MB/s | 350 MB/s |
| 앱 cold-start | 280 ms | 120 ms | 150 ms |
| 메모리 사용 (마운트) | ~2 MB | ~1 MB | ~1.5 MB |
mksquashfs 빌드 최적화
mksquashfs는 단순한 이미지 생성 도구를 넘어, 다양한 옵션으로 출력 이미지의 크기·성능·보안을 세밀하게 조절할 수 있습니다. 이 절에서는 실전 빌드 파이프라인에서 자주 사용되는 고급 옵션과 최적화 전략을 설명합니다.
고급 빌드 옵션
# === 재현 가능 빌드 (Reproducible Build) ===
# 동일 입력 → 동일 이미지 (CI/CD, 보안 감사에 필수)
$ mksquashfs rootfs/ image.sqsh \
-comp zstd -Xcompression-level 15 \
-mkfs-time 0 \ # superblock 생성 시간 고정
-all-time 0 \ # 모든 파일 mtime 고정
-all-root \ # 모든 파일 root:root
-nopad \ # 4KB 패딩 제거
-no-exports # NFS export 테이블 제거
# === Pseudo 파일 (소스 없이 특수 파일 생성) ===
$ mksquashfs rootfs/ image.sqsh \
-p "dev/console c 600 0 0 5 1" \ # 콘솔 디바이스
-p "dev/null c 666 0 0 1 3" \ # /dev/null
-p "dev/zero c 666 0 0 1 5" \ # /dev/zero
-p "tmp d 1777 0 0" # sticky bit tmp
# === 정규식 기반 파일 필터링 ===
$ mksquashfs rootfs/ image.sqsh \
-wildcards -e \
"*.pyc" \ # Python 바이트코드 제외
"*.o" \ # 오브젝트 파일 제외
"__pycache__" \ # Python 캐시 디렉터리 제외
".git" # Git 디렉터리 제외
# === 정렬 파일 (소트 우선순위) ===
# 부팅 시 먼저 필요한 파일을 이미지 앞쪽에 배치
$ cat sort.txt
/lib/ld-linux-x86-64.so.2 10000
/lib/libc.so.6 9000
/bin/bash 8000
$ mksquashfs rootfs/ image.sqsh -sort sort.txt
# === dm-verity 결합 ===
$ mksquashfs rootfs/ rootfs.sqsh -comp zstd
$ veritysetup format rootfs.sqsh rootfs.verity
# Root hash: a1b2c3d4e5f6...
# 부팅 시: dm-verity가 모든 블록 읽기를 해시 검증
mksquashfs 옵션 레퍼런스
| 옵션 | 기본값 | 설명 |
|---|---|---|
-comp <algo> | gzip | 압축 알고리즘 (gzip/lzo/lz4/xz/zstd) |
-b <size> | 128K | 데이터 블록 크기 (4K~1M, 2의 거듭제곱) |
-Xcompression-level <n> | 알고리즘별 | 압축 레벨 (gzip:1~9, zstd:1~22, xz:0~9) |
-processors <n> | nproc | 병렬 압축 스레드 수 |
-no-fragments | OFF | 프래그먼트 비활성화 |
-always-use-fragments | OFF | 모든 파일 끝을 프래그먼트로 처리 |
-no-duplicates | OFF | 중복 파일 탐지 비활성화 (빌드 속도 향상) |
-all-root | OFF | 모든 파일 소유자를 root:root로 변경 |
-force-uid <uid> | 원본 | 모든 파일의 UID 강제 지정 |
-force-gid <gid> | 원본 | 모든 파일의 GID 강제 지정 |
-mkfs-time <t> | 현재 | superblock 타임스탬프 고정 |
-all-time <t> | 원본 | 모든 파일의 mtime 고정 |
-nopad | OFF | 4KB 정렬 패딩 제거 |
-no-exports | OFF | NFS export 테이블 생략 |
-no-xattrs | OFF | 확장 속성 제거 |
-sort <file> | 없음 | 파일 배치 우선순위 지정 |
-p <spec> | 없음 | pseudo 파일 생성 (디바이스 노드 등) |
커널 내부 구현 심화
SquashFS 커널 코드는 fs/squashfs/ 디렉터리에 위치하며, 파일시스템 등록에서 데이터 읽기까지 모든 경로가 VFS 인터페이스를 통해 연결됩니다. 이 절에서는 핵심 함수들의 호출 관계와 내부 자료구조를 분석합니다.
squashfs_fill_super() — 마운트 초기화
/* 마운트 시 superblock 초기화 — fs/squashfs/super.c */
static int squashfs_fill_super(struct super_block *sb,
struct fs_context *fc)
{
struct squashfs_sb_info *msblk;
struct squashfs_super_block *sblk;
/* 1. squashfs_sb_info 할당 */
msblk = kzalloc(sizeof(*msblk), GFP_KERNEL);
/* 2. superblock 읽기 (디스크 오프셋 0) */
sblk = squashfs_read_table(sb, SQUASHFS_START,
sizeof(*sblk));
/* 3. 매직 넘버 검증 */
if (le32_to_cpu(sblk->s_magic) != SQUASHFS_MAGIC)
return -EINVAL;
/* 4. 압축 해제기 초기화 */
msblk->decompressor = squashfs_lookup_decompressor(
le16_to_cpu(sblk->compression));
msblk->stream = squashfs_decompressor_setup(sb,
le16_to_cpu(sblk->compression));
/* 5. 룩업 테이블 읽기 (fragment, inode, directory, id, xattr) */
msblk->fragment_index = squashfs_read_fragment_index_table(sb,
le64_to_cpu(sblk->fragment_table_start),
le64_to_cpu(sblk->next_table),
le32_to_cpu(sblk->fragments));
/* 6. 루트 inode 읽기 */
root = squashfs_iget(sb, le64_to_cpu(sblk->root_inode),
SQUASHFS_DIR_TYPE);
sb->s_root = d_make_root(root);
return 0;
}
squashfs_read_data() — 데이터 읽기 핵심
/* 블록 데이터 읽기 + 압축 해제 — fs/squashfs/block.c */
int squashfs_read_data(struct super_block *sb,
u64 block, int length, u64 *next_block,
struct squashfs_page_actor *output)
{
struct squashfs_sb_info *msblk = sb->s_fs_info;
struct bio *bio;
int compressed, offset;
u64 cur_block;
/* 1. 블록 헤더에서 압축 여부와 크기 파악 */
compressed = SQUASHFS_COMPRESSED(length);
length = SQUASHFS_COMPRESSED_SIZE(length);
/* 2. bio 구성 및 디스크 읽기 */
bio = bio_alloc(GFP_NOIO, ...);
submit_bio_wait(bio);
/* 3. 압축된 경우 decompressor 호출 */
if (compressed) {
struct decompress_io *stream =
squashfs_decompressor_get(msblk);
res = msblk->decompressor->decompress(
msblk, stream, bio, offset, length, output);
squashfs_decompressor_put(stream);
} else {
/* 비압축: 직접 복사 */
squashfs_bio_read(bio, offset, length, output);
}
return res;
}
압축 해제 동시성 모드
SquashFS는 세 가지 압축 해제 동시성 모드를 제공합니다. 성능과 메모리 사이의 트레이드오프를 환경에 맞게 선택할 수 있습니다.
| 모드 | 커널 설정 | 동작 | 적합 환경 |
|---|---|---|---|
| Single | CONFIG_SQUASHFS_DECOMP_SINGLE | 전역 mutex 1개, 직렬 해제 | 단일 CPU 임베디드 |
| Multi | CONFIG_SQUASHFS_DECOMP_MULTI | 요청별 workspace 동적 할당 | 메모리 제한 환경 |
| Per-CPU | CONFIG_SQUASHFS_DECOMP_MULTI_PERCPU | CPU별 workspace 사전 할당 | 서버/데스크톱 (권장) |
/* Per-CPU decompressor 워크스페이스 관리 — fs/squashfs/decompressor_multi_percpu.c */
static void *squashfs_decompressor_create(
struct squashfs_sb_info *msblk,
void *comp_opts)
{
struct squashfs_stream *stream;
int cpu;
stream = alloc_percpu(struct squashfs_stream);
for_each_possible_cpu(cpu) {
struct squashfs_stream *s = per_cpu_ptr(stream, cpu);
/* 각 CPU별로 decompressor workspace 초기화 */
s->stream = msblk->decompressor->init(msblk, comp_opts);
}
return stream;
}
/* 사용 시: preempt_disable → per_cpu_ptr → 해제 → preempt_enable */
static void *squashfs_decompressor_get(
struct squashfs_sb_info *msblk)
{
get_cpu(); /* preempt_disable */
return this_cpu_ptr(msblk->stream);
}
squashfs_sb_info — 마운트 상태 관리
/* 파일시스템 인스턴스별 상태 정보 — fs/squashfs/squashfs_fs_sb.h */
struct squashfs_sb_info {
int devblksize; /* 디바이스 블록 크기 */
int devblksize_log2; /* log2(devblksize) */
struct squashfs_cache *block_cache; /* 데이터 블록 캐시 */
struct squashfs_cache *fragment_cache; /* 프래그먼트 캐시 */
struct squashfs_cache *read_page; /* 읽기 버퍼 */
int next_meta_index; /* 다음 메타데이터 인덱스 */
__le64 *id_table; /* UID/GID 테이블 포인터 */
__le64 *fragment_index; /* 프래그먼트 인덱스 테이블 */
__le64 *xattr_id_table; /* Xattr ID 테이블 */
struct squashfs_decompressor *decompressor; /* 압축 해제기 */
void *stream; /* decompressor workspace */
unsigned short block_log; /* log2(블록 크기) */
unsigned short block_size; /* 데이터 블록 크기 */
unsigned int inodes; /* 총 inode 수 */
unsigned int fragments; /* 총 프래그먼트 수 */
u64 inode_table; /* inode 테이블 시작 오프셋 */
u64 directory_table; /* 디렉터리 테이블 시작 오프셋 */
};
squashfs_iget() — inode 읽기
/* inode 읽기 — fs/squashfs/inode.c */
struct inode *squashfs_iget(struct super_block *sb,
long long ino, unsigned int type)
{
struct inode *inode;
int err;
/* 1. inode 캐시에서 검색 (iget_locked) */
inode = iget_locked(sb, squashfs_inode_to_ino(ino));
if (!(inode->i_state & I_NEW))
return inode; /* 캐시 히트 */
/* 2. 디스크에서 inode 메타데이터 읽기 */
err = squashfs_read_inode(inode, ino);
if (err) {
iget_failed(inode);
return ERR_PTR(err);
}
/* 3. inode 타입에 따른 operations 설정 */
switch (type) {
case SQUASHFS_REG_TYPE:
case SQUASHFS_LREG_TYPE:
inode->i_fop = &generic_ro_fops;
inode->i_op = &squashfs_inode_ops;
inode->i_mapping->a_ops = &squashfs_aops;
break;
case SQUASHFS_DIR_TYPE:
case SQUASHFS_LDIR_TYPE:
inode->i_op = &squashfs_dir_inode_ops;
inode->i_fop = &squashfs_dir_ops;
break;
case SQUASHFS_SYMLINK_TYPE:
inode->i_op = &squashfs_symlink_inode_ops;
inode->i_data.a_ops = &squashfs_symlink_aops;
break;
}
unlock_new_inode(inode);
return inode;
}
디렉터리 조회 경로
/* 디렉터리 엔트리 검색 — fs/squashfs/namei.c */
static struct dentry *squashfs_lookup(struct inode *dir,
struct dentry *dentry, unsigned int flags)
{
struct squashfs_sb_info *msblk = dir->i_sb->s_fs_info;
struct squashfs_dir_header dirh;
struct squashfs_dir_entry *dire;
int length = i_size_read(dir);
/* 디렉터리 테이블을 순회하며 이름 매칭 */
while (length > 0) {
/* 1. 디렉터리 헤더 읽기 */
squashfs_read_metadata(dir->i_sb, &dirh, ...);
/* 2. 헤더의 count만큼 엔트리 반복 */
for (i = 0; i < dirh.count + 1; i++) {
squashfs_read_metadata(dir->i_sb, dire, ...);
/* 3. 이름 비교 (정렬된 순서이므로 조기 종료 가능) */
if (strcmp(dire->name, dentry->d_name.name) == 0) {
/* 매칭: inode 읽기 */
inode = squashfs_iget(dir->i_sb,
squashfs_make_inode(dirh.start_block,
dire->offset), dire->type);
return d_splice_alias(inode, dentry);
}
}
}
/* 이름 미발견 → negative dentry 캐시 */
return d_splice_alias(NULL, dentry);
}
확장 속성 (Xattr) 처리
/* Xattr 읽기 — fs/squashfs/xattr.c */
/*
* SquashFS xattr 저장 구조:
* 1. Xattr ID Table: inode별 xattr 인덱스 → xattr 메타데이터 위치
* 2. Xattr Metadata: prefix + name + value 순서로 압축 저장
*
* 지원 네임스페이스:
* - user.* (사용자 속성)
* - trusted.* (관리자 속성)
* - security.* (SELinux 라벨 등)
*/
struct squashfs_xattr_id {
__le64 xattr; /* xattr 메타데이터 위치 */
__le32 count; /* xattr 엔트리 수 */
__le32 size; /* 전체 xattr 크기 */
};
struct squashfs_xattr_entry {
__le16 type; /* 네임스페이스 + 인라인/참조 플래그 */
__le16 size; /* 이름 길이 */
};
struct squashfs_xattr_val {
__le32 vsize; /* 값 길이 */
};
/* SELinux 라벨 예시: security.selinux = "system_u:object_r:bin_t:s0" */
/* mksquashfs에서 -xattrs 옵션으로 보존, -no-xattrs로 제거 */
커널 소스 파일 구조
| 파일 | 역할 |
|---|---|
fs/squashfs/super.c | superblock 초기화, mount/umount |
fs/squashfs/block.c | 블록 I/O, squashfs_read_data() |
fs/squashfs/cache.c | 블록/프래그먼트 캐시 관리 |
fs/squashfs/inode.c | inode 읽기, squashfs_iget() |
fs/squashfs/dir.c | 디렉터리 읽기, lookup |
fs/squashfs/file.c | 파일 읽기, readpage/readahead |
fs/squashfs/file_direct.c | Direct I/O 경로 |
fs/squashfs/fragment.c | 프래그먼트 테이블 조회 |
fs/squashfs/symlink.c | 심볼릭 링크 읽기 |
fs/squashfs/namei.c | 이름 조회 (lookup) |
fs/squashfs/export.c | NFS export 지원 |
fs/squashfs/xattr.c | 확장 속성 처리 |
fs/squashfs/zlib_wrapper.c | gzip decompressor |
fs/squashfs/lzo_wrapper.c | LZO decompressor |
fs/squashfs/xz_wrapper.c | XZ decompressor |
fs/squashfs/lz4_wrapper.c | LZ4 decompressor |
fs/squashfs/zstd_wrapper.c | Zstd decompressor |
임베디드 시스템 활용
SquashFS는 임베디드 시스템에서 읽기 전용 루트 파일시스템으로 가장 널리 사용되는 파일시스템입니다. 플래시 메모리의 제한된 공간을 효율적으로 활용하면서 부팅 속도와 신뢰성을 확보할 수 있습니다.
initramfs에서의 SquashFS 활용
initramfs 안에 SquashFS 이미지를 포함시켜 실제 루트 파일시스템으로 전환(pivot_root)하는 패턴이 많이 사용됩니다.
# initramfs 내 init 스크립트 예시
#!/bin/sh
# 1. 필수 파일시스템 마운트
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
# 2. SquashFS 이미지 마운트 (NOR/NAND/USB에서)
mount -t squashfs /dev/mtdblock2 /mnt/lower
# 3. 쓰기 가능 레이어 준비 (tmpfs 또는 JFFS2)
mount -t tmpfs tmpfs /mnt/upper
mkdir -p /mnt/upper/data /mnt/upper/work
# 4. OverlayFS 통합 마운트
mount -t overlay overlay \
-o lowerdir=/mnt/lower,upperdir=/mnt/upper/data,workdir=/mnt/upper/work \
/mnt/merged
# 5. pivot_root로 루트 전환
cd /mnt/merged
pivot_root . oldroot
exec /sbin/init
A/B 파티션 펌웨어 업데이트
# A/B 파티션 구조로 무중단 펌웨어 업데이트
#
# 플래시 레이아웃:
# /dev/mtdblock0 bootloader (공유)
# /dev/mtdblock1 kernel_a (4MB)
# /dev/mtdblock2 rootfs_a (24MB, SquashFS)
# /dev/mtdblock3 kernel_b (4MB)
# /dev/mtdblock4 rootfs_b (24MB, SquashFS)
# /dev/mtdblock5 overlay (8MB, JFFS2)
# 업데이트 프로세스 (현재 A에서 부팅 중)
$ flash_erase /dev/mtd4 0 0
$ nandwrite -p /dev/mtd4 new-rootfs.sqsh
$ fw_setenv boot_partition b
# 재부팅 시 bootloader가 kernel_b + rootfs_b 사용
# 실패 시 watchdog으로 A로 자동 롤백
Snap/AppImage 패키징
# Snap 패키지 내부 구조 (SquashFS 기반)
$ unsquashfs -ll /snap/firefox/current.snap | head -10
drwxr-xr-x root/root 0 meta/
-rw-r--r-- root/root 245 meta/snap.yaml
drwxr-xr-x root/root 0 usr/
drwxr-xr-x root/root 0 usr/bin/
-rwxr-xr-x root/root 3456789 usr/bin/firefox
...
# AppImage 내부 (SquashFS + 자체 추출 헤더)
$ file myapp.AppImage
myapp.AppImage: ELF 64-bit, ... (embedded squashfs)
# AppImage에서 SquashFS 추출
$ ./myapp.AppImage --appimage-extract
# → squashfs-root/ 디렉터리에 압축 해제
ftrace/bpftrace 성능 분석
SquashFS의 읽기 성능 병목은 대부분 압축 해제 과정에서 발생합니다. ftrace와 bpftrace를 사용하면 커널 내부의 압축 해제 시간, 캐시 히트율, I/O 패턴을 실시간으로 관찰할 수 있습니다.
ftrace를 이용한 SquashFS 추적
# SquashFS 관련 커널 함수 목록 확인
$ cat /sys/kernel/debug/tracing/available_filter_functions | \
grep squashfs
squashfs_readahead
squashfs_read_data
squashfs_read_metadata
squashfs_lookup
squashfs_cache_get
...
# function_graph tracer로 호출 경로 추적
$ echo function_graph > /sys/kernel/debug/tracing/current_tracer
$ echo squashfs_readahead > /sys/kernel/debug/tracing/set_graph_function
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
# SquashFS 파일 읽기 트리거
$ cat /mnt/squashfs/some_file > /dev/null
# 트레이스 결과 확인
$ cat /sys/kernel/debug/tracing/trace
# 출력 예시:
3) | squashfs_readahead() {
3) | squashfs_read_data() {
3) 12.340 us | submit_bio_wait();
3) | squashfs_decompress() {
3) 45.678 us | zstd_decompress();
3) 46.123 us | }
3) 60.456 us | }
3) 62.789 us | }
bpftrace를 이용한 상세 분석
# 1. squashfs_read_data() 호출당 지연 시간 히스토그램
$ bpftrace -e '
kprobe:squashfs_read_data { @start[tid] = nsecs; }
kretprobe:squashfs_read_data /@start[tid]/ {
@usecs = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 출력 예시:
@usecs:
[4, 8) 12 |@@@@ |
[8, 16) 45 |@@@@@@@@@@@@@@@ |
[16, 32) 98 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[32, 64) 67 |@@@@@@@@@@@@@@@@@@@@ |
[64, 128) 23 |@@@@@@@@ |
[128, 256) 5 |@@ |
# 2. 프래그먼트 캐시 히트/미스 추적
$ bpftrace -e '
kprobe:squashfs_cache_get {
@total++;
}
kretprobe:squashfs_cache_get {
/* 캐시 히트 시 pending=0, 미스 시 새로 읽기 */
@returns = count();
}
END { printf("총 캐시 조회: %d\n", @total); }'
# 3. 압축 알고리즘별 해제 시간 비교
$ bpftrace -e '
kprobe:zstd_decompress_squashfs { @start[tid] = nsecs; @algo[tid] = "zstd"; }
kprobe:zlib_uncompress { @start[tid] = nsecs; @algo[tid] = "zlib"; }
kretprobe:zstd_decompress_squashfs,
kretprobe:zlib_uncompress /@start[tid]/ {
@decomp_us[@algo[tid]] = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]); delete(@algo[tid]);
}'
perf를 이용한 CPU 프로파일링
# SquashFS 읽기 시 CPU 사용 프로파일링
$ perf record -g -a -- cat /mnt/sqsh/large_file > /dev/null
$ perf report --sort comm,dso,symbol
# 출력 예시 (zstd 압축 사용 시):
# Overhead Command Shared Object Symbol
# ........ ....... ................ .........................
# 42.3% cat [kernel.vmlinux] ZSTD_decompressSequences
# 18.7% cat [kernel.vmlinux] squashfs_read_data
# 8.2% cat [kernel.vmlinux] copy_page
# 6.1% cat [kernel.vmlinux] submit_bio_wait
# I/O 지연 분석 (blktrace 결합)
$ blktrace -d /dev/loop0 -o - | blkparse -i - | \
grep -E "R|C" | head -20
# R = Request 제출, C = Complete
# 두 시간 차이 = 실제 I/O 지연
성능 분석 심화
SquashFS의 실제 성능은 압축 알고리즘, 블록 크기, 접근 패턴, 저장 매체 속도에 따라 크게 달라집니다. 이 절에서는 체계적인 벤치마크 방법과 성능 특성을 분석합니다.
벤치마크 방법론
# === 순차 읽기 성능 (ext4 대비 SquashFS) ===
# 동일 데이터로 ext4 이미지와 SquashFS 이미지 생성
$ dd if=/dev/zero of=ext4.img bs=1M count=2048
$ mkfs.ext4 ext4.img
$ mount -o loop ext4.img /mnt/ext4
$ cp -a testdata/* /mnt/ext4/
$ umount /mnt/ext4
$ mksquashfs testdata/ squashfs.img -comp zstd
# fio 벤치마크
$ fio --name=seqread --filename=/mnt/sqsh/largefile \
--rw=read --bs=128k --direct=0 --numjobs=1 \
--size=1G --runtime=30 --group_reporting
# === 랜덤 읽기 성능 ===
$ fio --name=randread --filename=/mnt/sqsh/largefile \
--rw=randread --bs=4k --direct=0 --numjobs=4 \
--iodepth=16 --size=1G --runtime=30 --group_reporting
# === 마운트 시간 측정 ===
$ time sudo mount -t squashfs -o loop image.sqsh /mnt
# real 0m0.012s (일반적으로 10~50ms)
# === 메타데이터 성능 (find/ls) ===
$ time find /mnt/sqsh -type f | wc -l
# SquashFS는 디렉터리 메타데이터도 압축되어
# 많은 파일이 있을 때 첫 번째 스캔이 느릴 수 있음
# (page cache 적재 후에는 빠름)
# === 메모리 사용량 측정 ===
$ echo 3 > /proc/sys/vm/drop_caches
$ free -m # before
$ find /mnt/sqsh -type f -exec cat {} + > /dev/null
$ free -m # after → 차이 = page cache 사용량
접근 패턴별 성능 특성
| 접근 패턴 | ext4 대비 SquashFS | 원인 | 최적화 전략 |
|---|---|---|---|
| 순차 읽기 (큰 파일) | 0.7~1.2배 | I/O 감소 vs 해제 CPU | 큰 블록(256K~1M), readahead |
| 랜덤 읽기 (4KB) | 0.3~0.8배 | 블록 전체 해제 필요 | 작은 블록(64K), LZ4/LZO |
| 메타데이터 (ls -lR) | 0.5~1.0배 | 메타데이터 블록 해제 | page cache 웜업 |
| 작은 파일 다수 읽기 | 0.8~1.5배 | 프래그먼트 패킹 효과 | 프래그먼트 캐시 크기 증가 |
| HDD 순차 읽기 | 1.5~3.0배 | I/O 크기 절감 효과 극대 | 고압축 (xz/zstd) |
| NVMe 랜덤 읽기 | 0.2~0.5배 | CPU가 병목 | LZ4, per-CPU 해제 |
성능 튜닝 체크리스트
- 느린 매체 (HDD, eMMC, NOR Flash) → 고압축(xz/zstd) 사용하여 I/O 감소가 해제 비용보다 큰 이점 확보
- 빠른 매체 (NVMe) → 저압축(LZ4/LZO) 사용하여 CPU 병목 방지
- 랜덤 읽기 중심 → 블록 크기 줄이기 (-b 64K), 프래그먼트 캐시 증가
- 순차 읽기 중심 → 블록 크기 키우기 (-b 256K~1M), readahead 활용
- 멀티코어 →
CONFIG_SQUASHFS_DECOMP_MULTI_PERCPU=y설정 - 메모리 제한 → LZ4 사용 (해제 시 메모리 64KB), XZ 딕셔너리 제한
SquashFS vs ext4 성능 비교
동일한 데이터셋(Linux 커널 소스 트리, 약 1.2GB)을 ext4와 SquashFS(zstd, 128KB 블록)로 각각 저장하고, 다양한 워크로드에서 성능을 비교한 결과입니다.
| 워크로드 | ext4 (NVMe) | SquashFS/zstd (NVMe) | ext4 (HDD) | SquashFS/zstd (HDD) |
|---|---|---|---|---|
| 순차 읽기 128KB (MB/s) | 2,800 | 1,200 | 150 | 280 |
| 랜덤 읽기 4KB (IOPS) | 450K | 85K | 180 | 320 |
find / -name "*.c" | 0.8초 | 1.2초 | 12초 | 4.5초 |
grep -r "CONFIG" / | 2.1초 | 3.8초 | 45초 | 18초 |
| 이미지 크기 | 1.2 GB | 362 MB | 1.2 GB | 362 MB |
| 마운트 시간 | ~1 ms | ~12 ms | ~5 ms | ~15 ms |
Page Cache 동작과 메모리 사용
SquashFS는 압축 해제된 데이터를 page cache에 저장합니다. 따라서 첫 번째 접근 시에만 압축 해제 비용이 발생하고, 이후 접근은 일반 파일시스템과 동일한 속도로 처리됩니다.
/* SquashFS readahead 구현 — fs/squashfs/file.c */
static void squashfs_readahead(struct readahead_control *ractl)
{
struct inode *inode = ractl->mapping->host;
struct squashfs_sb_info *msblk = inode->i_sb->s_fs_info;
size_t mask = (1UL << msblk->block_log) - 1;
unsigned int nr_pages = readahead_count(ractl);
struct page **pages;
int i, file_end, block;
/* 1. readahead 범위에 포함된 블록들 식별 */
/* 2. 각 블록에 대해 squashfs_read_data() 호출 */
/* 3. 압축 해제된 데이터를 page cache에 배치 */
while ((page = readahead_page(ractl)) != NULL) {
int block_idx = page->index >> (msblk->block_log - PAGE_SHIFT);
/* 블록 경계 정렬된 페이지 그룹 단위로 처리 */
/* 하나의 압축 블록 해제로 여러 페이지를 한 번에 채움 */
squashfs_read_data(inode->i_sb, block, length,
&next_block, actor);
for (i = 0; i < pages_per_block; i++) {
SetPageUptodate(pages[i]);
unlock_page(pages[i]);
}
}
}
Page cache 동작의 핵심 포인트:
- Readahead 효과 — SquashFS는 블록 단위로 압축 해제하므로, 하나의 128KB 블록을 해제하면 32개의 4KB 페이지가 한꺼번에 cache에 올라갑니다. 순차 읽기 시 이 prefetch 효과가 크게 작용합니다.
- Cache 압력 — 압축 해제된 데이터는 비압축 상태로 page cache에 저장되므로, 실제 메모리 사용량은 원본 파일 크기와 동일합니다. 메모리 제한 환경에서는 cache eviction이 자주 발생할 수 있습니다.
- Cold vs Warm 접근 — 첫 번째 접근(cold): I/O + 압축 해제 비용, 두 번째 접근(warm): page cache 히트로 일반 FS와 동일합니다.
멀티스레드 읽기 확장성
Per-CPU decompressor 모드에서의 동시 읽기 스레드 수에 따른 처리량 변화입니다.
| 동시 스레드 수 | 순차 읽기 (MB/s) | 랜덤 읽기 (IOPS) | CPU 사용률 |
|---|---|---|---|
| 1 | 420 | 22K | 12% |
| 2 | 780 | 42K | 23% |
| 4 | 1,350 | 75K | 45% |
| 8 | 1,800 | 85K | 72% |
| 16 | 1,950 | 88K | 95% |
CONFIG_SQUASHFS_DECOMP_SINGLE)에서는 전역 mutex로 인해 멀티스레드 효과가 거의 없습니다. 반드시 Per-CPU 모드를 사용하세요.
I/O 증폭과 read amplification
SquashFS에서 4KB 데이터를 읽기 위해 128KB 블록 전체를 해제해야 하는 현상을 read amplification이라 합니다. 이 비율(실제 읽기/요청 읽기)은 블록 크기에 비례합니다.
/* Read amplification 계산 예시 */
/* 블록 크기 128KB, 4KB 랜덤 읽기 요청 시 */
/* amplification = 128KB / 4KB = 32배 */
/*
* 실제 영향:
* 1. 디스크에서 압축된 블록 읽기 (압축률 50%면 ~64KB)
* 2. 128KB로 압축 해제 (CPU 사용)
* 3. 그 중 4KB만 사용, 나머지는 page cache에 저장
*
* 완화 방법:
* - 블록 크기 줄이기: -b 32K → amplification = 8배
* - Page cache로 반복 접근 시 해제 비용 상각
* - readahead로 순차 접근 시 prefetch 효과
*/
관련 문서
- F2FS 파일시스템 심화 — 로그 구조 쓰기 경로, NAT/SIT/SSA, 멀티 헤드 로깅과 GC, 체크포인트·롤포워드
- JFFS2 파일시스템 심화 — MTD 기반 raw node 구조, 마운트 스캔 비용, 압축·GC·XIP·Write Buf
- Btrfs 파일시스템 심화 — COW B-tree, 서브볼륨·스냅샷·send/receive, 체크섬·압축·RAID 프로파
- FUSE (Filesystem in Userspace) — 커널-유저 경계 아키텍처, /dev/fuse 프로토콜, libfuse 연산 경로, 캐시·일
- NTFS 파일시스템 심화 — MFT·속성 기반 구조, 인덱스(B-tree), $LogFile 복구, ACL/ADS 보안