JFFS2 파일시스템 심화

JFFS2를 임베디드 플래시 환경 기준으로 해부합니다. MTD erase block 위에 기록되는 raw node 구조, mount 시 전체 스캔과 요약 정보 처리, 압축 알고리즘 선택과 공간 효율, GC 스레드 동작과 마모 분산 한계, NOR/NAND에서의 쓰기 제약 대응, XIP/Write Buffering 경로, UBIFS와의 설계·성능·확장성 차이까지 실제 제품 운용 관점에서 상세히 설명합니다.

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

핵심 요약

  • 계층 이해 — VFS, 캐시, 하위 FS 경계를 구분합니다.
  • 메타데이터 우선 — inode/dentry 일관성을 먼저 확인합니다.
  • 저장 정책 — 저널링/압축/할당 정책 차이를 비교합니다.
  • 일관성 모델 — 로컬/원격/합성 FS의 반영 시점을 구분합니다.
  • 복구 관점 — 장애 시 재구성 경로를 함께 점검합니다.

단계별 이해

  1. 경계 계층 파악
    요청이 VFS에서 어디로 내려가는지 확인합니다.
  2. 메타/데이터 분리
    어느 경로에서 무엇이 갱신되는지 나눠 봅니다.
  3. 동기화/플러시 확인
    쓰기 반영 시점과 순서를 검증합니다.
  4. 복구 시나리오 점검
    비정상 종료 후 일관성 회복을 확인합니다.
관련 표준: MTD (Memory Technology Device) Interface, JFFS2 Design Documentation — Flash 메모리 파일시스템 인터페이스 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 페이지: JFFS2는 MTD(Memory Technology Device) 위에서 동작하는 로그 구조 Flash 파일시스템입니다. VFS 계층은 VFS, 블록 I/O는 Block I/O, 현대적 Flash 파일시스템은 F2FS, 디바이스 드라이버는 디바이스 드라이버, MTD 서브시스템은 MTD 페이지를 참고하세요.

개요 & 역사

JFFS2(Journalling Flash File System version 2)는 임베디드 Linux 시스템에서 NOR/NAND Flash 메모리 위에 직접 동작하도록 설계된 로그 구조(log-structured) 파일시스템입니다. 블록 디바이스 계층을 거치지 않고 MTD(Memory Technology Device) 인터페이스를 통해 Flash에 직접 접근합니다.

JFFS 원형 (v1)

JFFS(Journalling Flash File System)는 1999년 스웨덴의 Axis Communications AB가 자사 임베디드 Linux 제품을 위해 개발했습니다. 원래 NOR Flash 전용으로 설계되었으며, 다음과 같은 특징을 가졌습니다:

그러나 JFFS v1은 NAND Flash 미지원, 압축 미지원, 하드링크 미지원 등의 한계가 있었습니다.

JFFS2로의 진화

2001년 Red Hat의 David Woodhouse가 JFFS의 한계를 극복하기 위해 JFFS2를 개발했습니다. Linux 2.4.10에서 메인라인에 머지되었으며, 주요 개선 사항은 다음과 같습니다:

주요 연혁

시기이벤트
1999JFFS v1 — Axis Communications, NOR Flash 전용
2001JFFS2 — David Woodhouse (Red Hat), Linux 2.4.10 머지
2004NAND Flash 지원 (wbuf 도입)
2005Erase Block Summary (EBS) 지원
2006xattr / POSIX ACL / SELinux 지원
2008UBIFS 등장 — 대용량 NAND에서 JFFS2 대체 시작
현재소용량 NOR Flash 임베디드 시스템에서 여전히 활발히 사용

Flash 메모리 기초

JFFS2를 이해하려면 Flash 메모리의 물리적 특성을 먼저 알아야 합니다. Flash는 일반 블록 디바이스(HDD/SSD)와 근본적으로 다른 동작 방식을 가집니다.

NOR Flash vs NAND Flash

특성NOR FlashNAND Flash
인터페이스랜덤 액세스 (RAM-like)순차 페이지 액세스
읽기 단위바이트/워드페이지 (512B~16KB)
쓰기 단위바이트/워드페이지
삭제 단위Erase Block (64KB~256KB)Erase Block (128KB~512KB)
읽기 속도빠름 (XIP 가능)보통
쓰기 속도느림빠름
삭제 속도매우 느림 (~1초)빠름 (~2ms)
밀도/용량낮음 (1~256MB)높음 (128MB~TB)
비트 에러매우 드뭄빈번 (ECC 필수)
P/E Cycle10만~100만SLC:10만 / MLC:1만 / TLC:3천
XIP 지원가능불가
주요 용도부트 ROM, 펌웨어데이터 스토리지

Erase Block & P/E Cycle

Flash 메모리의 가장 중요한 제약은 erase-before-write입니다:

각 erase block은 유한한 P/E(Program/Erase) cycle을 가집니다. 특정 블록에 쓰기가 집중되면 해당 블록이 먼저 마모되어 불량 블록이 됩니다. 이를 방지하기 위해 wear leveling은 핵심 메커니즘입니다.

OOB 영역 & Bad Block 관리

NAND Flash의 각 페이지에는 데이터 영역 외에 OOB(Out-Of-Band) 또는 spare 영역이 있습니다:

MTD 추상화: JFFS2는 Flash 하드웨어에 직접 접근하지 않고 MTD 계층을 통해 접근합니다. MTD가 ECC, Bad Block 관리, OOB 접근 등을 추상화하므로 JFFS2는 Flash 종류(NOR/NAND)에 독립적인 로직을 유지할 수 있습니다.

MTD 서브시스템

MTD(Memory Technology Device)는 Flash 메모리와 같은 비휘발성 메모리를 위한 Linux 커널의 추상화 계층입니다. 일반 블록 디바이스(block_device)와 달리 MTD는 erase 연산을 직접 노출합니다.

User Space (mkfs.jffs2, flash_eraseall, mtd-utils) /dev/mtdN (mtdchar) /dev/mtdblockN (mtdblock) JFFS2 UBIFS (UBI) YAFFS2 LogFS MTD Core (mtd_info, mtd_read/write/erase) NOR Flash Driver NAND Flash Driver SPI-NOR / SPI-NAND Flash Hardware (NOR / NAND / SPI Flash Chips)

MTD 계층 아키텍처

MTD 서브시스템은 3개 계층으로 구성됩니다:

mtd_info 구조체

MTD 디바이스의 핵심 데이터 구조입니다:

/* include/linux/mtd/mtd.h */
struct mtd_info {
    u_char type;           /* MTD_NORFLASH, MTD_NANDFLASH, ... */
    uint32_t flags;        /* MTD_WRITEABLE, MTD_BIT_WRITEABLE, ... */
    uint64_t size;         /* 전체 MTD 파티션 크기 */
    uint32_t erasesize;    /* erase block 크기 */
    uint32_t writesize;    /* 최소 쓰기 단위 (NOR:1, NAND:page) */
    uint32_t oobsize;      /* OOB 영역 크기 (바이트) */

    /* 콜백 함수 포인터 */
    int (*_read)(struct mtd_info *mtd, loff_t from, size_t len,
                  size_t *retlen, u_char *buf);
    int (*_write)(struct mtd_info *mtd, loff_t to, size_t len,
                   size_t *retlen, const u_char *buf);
    int (*_erase)(struct mtd_info *mtd, struct erase_info *instr);
    int (*_read_oob)(struct mtd_info *mtd, loff_t from,
                      struct mtd_oob_ops *ops);
    int (*_write_oob)(struct mtd_info *mtd, loff_t to,
                       struct mtd_oob_ops *ops);
    int (*_block_isbad)(struct mtd_info *mtd, loff_t ofs);
    int (*_block_markbad)(struct mtd_info *mtd, loff_t ofs);
    ...
};

MTD 파티션

하나의 물리적 Flash 칩을 논리적으로 분할하여 각각 독립적인 MTD 디바이스로 사용할 수 있습니다. 파티션 정의 방법:

/* Device Tree 파티션 예 */
flash@0 {
    compatible = "jedec,spi-nor";
    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        bootloader@0 {
            label = "bootloader";
            reg = <0x0 0x40000>;   /* 256KB */
            read-only;
        };
        rootfs@40000 {
            label = "rootfs";
            reg = <0x40000 0xFC0000>; /* ~16MB */
        };
    };
};

유저스페이스 인터페이스

MTD는 두 가지 유저스페이스 인터페이스를 제공합니다:

인터페이스디바이스 노드특성용도
mtdchar/dev/mtdN문자 디바이스, ioctl로 erase 지원mtd-utils (flash_eraseall, nandwrite 등)
mtdblock/dev/mtdblockN블록 디바이스 에뮬레이션mount -t jffs2 /dev/mtdblockN /mnt

JFFS2 온디스크 구조

JFFS2의 온디스크 데이터는 가변 길이 노드(node)들의 연속 스트림입니다. Flash에 순차적으로 append되며, 모든 노드는 공통 헤더로 시작합니다.

노드 헤더 & 매직 마커 (0x1985)

모든 JFFS2 노드는 jffs2_unknown_node 공통 헤더로 시작합니다:

/* include/uapi/linux/jffs2.h */
struct jffs2_unknown_node {
    jint16_t magic;     /* 0x1985 — David Woodhouse 탄생년도 */
    jint16_t nodetype;  /* 노드 타입 식별자 */
    jint32_t totlen;    /* 패딩 포함 전체 노드 길이 */
    jint32_t hdr_crc;   /* 헤더 CRC-32 */
} __attribute__((packed));

/* 노드 타입 상수 */
#define JFFS2_NODETYPE_DIRENT       0x0001  /* 디렉토리 엔트리 */
#define JFFS2_NODETYPE_INODE        0x0002  /* inode 데이터 */
#define JFFS2_NODETYPE_CLEANMARKER  0x2003  /* 삭제된 블록 표시 */
#define JFFS2_NODETYPE_PADDING      0x2004  /* 패딩 노드 */
#define JFFS2_NODETYPE_SUMMARY      0x2006  /* EBS 요약 노드 */
#define JFFS2_NODETYPE_XATTR        0x0008  /* 확장 속성 */
#define JFFS2_NODETYPE_XREF         0x0009  /* xattr 참조 */

매직 넘버 0x1985는 JFFS2 개발자 David Woodhouse의 탄생년도입니다. Flash 스캔 시 이 매직 마커를 찾아 노드 시작점을 식별합니다.

jffs2_raw_inode

파일/디렉토리의 inode 메타데이터와 데이터를 저장하는 노드입니다:

struct jffs2_raw_inode {
    jint16_t magic;         /* 0x1985 */
    jint16_t nodetype;      /* JFFS2_NODETYPE_INODE (0x0002) */
    jint32_t totlen;        /* 전체 노드 길이 */
    jint32_t hdr_crc;       /* 헤더 CRC */

    jint32_t ino;           /* inode 번호 */
    jint32_t version;       /* inode 버전 (새 쓰기마다 증가) */
    jmode_t  mode;          /* 파일 모드/권한 */
    jint16_t uid, gid;     /* 소유자 */
    jint32_t isize;         /* 파일 크기 (inode 레벨) */
    jint32_t atime, mtime, ctime; /* 타임스탬프 */

    jint32_t offset;        /* 파일 내 데이터 시작 오프셋 */
    jint32_t csize;         /* 압축된 데이터 크기 */
    jint32_t dsize;         /* 원본 데이터 크기 */
    jint8_t  compr;         /* 압축 알고리즘 ID */
    jint8_t  usercompr;     /* 유저 요청 압축 */
    jint16_t flags;         /* 플래그 */
    jint32_t data_crc;      /* 데이터 영역 CRC */
    jint32_t node_crc;      /* 헤더+메타 CRC */
    jint8_t  data[0];       /* 가변 길이 데이터 */
} __attribute__((packed));

하나의 파일이 여러 jffs2_raw_inode 노드로 분할 저장될 수 있습니다. 각 노드는 offsetdsize로 파일 내 위치를 지정하며, version이 높은 노드가 해당 범위의 최신 데이터를 나타냅니다.

jffs2_raw_dirent

디렉토리 엔트리(파일명 → inode 매핑)를 저장합니다:

struct jffs2_raw_dirent {
    jint16_t magic;         /* 0x1985 */
    jint16_t nodetype;      /* JFFS2_NODETYPE_DIRENT (0x0001) */
    jint32_t totlen;        /* 전체 노드 길이 */
    jint32_t hdr_crc;       /* 헤더 CRC */

    jint32_t pino;          /* 부모 디렉토리 inode 번호 */
    jint32_t version;       /* dirent 버전 */
    jint32_t ino;           /* 대상 inode 번호 (0이면 삭제) */
    jint32_t mctime;        /* 수정 시간 */
    jint8_t  nsize;         /* 파일명 길이 */
    jint8_t  type;          /* DT_REG, DT_DIR, DT_LNK, ... */
    jint8_t  unused[2];
    jint32_t node_crc;      /* 메타 CRC */
    jint32_t name_crc;      /* 파일명 CRC */
    jint8_t  name[0];       /* 가변 길이 파일명 */
} __attribute__((packed));

파일 삭제 시 ino=0인 dirent 노드를 새로 append하여 기존 이름을 무효화합니다. 실제 데이터 삭제는 GC가 처리합니다.

jffs2_raw_xattr & jffs2_raw_xref

Linux 2.6.18부터 JFFS2는 확장 속성(xattr)을 지원합니다:

이 설계를 통해 SELinux 보안 레이블, POSIX ACL 등을 효율적으로 저장합니다.

jffs2_raw_summary (EBS)

Erase Block Summary(EBS)는 각 erase block의 끝에 배치되는 요약 노드입니다:

CRC 보호

JFFS2는 모든 온디스크 데이터를 CRC-32로 보호합니다:

Flash 비트 플립이나 불완전 쓰기를 탐지하여 손상된 노드를 무시하고 이전 유효 버전을 사용합니다.

로그 구조 설계

JFFS2는 Flash의 erase-before-write 제약에 최적화된 순수 로그 구조(purely log-structured) 파일시스템입니다. 모든 변경 사항은 Flash의 다음 빈 공간에 순차적으로 append됩니다.

Erase Block 0 (Clean) Erase Block 1 (Dirty) Erase Block 2 (쓰기 중) Erase Block 3 (Free) inode #5 v1 dirent A inode #7 v1 dirent B summary inode #5 v1 ✗ inode #8 v1 dirent C ✗ inode #9 v1 summary inode #5 v2 dirent D ← write ptr [ free space ] [ 0xFF ... erased ... clean marker ] 유효 노드 무효(obsolete) 노드 summary

Append-Only 쓰기 모델

JFFS2의 모든 쓰기는 append-only입니다:

Flash에서 제자리 덮어쓰기(in-place update)가 불가능하므로, 이전 노드는 obsolete 상태로 남아있다가 GC에 의해 정리됩니다.

공간 상태: Clean / Dirty / Free / Wasted

JFFS2는 각 erase block의 공간 상태를 4가지로 분류합니다:

상태설명GC 대상
Clean (used_size)유효한 노드가 차지하는 공간아니오
Dirty (dirty_size)무효화된(obsolete) 노드가 차지하는 공간예 — 회수 대상
Free (free_size)아직 한 번도 쓰여지지 않은 빈 공간아니오
Wasted (wasted_size)CRC 오류 등으로 사용 불가한 공간예 — 블록 삭제 시 회수

GC는 dirty 비율이 높은 블록을 선택하여 유효 노드만 다른 블록으로 복사한 뒤 해당 블록을 삭제(erase)합니다.

노드 무효화

노드가 무효화되는 경우:

가비지 컬렉션 (GC)

로그 구조 파일시스템의 핵심 메커니즘인 GC는 obsolete 노드가 차지하는 공간을 회수합니다. JFFS2는 Foreground GC와 Background GC 두 가지 모드를 제공합니다.

1. Dirty 블록 선택 obs valid obs valid obs 2. 유효 노드 복사 valid valid free → 현재 쓰기 블록에 append 3. 블록 삭제 → Free 0xFF ... (erased) → Free 블록 풀 반환 GC 사이클: dirty 비율이 높은 블록 선택 → 유효 노드만 현재 쓰기 블록으로 복사 → 원본 블록 erase → free 풀 반환 Wear Leveling 전략: 99% 확률: dirty 비율이 가장 높은 블록 선택 (dirty_list) — 공간 효율 1% 확률: 유효 데이터만 있는 clean 블록 선택 (clean_list) — wear leveling (정적 데이터 재배치)
Write Amplification: GC 과정에서 유효 노드를 복사하는 것은 추가 Flash 쓰기를 발생시킵니다(write amplification). Flash가 거의 가득 찬 상태에서는 GC 효율이 급격히 떨어지며 성능이 저하됩니다. JFFS2에서 최소 5~10%의 여유 공간을 유지하는 것이 권장됩니다.

GC 트리거 조건

GC가 시작되는 조건:

Foreground GC

쓰기 경로에서 free 블록이 부족할 때 동기적으로 실행됩니다:

Background GC (gcthread)

커널 스레드 jffs2_gcd_mtdN이 백그라운드에서 GC를 수행합니다:

jffs2_garbage_collect_pass() 분석

GC의 핵심 함수입니다:

/* fs/jffs2/gc.c — 단순화된 GC 패스 로직 */
int jffs2_garbage_collect_pass(struct jffs2_sb_info *c)
{
    struct jffs2_eraseblock *jeb;
    struct jffs2_raw_node_ref *raw;
    int ret;

    /* 1. GC 대상 블록 선택 */
    if (!c->gcblock) {
        if (jffs2_should_verify_write(c))
            c->gcblock = jffs2_find_gc_block(c);
        /* 99%: dirty_list에서, 1%: clean_list에서 (wear leveling) */
    }

    jeb = c->gcblock;
    if (!jeb)
        return 0;

    /* 2. 블록 내 다음 노드 가져오기 */
    raw = jeb->gc_node;

    /* 3. 노드가 유효한지 확인 */
    if (ref_obsolete(raw)) {
        /* obsolete 노드 — 건너뛰기 */
        jeb->gc_node = ref_next(raw);
        return 0;
    }

    /* 4. 유효 노드를 현재 쓰기 블록으로 복사 */
    ret = jffs2_garbage_collect_live(c, jeb, raw, ...);

    /* 5. 블록의 모든 노드 처리 완료 시 erase 예약 */
    if (jeb->gc_node == jeb->last_node) {
        jffs2_erase_pending_trigger(c);
        c->gcblock = NULL;
    }

    return ret;
}

Wear Leveling

JFFS2의 wear leveling 전략:

압축

JFFS2는 데이터를 Flash에 쓰기 전에 투명하게 압축합니다. Flash 용량이 제한적인 임베디드 환경에서 저장 효율을 크게 향상시킵니다.

압축 알고리즘

알고리즘CONFIG 옵션압축률속도특징
zlibCONFIG_JFFS2_ZLIB높음느림deflate 기반, 가장 높은 압축률
rtime항상 포함낮음매우 빠름Run-Length Encoding 변형, 기본 폴백
lzoCONFIG_JFFS2_LZO보통빠름압축/해제 모두 빠름, CPU 부하 적음
rubinCONFIG_JFFS2_RUBIN보통느림산술 코딩, 실용성 낮음 (거의 사용 안 됨)

compr_priority & 자동 선택

JFFS2는 여러 압축 알고리즘을 우선순위에 따라 순차적으로 시도합니다:

/* 압축 모드 설정 (mount 옵션 또는 ioctl) */
JFFS2_COMPR_MODE_NONE       /* 압축 비활성화 */
JFFS2_COMPR_MODE_PRIORITY   /* 우선순위 기반 (기본) */
JFFS2_COMPR_MODE_SIZE       /* 최소 크기 선택 */
JFFS2_COMPR_MODE_FAVOURLZO  /* LZO 우선 */

커널 설정 옵션

# 압축 관련 Kconfig
CONFIG_JFFS2_ZLIB=y      # zlib 압축 지원
CONFIG_JFFS2_LZO=y       # LZO 압축 지원
CONFIG_JFFS2_RTIME=y     # rtime 압축 (항상 포함)
CONFIG_JFFS2_RUBIN=n     # rubin (비권장)
CONFIG_JFFS2_CMODE_NONE=n
CONFIG_JFFS2_CMODE_PRIORITY=y  # 기본 압축 모드
CONFIG_JFFS2_CMODE_SIZE=n
CONFIG_JFFS2_CMODE_FAVOURLZO=n

마운트 과정

JFFS2 마운트는 Flash의 모든 erase block을 스캔하여 인메모리 자료구조를 구축하는 과정입니다. 이 과정이 JFFS2의 가장 큰 약점 중 하나입니다.

Flash 스캐닝

마운트 시 JFFS2는 Flash의 모든 erase block을 순차적으로 읽습니다:

  1. 각 블록의 시작부터 매직 마커(0x1985)를 찾으며 노드를 파싱
  2. 각 노드의 CRC를 검증하여 유효성 확인
  3. 유효한 노드의 참조를 인메모리 해시 테이블에 기록
  4. 각 블록의 clean/dirty/free 상태 계산
느린 마운트: Summary 미사용 시 전체 Flash를 바이트 단위로 스캔해야 합니다. 128MB Flash의 경우 수십 초가 걸릴 수 있으며, 임베디드 시스템의 부팅 시간에 직접적인 영향을 미칩니다. CONFIG_JFFS2_SUMMARY를 반드시 활성화하세요.

Inode 캐시 구축

Flash 스캔 완료 후 inode별로 최신 노드를 결정합니다:

Summary 지원 (빠른 마운트)

EBS(Erase Block Summary)가 활성화되면:

  1. 각 erase block의 끝에서 summary 노드를 먼저 확인
  2. summary가 존재하면 해당 블록의 전체 스캔을 건너뛰고 요약 정보만 사용
  3. summary가 없는 블록만 전체 스캔 수행 (이전 버전 호환)
  4. 마운트 시간을 10~50배 단축 가능

마운트 옵션

옵션설명
compr=none|priority|size|favourlzo압축 모드 선택
rp_size=Nroot 전용 예약 공간 (바이트)
override_compr=N모든 파일에 특정 압축 알고리즘 강제
# 마운트 예
mount -t jffs2 /dev/mtdblock2 /mnt/flash
mount -t jffs2 -o compr=lzo /dev/mtdblock2 /mnt/flash

XIP (eXecute In Place)

XIP 개념 & NOR Flash

XIP(eXecute In Place)는 Flash에 저장된 코드를 RAM에 복사하지 않고 Flash에서 직접 실행하는 기술입니다:

NOR Flash 전용: XIP는 NOR Flash에서만 가능합니다. NAND Flash는 페이지 단위 순차 접근만 지원하므로 CPU가 직접 코드를 fetch할 수 없습니다. NAND 기반 시스템에서는 반드시 RAM으로 로드 후 실행해야 합니다.

커널 XIP 구현

JFFS2 XIP 지원을 위해 필요한 설정:

Write Buffering (wbuf)

NAND 쓰기 정렬

NAND Flash는 페이지 단위(512B~16KB)로만 쓸 수 있지만, JFFS2 노드는 가변 길이입니다. 이 불일치를 해결하기 위해 JFFS2는 wbuf(write buffer)를 사용합니다:

wbuf 플러시 & 패딩

wbuf는 다음 상황에서 Flash에 플러시됩니다:

플러시 시 버퍼가 페이지 크기에 미달하면 패딩 노드(JFFS2_NODETYPE_PADDING)를 추가하여 페이지를 채웁니다.

전원 차단 시 wbuf에 있던 미플러시 데이터는 손실됩니다. 그러나 JFFS2의 로그 구조 특성상 이전 유효 데이터는 보존되므로 파일시스템 일관성은 유지됩니다.

핵심 커널 자료구조

jffs2_sb_info

JFFS2 슈퍼블록 정보 — 마운트된 파일시스템의 전체 상태를 관리합니다:

/* fs/jffs2/jffs2_fs_sb.h — 핵심 필드 */
struct jffs2_sb_info {
    struct mtd_info *mtd;           /* 기반 MTD 디바이스 */

    uint32_t flash_size;             /* Flash 전체 크기 */
    uint32_t used_size;              /* 유효 노드 총 크기 */
    uint32_t dirty_size;             /* obsolete 노드 총 크기 */
    uint32_t free_size;              /* 미사용 공간 총 크기 */
    uint32_t wasted_size;            /* 손상/wasted 총 크기 */

    uint32_t nr_free;                /* free erase block 수 */
    uint32_t nr_erasing;             /* 삭제 진행 중인 블록 수 */

    /* Erase block 리스트 */
    struct list_head clean_list;     /* 유효 노드만 있는 블록 */
    struct list_head dirty_list;     /* dirty 노드가 있는 블록 */
    struct list_head very_dirty_list;/* dirty 비율 높은 블록 */
    struct list_head free_list;      /* 빈 블록 */
    struct list_head erasable_list;  /* erase 대기 블록 */
    struct list_head erasing_list;   /* erase 진행 중 블록 */
    struct list_head erase_pending_list;
    struct list_head bad_list;       /* bad block 목록 */

    struct jffs2_eraseblock *nextblock; /* 현재 쓰기 블록 */
    struct jffs2_eraseblock *gcblock;   /* 현재 GC 대상 블록 */

    /* Write buffer (NAND) */
    unsigned char *wbuf;             /* write buffer 포인터 */
    uint32_t wbuf_ofs;               /* wbuf의 Flash 오프셋 */
    uint32_t wbuf_len;               /* wbuf 내 유효 데이터 크기 */
    uint32_t wbuf_pagesize;          /* NAND 페이지 크기 */

    /* Inode 캐시 해시 테이블 */
    struct jffs2_inode_cache **inocache_list;
    ...
};

jffs2_inode_info

VFS inode에 대응하는 JFFS2 고유 정보입니다:

jffs2_raw_node_ref

Flash에 저장된 각 노드에 대한 인메모리 참조입니다:

jffs2_full_dnode & jffs2_full_dirent

UBIFS 비교

UBIFS(Unsorted Block Image File System)는 JFFS2의 한계를 극복하기 위해 설계된 차세대 Flash 파일시스템입니다. MTD 위에 UBI(Unsorted Block Images) 계층을 추가합니다.

UBI 계층

UBI는 MTD와 파일시스템 사이에 위치하는 볼륨 관리 계층입니다:

UBIFS 장점

UBIFS 권장: 신규 NAND Flash 프로젝트에서는 JFFS2 대신 UBIFS를 사용하는 것이 권장됩니다. JFFS2는 소용량 NOR Flash(~64MB)에서는 여전히 합리적인 선택이지만, 대용량 NAND에서는 마운트 시간과 RAM 오버헤드 면에서 UBIFS가 압도적으로 우수합니다.

마이그레이션 고려사항

실전 활용

커널 설정 (CONFIG_JFFS2_*)

# 핵심 JFFS2 커널 설정
CONFIG_JFFS2_FS=y                # JFFS2 파일시스템 지원
CONFIG_JFFS2_FS_DEBUG=0          # 디버그 레벨 (0: 비활성, 1: 에러, 2: 상세)
CONFIG_JFFS2_FS_WRITEBUFFER=y    # NAND용 wbuf (자동 활성화)
CONFIG_JFFS2_FS_WBUF_VERIFY=n   # wbuf 쓰기 후 검증 (디버깅용)
CONFIG_JFFS2_SUMMARY=y           # EBS 빠른 마운트 (강력 권장)
CONFIG_JFFS2_FS_XATTR=y         # xattr 지원
CONFIG_JFFS2_FS_POSIXACL=y      # POSIX ACL
CONFIG_JFFS2_FS_SECURITY=y      # 보안 레이블 (SELinux)

# 압축
CONFIG_JFFS2_ZLIB=y              # zlib 압축
CONFIG_JFFS2_LZO=y               # LZO 압축
CONFIG_JFFS2_RTIME=y             # rtime 압축
CONFIG_JFFS2_CMODE_PRIORITY=y    # 기본 압축 모드

mkfs.jffs2 (이미지 생성)

JFFS2 이미지를 호스트에서 미리 생성하여 Flash에 기록합니다:

# mtd-utils 패키지 설치
apt-get install mtd-utils     # Debian/Ubuntu
yum install mtd-utils         # RHEL/CentOS

# NOR Flash용 JFFS2 이미지 생성
mkfs.jffs2 \
    --root=/path/to/rootfs \   # 루트 디렉토리
    --output=rootfs.jffs2 \    # 출력 이미지
    --eraseblock=0x10000 \     # erase block 크기 (64KB)
    --pad=0x1000000 \          # 이미지 크기 패딩 (16MB)
    --no-cleanmarkers \        # NAND: cleanmarker 생략 (OOB 사용 시) */
    --compression-mode=priority

# NAND Flash용 (페이지 크기 고려)
mkfs.jffs2 \
    --root=/path/to/rootfs \
    --output=rootfs.jffs2 \
    --eraseblock=0x20000 \     # 128KB erase block
    --pagesize=0x800 \         # 2KB 페이지
    --no-cleanmarkers \        # NAND: OOB에 cleanmarker 저장
    --squash-uids              # UID/GID를 0으로 (보안) */

# Flash에 이미지 기록
flash_eraseall /dev/mtd2       # 먼저 Flash 전체 삭제
nandwrite -p /dev/mtd2 rootfs.jffs2  # NAND에 기록 (-p: 패딩)

# 또는 mtdblock으로 마운트 후 복사
mount -t jffs2 /dev/mtdblock2 /mnt/flash
cp -a /path/to/rootfs/* /mnt/flash/

임베디드 시스템 활용 사례

성능 & 제한사항

확장성 한계

JFFS2는 소용량 Flash에 최적화되어 있으며, 대용량에서는 심각한 확장성 문제가 발생합니다:

마운트 시간 문제

Summary 미사용 시 마운트 시간 예시:

Flash 크기Summary 미사용Summary 사용
16 MB~2초<0.5초
64 MB~8초<1초
128 MB~20초<2초
256 MB~45초~4초

RAM 오버헤드

RAM 오버헤드: JFFS2는 Flash의 모든 노드에 대한 참조를 RAM에 유지합니다. 대략 Flash 1MB당 4~8KB의 RAM이 필요합니다. 128MB Flash의 경우 512KB~1MB의 RAM이 JFFS2 메타데이터만으로 소비될 수 있으며, 이는 RAM이 제한적인 임베디드 시스템에서 심각한 문제가 됩니다.

대안 파일시스템

시나리오권장 파일시스템
소용량 NOR Flash (<64MB)JFFS2 (여전히 합리적)
대용량 NAND Flash (>64MB)UBIFS
SSD / eMMC (FTL 있음)F2FS, ext4
MCU / 초소형 임베디드LittleFS
읽기 전용 rootfssquashfs + JFFS2 overlay

JFFS2 / UBIFS / YAFFS2 / LittleFS 비교

항목JFFS2UBIFSYAFFS2LittleFS
기반 계층MTD 직접UBI → MTDMTD 직접 / 자체 NAND블록 디바이스 추상화
Flash 지원NOR + NANDNAND (NOR 가능)NAND 전용NOR / eMMC / 임의
설계순수 로그 구조wandering tree로그 구조 (YAFFS 고유)COW + bounded log
마운트 속도느림 (전체 스캔)빠름 (인덱스 기반)빠름 (순차 스캔)빠름
RAM 오버헤드높음 (O(n))낮음보통매우 낮음
확장성~64MB수 GB+수 GB~수 MB
Wear Leveling확률적 (GC)정밀 (UBI erase count)기본적기본적
압축zlib, lzo, rtimelzo, zlib, zstd없음없음
XIP가능 (NOR)불가불가불가
라이선스GPL v2GPL v2GPL v2 / 상용BSD-3
Linux 메인라인2.4.10+2.6.27+미포함 (out-of-tree)미포함 (RTOS)
주요 용도소용량 NOR 임베디드대용량 NAND 임베디드Android (과거)MCU / RTOS
Flash 파일시스템 선택 가이드:
  • 소용량 NOR Flash + 간단한 요구사항 → JFFS2
  • 대용량 NAND Flash + 빠른 마운트/확장성 필요 → UBIFS
  • SSD/eMMC (FTL 내장) → F2FS 또는 ext4
  • MCU/RTOS + 극히 제한된 리소스 → LittleFS
  • 읽기 전용 rootfs + 최소 쓰기 영역 → squashfs + JFFS2 overlay

웨어 레벨링 심화

Flash 메모리의 각 erase block은 유한한 P/E(Program/Erase) cycle을 가집니다. 특정 블록에 쓰기가 편중되면 해당 블록이 먼저 마모되어 전체 디바이스의 수명이 단축됩니다. JFFS2의 웨어 레벨링은 GC 기반 확률적 분산 전략으로, 전용 FTL이나 UBI 계층 없이 파일시스템 자체에서 마모를 분산합니다.

JFFS2 웨어 레벨링 전략 동적 웨어 레벨링 (99%) Dirty Block P/E=45000 GC: 유효 노드 → 새 블록 복사 Erase → Free 블록 반환 dirty 비율 높은 블록 우선 → 자연스러운 쓰기 분산 정적 웨어 레벨링 (1%) Clean Block P/E=200 (냉각) 정적 데이터 → 다른 블록 이동 냉각 블록 erase → P/E 균일화 장기 고정 데이터의 블록 마모 방지 GC 연동 흐름: jffs2_find_gc_block() 난수 생성 0~99 범위 값 < 99 → dirty_list 선택 값 == 99 → clean_list 선택 유효 노드 복사 → 현재 쓰기 블록 블록 Erase → free_list

동적 웨어 레벨링

JFFS2의 동적 웨어 레벨링은 로그 구조 특성에서 자연스럽게 발생합니다:

이 메커니즘은 자주 변경되는 데이터(hot data)에 대해 효과적으로 쓰기를 분산합니다. 그러나 한 번 쓰고 거의 변경되지 않는 정적 데이터(cold data)에 대해서는 한계가 있습니다.

정적 웨어 레벨링

정적 웨어 레벨링은 냉각 블록(cold block)에 고정된 유효 데이터를 강제로 이동시켜 해당 블록도 erase cycle에 참여하게 합니다:

/* fs/jffs2/nodemgmt.c — GC 블록 선택 로직 (단순화) */
static struct jffs2_eraseblock *jffs2_find_gc_block(
    struct jffs2_sb_info *c)
{
    struct jffs2_eraseblock *ret;
    struct list_head *nextlist = NULL;
    int n = jffs2_get_sb_count(c) % 100;

    /* 1% 확률로 clean 블록 선택 → 정적 웨어 레벨링 */
    if (n == 0 && !list_empty(&c->clean_list)) {
        nextlist = &c->clean_list;
    }
    /* 나머지 99%: dirty 비율 높은 블록 우선 */
    else if (!list_empty(&c->very_dirty_list)) {
        nextlist = &c->very_dirty_list;
    } else if (!list_empty(&c->dirty_list)) {
        nextlist = &c->dirty_list;
    } else if (!list_empty(&c->clean_list)) {
        nextlist = &c->clean_list;
    }

    if (!nextlist)
        return NULL;

    ret = list_entry(nextlist->next,
                     struct jffs2_eraseblock, list);
    list_del(&ret->list);
    return ret;
}

웨어 레벨링 한계와 대안

JFFS2 웨어 레벨링의 근본적 한계:

한계점영향대안
erase count 미추적블록별 마모 수준을 정확히 알 수 없음UBI: erase count 헤더 기록
확률적 분산 (1%)장기적으로 편차 발생 가능UBI: 임계값 기반 자동 균형
GC 의존적GC 미실행 시 정적 데이터 고정UBI: background 스레드 상시 동작
Bad Block 미보상bad block 발생 시 해당 영역 영구 손실UBI: PEB 풀로 자동 대체
실무 권장: NOR Flash에서 10만 P/E cycle 기준으로, 1% 정적 분산 확률은 대부분의 임베디드 제품 수명(10년)에서 충분합니다. 그러나 NAND Flash(SLC 10만, MLC 1만)에서는 UBI의 정밀한 erase count 기반 웨어 레벨링이 압도적으로 유리합니다.

로그 구조 쓰기 심화

JFFS2의 로그 구조는 전통적인 LFS(Log-structured File System)와 유사하지만, Flash 메모리의 고유한 제약에 맞게 변형되었습니다. 여기서는 순차 쓰기 경로, 노드 배치 정책, 그리고 CRC 기반 무결성 보호 메커니즘을 커널 코드 수준에서 분석합니다.

로그 구조 쓰기 경로 (write path) VFS write() 사용자 데이터 jffs2_write_inode() 데이터 압축 raw_inode 생성 CRC 계산 Flash append jffs2_raw_inode 노드 구조 (Flash 온디스크 레이아웃) magic 0x1985 nodetype 0x0002 totlen 전체 크기 hdr_crc 헤더 CRC ino/ver inode/버전 offset/size 범위 정보 node_crc 메타 CRC data[] 압축 데이터 ← hdr_crc 범위 → ← node_crc 범위 → 시간순 Append 흐름 (Erase Block 내부) inode #3 v1 t=0 dirent #3 t=1 inode #5 v2 t=2 inode #3 v2 t=3 free space write ptr 시간 흐름 → (항상 앞으로만 쓰기, 되돌아가지 않음)

쓰기 경로 상세

VFS write() 호출부터 Flash에 노드가 기록되기까지의 과정입니다:

/* fs/jffs2/write.c — 쓰기 경로 핵심 (단순화) */
int jffs2_write_inode_range(struct jffs2_sb_info *c,
    struct jffs2_inode_info *f,
    struct jffs2_raw_inode *ri,
    unsigned char *buf, uint32_t offset,
    uint32_t writelen)
{
    struct jffs2_full_dnode *fn;
    unsigned char *comprbuf = NULL;
    uint16_t comprtype;
    uint32_t cdatalen;

    /* 1. 데이터 압축 */
    comprtype = jffs2_compress(c, f, buf, &comprbuf,
                              &writelen, &cdatalen);

    ri->compr = comprtype;
    ri->dsize = cpu_to_je32(writelen);
    ri->csize = cpu_to_je32(cdatalen);
    ri->offset = cpu_to_je32(offset);

    /* 2. CRC 계산 */
    ri->node_crc = cpu_to_je32(
        crc32(0, ri, sizeof(*ri) - 8));
    ri->data_crc = cpu_to_je32(
        crc32(0, comprbuf, cdatalen));

    /* 3. Flash에 노드 append (공간 예약 + 쓰기) */
    fn = jffs2_write_dnode(c, f, ri, comprbuf,
                           cdatalen, ALLOC_NORMAL);

    /* 4. fragtree에 새 노드 등록, 이전 노드 obsolete 처리 */
    jffs2_add_full_dnode_to_inode(c, f, fn);

    return 0;
}

노드 버전 관리와 충돌 해결

같은 inode의 여러 노드가 Flash에 공존할 때 최신 데이터를 결정하는 규칙:

비교: 디스크 기반 LFS vs JFFS2: 전통적인 LFS(예: Sprite LFS)는 디스크의 세그먼트 단위로 로그를 관리하고, inode map을 통해 최신 위치를 추적합니다. JFFS2는 Flash의 erase block을 세그먼트로 사용하되, 별도의 inode map 없이 전체 스캔 또는 summary 노드로 최신 상태를 재구성합니다.

JFFS2 vs UBIFS 비교 심화

JFFS2와 UBIFS는 동일한 Flash 하드웨어를 대상으로 하지만, 설계 철학이 근본적으로 다릅니다. 이 섹션에서는 아키텍처, 확장성, 마운트 시간, 메모리 사용량의 차이를 정량적으로 비교합니다.

JFFS2 vs UBIFS 아키텍처 비교 JFFS2 스택 VFS 계층 JFFS2 로그 구조 + GC + 인메모리 인덱스 MTD Core Flash Hardware UBIFS 스택 VFS 계층 UBIFS (wandering tree + journal) UBI (볼륨 관리 + Wear Leveling) MTD Core Flash Hardware 256MB NAND Flash 기준 정량 비교 JFFS2 마운트: ~45초 (Summary 미사용) JFFS2 RAM: ~1MB 이상 UBIFS: ~2초 UBIFS RAM: ~200KB

아키텍처 차이

측면JFFS2UBIFS
인덱싱인메모리 전체 (해시 테이블 + fragtree)온디스크 B+tree (TNC: Tree Node Cache)
Wear LevelingGC 기반 확률적 (1% 정적)UBI: erase count 기반 정밀 제어
쓰기 모델순수 로그 (append-only)wandering tree + journal
GC 단위erase block 내 개별 노드LEB (Logical Erase Block) 단위
압축노드 단위 압축노드 단위 압축 + bulk-read 최적화
전원 안전성로그 구조 + CRC (저널 없음)전용 저널 + commit master node
Bad Block 관리MTD BBT에 의존UBI: 자동 PEB 대체 + scrubbing

확장성 비교

Flash 크기JFFS2 마운트UBIFS 마운트JFFS2 RAMUBIFS RAM
16 MB~2초<0.5초~128KB~80KB
64 MB~8초<1초~512KB~120KB
256 MB~45초~2초~1MB+~200KB
1 GB수 분 (비실용)~3초~4MB+~300KB
전환점: 64MB — Flash 용량이 64MB를 초과하면 JFFS2의 O(n) 마운트 시간과 O(n) RAM 사용이 실용적 한계에 도달합니다. 이 시점에서 UBIFS로의 마이그레이션을 진지하게 고려해야 합니다.

JFFS2가 여전히 유리한 경우

GC 알고리즘 심화

JFFS2의 GC(Garbage Collection)는 로그 구조 파일시스템의 공간 회수 메커니즘으로, victim 블록 선택, 유효 노드 이동, wbuf 연동까지의 전체 과정을 상세히 분석합니다.

GC 알고리즘 상세 흐름 Phase 1: GC 트리거 free_size < threshold gcthread 주기 타이머 쓰기 시 공간 부족 Phase 2: Victim 블록 선택 (jffs2_find_gc_block) very_dirty_list dirty > 50% dirty_list dirty > 0 clean_list (1%) 정적 WL용 우선순위: very_dirty → dirty → clean Phase 3: 노드 처리 (jffs2_garbage_collect_pass) obsolete 노드 → 건너뛰기 유효 노드 → nextblock에 복사 wbuf를 통한 NAND 쓰기 정렬 Phase 4: 블록 삭제 및 회수 모든 노드 처리 완료 확인 mtd_erase() 호출 clean marker 기록 → free_list

Victim 블록 선택 전략

GC의 효율은 어떤 블록을 선택하느냐에 크게 좌우됩니다. JFFS2는 다음 우선순위로 victim 블록을 선택합니다:

  1. very_dirty_list: dirty 공간이 블록 크기의 50% 이상인 블록. 유효 노드가 적어 복사 비용이 낮고 공간 회수 효율이 높음
  2. dirty_list: dirty 공간이 있지만 50% 미만인 블록
  3. clean_list: 유효 노드만 있는 블록 (1% 확률로 선택 — 정적 웨어 레벨링)

유효 노드 이동 과정

/* fs/jffs2/gc.c — 유효 노드 이동 (단순화) */
static int jffs2_garbage_collect_live(
    struct jffs2_sb_info *c,
    struct jffs2_eraseblock *jeb,
    struct jffs2_raw_node_ref *raw,
    struct jffs2_inode_info *f)
{
    struct jffs2_node_frag *frag;
    int ret;

    /* 노드 타입에 따라 분기 */
    if (ref_flags(raw) == REF_PRISTINE) {
        /* pristine 노드: Flash에서 읽어 그대로 복사 */
        ret = jffs2_garbage_collect_pristine(c, f, raw);
    } else {
        /* 데이터 노드: 최신 데이터 기준으로 재작성 */
        frag = jffs2_lookup_node_frag(&f->fragtree,
                    je32_to_cpu(raw->offset));
        if (frag && frag->node->raw == raw) {
            /* 이 노드가 해당 범위의 최신 → 복사 */
            ret = jffs2_garbage_collect_dnode(c, jeb, f, frag);
        } else {
            /* 이미 superseded됨 → obsolete 처리 */
            jffs2_mark_node_obsolete(c, raw);
            ret = 0;
        }
    }
    return ret;
}

GC와 wbuf 연동

NAND Flash에서 GC가 유효 노드를 현재 쓰기 블록(nextblock)으로 복사할 때, wbuf를 통해 페이지 정렬된 쓰기를 수행합니다:

GC 스톰(GC Storm): Flash 사용률이 90%를 초과하면 GC가 충분한 free 블록을 확보하기 위해 반복적으로 실행되며, 각 패스에서 이동할 유효 노드 비율이 높아져 write amplification이 급격히 증가합니다. 최악의 경우 쓰기 1바이트를 위해 전체 erase block 크기만큼의 Flash 쓰기가 발생할 수 있습니다.

노드 타입 심화

JFFS2의 모든 온디스크 데이터는 타입별 노드(typed node)로 구성됩니다. 각 노드 타입의 내부 구조와 역할, 그리고 노드 간 관계를 상세히 분석합니다.

JFFS2 노드 타입 관계도 jffs2_unknown_node (공통 헤더) magic(0x1985) | nodetype | totlen | hdr_crc jffs2_raw_inode nodetype = 0x0002 ino, version, mode, uid, gid offset, csize, dsize, compr data_crc, node_crc, data[] jffs2_raw_dirent nodetype = 0x0001 pino, version, ino, type nsize, name_crc name[] (가변 길이 파일명) 특수 노드 CLEANMARKER (0x2003) PADDING (0x2004) SUMMARY (0x2006) XATTR (0x0008) ino Erase Block 내부 노드 배치 예 clean marker inode #3 v1 + data dirent "hello" inode #5 v1 + data dirent "world" pad summary node 블록 시작(0x00) ─────────────────── 블록 끝(erasesize)

클린마커(Cleanmarker) 상세

클린마커는 erase block이 정상적으로 삭제되었음을 표시하는 특수 노드입니다:

/* NOR Flash: 블록 시작에 cleanmarker 노드 기록 */
struct jffs2_unknown_node cleanmarker = {
    .magic    = cpu_to_je16(JFFS2_MAGIC_BITMASK),  /* 0x1985 */
    .nodetype = cpu_to_je16(JFFS2_NODETYPE_CLEANMARKER),
    .totlen   = cpu_to_je32(sizeof(struct jffs2_unknown_node)),
    .hdr_crc  = cpu_to_je32(0)   /* 계산된 CRC */
};

/* NAND Flash: OOB 영역에 cleanmarker 기록 (데이터 영역 절약) */
/* MTD의 oob_write()를 사용하여 spare area에 저장 */

노드 타입 플래그

nodetype 필드의 상위 비트는 특별한 의미를 가집니다:

비트의미설명
bit 15 (0x8000)JFFS2_NODE_ACCURATE노드가 최신/유효한 상태임을 표시
bit 14 (0x4000)JFFS2_FEATURE_INCOMPAT인식 못하면 마운트 거부
bit 13 (0x2000)JFFS2_FEATURE_ROCOMPAT인식 못하면 읽기 전용 마운트
bit 12 (0x1000)JFFS2_FEATURE_RWCOMPAT_DELETE인식 못하면 노드 삭제 가능
호환성 설계: JFFS2의 노드 타입 플래그는 ext4의 feature flag와 유사한 전진 호환성 메커니즘입니다. 새로운 노드 타입이 추가되어도 기존 커널이 적절히 처리할 수 있도록 설계되었습니다.

마운트 과정 심화

JFFS2 마운트는 Flash의 물리적 상태를 인메모리 자료구조로 재구성하는 핵심 과정입니다. 전체 스캔, inode 빌드, summary 노드를 통한 최적화까지의 내부 흐름을 커널 코드 수준에서 분석합니다.

JFFS2 마운트 과정 상세 1. jffs2_do_mount_fs() MTD 디바이스 열기 eraseblock 배열 할당 inocache 해시 초기화 2. jffs2_scan_medium() erase block 순회 summary 확인 또는 전체 노드 스캔 3. 블록 분류 clean/dirty/free 리스트에 블록 배치 bad block 제외 4. jffs2_scan_eraseblock() 상세 — 블록별 스캔 로직 summary 노드 존재? → 요약 사용 0x1985 매직 → 노드 파싱 CRC 검증 → 유효/무효 판정 0xFF 감지 → free 공간 시작 5. jffs2_build_filesystem() inode별 최신 노드 결정 (version 비교) obsolete 노드 마킹 dirty_size / used_size 계산 6. 마운트 완료 GC 스레드 시작 (jffs2_gcd_mtdN) nextblock 설정 (쓰기 시작점) VFS 슈퍼블록 반환 Summary 활성화 시: Step 4에서 블록별 전체 스캔 대신 요약 노드만 읽어 10~50배 마운트 시간 단축

jffs2_scan_medium() 분석

/* fs/jffs2/scan.c — Flash 스캔 핵심 루프 (단순화) */
int jffs2_scan_medium(struct jffs2_sb_info *c)
{
    uint32_t ofs;
    struct jffs2_eraseblock *jeb;
    int i, ret;

    /* 모든 erase block을 순회 */
    for (i = 0; i < c->nr_blocks; i++) {
        jeb = &c->blocks[i];
        ofs = jeb->offset;

        /* bad block 확인 */
        if (jffs2_check_nand_cleanmarker(c, jeb)) {
            list_add(&jeb->list, &c->bad_list);
            continue;
        }

        /* 블록 스캔 (summary 또는 전체) */
        ret = jffs2_scan_eraseblock(c, jeb, ...);

        /* 스캔 결과에 따라 블록 리스트 배치 */
        switch (ret) {
        case BLK_STATE_CLEAN:
            list_add(&jeb->list, &c->clean_list);
            break;
        case BLK_STATE_PARTDIRTY:
            if (jeb->dirty_size > c->sector_size / 2)
                list_add(&jeb->list, &c->very_dirty_list);
            else
                list_add(&jeb->list, &c->dirty_list);
            break;
        case BLK_STATE_CLEANMARKER:
            list_add(&jeb->list, &c->free_list);
            c->nr_free++;
            break;
        case BLK_STATE_ALLFF:
            /* 삭제되었으나 cleanmarker 없음 → 재삭제 예약 */
            list_add(&jeb->list, &c->erase_pending_list);
            break;
        }
    }
    return 0;
}

jffs2_build_filesystem() 분석

전체 Flash 스캔 후, 수집된 노드 참조들을 inode 단위로 정리합니다:

  1. inode 캐시 순회: 각 inode에 연결된 모든 raw_node_ref를 확인
  2. 최신 버전 결정: 같은 offset 범위의 노드들 중 version이 가장 높은 것을 유효로 판정
  3. obsolete 마킹: 이전 버전의 노드를 obsolete로 표시하고 해당 블록의 dirty_size 증가
  4. fragtree 구성: 유효 데이터 노드들을 RB-tree 기반의 fragment tree로 구축
  5. 블록 리스트 갱신: dirty_size 변경에 따라 블록의 리스트 위치를 재조정

Summary 노드 구조

/* include/linux/jffs2.h — Summary 노드 */
struct jffs2_raw_summary {
    jint16_t magic;       /* 0x1985 */
    jint16_t nodetype;    /* JFFS2_NODETYPE_SUMMARY */
    jint32_t totlen;
    jint32_t hdr_crc;
    jint32_t sum_num;     /* 요약된 노드 수 */
    jint32_t cln_mkr;     /* cleanmarker 크기 */
    jint32_t padded;      /* 패딩 크기 */
    jint32_t sum_crc;     /* 요약 데이터 CRC */
    jint32_t node_crc;    /* 헤더 CRC */
    jint32_t sum[0];      /* 가변 길이 요약 배열 */
};
Summary 활성화 필수: 프로덕션 환경에서는 반드시 CONFIG_JFFS2_SUMMARY=y로 빌드하세요. Summary가 없으면 모든 블록의 모든 노드를 바이트 단위로 읽고 CRC를 검증해야 합니다. 16MB Flash에서 ~2초 vs <0.5초, 128MB에서는 ~20초 vs <2초로 차이가 극적입니다.

압축 심화

JFFS2의 압축 서브시스템은 Flash 공간 효율을 극대화하기 위해 설계되었습니다. 알고리즘 선택 전략, miniLZO 최적화, 압축 모드별 성능 특성을 상세히 분석합니다.

JFFS2 압축 알고리즘 선택 흐름 원본 데이터 4KB 페이지 단위 jffs2_compress() 압축 모드 분기 compr_mode 확인 PRIORITY 모드 (기본) 1. zlib 시도 (priority=60) 2. lzo 시도 (priority=80) 3. rtime 폴백 (priority=100) 첫 성공 시 사용 SIZE 모드 모든 알고리즘 시도: zlib → lzo → rtime 각 결과 크기 비교 최소 크기 결과 선택 (느림) FAVOURLZO 모드 1. lzo 우선 시도 2. 결과가 원본보다 작으면 즉시 사용 CPU 효율 우선 (빠름) 결과 검증 compressed >= original? → NONE ri->compr에 알고리즘 ID 기록

압축 서브시스템 내부

/* fs/jffs2/compr.c — 압축 선택 로직 (단순화) */
uint16_t jffs2_compress(struct jffs2_sb_info *c,
    struct jffs2_inode_info *f,
    unsigned char *data_in, unsigned char **cpage_out,
    uint32_t *datalen, uint32_t *cdatalen)
{
    struct jffs2_compressor *this;
    int compr_ret;
    int best = JFFS2_COMPR_NONE;
    uint32_t best_size = *datalen;  /* 원본 크기가 초기 최적 */

    switch (c->mount_opts.compr) {
    case JFFS2_COMPR_MODE_NONE:
        return JFFS2_COMPR_NONE;

    case JFFS2_COMPR_MODE_PRIORITY:
        /* 우선순위순으로 시도, 첫 성공 사용 */
        list_for_each_entry(this, &jffs2_compressor_list, list) {
            if (!this->compress)
                continue;
            compr_ret = this->compress(data_in, *cpage_out,
                                      datalen, cdatalen);
            if (!compr_ret && *cdatalen < *datalen)
                return this->compr;  /* 성공! */
        }
        return JFFS2_COMPR_NONE;

    case JFFS2_COMPR_MODE_SIZE:
        /* 모든 알고리즘 시도, 최소 크기 선택 */
        list_for_each_entry(this, &jffs2_compressor_list, list) {
            compr_ret = this->compress(...);
            if (!compr_ret && *cdatalen < best_size) {
                best = this->compr;
                best_size = *cdatalen;
            }
        }
        return best;
    }
}

miniLZO 최적화

LZO(Lempel-Ziv-Oberhumer)는 임베디드 환경에서 가장 실용적인 압축 알고리즘입니다:

특성zlib (deflate)LZO (miniLZO)rtime
압축 속도~10 MB/s~100 MB/s~200 MB/s
해제 속도~30 MB/s~200 MB/s~300 MB/s
압축률 (텍스트)~65%~45%~20%
메모리 사용~256KB~16KB거의 없음
CPU 부하높음낮음매우 낮음
적합 환경공간 우선균형 (권장)CPU 제한
임베디드 권장 설정: CPU가 200MHz 이상이면 compr=favourlzo를 권장합니다. zlib 대비 약 10배 빠른 압축/해제 속도로 Flash I/O 지연을 최소화하면서도 45% 수준의 합리적인 압축률을 제공합니다. CPU가 100MHz 이하의 초저전력 환경이면 compr=rtime 또는 compr=none을 고려하세요.

파일별 압축 제어

JFFS2는 파일 단위의 세밀한 압축 제어를 지원합니다:

# mkfs.jffs2로 이미지 생성 시 파일별 압축 제어
# -X: 기본 압축 알고리즘 지정
mkfs.jffs2 --root=/rootfs -o rootfs.jffs2 -X lzo

# 특정 파일을 비압축으로 지정 (XIP 대상 바이너리 등)
# fakeroot 환경에서 chattr로 nocompress 설정
chattr +c /rootfs/usr/bin/critical_app  # 압축 제외

# 런타임 압축 모드 변경 (mount 옵션)
mount -t jffs2 -o remount,compr=lzo /mnt/flash

커널 내부 자료구조 심화

JFFS2의 인메모리 자료구조는 Flash의 물리적 레이아웃을 효율적으로 추상화합니다. jffs2_sb_info, jffs2_inode_info, 해시 테이블 간의 연결 구조와 데이터 흐름을 분석합니다.

JFFS2 커널 내부 자료구조 관계 jffs2_sb_info mtd *mtd flash_size, used_size dirty_size, free_size clean_list, dirty_list free_list, bad_list *nextblock (쓰기 블록) *gcblock (GC 대상) *wbuf (write buffer) **inocache_list → blocks[] (eraseblock 배열) inocache 해시 테이블 [0] → inocache #1 [1] → inocache #5 [2] → NULL [3] → inocache #7 ... INOCACHE_HASHSIZE 버킷 jffs2_inode_cache ino (inode 번호) *nodes (raw_node_ref) nlink state (빌드 상태) *next (체인) jffs2_inode_info vfs_inode (VFS inode 내장) highest_version fragtree (RB-tree) → *metadata (최신 메타 노드) *dents (dirent 리스트) sem (읽기/쓰기 세마포어) *inocache → inode_cache fragtree (RB-tree) [0-4095] [0-2047] [2048-4095] 각 노드 = jffs2_node_frag (offset, size, *node) O(log n) 범위 검색 jffs2_raw_node_ref flash_offset (bit 0 = obsolete) *next_in_ino *next_phys (블록 내 물리 순서)

inocache 해시 테이블

JFFS2는 inode 번호를 키로 사용하는 해시 테이블로 inode 캐시를 관리합니다:

/* fs/jffs2/nodelist.h */
#define INOCACHE_HASHSIZE  128  /* 해시 버킷 수 */

struct jffs2_inode_cache {
    struct jffs2_full_dirent *scan_dents;  /* 스캔 중 임시 dent */
    struct jffs2_inode_cache *next;       /* 해시 체인 */
    struct jffs2_raw_node_ref *nodes;    /* 이 inode의 노드 목록 */
    uint32_t ino;                         /* inode 번호 */
    int nlink;                             /* 링크 수 */
    int state;                             /* 캐시 상태 */
};

/* inode 캐시 검색 */
static inline struct jffs2_inode_cache *
jffs2_get_ino_cache(struct jffs2_sb_info *c, uint32_t ino)
{
    struct jffs2_inode_cache *ret;
    int bin = ino % INOCACHE_HASHSIZE;

    ret = c->inocache_list[bin];
    while (ret && ret->ino != ino)
        ret = ret->next;
    return ret;
}

fragtree (Fragment Tree) 상세

fragtree는 파일의 데이터 범위를 관리하는 RB-tree(Red-Black Tree) 기반 자료구조입니다:

RAM 비용:jffs2_raw_node_ref는 약 12~16바이트, jffs2_inode_cache는 약 28바이트를 사용합니다. 10만 개의 노드를 가진 Flash에서는 약 1.6MB의 RAM이 이 자료구조에만 소비됩니다. 이것이 대용량 Flash에서 JFFS2의 RAM 오버헤드가 문제되는 근본 원인입니다.

ftrace/bpftrace JFFS2 추적

JFFS2의 GC 동작, 쓰기 지연, 마운트 성능을 실시간으로 분석하기 위한 ftrace 및 bpftrace 기법입니다. 커널 내부의 핵심 함수에 트레이스포인트를 설정하여 운영 중인 시스템의 Flash I/O 패턴을 관찰합니다.

JFFS2 추적 포인트 맵 GC 경로 추적 kprobe: jffs2_garbage_collect_pass kprobe: jffs2_find_gc_block kprobe: jffs2_garbage_collect_live kprobe: jffs2_erase_pending_trigger 쓰기 경로 추적 kprobe: jffs2_write_inode_range kprobe: jffs2_write_dnode kprobe: jffs2_compress kprobe: jffs2_flush_wbuf_pad MTD/Flash 추적 kprobe: mtd_write kprobe: mtd_erase kprobe: mtd_read kprobe: mtd_block_isbad 핵심 측정 지표 GC 패스 지연(us) 쓰기 지연(us) Flash I/O 횟수 Write Amplification 비율 bpftrace 분석 패턴: 1. kprobe + kretprobe 쌍으로 함수 실행 시간 측정 (hist 출력) 2. mtd_write 인자로 실제 Flash 쓰기 바이트 누적 → 사용자 쓰기와 비교 (WA 비율)

ftrace로 GC 추적

# GC 관련 함수 추적 활성화
cd /sys/kernel/debug/tracing

# function_graph 트레이서 설정
echo function_graph > current_tracer

# JFFS2 GC 함수 필터
echo 'jffs2_garbage_collect_pass' > set_graph_function
echo 'jffs2_find_gc_block' >> set_graph_function
echo 'jffs2_garbage_collect_live' >> set_graph_function
echo 'jffs2_garbage_collect_pristine' >> set_graph_function
echo 'jffs2_garbage_collect_dnode' >> set_graph_function

# 트레이싱 시작
echo 1 > tracing_on

# 결과 확인
cat trace

# 출력 예:
#  3)               |  jffs2_garbage_collect_pass() {
#  3)   0.845 us    |    jffs2_find_gc_block();
#  3)               |    jffs2_garbage_collect_live() {
#  3)               |      jffs2_garbage_collect_dnode() {
#  3)  12.456 us    |        jffs2_write_dnode();
#  3)  15.234 us    |      }
#  3)  16.012 us    |    }
#  3)  18.567 us    |  }

쓰기 지연 측정

# kprobe를 이용한 쓰기 함수 지연 측정
cd /sys/kernel/debug/tracing

# jffs2_write_inode_range 진입/반환 프로브 설정
echo 'p:jffs2_wr jffs2_write_inode_range' > kprobe_events
echo 'r:jffs2_wr_ret jffs2_write_inode_range $retval' >> kprobe_events

# 이벤트 활성화
echo 1 > events/kprobes/jffs2_wr/enable
echo 1 > events/kprobes/jffs2_wr_ret/enable
echo 1 > tracing_on

# 히스토그램으로 지연 분포 확인
echo 'hist:keys=common_pid:vals=hitcount:sort=hitcount' > \
    events/kprobes/jffs2_wr/trigger

bpftrace GC 분석

#!/usr/bin/env bpftrace
/* jffs2_gc_latency.bt — GC 패스 지연 히스토그램 */

kprobe:jffs2_garbage_collect_pass
{
    @start[tid] = nsecs;
}

kretprobe:jffs2_garbage_collect_pass
/@start[tid]/
{
    $delta = nsecs - @start[tid];
    @gc_latency_us = hist($delta / 1000);  /* 마이크로초 */
    @gc_count = count();
    delete(@start[tid]);
}

kprobe:jffs2_find_gc_block
{
    @find_start[tid] = nsecs;
}

kretprobe:jffs2_find_gc_block
/@find_start[tid]/
{
    $delta = nsecs - @find_start[tid];
    @find_latency_us = hist($delta / 1000);
    delete(@find_start[tid]);
}

interval:s:10
{
    printf("--- GC Stats (10s interval) ---\n");
    print(@gc_latency_us);
    printf("GC pass count: ");
    print(@gc_count);
}

Write Amplification 측정

#!/usr/bin/env bpftrace
/* jffs2_write_amp.bt — 쓰기 증폭 비율 측정 */

/* 사용자 쓰기 바이트 추적 */
kprobe:jffs2_write_inode_range
{
    @user_writes = sum(arg4);  /* writelen 파라미터 */
}

/* 실제 Flash 쓰기 바이트 추적 (MTD 레벨) */
kprobe:mtd_write
{
    @flash_writes = sum(arg3);  /* len 파라미터 */
}

/* Flash erase 횟수 추적 */
kprobe:mtd_erase
{
    @erase_count = count();
}

interval:s:30
{
    printf("--- Write Amplification (30s) ---\n");
    printf("User writes (bytes): ");
    print(@user_writes);
    printf("Flash writes (bytes): ");
    print(@flash_writes);
    printf("Erase operations: ");
    print(@erase_count);
}

JFFS2 디버그 인터페이스

런타임에서 JFFS2 파일시스템 상태를 확인하는 방법:

# /proc/jffs2/mtdN 또는 debugfs 경유 (커널 빌드 옵션에 따라)
# CONFIG_JFFS2_FS_DEBUG > 0 설정 시 커널 로그에 상세 정보 출력

# MTD 디바이스 정보
cat /proc/mtd
# dev:    size   erasesize  name
# mtd0: 00040000 00010000 "bootloader"
# mtd1: 00010000 00010000 "env"
# mtd2: 00fa0000 00010000 "rootfs"

# 마운트된 JFFS2 파일시스템 통계
df -T /mnt/flash
# Filesystem     Type  1K-blocks  Used Available Use% Mounted on
# /dev/mtdblock2 jffs2    15872  8432      7440  53% /mnt/flash

# GC 스레드 확인
ps aux | grep jffs2_gcd
# root  123  0.0  0.0  0  0 ?  S  00:00  0:01 [jffs2_gcd_mtd2]

# dmesg에서 JFFS2 마운트 로그 확인
dmesg | grep -i jffs2
# jffs2: notice: (123) jffs2_build_xattr_subsystem: complete
# jffs2: notice: (123) jffs2_build_filesystem: unlocking the mtd device

임베디드 활용 심화

JFFS2는 2024년 현재에도 소용량 NOR Flash 기반 임베디드 시스템에서 활발히 사용됩니다. MTD 연동, OpenWrt/Buildroot 통합, 파티션 설계 등의 실전 지침을 다룹니다.

임베디드 시스템 파티션 설계 예 16MB NOR Flash (SPI-NOR) erase block: 64KB × 256 blocks U-Boot 256KB (ro) Env 64KB DTB 64KB Kernel (zImage) 4MB (ro) SquashFS rootfs 8MB (ro, 압축) JFFS2 3.5MB (rw) OverlayFS (OpenWrt 패턴) Lower: SquashFS rootfs (읽기 전용) Upper: JFFS2 /overlay (읽기-쓰기) Merged: / (통합 루트) Device Tree 파티션 정의 flash@0 { partitions { compatible = "fixed-partitions"; uboot@0 { reg = <0x000000 0x040000>; read-only; }; env@40000 { reg = <0x040000 0x010000>; }; kernel@50000 { reg = <0x050000 0x400000>; }; rootfs@450000{ reg = <0x450000 0xBB0000>; }; /* SquashFS+JFFS2 */

OpenWrt에서의 JFFS2 활용

OpenWrt는 JFFS2를 OverlayFS의 읽기-쓰기(upper) 레이어로 사용하는 대표적인 사례입니다:

# OpenWrt 부팅 시 OverlayFS 마운트 순서

# 1. SquashFS rootfs 마운트 (읽기 전용)
mount -t squashfs /dev/mtdblock3 /rom

# 2. JFFS2 오버레이 파티션 마운트
mount -t jffs2 /dev/mtdblock4 /overlay

# 3. OverlayFS 통합 마운트
mount -t overlay overlay -o \
    lowerdir=/rom,\
    upperdir=/overlay/upper,\
    workdir=/overlay/work \
    /mnt

# 4. pivot_root로 새 루트 전환
pivot_root /mnt /mnt/rom

# 이 구조의 장점:
# - 패키지 설치/설정 변경은 JFFS2 오버레이에 저장
# - 공장 초기화: JFFS2 파티션만 삭제하면 원래 상태 복원
# - SquashFS의 높은 압축률로 공간 절약

# JFFS2 파티션 초기화 (공장 초기화)
firstboot    # OpenWrt 전용 명령
# 또는 수동:
mtd erase rootfs_data   # JFFS2 파티션 삭제
reboot

Buildroot에서의 JFFS2 이미지 생성

# Buildroot 설정 (menuconfig)
# Filesystem images → jffs2 root filesystem
BR2_TARGET_ROOTFS_JFFS2=y
BR2_TARGET_ROOTFS_JFFS2_FLASH_SIZE=0x1000000   # 16MB
BR2_TARGET_ROOTFS_JFFS2_EBSIZE=0x10000        # 64KB erase block
BR2_TARGET_ROOTFS_JFFS2_NOCLEANMARK=y         # NAND용
BR2_TARGET_ROOTFS_JFFS2_PAD=y
BR2_TARGET_ROOTFS_JFFS2_PADSIZE=0x1000000    # 패딩

# 생성된 이미지: output/images/rootfs.jffs2
# Flash에 기록:
flash_eraseall /dev/mtd2
nandwrite -p /dev/mtd2 rootfs.jffs2

파티션 설계 가이드

고려사항권장이유
JFFS2 여유 공간최소 10~15% 확보GC 효율 유지, write amplification 최소화
erase block 정렬파티션 경계를 erase block에 정렬정렬 안 하면 첫/마지막 블록이 부분 사용됨
Summary 활성화프로덕션에서 필수마운트 시간 10~50배 단축
압축 모드lzo (favourlzo)공간/속도 균형, CPU 부하 최소
읽기 전용 rootfssquashfs + JFFS2 overlay공간 절약, 공장 초기화 용이
로그 파티션별도 JFFS2 또는 tmpfs과도한 쓰기로 다른 파티션 마모 방지
Bad Block 예비NAND: 2~5% PEB 예비제조/런타임 bad block 대체용

운영 유지보수

# JFFS2 파일시스템 상태 모니터링

# 1. 여유 공간 확인 (df)
df -h /mnt/flash
# 사용률 90% 초과 시 GC 스톰 위험 → 정리 필요

# 2. MTD 파티션 정보
cat /proc/mtd
mtdinfo /dev/mtd2     # mtd-utils 패키지 필요

# 3. Bad Block 확인 (NAND)
nandtest -k /dev/mtd2  # 비파괴 bad block 스캔

# 4. 마운트된 상태에서 강제 GC 유도
# GC 스레드를 깨우기 위해 작은 파일 쓰기 후 삭제
dd if=/dev/zero of=/mnt/flash/.gc_trigger bs=4k count=10
rm /mnt/flash/.gc_trigger
sync

# 5. JFFS2 파티션 완전 재생성
umount /mnt/flash
flash_eraseall /dev/mtd2         # NOR: 전체 삭제
# 또는
flash_erase /dev/mtd2 0 0        # NAND: bad block 보존
mount -t jffs2 /dev/mtdblock2 /mnt/flash
Flash 수명 관리: JFFS2 파티션에 syslog나 빈번한 임시 파일을 기록하면 Flash 수명이 급격히 단축됩니다. 로그는 tmpfs(/tmp)에 기록하고 필요 시에만 Flash에 동기화하세요. 저널 기반 로그 시스템(systemd-journald)의 경우 Storage=volatile로 설정하여 RAM에만 유지하는 것을 권장합니다.

JFFS2와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.