SquashFS 파일시스템

SquashFS와 EROFS는 이미지 기반 배포에 최적화된 읽기 전용 압축 파일시스템입니다. 본 문서에서는 inode/디렉터리/데이터 블록의 온디스크 배치, 압축 단위와 랜덤 읽기 지연의 상관관계, gzip/lzo/xz/zstd 및 lz4 선택 기준, `mksquashfs`/`mkfs.erofs` 이미지 생성 전략, dm-verity 결합 무결성 모델, Live 시스템·컨테이너·임베디드 펌웨어에서의 성능·용량 절충점을 상세히 설명합니다.

전제 조건: MTDVFS 문서를 먼저 읽으세요. 플래시/읽기전용 계열 파일시스템은 쓰기 제약과 압축 정책이 성능/내구성에 직접 연결되므로 저장 매체 특성을 먼저 이해해야 합니다.
일상 비유: 이 주제는 지우기 어려운 메모지 관리와 비슷합니다. 한 번 쓴 내용을 쉽게 덮어쓸 수 없다는 제약을 전제로 배치와 정리를 설계해야 비용을 줄일 수 있습니다.

핵심 요약

  • 읽기 전용 — 런타임 변경 불가, 무결성/재현성 확보
  • 고압축 이미지 — 저장 공간/배포 크기 절감
  • 블록 단위 압축 — 필요한 블록만 해제해 랜덤 접근 성능 확보
  • 대표 조합 — SquashFS(RO) + OverlayFS(RW)
  • 주요 활용 — Live OS, 임베디드 펌웨어, 앱 패키징

단계별 이해

  1. 읽기 전용 모델 파악
    이미지 생성 후 수정하지 않는 배포 모델을 먼저 이해합니다.
  2. 압축 알고리즘 선택
    부팅 시간/CPU/용량 제약을 기준으로 gzip/lzo/xz/zstd를 선택합니다.
  3. 실서비스 레이어링
    쓰기 요구가 있으면 OverlayFS upperdir를 결합해 운영 모델을 구성합니다.
  4. 복구/업데이트 전략 수립
    파일 단위 수정 대신 이미지 교체 방식으로 롤백과 업데이트를 단순화합니다.
관련 문서: VFS (파일시스템 인터페이스), OverlayFS (읽기 쓰기 레이어), EROFS (Android 압축 FS), Block I/O (블록 장치)

개요

SquashFS는 1990년대 후반 Phillip Lougher가 개발한 읽기 전용 압축 파일시스템으로, 높은 압축률과 빠른 랜덤 액세스를 제공합니다.

Image Build mksquashfs Compressed Blocks inode/meta/data Read-Only Mount 빠른 random read

주요 특징

활용 사례

디스크 레이아웃

파일시스템 구조

Superblock (96 bytes) - Offset 0 Compression Options Data Blocks (compressed) Block Header (2 bytes) + Compressed Data (variable) Metadata Blocks (compressed) Inodes | Directories | Fragments | Xattrs UID/GID Lookup Table Fragment Table Export Table (NFS support) Xattr ID Table Inode Table Index Directory Table Index

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 범용성, 안정성

블록 크기 최적화

# 대용량 순차 읽기 (비디오 스트리밍)
$ 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 손실을 최소화합니다.

Logical Read folio/page 단위 pcluster Mapping logical → compressed In-place Decompress 낮은 cold-start 지연

EROFS vs SquashFS 비교

항목EROFSSquashFS
주요 타깃모바일/시스템 이미지범용 RO 압축 이미지
random readpcluster 중심 최적화블록 단위 압축
운영 패턴Android dynamic partition에 적합Live CD/임베디드 배포에 강점
압축 알고리즘LZ4/LZMA/Zstdgzip/lzo/xz/zstd
읽기 지연in-place decompress로 최소화블록 캐시로 접근

압축 모델과 pcluster

EROFS의 핵심은 logical block과 physical compressed extent를 분리하는 pcluster 모델입니다. 작은 read에도 필요한 압축 덩어리만 해제하도록 메타데이터를 구성합니다.

온디스크 포맷 (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

참고자료

다음 학습:
  • OverlayFS — SquashFS + RW 레이어
  • EROFS — Android 압축 파일시스템
  • VFS — 파일시스템 인터페이스
  • Block I/O — 블록 장치 관리

블록/프래그먼트 레이아웃

SquashFS 이미지는 크게 superblock → compression options → data blocks → metadata blocks → lookup tables의 순서로 배치됩니다. 데이터 블록은 파일 내용을 압축 단위(기본 128KB)로 분할한 것이고, 메타데이터 블록은 inode·디렉터리 엔트리·프래그먼트 정보를 8KB 단위로 압축합니다.

블록 크기와 압축 단위: 데이터 블록의 기본 크기는 128KB이지만, -b 옵션으로 4KB~1MB 범위에서 조정할 수 있습니다. 메타데이터 블록은 항상 8KB 단위입니다.
SquashFS 이미지 온디스크 레이아웃 Superblock (96 bytes) — 오프셋 0 Compression Options (선택적) Data Blocks (압축된 파일 데이터) Block 0 (128KB) Block 1 (128KB) Block 2 (128KB) ··· Block N (가변 크기) Fragment Blocks (블록 미만 잔여 데이터) Metadata Blocks (8KB 단위 압축) Inode Table Directory Table Xattr Table UID/GID Table Fragment Table (인덱스) Export Table (NFS inode 번호 조회) Superblock 데이터 영역 프래그먼트 메타데이터 --- Superblock 오프셋 참조

압축 블록 헤더 포맷

각 데이터 블록은 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의 프래그먼트는 블록 크기보다 작은 파일 데이터를 효율적으로 저장하는 핵심 메커니즘입니다. 파일의 마지막 부분이 블록 크기 미만이면 독립 블록 대신 다른 파일의 잔여 데이터와 함께 하나의 프래그먼트 블록에 묶여 저장됩니다.

왜 프래그먼트가 중요한가: 블록 크기가 128KB인 환경에서 1KB 파일 1,000개를 저장한다고 가정합니다. 프래그먼트 없이는 128MB(128KB × 1,000)가 필요하지만, 프래그먼트를 사용하면 약 1MB 이내에 모두 저장할 수 있습니다. 특히 /etc 아래 설정 파일이나 소규모 스크립트가 많은 시스템에서 극적인 공간 절감 효과를 보입니다.
프래그먼트 블록 패킹 과정 입력 파일들 fileA.conf (2KB) fileB.sh (800B) fileC.txt (3KB) 패킹 Fragment Block #0 fileA 끝 fileB fileC 끝 → 압축 후 디스크에 저장 압축 디스크 저장 압축된 프래그먼트 (원본 5.8KB → ~2KB) Fragment Table 구조 squashfs_fragment_entry start_block + size inode.fragment 프래그먼트 인덱스 참조 inode.offset 블록 내 바이트 오프셋 읽기 시: inode.fragment → Fragment Table → start_block 위치에서 압축 해제 → inode.offset에서 데이터 추출
/* 프래그먼트 테이블 엔트리 — 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_SIZE3캐시 엔트리 수 (클수록 히트율 향상, 메모리 사용 증가)
-no-fragments (mksquashfs)OFF프래그먼트 비활성화 → 작은 파일도 독립 블록 할당
-always-use-fragmentsOFF블록 크기 초과 파일의 마지막 블록도 프래그먼트로 처리
-no-tailendsOFF-no-fragments와 동일 효과 (하위 호환)

압축 알고리즘 비교 심화

SquashFS는 5가지 압축 알고리즘을 지원하며, 각 알고리즘은 압축률·속도·메모리 사용량에서 뚜렷한 특성 차이를 보입니다. 이 절에서는 커널 내부 압축 인터페이스와 각 알고리즘의 정량적 성능 차이를 분석합니다.

압축 알고리즘 특성 스펙트럼 빠른 압축 해제 높은 압축률 LZ4 해제: ~4.5 GB/s 압축률: ~45% LZO 해제: ~2.0 GB/s 압축률: ~50% gzip (zlib) 해제: ~800 MB/s 압축률: ~60% Zstd 해제: ~1.4 GB/s 압축률: ~70% XZ (LZMA2) 해제: ~300 MB/s 압축률: ~80% 커널 내부 압축 해제 경로 squashfs_read_data() squashfs_decompress() 알고리즘별 decompressor zlib / lzo / xz / lz4 / zstd decompressor는 squashfs_decompressor 구조체의 decompress() 콜백으로 호출 per-CPU 또는 per-request 워크스페이스 할당 → 멀티코어 병렬 해제 지원
/* 압축 해제기 인터페이스 — 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 MB45%4.2초4,500 MB/s~64 KB
LZO기본598 MB50%5.8초2,000 MB/s~64 KB
gzip-Xcompression-level 9478 MB60%42초800 MB/s~256 KB
Zstd-Xcompression-level 15362 MB70%28초1,400 MB/s~128 KB
Zstd-Xcompression-level 22338 MB72%185초1,400 MB/s~128 KB
XZ-Xcompression-level 9240 MB80%320초300 MB/s~40 MB
XZ 메모리 주의: XZ는 압축 해제 시 딕셔너리 크기만큼의 메모리가 필요합니다. 임베디드 시스템에서 XZ를 사용하려면 -Xdict-size를 제한하여 메모리 사용을 조절해야 합니다. 예: mksquashfs src/ img.sqsh -comp xz -Xdict-size 32K

OverlayFS 연동 심화

SquashFS의 읽기 전용 특성은 OverlayFS와 결합하여 실용적인 읽기/쓰기 파일시스템으로 확장됩니다. 이 패턴은 Live OS, 임베디드 펌웨어, 컨테이너 이미지 등에서 핵심 배포 모델로 사용됩니다.

SquashFS + OverlayFS 레이어 아키텍처 사용자 프로세스 VFS를 통한 통합 파일 접근 (/mnt/merged) OverlayFS (VFS 레이어) copy-up / whiteout / opaque directory 관리 읽기 쓰기 Lower Layer (SquashFS) 읽기 전용 / 압축된 이미지 불변: /usr, /lib, /bin, /etc (기본) dm-verity 무결성 검증 가능 Upper Layer (tmpfs/ext4) 읽기/쓰기 가능 변경/추가: 설정 변경, 로그, 임시 파일 재부팅 시 초기화 (tmpfs) 또는 영속 (ext4) Docker 컨테이너 레이어 구조 (overlay2 드라이버) base image (RO) layer 1 (RO) layer 2 (RO) ··· container layer (RW)

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 vs EROFS 핵심 설계 차이 SquashFS 블록 단위 압축 파일 → 128KB 블록 분할 → 개별 압축 프래그먼트 패킹 작은 파일 → 프래그먼트 블록에 통합 메타데이터 8KB 압축 inode/dir 테이블도 압축 저장 랜덤 읽기 블록 인덱스 → 해당 블록만 해제 강점: 범용성 · 5개 압축 알고리즘 · 높은 압축률 EROFS pcluster 압축 논리 블록 → pcluster 매핑 → 최소 해제 인라인 데이터 작은 파일 → inode에 직접 저장 (tail-packing) 고정 크기 inode 비압축 메타데이터 → O(1) 조회 In-place 압축 해제 bounce buffer 없이 페이지에 직접 해제 강점: 낮은 지연 · cold-start · 모바일 최적화 VS

상세 기능 비교

비교 항목SquashFSEROFS
메인라인 병합Linux 2.6.29 (2009)Linux 4.19 (2018)
압축 단위고정 블록 (4KB~1MB)가변 pcluster
작은 파일 최적화프래그먼트 패킹인라인 데이터 (tail-packing)
메타데이터 압축예 (8KB 블록)아니오 (고정 크기, 비압축)
압축 알고리즘gzip, lzo, xz, lz4, zstdlz4, 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, AppImageAndroid system, 컨테이너 (containerd)
커널 소스 위치fs/squashfs/fs/erofs/

성능 벤치마크 비교

동일한 루트 파일시스템(Ubuntu 22.04, 약 2GB)을 SquashFS와 EROFS로 각각 만들었을 때의 비교입니다.

측정 항목SquashFS (zstd)EROFS (lz4hc)EROFS (zstd)
이미지 크기620 MB810 MB650 MB
마운트 시간12 ms4 ms5 ms
순차 읽기 (4KB)420 MB/s680 MB/s520 MB/s
랜덤 읽기 (4KB)180 MB/s450 MB/s350 MB/s
앱 cold-start280 ms120 ms150 ms
메모리 사용 (마운트)~2 MB~1 MB~1.5 MB
선택 기준: 이미지 크기가 최우선이면 SquashFS(xz/zstd), cold-start 지연이 중요하면 EROFS(lz4hc), 압축률과 성능의 균형이 필요하면 EROFS(zstd)를 선택하세요.

mksquashfs 빌드 최적화

mksquashfs는 단순한 이미지 생성 도구를 넘어, 다양한 옵션으로 출력 이미지의 크기·성능·보안을 세밀하게 조절할 수 있습니다. 이 절에서는 실전 빌드 파이프라인에서 자주 사용되는 고급 옵션과 최적화 전략을 설명합니다.

mksquashfs 빌드 파이프라인 소스 디렉터리 /rootfs/* 파일/디렉터리/심링크 필터링 -e (제외) -wildcards -regex 압축 엔진 -comp (알고리즘) -b (블록 크기) -Xcompression-level 출력 이미지 rootfs.sqsh superblock+data+meta 주요 옵션 카테고리 크기 최적화 -comp xz/zstd -b 256K, -nopad 성능 최적화 -comp lz4/lzo -b 64K, -processors N 보안 강화 -all-root -no-xattrs, -force-uid 재현성 보장 -mkfs-time 0 -all-time 0, -nopad 병렬 빌드: -processors $(nproc) → 멀티코어 활용으로 빌드 시간 단축 재현 빌드: -mkfs-time 0 -all-time 0 → 동일 입력 → 동일 이미지 (bit-for-bit) pseudo 파일: -p 옵션으로 디바이스 노드/디렉터리를 소스 없이 생성 가능

고급 빌드 옵션

# === 재현 가능 빌드 (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-fragmentsOFF프래그먼트 비활성화
-always-use-fragmentsOFF모든 파일 끝을 프래그먼트로 처리
-no-duplicatesOFF중복 파일 탐지 비활성화 (빌드 속도 향상)
-all-rootOFF모든 파일 소유자를 root:root로 변경
-force-uid <uid>원본모든 파일의 UID 강제 지정
-force-gid <gid>원본모든 파일의 GID 강제 지정
-mkfs-time <t>현재superblock 타임스탬프 고정
-all-time <t>원본모든 파일의 mtime 고정
-nopadOFF4KB 정렬 패딩 제거
-no-exportsOFFNFS export 테이블 생략
-no-xattrsOFF확장 속성 제거
-sort <file>없음파일 배치 우선순위 지정
-p <spec>없음pseudo 파일 생성 (디바이스 노드 등)

커널 내부 구현 심화

SquashFS 커널 코드는 fs/squashfs/ 디렉터리에 위치하며, 파일시스템 등록에서 데이터 읽기까지 모든 경로가 VFS 인터페이스를 통해 연결됩니다. 이 절에서는 핵심 함수들의 호출 관계와 내부 자료구조를 분석합니다.

SquashFS 커널 내부 아키텍처 VFS (read/readdir/lookup/stat) SquashFS VFS Operations file_operations inode_operations dir_operations super_operations squashfs_readahead() readahead 배치 읽기 squashfs_lookup() 디렉터리 엔트리 검색 squashfs_fill_super() 마운트 시 초기화 squashfs_read_data() 블록 I/O + 압축 해제 squashfs_read_metadata() 메타데이터 블록 읽기 decompressor->decompress() per-CPU workspace → 알고리즘별 해제 Page Cache Block Cache

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는 세 가지 압축 해제 동시성 모드를 제공합니다. 성능과 메모리 사이의 트레이드오프를 환경에 맞게 선택할 수 있습니다.

모드커널 설정동작적합 환경
SingleCONFIG_SQUASHFS_DECOMP_SINGLE전역 mutex 1개, 직렬 해제단일 CPU 임베디드
MultiCONFIG_SQUASHFS_DECOMP_MULTI요청별 workspace 동적 할당메모리 제한 환경
Per-CPUCONFIG_SQUASHFS_DECOMP_MULTI_PERCPUCPU별 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.csuperblock 초기화, mount/umount
fs/squashfs/block.c블록 I/O, squashfs_read_data()
fs/squashfs/cache.c블록/프래그먼트 캐시 관리
fs/squashfs/inode.cinode 읽기, squashfs_iget()
fs/squashfs/dir.c디렉터리 읽기, lookup
fs/squashfs/file.c파일 읽기, readpage/readahead
fs/squashfs/file_direct.cDirect I/O 경로
fs/squashfs/fragment.c프래그먼트 테이블 조회
fs/squashfs/symlink.c심볼릭 링크 읽기
fs/squashfs/namei.c이름 조회 (lookup)
fs/squashfs/export.cNFS export 지원
fs/squashfs/xattr.c확장 속성 처리
fs/squashfs/zlib_wrapper.cgzip decompressor
fs/squashfs/lzo_wrapper.cLZO decompressor
fs/squashfs/xz_wrapper.cXZ decompressor
fs/squashfs/lz4_wrapper.cLZ4 decompressor
fs/squashfs/zstd_wrapper.cZstd decompressor

임베디드 시스템 활용

SquashFS는 임베디드 시스템에서 읽기 전용 루트 파일시스템으로 가장 널리 사용되는 파일시스템입니다. 플래시 메모리의 제한된 공간을 효율적으로 활용하면서 부팅 속도와 신뢰성을 확보할 수 있습니다.

임베디드 시스템 SquashFS 배치 전략 플래시 파티션 레이아웃 (예: 64MB NOR Flash) Bootloader 256KB Kernel 4MB RootFS (SquashFS) 48MB (읽기 전용) Overlay (JFFS2) 10MB (읽기/쓰기) Config 1.75MB (NVRAM) 부팅 흐름 및 파일시스템 계층 U-Boot 커널+DTB 로드 Linux Kernel SquashFS 마운트 RootFS (SquashFS) / (읽기 전용 마운트) OverlayFS 설정/로그 쓰기 OpenWrt 라우터 SquashFS root + JFFS2 overlay Yocto/Buildroot SquashFS 이미지 빌드 레시피 Live USB/CD SquashFS + tmpfs overlay 펌웨어 업데이트: 새 SquashFS 이미지를 플래시에 쓰고 재부팅 (A/B 파티션 전략) 공장 초기화: overlay 파티션만 포맷 → 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의 읽기 성능 병목은 대부분 압축 해제 과정에서 발생합니다. ftracebpftrace를 사용하면 커널 내부의 압축 해제 시간, 캐시 히트율, 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의 실제 성능은 압축 알고리즘, 블록 크기, 접근 패턴, 저장 매체 속도에 따라 크게 달라집니다. 이 절에서는 체계적인 벤치마크 방법과 성능 특성을 분석합니다.

SquashFS 성능 영향 요인 분석 SquashFS 읽기 성능 I/O 시간 + 압축 해제 시간 저장 매체 속도 HDD: ~150 MB/s (I/O 병목) SATA SSD: ~550 MB/s NVMe: ~3,500 MB/s (CPU 병목) 압축 알고리즘 LZ4: CPU 최소, 이미지 큼 Zstd: 균형잡힌 선택 XZ: 이미지 작음, CPU 높음 블록 크기 큰 블록(1M): 높은 압축률 → 랜덤 읽기 시 낭비 해제 증가 작은 블록(64K): 랜덤 읽기 유리 접근 패턴 순차 읽기: readahead 효과 큼 랜덤 읽기: 캐시 미스 시 해제 비용 메타데이터 조회: 디렉터리 탐색 느린 매체 + 고압축 = I/O 절감 이점 | 빠른 매체 + 저압축 = CPU 병목 회피

벤치마크 방법론

# === 순차 읽기 성능 (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,8001,200150280
랜덤 읽기 4KB (IOPS)450K85K180320
find / -name "*.c"0.8초1.2초12초4.5초
grep -r "CONFIG" /2.1초3.8초45초18초
이미지 크기1.2 GB362 MB1.2 GB362 MB
마운트 시간~1 ms~12 ms~5 ms~15 ms
핵심 관찰: NVMe처럼 빠른 매체에서는 ext4가 대부분의 워크로드에서 우세하지만, HDD처럼 느린 매체에서는 SquashFS의 압축 효과로 인해 오히려 더 빠른 결과를 보입니다. 이는 I/O 감소량이 압축 해제 CPU 비용을 초과하기 때문입니다.

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 동작의 핵심 포인트:

멀티스레드 읽기 확장성

Per-CPU decompressor 모드에서의 동시 읽기 스레드 수에 따른 처리량 변화입니다.

동시 스레드 수순차 읽기 (MB/s)랜덤 읽기 (IOPS)CPU 사용률
142022K12%
278042K23%
41,35075K45%
81,80085K72%
161,95088K95%
확장성 한계: CPU 코어 수를 초과하는 스레드에서는 성능 향상이 제한됩니다. Single 모드(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 효과
 */
필수 관련 문서: 참고 문서: