JFFS2 파일시스템 심화

Journalling Flash File System version 2(JFFS2)의 로그 구조 설계, MTD 서브시스템 연동, 온디스크 노드 구조, 가비지 컬렉션, 압축, XIP, Write Buffering, UBIFS 비교까지 임베디드 Linux 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