JFFS2 파일시스템(Filesystem)

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

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

핵심 요약

  • Log-Structured Flash FS — JFFS2는 Flash 메모리 위에서 로그 구조로 동작하며, MTD 인터페이스를 통해 직접 접근합니다.
  • Wear Leveling — Flash 블록의 지우기 횟수를 균등하게 분배하여 수명을 연장하는 마모 평준화 기법입니다.
  • Garbage Collection — 무효화된 노드를 포함하는 erase block을 정리하여 여유 공간을 확보합니다.
  • zlib / lzo 압축 — 데이터를 압축 저장하여 Flash 사용량을 줄이고, 압축 알고리즘을 선택할 수 있습니다.
  • NOR / NAND 지원 — NOR Flash의 바이트 단위 접근과 NAND Flash의 페이지 단위 접근 모두 대응합니다.
  • 노드 타입 (dirent / inode) — 디렉토리 엔트리 노드와 아이노드(inode) 노드로 메타데이터를 구분하여 기록합니다.
  • Clean / Dirty / Free 마커 — 각 erase block의 상태를 마커로 관리하여 GC와 마운트 시 스캔 효율을 높입니다.
  • Erase Block 관리 — Flash의 물리적 지우기 단위인 erase block을 중심으로 공간 할당과 회수를 수행합니다.

단계별 이해

  1. Flash 메모리 특성 이해 — NOR/NAND Flash의 물리적 제약을 먼저 파악합니다.

    덮어쓰기 불가, erase block 단위 지우기, 제한된 P/E 사이클 등의 제약이 JFFS2 설계의 근본 이유입니다.

  2. 로그 구조와 노드 포맷 파악 — 데이터와 메타데이터가 노드 단위로 순차 기록되는 방식을 확인합니다.

    dirent 노드, inode 노드, data 노드가 로그에 순서대로 쌓이며, 최신 노드가 이전 버전을 무효화합니다.

  3. 마운트 시 스캔 과정 추적 — 전체 Flash를 스캔하여 메모리에 파일시스템 트리를 구축하는 과정을 이해합니다.

    모든 노드를 읽어 최신 유효 버전을 선별하므로, Flash 용량이 클수록 마운트 시간이 길어집니다.

  4. GC와 Wear Leveling 동작 분석 — 공간 부족 시 GC가 erase block을 정리하는 과정을 확인합니다.

    유효 노드를 다른 블록으로 복사한 뒤 원본 블록을 지우며, 지우기 횟수를 고려하여 마모를 균등화합니다.

  5. 압축 정책과 성능 트레이드오프 검토 — zlib/lzo/none 압축 모드에 따른 CPU 부하와 공간 절약 비율을 비교합니다.

    임베디드 환경에서 CPU 성능과 Flash 용량 사이의 균형을 압축 알고리즘 선택으로 조절합니다.

관련 표준: MTD (Memory Technology Device) Interface, JFFS2 Design Documentation — Flash 메모리 파일시스템 인터페이스 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 페이지(Page): 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, 펌웨어(Firmware)데이터 스토리지

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 매핑(Mapping))를 저장합니다:

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하여 기존 이름을 무효화(Invalidation)합니다. 실제 데이터 삭제는 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)

커널 스레드(Kernel Thread) 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는 여러 압축 알고리즘을 우선순위(Priority)에 따라 순차적으로 시도합니다:

/* 압축 모드 설정 (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. 유효한 노드의 참조를 인메모리 해시 테이블(Hash Table)에 기록
  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 플러시 & 패딩(Padding)

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

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

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

핵심 커널 자료구조

jffs2_sb_info

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

/* 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 기반 무결성(Integrity) 보호 메커니즘을 커널 코드 수준에서 분석합니다.

로그 구조 쓰기 경로 (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 동작, 쓰기 지연, 마운트 성능을 실시간(Real-time)으로 분석하기 위한 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에만 유지하는 것을 권장합니다.

mkfs.jffs2 이미지 생성 실전

JFFS2 파일시스템을 Flash에 배포하려면 호스트에서 이미지를 생성하고, MTD 파티션에 기록한 뒤 마운트하는 일련의 과정이 필요합니다. 대상 Flash 타입(NOR/NAND), erase block 크기, 페이지 크기에 따라 옵션이 달라지며 실수 시 마운트 실패로 이어집니다.

mkfs.jffs2 → MTD → mount 워크플로 mkfs.jffs2 호스트에서 디렉터리 → JFFS2 바이너리 이미지 생성 sumtool (선택) summary 노드 삽입 → 마운트 시간 단축 Flash 기록 flash_erase → 전체 삭제 nandwrite / dd → 기록 mount mount -t jffs2 /dev/mtdblockN NOR Flash 경로 • erase block 64~256KB, 페이지 = 1바이트 • dd if=image.jffs2 of=/dev/mtdN 가능 • cleanmarker: 블록 내부 (12B) • --no-cleanmarkers 불필요 NAND Flash 경로 • erase block 128KB~, 페이지 2~16KB • nandwrite -p 필수 (페이지 정렬) • cleanmarker: OOB 영역 • --no-cleanmarkers 권장 (일부 컨트롤러)

mkfs.jffs2 옵션별 이미지 생성

# ───────── NOR Flash (64KB erase block) ─────────
# 기본 JFFS2 이미지 생성
mkfs.jffs2 \
    --root=./rootfs \
    --output=nor-rootfs.jffs2 \
    --eraseblock=0x10000 \
    --pad=0x800000 \
    --little-endian \
    --squash-uids \
    --faketime

# ───────── NAND Flash (128KB erase block, 2KB 페이지) ─────────
mkfs.jffs2 \
    --root=./rootfs \
    --output=nand-rootfs.jffs2 \
    --eraseblock=0x20000 \
    --pagesize=0x800 \
    --no-cleanmarkers \
    --little-endian \
    --pad=0x2000000 \
    --compression-mode=favourlzo

# 주요 옵션 설명:
# --root         : 이미지에 포함할 호스트 디렉터리
# --eraseblock   : 대상 Flash의 erase block 크기
# --pagesize     : NAND 페이지 크기 (NOR에서는 불필요)
# --pad          : 이미지를 파티션 크기에 맞춰 0xFF 패딩
# --no-cleanmarkers : NAND OOB cleanmarker 생략
# --squash-uids  : 모든 파일 소유자를 root(0)로 통일
# --faketime     : 타임스탬프를 0으로 고정 (재현 가능 빌드)
# --compression-mode : none / priority / size / favourlzo

sumtool로 요약 노드 추가

# sumtool: mkfs.jffs2 출력에 summary 노드를 삽입
# summary 활성화 시 마운트 속도 10~50배 향상

# NOR Flash용
sumtool \
    --input=nor-rootfs.jffs2 \
    --output=nor-rootfs-sum.jffs2 \
    --eraseblock=0x10000 \
    --pad

# NAND Flash용 (cleanmarker 생략)
sumtool \
    --input=nand-rootfs.jffs2 \
    --output=nand-rootfs-sum.jffs2 \
    --eraseblock=0x20000 \
    --no-cleanmarkers \
    --pad

# 커널 측: CONFIG_JFFS2_SUMMARY=y 필수
# summary 없이 생성한 이미지를 summary 커널에서 마운트해도 동작하지만
# 최초 마운트 시 전체 스캔이 발생하고 이후 GC가 summary를 추가함

flash_erase + nandwrite 기록 순서

# ───────── NOR Flash 기록 ─────────
# 1. 전체 삭제
flash_erase /dev/mtd2 0 0

# 2. 이미지 기록 (NOR은 dd 사용 가능)
dd if=nor-rootfs-sum.jffs2 of=/dev/mtd2 bs=4096

# 3. 마운트
mount -t jffs2 /dev/mtdblock2 /mnt/flash

# ───────── NAND Flash 기록 ─────────
# 1. 전체 삭제 (bad block 보존)
flash_erase /dev/mtd3 0 0

# 2. 이미지 기록 (nandwrite -p: 페이지 정렬 + bad block 건너뛰기)
nandwrite -p /dev/mtd3 nand-rootfs-sum.jffs2

# 3. 마운트
mount -t jffs2 /dev/mtdblock3 /mnt/nand

# 주의: NAND에 dd 사용 시 OOB 영역 손상 및 bad block 덮어쓰기 위험!
# 반드시 nandwrite 사용

QEMU MTD 시뮬레이션

# QEMU에서 MTD/JFFS2 테스트 환경 구축

# 1. NOR Flash 시뮬레이션 (pflash)
# 16MB NOR Flash 이미지 생성
dd if=/dev/zero of=nor-flash.img bs=1M count=16
dd if=nor-rootfs-sum.jffs2 of=nor-flash.img conv=notrunc

qemu-system-arm \
    -M vexpress-a9 \
    -kernel zImage -dtb vexpress-v2p-ca9.dtb \
    -drive file=nor-flash.img,if=pflash,format=raw \
    -append "root=/dev/mtdblock0 rootfstype=jffs2" \
    -nographic

# 2. nandsim 모듈을 이용한 NAND 시뮬레이션 (커널 내장)
# 128MB NAND, 128KB erase block, 2KB 페이지
modprobe nandsim first_id_byte=0x20 \
    second_id_byte=0xaa \
    third_id_byte=0x00 \
    fourth_id_byte=0x15

# MTD 디바이스 확인
cat /proc/mtd
# dev:    size   erasesize  name
# mtd0: 08000000 00020000 "NAND simulator partition 0"

# JFFS2 이미지 기록 및 마운트
flash_erase /dev/mtd0 0 0
nandwrite -p /dev/mtd0 nand-rootfs-sum.jffs2
mount -t jffs2 /dev/mtdblock0 /mnt/test

# 3. mtdram 모듈 (RAM 기반 MTD — 빠른 테스트용)
modprobe mtdram total_size=16384 erase_size=64  # 16MB, 64KB
dd if=nor-rootfs-sum.jffs2 of=/dev/mtd0
mount -t jffs2 /dev/mtdblock0 /mnt/ramtest

GC 알고리즘 상세 코드

JFFS2 GC(Garbage Collection)는 obsolete 노드가 포함된 erase block을 회수하여 여유 공간을 확보하는 핵심 메커니즘입니다. 커널 스레드 jffs2_gcd_mtdN이 비동기로 동작하며, 공간 부족 시 동기 GC도 수행됩니다.

jffs2_garbage_collect_pass 핵심 로직

/* 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 = 0;

    /* 1. GC 대상 블록이 아직 선택되지 않았으면 선택 */
    if (!c->gcblock) {
        c->gcblock = jffs2_find_gc_block(c);
        if (!c->gcblock)
            return 0;  /* 회수할 블록 없음 */
    }
    jeb = c->gcblock;

    /* 2. 블록 내 노드 순회: gc_node가 이전 위치 기억 */
    raw = jeb->gc_node;
    if (!raw) {
        /* 블록 시작부터 */
        raw = jeb->first_node;
    }

    /* 3. 유효(valid) 노드인지 확인 */
    while (raw) {
        if (ref_flags(raw) == REF_PRISTINE ||
            ref_flags(raw) == REF_NORMAL) {
            /* 유효 노드 → 다른 블록으로 이동(relocate) */
            ret = jffs2_garbage_collect_live(c, jeb, raw, ...);
            break;
        }
        /* obsolete/unchecked 노드 → 건너뜀 */
        raw = ref_next(raw);
    }

    /* 4. 블록 내 모든 유효 노드 이동 완료 시 블록 삭제 큐에 등록 */
    if (!raw || jeb->used_size == 0) {
        c->gcblock = NULL;
        jffs2_erase_pending_trigger(c);
    } else {
        jeb->gc_node = ref_next(raw);
    }

    return ret;
}

victim 블록 선택 (cost-benefit)

/* fs/jffs2/gc.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 = jiffies % 128;

    /* 확률적 선택: 대부분 dirty 리스트, 가끔 clean 리스트
     * → wear leveling 효과: clean 블록도 주기적으로 재배치 */

    if (n < 50 && !list_empty(&c->erase_complete_list)) {
        /* 삭제 완료 대기 블록 우선 처리 */
        nextlist = &c->erase_complete_list;
    } else if (n < 126 && !list_empty(&c->very_dirty_list)) {
        /* dirty 비율 높은 블록 → 효율적 회수 */
        nextlist = &c->very_dirty_list;
    } else if (n < 127 && !list_empty(&c->dirty_list)) {
        nextlist = &c->dirty_list;
    } else if (!list_empty(&c->clean_list)) {
        /* 1/128 확률로 clean 블록 선택 (wear leveling) */
        nextlist = &c->clean_list;
    }

    if (!nextlist)
        return NULL;

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

    return ret;
}
확률적 선택의 의미: jiffies % 128을 사용하여 대부분(약 98%)은 dirty/very_dirty 블록에서 GC를 수행하되, 약 1%의 확률로 clean 블록을 선택합니다. 이를 통해 데이터가 오래 머문 clean 블록도 주기적으로 재배치되어 wear leveling 효과를 얻습니다.

노드 이동(relocation) 코드

/* 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)
{
    /* inode 정보에서 노드 타입 판별 */
    if (S_ISDIR(f->vfs_inode.i_mode)) {
        /* 디렉터리 노드: dirent 재기록 */
        return jffs2_garbage_collect_dirent(c, jeb, f, raw);
    }

    /* 일반 파일: 데이터 노드 재기록 */
    /* 1. Flash에서 원본 데이터 읽기 */
    /* 2. 압축 상태 유지 또는 재압축 */
    /* 3. 새 위치에 쓰기 (wbuf 경유) */
    /* 4. 원본 노드를 obsolete로 마킹 */
    return jffs2_garbage_collect_hole(c, jeb, f, raw, ...);
    /* 또는 */
    return jffs2_garbage_collect_dnode(c, jeb, f, raw, ...);
}

/* 데이터 노드 GC: 읽기 → 재기록 → 원본 obsolete */
static int jffs2_garbage_collect_dnode(
    struct jffs2_sb_info *c,
    struct jffs2_eraseblock *orig_jeb,
    struct jffs2_inode_info *f,
    struct jffs2_full_dnode *fn)
{
    unsigned char *pg_ptr;
    int ret;

    /* 페이지 캐시에서 데이터 로드 */
    pg_ptr = jffs2_gc_fetch_page(c, f, fn->ofs, &pg);

    /* 새 위치에 데이터 노드 기록 */
    ret = jffs2_write_dnode(c, f, ..., pg_ptr, ...);

    /* 성공 시 원본 노드 obsolete 처리 → dirty_size 증가 */
    jffs2_mark_node_obsolete(c, fn->raw);

    return ret;
}

GC 트리거 조건

/* fs/jffs2/nodemgmt.c — 공간 부족 시 GC 트리거 판단 */

/* GC 스레드 wakeup 조건 */
static int jffs2_thread_should_wake(struct jffs2_sb_info *c)
{
    int nr_very_dirty = 0;
    struct jffs2_eraseblock *jeb;

    /* dirty+wasted가 free보다 크면 GC 필요 */
    if (c->dirty_size + c->wasted_size >
        c->free_size + c->erasing_size) {
        return 1;
    }

    /* very_dirty 블록이 임계치 초과 시 */
    list_for_each_entry(jeb, &c->very_dirty_list, list)
        nr_very_dirty++;

    if (nr_very_dirty > c->nr_blocks / 4)
        return 1;

    return 0;
}

/* 동기 GC: 쓰기 요청 시 공간이 부족하면 호출 */
int jffs2_reserve_space(struct jffs2_sb_info *c,
    uint32_t minsize, uint32_t *len, int prio)
{
    while (c->nr_free_blocks + c->nr_erasing_blocks
           < c->resv_blocks_write) {
        /* 여유 블록 부족 → 동기 GC 수행 */
        jffs2_garbage_collect_pass(c);
        /* 여전히 부족하면 -ENOSPC 반환 */
    }
    return 0;
}

Wear Leveling 커널 API

JFFS2의 wear leveling은 전용 레이어 없이 GC의 확률적 블록 선택에 의존합니다. erase count를 직접 추적하지 않으므로 완벽한 wear leveling은 불가능하지만, clean 블록 재배치와 순환적 사용으로 실질적인 분산 효과를 달성합니다.

JFFS2 Wear Leveling 결정 흐름 n = jiffies % 128 n < 50: erase_complete 삭제 완료 대기 블록 (39%) n < 127: dirty 리스트 very_dirty + dirty (60%) n = 127: clean 리스트 유효 데이터만 있는 블록 (~1%) 즉시 재사용 가능 유효 노드 이동 → 블록 삭제 Wear Leveling: 데이터 재배치 Clean 블록 재배치 = 암묵적 Wear Leveling (erase count 미추적)

jffs2_wbuf_process 내부

/* fs/jffs2/wbuf.c — Write Buffer 타임아웃 플러시 */
static void jffs2_wbuf_timeout(struct timer_list *t)
{
    struct jffs2_sb_info *c =
        from_timer(c, t, wbuf_timer);
    /* 워크큐에 플러시 작업 예약 */
    schedule_work(&c->wbuf_work);
}

static void jffs2_wbuf_process(struct work_struct *w)
{
    struct jffs2_sb_info *c =
        container_of(w, struct jffs2_sb_info, wbuf_work);

    mutex_lock(&c->alloc_sem);

    /* wbuf에 미기록 데이터가 있으면 Flash에 플러시 */
    if (c->wbuf_len) {
        /* 나머지를 0xFF로 패딩하여 페이지 단위 기록 */
        __jffs2_flush_wbuf(c, PAD_NOACCOUNT);
    }

    mutex_unlock(&c->alloc_sem);
}

/* wbuf 크기: NAND 페이지 크기와 동일 (보통 2KB~4KB) */
/* NOR Flash: wbuf 미사용 (바이트 단위 쓰기 가능) */
/* 타임아웃: HZ/2 (0.5초) — 마지막 쓰기 후 0.5초간 추가 쓰기 없으면 플러시 */

erase block 통계 확인

/* fs/jffs2/build.c — 마운트 완료 후 블록 통계 */
struct jffs2_sb_info {
    /* 블록 리스트 — GC가 이 리스트들을 순회 */
    struct list_head clean_list;       /* 유효 데이터만 */
    struct list_head very_dirty_list;  /* dirty > 50% */
    struct list_head dirty_list;       /* dirty ≤ 50% */
    struct list_head erasable_list;    /* 100% dirty → 삭제 가능 */
    struct list_head free_list;        /* 삭제 완료, 사용 가능 */
    struct list_head bad_list;         /* bad block */

    /* 크기 통계 (바이트) */
    uint32_t free_size;     /* free 블록의 총 크기 */
    uint32_t dirty_size;    /* obsolete 노드가 차지하는 총 크기 */
    uint32_t used_size;     /* 유효 노드가 차지하는 총 크기 */
    uint32_t wasted_size;   /* 패딩/정렬 낭비 */
    uint32_t erasing_size;  /* 삭제 진행 중 블록 */

    int nr_free_blocks;
    int nr_erasing_blocks;
    int nr_blocks;          /* 전체 erase block 수 */
};

wear leveling 모니터링

# JFFS2는 per-block erase count를 추적하지 않으므로
# 간접적인 방법으로 wear 상태를 모니터링합니다.

# 1. Flash 전체 통계 확인
cat /sys/class/mtd/mtd2/erase_failures   # 삭제 실패 횟수
cat /sys/class/mtd/mtd2/bad_blocks       # bad block 수
cat /sys/class/mtd/mtd2/bbt_blocks       # BBT 예약 블록
cat /sys/class/mtd/mtd2/corrected_bits   # ECC 보정 비트 수

# 2. JFFS2 공간 사용률 (GC 부하 간접 지표)
df -h /mnt/flash
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/mtdblock2  3.5M  2.1M  1.4M  60% /mnt/flash
# 80% 초과 시 GC 빈도 급증 → write 지연 증가

# 3. 커널 로그에서 bad block/ECC 이벤트 모니터링
dmesg | grep -i "jffs2\|mtd\|nand\|ecc\|bad.block"

# 4. nandtest로 비파괴 점검 (운영 중 사용 주의)
nandtest -k /dev/mtd2    # -k: 원본 데이터 보존

# 5. UBIFS의 경우 erase count 추적 가능 (비교 참고)
# ubinfo -a /dev/ubi0 → PEB별 erase count 확인 가능

노드 파싱과 스캔

JFFS2 마운트 시 Flash 전체를 스캔하여 노드 헤더를 파싱하고 인메모리 자료구조를 구축합니다. 각 노드의 CRC를 검증하고 유효성을 판단하는 과정은 마운트 시간의 대부분을 차지하며, summary 노드는 이 비용을 극적으로 줄입니다.

jffs2_scan_medium 코드

/* fs/jffs2/scan.c — Flash 전체 스캔의 핵심 함수 */
int jffs2_scan_medium(struct jffs2_sb_info *c)
{
    uint32_t ofs = 0;
    struct jffs2_eraseblock *jeb;
    int i, ret;

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

        /* Summary 노드 확인: 블록 끝에서 역방향 탐색 */
#ifdef CONFIG_JFFS2_SUMMARY
        ret = jffs2_sum_scan_sumnode(c, jeb, ofs, ...);
        if (ret == 0) {
            /* summary 발견 → 개별 노드 스캔 생략! */
            ofs += c->sector_size;
            continue;
        }
#endif

        /* Summary 없음: 블록 내 모든 노드를 개별 파싱 */
        ret = jffs2_scan_eraseblock(c, jeb, ...);

        /* 블록 상태에 따라 적절한 리스트에 배치 */
        if (jeb->used_size == 0 && jeb->dirty_size == 0)
            list_add(&jeb->list, &c->free_list);
        else if (jeb->dirty_size > jeb->used_size)
            list_add(&jeb->list, &c->very_dirty_list);
        else if (jeb->dirty_size)
            list_add(&jeb->list, &c->dirty_list);
        else
            list_add(&jeb->list, &c->clean_list);

        ofs += c->sector_size;
    }
    return 0;
}

jffs2_raw_dirent / jffs2_raw_inode 파싱

/* fs/jffs2/scan.c — 개별 노드 타입별 파싱 */
static int jffs2_scan_eraseblock(struct jffs2_sb_info *c,
    struct jffs2_eraseblock *jeb, unsigned char *buf,
    uint32_t buf_ofs, uint32_t buf_len)
{
    struct jffs2_unknown_node *node;
    uint32_t ofs = jeb->offset;

    while (ofs < jeb->offset + c->sector_size) {
        node = (struct jffs2_unknown_node *)(buf + ofs - buf_ofs);

        /* magic 바이트 확인 */
        if (je16_to_cpu(node->magic) != JFFS2_MAGIC_BITMASK) {
            /* 0xFFFF → 빈 영역 (NOR) / 불량 데이터 */
            ofs += 4;
            continue;
        }

        /* 헤더 CRC 검증 */
        if (!hdr_crc_valid(node)) {
            jffs2_dbg(1, "Header CRC failed at %#08x\n", ofs);
            ofs += 4;
            continue;
        }

        switch (je16_to_cpu(node->nodetype) & JFFS2_NODE_MASK) {
        case JFFS2_NODETYPE_INODE: {
            struct jffs2_raw_inode *ri =
                (struct jffs2_raw_inode *)node;
            /* inode 메타 + 데이터 오프셋/크기 기록 */
            jffs2_scan_inode_node(c, jeb, ri, ofs);
            break;
        }
        case JFFS2_NODETYPE_DIRENT: {
            struct jffs2_raw_dirent *rd =
                (struct jffs2_raw_dirent *)node;
            /* 디렉터리 엔트리: 이름 → inode 매핑 */
            jffs2_scan_dirent_node(c, jeb, rd, ofs);
            break;
        }
        case JFFS2_NODETYPE_CLEANMARKER:
            /* 정상 삭제 마커 — 블록 clean 표시 */
            break;
        case JFFS2_NODETYPE_PADDING:
            /* 정렬용 패딩 → dirty 처리 */
            break;
        }
        ofs += PAD(je32_to_cpu(node->totlen));
    }
    return 0;
}

CRC 검증 코드

/* fs/jffs2/scan.c — 노드 CRC 검증 */

/* 헤더 CRC: magic/nodetype/totlen 필드를 검증 */
static inline int hdr_crc_valid(
    struct jffs2_unknown_node *node)
{
    uint32_t crc = crc32(0, node,
        sizeof(struct jffs2_unknown_node) - 4);
    return crc == je32_to_cpu(node->hdr_crc);
}

/* inode 노드: 헤더 CRC + 데이터 CRC 이중 검증 */
static int jffs2_scan_inode_node(struct jffs2_sb_info *c,
    struct jffs2_eraseblock *jeb,
    struct jffs2_raw_inode *ri, uint32_t ofs)
{
    uint32_t node_crc, data_crc;

    /* 1. 노드 메타데이터 CRC */
    node_crc = crc32(0, ri,
        sizeof(*ri) - 8);  /* node_crc, data_crc 제외 */
    if (node_crc != je32_to_cpu(ri->node_crc)) {
        pr_warn("JFFS2: node CRC fail at %#08x\n", ofs);
        return 0;  /* 노드 무시 */
    }

    /* 2. 데이터 CRC (dsize > 0인 경우만) */
    if (je32_to_cpu(ri->dsize) > 0) {
        data_crc = crc32(0,
            (unsigned char *)ri + sizeof(*ri),
            je32_to_cpu(ri->csize));  /* 압축 크기 */
        if (data_crc != je32_to_cpu(ri->data_crc)) {
            pr_warn("JFFS2: data CRC fail at %#08x\n", ofs);
            /* 메타는 유효, 데이터 손상 → 부분 복구 가능 */
        }
    }

    /* inocache에 등록 */
    return jffs2_add_raw_node_to_inocache(c, jeb, ri, ofs);
}

쓰기 경로

VFS의 write 요청이 JFFS2를 통해 Flash에 기록되기까지의 전체 경로를 추적합니다. 페이지 캐시(Page Cache), 압축, wbuf, Flash 기록 순서와 각 단계의 에러 처리가 중요합니다.

JFFS2 쓰기 경로 (Write Path) VFS write() 사용자 데이터 write_begin() 페이지 캐시 확보 공간 예약 write_end() 압축(zlib/lzo) 노드 헤더 생성 write_dnode() full_dnode 생성 CRC 계산 wbuf → Flash 페이지 정렬 기록 MTD write() 에러 처리 경로 • 공간 부족 → jffs2_reserve_space() → 동기 GC → 재시도 → -ENOSPC • Flash 쓰기 실패 → wbuf 복구 → 해당 블록 bad 마킹 → 다른 블록 재시도 • ECC 오류(NAND) → MTD에서 -EIO → 블록 bad_list로 이동

jffs2_write_begin / write_end 코드

/* fs/jffs2/file.c — VFS address_space_operations */

static int jffs2_write_begin(struct file *filp,
    struct address_space *mapping,
    loff_t pos, unsigned len,
    struct page **pagep, void **fsdata)
{
    struct page *pg;
    pgoff_t index = pos >> PAGE_SHIFT;
    uint32_t pageofs = index << PAGE_SHIFT;
    int ret = 0;

    /* 1. 페이지 캐시에서 페이지 확보 */
    pg = grab_cache_page_write_begin(mapping, index, 0);
    if (!pg)
        return -ENOMEM;

    /* 2. 페이지가 최신이 아니면 Flash에서 읽기 */
    if (!PageUptodate(pg)) {
        if (pageofs < i_size_read(mapping->host)) {
            /* 기존 데이터 로드 (압축 해제 포함) */
            ret = jffs2_do_readpage_nolock(mapping->host, pg);
        } else {
            /* 파일 끝 이후: 0으로 초기화 */
            zero_user(pg, 0, PAGE_SIZE);
        }
    }

    *pagep = pg;
    return ret;
}

static int jffs2_write_end(struct file *filp,
    struct address_space *mapping,
    loff_t pos, unsigned len, unsigned copied,
    struct page *pg, void *fsdata)
{
    struct inode *inode = mapping->host;
    struct jffs2_inode_info *f = JFFS2_INODE_INFO(inode);
    int ret;

    /* 공간 예약: 부족 시 동기 GC */
    ret = jffs2_reserve_space(c, ...);

    /* 데이터 압축 + Flash 기록 */
    ret = jffs2_write_inode_range(c, f,
        page_address(pg) + (pos & ~PAGE_MASK),
        pos, copied);

    /* inode 크기 갱신 */
    if (pos + copied > inode->i_size)
        inode->i_size = pos + copied;

    return copied;
}

full_dnode 생성

/* fs/jffs2/write.c — 데이터 노드 생성 및 Flash 기록 */
struct jffs2_full_dnode *jffs2_write_dnode(
    struct jffs2_sb_info *c,
    struct jffs2_inode_info *f,
    struct jffs2_raw_inode *ri,
    const unsigned char *data,
    uint32_t datalen)
{
    struct jffs2_full_dnode *fn;
    uint32_t flash_ofs;
    int ret;

    /* CRC 계산 */
    ri->node_crc = cpu_to_je32(
        crc32(0, ri, sizeof(*ri) - 8));
    ri->data_crc = cpu_to_je32(
        crc32(0, data, datalen));

    /* Flash에 기록 (wbuf 경유) */
    ret = jffs2_flash_writev(c, ..., &flash_ofs);
    if (ret)
        return ERR_PTR(ret);

    /* full_dnode 구조체 할당 */
    fn = jffs2_alloc_full_dnode();
    fn->ofs = je32_to_cpu(ri->offset);
    fn->size = je32_to_cpu(ri->dsize);
    fn->raw = ...;  /* raw_node_ref 연결 */

    /* fragtree에 삽입 */
    jffs2_add_full_dnode_to_inode(c, f, fn);

    return fn;
}

wbuf 플러시 상세

/* fs/jffs2/wbuf.c — NAND용 Write Buffer 플러시 */
static int __jffs2_flush_wbuf(struct jffs2_sb_info *c,
    int pad)
{
    int ret;
    size_t retlen;

    /* wbuf에 쌓인 데이터가 페이지 크기 미만이면 패딩 */
    if (pad) {
        /* 나머지를 0xFF(NAND 소거 상태)로 채움 */
        memset(c->wbuf + c->wbuf_len, 0xff,
               c->wbuf_pagesize - c->wbuf_len);

        /* 패딩 노드 삽입 (남은 공간을 명시적 패딩으로 표시) */
        if (c->wbuf_len > sizeof(struct jffs2_unknown_node)) {
            struct jffs2_unknown_node *padnode =
                (void *)(c->wbuf + c->wbuf_len);
            padnode->magic = cpu_to_je16(JFFS2_MAGIC_BITMASK);
            padnode->nodetype = cpu_to_je16(JFFS2_NODETYPE_PADDING);
            padnode->totlen = cpu_to_je32(
                c->wbuf_pagesize - c->wbuf_len);
        }
    }

    /* MTD write: 페이지 단위로 NAND에 기록 */
    ret = mtd_write(c->mtd, c->wbuf_ofs,
        c->wbuf_pagesize, &retlen, c->wbuf);

    if (ret) {
        /* 쓰기 실패: 해당 블록을 bad로 마킹 */
        pr_err("Write to %#08x failed: %d\n",
               c->wbuf_ofs, ret);
        jffs2_wbuf_recover(c);
        return ret;
    }

    /* wbuf 초기화 */
    c->wbuf_len = 0;
    c->wbuf_ofs += c->wbuf_pagesize;

    return 0;
}

비정상 종료 복구

JFFS2는 로그 구조 파일시스템이므로 별도의 fsck 없이 마운트 시 자동 복구됩니다. 전원 차단이나 비정상 종료 후에도 Flash에 기록된 노드의 CRC를 검증하여 불완전한 노드를 무시하고 일관된 상태를 재구성합니다.

마운트 시 스캔/복구 과정

/* fs/jffs2/scan.c — 비정상 종료 후 복구 시나리오 */

/* 마운트 시 스캔은 다음 비정상 상황을 자동 처리: */

/* 1. 불완전 기록 노드 (전원 차단 중 쓰기) */
/*    → CRC 불일치로 감지 → 노드 무시 (dirty 처리) */
/*    → 이전 버전 노드가 자동으로 유효 데이터가 됨 */

/* 2. 불완전 삭제 블록 (erase 중 전원 차단) */
static int jffs2_check_erased_block(
    struct jffs2_sb_info *c,
    struct jffs2_eraseblock *jeb)
{
    uint32_t ofs = jeb->offset;
    int page_count = c->sector_size / c->wbuf_pagesize;
    int i;

    for (i = 0; i < page_count; i++) {
        /* 각 페이지가 0xFF(소거 상태)인지 확인 */
        if (!jffs2_is_erased(c, ofs, c->wbuf_pagesize)) {
            /* 0xFF가 아닌 데이터 존재 → 불완전 삭제 */
            jffs2_dbg(1, "Block at %#08x not fully erased\n", ofs);
            return 0;  /* → 재삭제 큐에 등록 */
        }
        ofs += c->wbuf_pagesize;
    }
    return 1;  /* 완전 소거 확인 → free_list */
}

/* 3. 블록 재삭제 처리 */
static void jffs2_handle_dirty_block(
    struct jffs2_sb_info *c,
    struct jffs2_eraseblock *jeb)
{
    if (!jffs2_check_erased_block(c, jeb)) {
        /* cleanmarker 없고 데이터 잔재 → erasable 리스트 */
        list_add(&jeb->list, &c->erasable_list);
        c->nr_erasing_blocks++;
    }
}

불완전 노드 탐지

/* fs/jffs2/scan.c — 다양한 불완전 노드 패턴 감지 */

/* Case 1: magic은 맞지만 CRC 불일치 */
if (je16_to_cpu(node->magic) == JFFS2_MAGIC_BITMASK &&
    !hdr_crc_valid(node)) {
    /* 전원 차단 중 헤더 기록 불완전 → dirty 처리 */
    jffs2_sum_add_waste(c, jeb, 4);
    jffs2_dirty_space(c, jeb, 4);
}

/* Case 2: totlen이 비정상 (블록 경계 초과) */
if (je32_to_cpu(node->totlen) + ofs >
    jeb->offset + c->sector_size) {
    /* 노드가 블록 경계를 넘음 → 손상된 노드 */
    pr_warn("JFFS2: node overflow at %#08x\n", ofs);
}

/* Case 3: 0xFF로 시작하는 빈 영역 (NOR) */
if (*((uint32_t *)buf) == 0xFFFFFFFF) {
    /* 미사용 공간 → free 영역으로 집계 */
    jeb->free_size += empty_len;
}

/* Case 4: inode 노드의 version 충돌 */
/* 동일 inode에 같은 version의 노드가 2개 존재 */
/* → 나중에 기록된(오프셋이 큰) 노드를 유효로 선택 */
/* → 이전 노드는 obsolete 처리 */

ECC 오류 처리

/* drivers/mtd/nand/ — NAND ECC 오류와 JFFS2 */

/* MTD 레이어에서 ECC 결과를 JFFS2에 전달 */
/* mtd_read() 반환값:
 *   0         → 정상
 *   -EUCLEAN  → ECC 보정 성공 (비트 플립 발생, 데이터 유효)
 *   -EBADMSG  → ECC 보정 불가 (데이터 손상)
 *   -EIO      → 하드웨어 오류 */

/* fs/jffs2/readinode.c */
static int jffs2_read_dnode(struct jffs2_sb_info *c,
    struct jffs2_inode_info *f,
    struct jffs2_full_dnode *fd,
    unsigned char *buf)
{
    int ret;
    size_t retlen;

    ret = mtd_read(c->mtd, ref_offset(fd->raw),
                   fd->size, &retlen, buf);

    if (ret == -EUCLEAN) {
        /* ECC 보정됨: 데이터는 유효하지만 블록 열화 징후 */
        pr_notice("ECC corrected read at %#08x\n",
                  ref_offset(fd->raw));
        /* GC가 이 블록의 데이터를 재배치하도록 유도 */
        jffs2_add_to_recheck_list(c, fd);
        ret = 0;  /* 데이터 자체는 유효 */
    } else if (ret == -EBADMSG) {
        /* ECC 복구 불가: 이 노드의 데이터 손실 */
        pr_err("Uncorrectable ECC error at %#08x\n",
               ref_offset(fd->raw));
        /* 이전 버전 노드가 있다면 fallback */
    }

    return ret;
}
로그 구조의 복구 원리: JFFS2는 데이터를 절대 "제자리 갱신(in-place update)"하지 않습니다. 새 데이터는 항상 새 위치에 기록(copy-on-write)되므로, 쓰기 중 전원이 차단되어도 이전 버전이 Flash에 온전히 남아 있습니다. 마운트 시 CRC가 유효한 최신 버전을 선택하면 일관된 상태가 복구됩니다.

디버깅 도구와 분석

JFFS2 문제를 진단하려면 커널 디버그 레벨 설정, jffs2dump 분석, MTD 유틸리티 활용이 필수입니다. 마운트 지연, GC 스톰, 공간 부족 등 일반적인 문제의 원인을 추적하는 방법을 정리합니다.

jffs2dump 사용법

# jffs2dump: JFFS2 이미지를 파싱하여 노드 구조 출력 (mtd-utils 패키지)

# 1. 이미지 파일 분석 (오프라인)
jffs2dump -c rootfs.jffs2
# 출력 예시:
# Dirent  node at 0x00000000, totlen 0x0000002c, #ino 2, #pino 1, ver 1, "."
# Dirent  node at 0x0000002c, totlen 0x0000002c, #ino 2, #pino 1, ver 1, ".."
# Inode   node at 0x00000058, totlen 0x00000044, #ino 2, ver 1, mode 040755
# Dirent  node at 0x0000009c, totlen 0x00000034, #ino 3, #pino 2, ver 1, "etc"

# 2. 상세 모드 (-v): 데이터 CRC, 압축 정보 표시
jffs2dump -v rootfs.jffs2 | head -50

# 3. 엔디안 지정
jffs2dump -b rootfs.jffs2   # big-endian
jffs2dump -l rootfs.jffs2   # little-endian (기본)

# 4. Flash에서 직접 덤프 후 분석
nanddump -f /tmp/mtd2.bin /dev/mtd2
jffs2dump -c /tmp/mtd2.bin

# 5. 노드 통계 요약
jffs2dump -c rootfs.jffs2 | grep -c "^Inode"    # inode 노드 수
jffs2dump -c rootfs.jffs2 | grep -c "^Dirent"   # dirent 노드 수
jffs2dump -c rootfs.jffs2 | grep -c "^Clean"    # cleanmarker 수

/proc 및 sysfs 통계

# JFFS2 자체는 /proc/jffs2를 제공하지 않지만
# MTD sysfs와 VFS 통계로 간접 진단 가능

# 1. MTD 디바이스 정보
cat /sys/class/mtd/mtd2/type          # nand 또는 nor
cat /sys/class/mtd/mtd2/erasesize     # erase block 크기 (바이트)
cat /sys/class/mtd/mtd2/writesize     # 최소 쓰기 단위 (페이지 크기)
cat /sys/class/mtd/mtd2/size          # 파티션 총 크기
cat /sys/class/mtd/mtd2/numeraseregions  # erase region 수

# 2. NAND 전용 통계
cat /sys/class/mtd/mtd2/ecc_strength  # ECC 보정 능력 (비트)
cat /sys/class/mtd/mtd2/bitflip_threshold  # -EUCLEAN 반환 기준
cat /sys/class/mtd/mtd2/ecc_step_size # ECC 단위 크기

# 3. 마운트된 JFFS2 공간 사용량
df -h /mnt/flash
stat -f /mnt/flash  # 파일시스템 상세 (블록 크기, 총 블록 등)

# 4. VFS 캐시 통계
cat /proc/slabinfo | grep jffs2
# jffs2_inode_cache   150  150   48  ... 
# jffs2_raw_node_ref  820  820   20  ...
# jffs2_node_frag     340  340   24  ...

CONFIG_JFFS2_FS_DEBUG 설정

# 커널 설정에서 JFFS2 디버그 레벨 조정

# .config 옵션:
# CONFIG_JFFS2_FS_DEBUG=0  # 기본: 디버그 출력 없음
# CONFIG_JFFS2_FS_DEBUG=1  # 주요 동작 로그
# CONFIG_JFFS2_FS_DEBUG=2  # 상세 디버그 (매우 많은 출력)

# Kconfig 위치: fs/jffs2/Kconfig
# config JFFS2_FS_DEBUG
#     int "JFFS2 debugging verbosity (0 = quiet, 2 = noisy)"
#     default 0

# 디버그 레벨 1 출력 예시:
# jffs2: jffs2_scan_medium(): Scanning block at 0x00020000
# jffs2: jffs2_scan_inode_node(): ino #2 ver 5 at 0x0002004c
# jffs2: jffs2_build_filesystem(): scanned flash completely

# 추가 디버그 옵션:
# CONFIG_JFFS2_FS_WRITEBUFFER=y   # wbuf 코드 (NAND 필수)
# CONFIG_JFFS2_FS_WBUF_VERIFY=y   # wbuf 기록 후 재검증
# CONFIG_JFFS2_SUMMARY=y          # summary 노드 지원
# CONFIG_JFFS2_SLAB_CACHE=y       # slab 캐시 사용 (메모리 효율)

# 런타임 동적 디버그 (CONFIG_DYNAMIC_DEBUG 활성 시):
echo "module jffs2 +p" > /sys/kernel/debug/dynamic_debug/control
# 또는 특정 파일만:
echo "file fs/jffs2/gc.c +p" > /sys/kernel/debug/dynamic_debug/control
echo "file fs/jffs2/wbuf.c +p" > /sys/kernel/debug/dynamic_debug/control

mtd-utils 진단 명령

# mtd-utils 패키지의 주요 진단 도구 모음

# ───────── 파티션 정보 ─────────
mtdinfo -a                    # 전체 MTD 디바이스 요약
mtdinfo /dev/mtd2             # 특정 파티션 상세
# Name:      rootfs
# Type:      nand
# Eraseblock size:  131072 bytes (128.0 KiB)
# Amount of eraseblocks: 256 (33554432 bytes, 32.0 MiB)
# Minimum input/output unit size: 2048 bytes
# Sub-page size: 512 bytes
# OOB size: 64 bytes

# ───────── NAND 진단 ─────────
nandtest /dev/mtd2            # 읽기/쓰기/삭제 사이클 테스트 (파괴적!)
nandtest -k /dev/mtd2         # 비파괴 읽기 테스트
nandtest -s 0x100000 -l 0x20000 /dev/mtd2  # 범위 지정

# ───────── 데이터 덤프 ─────────
nanddump /dev/mtd2 -f dump.bin              # 데이터만 덤프
nanddump /dev/mtd2 -f dump.bin --oob        # OOB 포함 덤프
nanddump /dev/mtd2 -s 0x20000 -l 0x20000  # 1블록만 덤프

# ───────── 삭제 ─────────
flash_erase /dev/mtd2 0 0    # 전체 삭제 (bad block 보존)
flash_erase /dev/mtd2 0x20000 1  # 특정 블록 1개만 삭제

# ───────── 이미지 비교/검증 ─────────
mtd_debug read /dev/mtd2 0 0x10000 block0.bin
md5sum block0.bin rootfs.jffs2  # 기록된 데이터 검증

JFFS2에서 UBIFS/SquashFS 마이그레이션

Flash 용량 증가와 마운트 시간 요구사항으로 인해 JFFS2에서 UBIFS 또는 SquashFS+overlay로 마이그레이션하는 사례가 늘고 있습니다. 각 시나리오의 전환 절차와 주의사항을 정리합니다.

UBIFS 마이그레이션 절차

# JFFS2 → UBIFS 마이그레이션 단계별 절차

# ───── 1단계: 현재 JFFS2 백업 ─────
mount -t jffs2 /dev/mtdblock2 /mnt/jffs2
tar czf /tmp/rootfs-backup.tar.gz -C /mnt/jffs2 .
umount /mnt/jffs2

# ───── 2단계: 커널 설정 변경 ─────
# .config에 UBI/UBIFS 관련 옵션 활성화:
# CONFIG_MTD_UBI=y
# CONFIG_MTD_UBI_WL_THRESHOLD=4096
# CONFIG_MTD_UBI_BEB_LIMIT=20
# CONFIG_UBIFS_FS=y
# CONFIG_UBIFS_FS_ZLIB=y
# CONFIG_UBIFS_FS_LZO=y

# ───── 3단계: UBI 이미지 생성 (호스트) ─────
# 3-1. UBIFS 이미지 생성
mkfs.ubifs \
    --root=./rootfs \
    --output=ubifs.img \
    --min-io-size=2048 \
    --leb-size=126976 \
    --max-leb-cnt=256 \
    --compr=lzo

# 3-2. UBI 이미지 생성 (ubinize)
cat > ubi.cfg << 'EOF'
[rootfs]
mode=ubi
image=ubifs.img
vol_id=0
vol_name=rootfs
vol_size=28MiB
vol_type=dynamic
vol_flags=autoresize
EOF

ubinize \
    --output=ubi.img \
    --min-io-size=2048 \
    --peb-size=131072 \
    --sub-page-size=512 \
    ubi.cfg

# ───── 4단계: Flash 기록 ─────
flash_erase /dev/mtd2 0 0
nandwrite -p /dev/mtd2 ubi.img

# ───── 5단계: 부트로더 cmdline 변경 ─────
# 기존: root=/dev/mtdblock2 rootfstype=jffs2
# 변경: ubi.mtd=2 root=ubi0:rootfs rootfstype=ubifs

# ───── 6단계: 마운트 확인 ─────
ubiattach /dev/ubi_ctrl -m 2
mount -t ubifs ubi0:rootfs /mnt/ubifs
UBIFS 전환 장점: O(1) 마운트 시간 (JFFS2의 O(n) 대비), 내장 wear leveling (erase count 추적), 쓰기 캐싱(write-back), 대용량 Flash 지원 (수 GB), 인덱스 기반 빠른 파일 탐색. 단, UBI 레이어의 오버헤드(PEB 헤더 2개/블록)로 소용량(4MB 이하)에서는 JFFS2가 공간 효율적일 수 있습니다.

읽기전용 SquashFS 전환

# 읽기전용 rootfs를 SquashFS로 전환 (공간 절약)

# ───── 1단계: SquashFS 이미지 생성 ─────
mksquashfs ./rootfs squashfs.img \
    -comp xz \
    -b 262144 \
    -Xbcj arm \
    -no-xattrs \
    -all-root

# 주요 옵션:
# -comp xz     : 최고 압축률 (zlib 대비 20~30% 절감)
# -b 262144    : 블록 크기 256KB (큰 블록 = 높은 압축률)
# -Xbcj arm    : ARM 바이너리 전처리 (추가 압축 개선)
# -no-xattrs   : 확장 속성 생략 (용량 절약)

# ───── 2단계: Flash에 기록 ─────
flash_erase /dev/mtd2 0 0
nandwrite -p /dev/mtd2 squashfs.img

# ───── 3단계: 마운트 ─────
mount -t squashfs /dev/mtdblock2 /mnt/rom -o ro

# 압축 비율 비교 (일반적인 임베디드 rootfs):
# JFFS2 (lzo): 12MB → ~7MB (58%)
# SquashFS (xz): 12MB → ~4MB (33%)

하이브리드 구성 (SquashFS + JFFS2 overlay)

# SquashFS(읽기전용) + JFFS2(읽기-쓰기) 하이브리드
# → OpenWrt 스타일: 공장 초기화 + 설정 유지

# ───── Flash 파티션 설계 ─────
# mtd0: bootloader  (256KB, 읽기전용)
# mtd1: kernel      (4MB, 읽기전용)
# mtd2: squashfs    (8MB, 읽기전용 rootfs)
# mtd3: jffs2       (나머지, 읽기-쓰기 overlay)

# ───── 부팅 스크립트 ─────
#!/bin/sh
# 1. SquashFS 마운트
mount -t squashfs /dev/mtdblock2 /rom

# 2. JFFS2 overlay 마운트
# 첫 부팅 시 자동 포맷 (비어있으면)
if ! mount -t jffs2 /dev/mtdblock3 /overlay 2>/dev/null; then
    # 파티션 초기화
    flash_erase /dev/mtd3 0 0
    mount -t jffs2 /dev/mtdblock3 /overlay
    mkdir -p /overlay/upper /overlay/work
fi

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

# 4. pivot_root
pivot_root /mnt/merged /mnt/merged/rom

# ───── 공장 초기화 ─────
# JFFS2 overlay만 삭제하면 원래 상태 복원
umount /overlay
flash_erase /dev/mtd3 0 0
reboot

# ───── 설정 백업/복원 ─────
tar czf /tmp/config-backup.tar.gz -C /overlay/upper .
# 복원:
tar xzf /tmp/config-backup.tar.gz -C /overlay/upper
마이그레이션 주의사항: JFFS2에서 UBIFS로 전환 시 부트로더(U-Boot 등)의 UBI 지원 여부를 반드시 확인하세요. 구형 부트로더는 UBI 레이어를 인식하지 못하여 커널 로드에 실패할 수 있습니다. U-Boot의 경우 CONFIG_CMD_UBI=yCONFIG_CMD_UBIFS=y가 필요합니다.

참고자료

공식 문서

LWN.net 기사

커널 소스 경로

관련 도구

발표 및 논문

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