JFFS2 파일시스템 심화
JFFS2를 임베디드 플래시 환경 기준으로 해부합니다. MTD erase block 위에 기록되는 raw node 구조, mount 시 전체 스캔과 요약 정보 처리, 압축 알고리즘 선택과 공간 효율, GC 스레드 동작과 마모 분산 한계, NOR/NAND에서의 쓰기 제약 대응, XIP/Write Buffering 경로, UBIFS와의 설계·성능·확장성 차이까지 실제 제품 운용 관점에서 상세히 설명합니다.
핵심 요약
- 계층 이해 — VFS, 캐시, 하위 FS 경계를 구분합니다.
- 메타데이터 우선 — inode/dentry 일관성을 먼저 확인합니다.
- 저장 정책 — 저널링/압축/할당 정책 차이를 비교합니다.
- 일관성 모델 — 로컬/원격/합성 FS의 반영 시점을 구분합니다.
- 복구 관점 — 장애 시 재구성 경로를 함께 점검합니다.
단계별 이해
- 경계 계층 파악
요청이 VFS에서 어디로 내려가는지 확인합니다. - 메타/데이터 분리
어느 경로에서 무엇이 갱신되는지 나눠 봅니다. - 동기화/플러시 확인
쓰기 반영 시점과 순서를 검증합니다. - 복구 시나리오 점검
비정상 종료 후 일관성 회복을 확인합니다.
개요 & 역사
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 전용으로 설계되었으며, 다음과 같은 특징을 가졌습니다:
- 순수 로그 구조: Flash의 erase-before-write 제약에 최적화된 append-only 쓰기
- NOR Flash 전용: 바이트 단위 읽기/쓰기가 가능한 NOR Flash만 지원
- Wear Leveling: 모든 erase block에 골고루 쓰기를 분산하여 수명 연장
- 전원 안전성: 로그 구조 특성상 갑작스러운 전원 차단에도 데이터 일관성 유지
그러나 JFFS v1은 NAND Flash 미지원, 압축 미지원, 하드링크 미지원 등의 한계가 있었습니다.
JFFS2로의 진화
2001년 Red Hat의 David Woodhouse가 JFFS의 한계를 극복하기 위해 JFFS2를 개발했습니다. Linux 2.4.10에서 메인라인에 머지되었으며, 주요 개선 사항은 다음과 같습니다:
- NAND Flash 지원: Write Buffering(wbuf)을 통한 페이지 정렬 쓰기
- 압축 지원: zlib, rtime, lzo, rubin 다중 압축 알고리즘
- 하드링크 지원: POSIX 호환성 향상
- 향상된 GC: Background/Foreground GC, 개선된 wear leveling
- xattr 지원: 확장 속성 (POSIX ACL, SELinux 보안 레이블)
- Erase Block Summary (EBS): 빠른 마운트를 위한 요약 노드
주요 연혁
| 시기 | 이벤트 |
|---|---|
| 1999 | JFFS v1 — Axis Communications, NOR Flash 전용 |
| 2001 | JFFS2 — David Woodhouse (Red Hat), Linux 2.4.10 머지 |
| 2004 | NAND Flash 지원 (wbuf 도입) |
| 2005 | Erase Block Summary (EBS) 지원 |
| 2006 | xattr / POSIX ACL / SELinux 지원 |
| 2008 | UBIFS 등장 — 대용량 NAND에서 JFFS2 대체 시작 |
| 현재 | 소용량 NOR Flash 임베디드 시스템에서 여전히 활발히 사용 |
Flash 메모리 기초
JFFS2를 이해하려면 Flash 메모리의 물리적 특성을 먼저 알아야 합니다. Flash는 일반 블록 디바이스(HDD/SSD)와 근본적으로 다른 동작 방식을 가집니다.
NOR Flash vs NAND Flash
| 특성 | NOR Flash | NAND Flash |
|---|---|---|
| 인터페이스 | 랜덤 액세스 (RAM-like) | 순차 페이지 액세스 |
| 읽기 단위 | 바이트/워드 | 페이지 (512B~16KB) |
| 쓰기 단위 | 바이트/워드 | 페이지 |
| 삭제 단위 | Erase Block (64KB~256KB) | Erase Block (128KB~512KB) |
| 읽기 속도 | 빠름 (XIP 가능) | 보통 |
| 쓰기 속도 | 느림 | 빠름 |
| 삭제 속도 | 매우 느림 (~1초) | 빠름 (~2ms) |
| 밀도/용량 | 낮음 (1~256MB) | 높음 (128MB~TB) |
| 비트 에러 | 매우 드뭄 | 빈번 (ECC 필수) |
| P/E Cycle | 10만~100만 | SLC:10만 / MLC:1만 / TLC:3천 |
| XIP 지원 | 가능 | 불가 |
| 주요 용도 | 부트 ROM, 펌웨어 | 데이터 스토리지 |
Erase Block & P/E Cycle
Flash 메모리의 가장 중요한 제약은 erase-before-write입니다:
- 쓰기: 비트를 1→0으로만 변경 가능 (프로그래밍)
- 삭제: erase block 전체를 한 번에 1로 리셋 (0→1)
- 제자리 덮어쓰기 불가: 기존 데이터를 수정하려면 해당 블록을 먼저 삭제해야 함
각 erase block은 유한한 P/E(Program/Erase) cycle을 가집니다. 특정 블록에 쓰기가 집중되면 해당 블록이 먼저 마모되어 불량 블록이 됩니다. 이를 방지하기 위해 wear leveling은 핵심 메커니즘입니다.
OOB 영역 & Bad Block 관리
NAND Flash의 각 페이지에는 데이터 영역 외에 OOB(Out-Of-Band) 또는 spare 영역이 있습니다:
- OOB 용도: ECC 데이터, Bad Block 마커, 파일시스템 메타데이터 (JFFS2의 clean marker 등)
- Bad Block: NAND Flash는 제조 시부터 불량 블록이 존재할 수 있으며(factory bad), 사용 중에도 발생(runtime bad)
- BBT (Bad Block Table): MTD 서브시스템이 관리하는 불량 블록 테이블
MTD 서브시스템
MTD(Memory Technology Device)는 Flash 메모리와 같은 비휘발성 메모리를 위한 Linux 커널의 추상화 계층입니다. 일반 블록 디바이스(block_device)와 달리 MTD는 erase 연산을 직접 노출합니다.
MTD 계층 아키텍처
MTD 서브시스템은 3개 계층으로 구성됩니다:
- MTD User 모듈: mtdchar(문자 디바이스), mtdblock(블록 디바이스 에뮬레이션), JFFS2, UBIFS 등 MTD를 사용하는 상위 모듈
- MTD Core:
mtd_info구조체를 중심으로 read/write/erase 등 공통 API를 제공하는 핵심 계층 - MTD Hardware 드라이버: 실제 Flash 칩(NOR, NAND, SPI-NOR, SPI-NAND)에 대한 하드웨어별 드라이버
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 디바이스로 사용할 수 있습니다. 파티션 정의 방법:
- 커널 커맨드 라인:
mtdparts=spi0.0:256k(bootloader),64k(env),-(rootfs) - Device Tree:
partitions노드로 정의 - 보드 코드:
mtd_partition배열을 플랫폼 데이터로 전달
/* 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 노드로 분할 저장될 수 있습니다. 각 노드는 offset과 dsize로 파일 내 위치를 지정하며, 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)을 지원합니다:
- jffs2_raw_xattr: xattr 값(name + value)을 저장하는 노드. 같은 xattr 데이터를 여러 inode가 공유 가능
- jffs2_raw_xref: inode → xattr 참조 링크. 하나의 inode가 여러 xattr를 가질 수 있음
이 설계를 통해 SELinux 보안 레이블, POSIX ACL 등을 효율적으로 저장합니다.
jffs2_raw_summary (EBS)
Erase Block Summary(EBS)는 각 erase block의 끝에 배치되는 요약 노드입니다:
- 블록 내 모든 유효 노드의 타입, inode 번호, 오프셋을 요약
- 마운트 시 전체 Flash를 스캔하지 않고 요약 노드만 읽으면 됨
CONFIG_JFFS2_SUMMARY커널 설정으로 활성화
CRC 보호
JFFS2는 모든 온디스크 데이터를 CRC-32로 보호합니다:
- hdr_crc: 공통 헤더(magic, nodetype, totlen)의 CRC
- node_crc: 헤더 + 메타데이터 영역의 CRC
- data_crc: 실제 데이터 영역의 CRC (inode 노드만)
- name_crc: 파일명의 CRC (dirent 노드만, 빠른 이름 비교용)
Flash 비트 플립이나 불완전 쓰기를 탐지하여 손상된 노드를 무시하고 이전 유효 버전을 사용합니다.
로그 구조 설계
JFFS2는 Flash의 erase-before-write 제약에 최적화된 순수 로그 구조(purely log-structured) 파일시스템입니다. 모든 변경 사항은 Flash의 다음 빈 공간에 순차적으로 append됩니다.
Append-Only 쓰기 모델
JFFS2의 모든 쓰기는 append-only입니다:
- 파일 수정: 변경된 데이터를 새
jffs2_raw_inode노드로 append. version 번호 증가 - 파일 삭제:
ino=0인jffs2_raw_dirent를 append하여 이전 dirent를 무효화 - 메타데이터 변경: chmod, chown 등도 새 inode 노드를 append (데이터 없이 메타만)
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)합니다.
노드 무효화
노드가 무효화되는 경우:
- 같은 inode의 새 버전: version이 더 높은 inode 노드가 append되면 이전 버전은 obsolete
- 같은 데이터 범위의 새 쓰기: 파일의 같은 offset 범위에 대한 새 노드가 쓰여지면 이전 노드는 obsolete
- 파일/디렉토리 삭제: ino=0인 dirent가 쓰여지면 이전 dirent와 해당 inode의 모든 데이터 노드가 obsolete
- GC에 의한 이동: GC가 유효 노드를 새 블록으로 복사하면 원래 노드는 obsolete
가비지 컬렉션 (GC)
로그 구조 파일시스템의 핵심 메커니즘인 GC는 obsolete 노드가 차지하는 공간을 회수합니다. JFFS2는 Foreground GC와 Background GC 두 가지 모드를 제공합니다.
GC 트리거 조건
GC가 시작되는 조건:
- 여유 공간 부족: 쓰기 요청 시 free erase block이 부족하면 foreground GC 발동
- 주기적 정리: background GC 스레드가 주기적으로 실행
- dirty 비율 임계값: 전체 dirty 공간이 일정 비율을 초과하면 GC 우선 실행
Foreground GC
쓰기 경로에서 free 블록이 부족할 때 동기적으로 실행됩니다:
- 쓰기를 요청한 프로세스 컨텍스트에서 직접 실행
- 최소 하나의 erase block을 확보할 때까지 반복
- 쓰기 지연시간에 직접적인 영향 — 성능에 민감
Background GC (gcthread)
커널 스레드 jffs2_gcd_mtdN이 백그라운드에서 GC를 수행합니다:
- 여유 공간이 충분해도 dirty 블록이 있으면 주기적으로 정리
- 시스템 부하가 낮을 때 wear leveling을 위해 clean 블록도 재배치
- foreground 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 전략:
- 동적 wear leveling: 새 쓰기는 항상 free 블록에 순차적으로 진행되므로 자연스럽게 분산
- 정적 wear leveling: GC가 1% 확률로 clean 블록(유효 데이터만 있는)을 선택하여 정적 데이터를 재배치. 오래된 블록에 고정된 데이터가 있으면 해당 블록의 P/E cycle이 낮아지므로 이를 방지
- 한계: erase count를 직접 추적하지 않으므로 UBI의 wear leveling보다 정밀도가 낮음
압축
JFFS2는 데이터를 Flash에 쓰기 전에 투명하게 압축합니다. Flash 용량이 제한적인 임베디드 환경에서 저장 효율을 크게 향상시킵니다.
압축 알고리즘
| 알고리즘 | CONFIG 옵션 | 압축률 | 속도 | 특징 |
|---|---|---|---|---|
| zlib | CONFIG_JFFS2_ZLIB | 높음 | 느림 | deflate 기반, 가장 높은 압축률 |
| rtime | 항상 포함 | 낮음 | 매우 빠름 | Run-Length Encoding 변형, 기본 폴백 |
| lzo | CONFIG_JFFS2_LZO | 보통 | 빠름 | 압축/해제 모두 빠름, CPU 부하 적음 |
| rubin | CONFIG_JFFS2_RUBIN | 보통 | 느림 | 산술 코딩, 실용성 낮음 (거의 사용 안 됨) |
compr_priority & 자동 선택
JFFS2는 여러 압축 알고리즘을 우선순위에 따라 순차적으로 시도합니다:
- priority 모드 (기본): 우선순위가 높은 알고리즘부터 시도, 압축이 성공하면 사용
- size 모드: 모든 알고리즘을 시도하고 가장 작은 결과를 선택 (느리지만 최적 압축)
- 압축 결과가 원본보다 크면 비압축(JFFS2_COMPR_NONE)으로 저장
/* 압축 모드 설정 (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을 순차적으로 읽습니다:
- 각 블록의 시작부터 매직 마커(
0x1985)를 찾으며 노드를 파싱 - 각 노드의 CRC를 검증하여 유효성 확인
- 유효한 노드의 참조를 인메모리 해시 테이블에 기록
- 각 블록의 clean/dirty/free 상태 계산
CONFIG_JFFS2_SUMMARY를 반드시 활성화하세요.
Inode 캐시 구축
Flash 스캔 완료 후 inode별로 최신 노드를 결정합니다:
- 같은 inode 번호의 노드들 중 version이 가장 높은 것이 최신
- 파일 데이터는 offset+dsize 범위별로 최신 version의 노드가 유효
- 결과적으로 각 inode의 node tree(유효 데이터 노드의 범위 트리)를 구성
Summary 지원 (빠른 마운트)
EBS(Erase Block Summary)가 활성화되면:
- 각 erase block의 끝에서 summary 노드를 먼저 확인
- summary가 존재하면 해당 블록의 전체 스캔을 건너뛰고 요약 정보만 사용
- summary가 없는 블록만 전체 스캔 수행 (이전 버전 호환)
- 마운트 시간을 10~50배 단축 가능
마운트 옵션
| 옵션 | 설명 |
|---|---|
compr=none|priority|size|favourlzo | 압축 모드 선택 |
rp_size=N | root 전용 예약 공간 (바이트) |
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 전용: NOR Flash는 바이트 단위 랜덤 읽기가 가능하여 CPU가 직접 fetch 가능
- RAM 절약: 코드 세그먼트를 RAM에 로드하지 않으므로 메모리 절약
- 부팅 속도: Flash → RAM 복사 과정 생략으로 빠른 실행 시작
- 제약: 압축된 파일은 XIP 불가, NAND Flash는 XIP 불가
커널 XIP 구현
JFFS2 XIP 지원을 위해 필요한 설정:
CONFIG_JFFS2_FS_XATTR: xattr 지원 (XIP과 직접 관련은 없으나 함께 사용)- NOR Flash의 물리 주소가 CPU 메모리 맵에 직접 매핑되어야 함
- XIP 대상 파일은 비압축 상태로 저장 (
JFFS2_COMPR_NONE) mmap()시 Flash의 물리 주소를 직접 반환하여 page fault 없이 접근
Write Buffering (wbuf)
NAND 쓰기 정렬
NAND Flash는 페이지 단위(512B~16KB)로만 쓸 수 있지만, JFFS2 노드는 가변 길이입니다. 이 불일치를 해결하기 위해 JFFS2는 wbuf(write buffer)를 사용합니다:
- 노드들을 NAND 페이지 크기에 맞춰 버퍼링
- 버퍼가 가득 차면 한 번에 Flash에 기록
- NOR Flash에서는 wbuf가 불필요 (바이트 단위 쓰기 가능)
wbuf 플러시 & 패딩
wbuf는 다음 상황에서 Flash에 플러시됩니다:
- 버퍼 가득 참: 페이지 크기만큼 데이터가 모이면 자동 플러시
- 타임아웃: 일정 시간(기본 ~250ms) 내에 추가 쓰기가 없으면 타이머에 의해 플러시
- sync/fsync: 명시적 동기화 요청 시
- erase block 변경: 현재 쓰기 블록이 바뀔 때
플러시 시 버퍼가 페이지 크기에 미달하면 패딩 노드(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 고유 정보입니다:
struct jffs2_full_dnode트리: 파일 데이터의 범위별 유효 노드 참조 (fragtree)struct jffs2_full_dirent리스트: 디렉토리의 유효 엔트리 목록highest_version: 이 inode의 최신 version 번호metadata: 최신 메타데이터(mode, uid, gid, timestamps) 노드 참조
jffs2_raw_node_ref
Flash에 저장된 각 노드에 대한 인메모리 참조입니다:
flash_offset: Flash 내 물리적 오프셋 (최하위 비트로 obsolete 여부 표시)next_in_ino: 같은 inode의 다음 노드 참조 (체인)next_phys: 같은 erase block의 다음 노드 (물리적 순서)- 매우 컴팩트한 구조로 설계되어 RAM 오버헤드 최소화
jffs2_full_dnode & jffs2_full_dirent
- jffs2_full_dnode: 파일의 특정 offset 범위에 대한 유효 데이터 노드. 범위 트리(fragtree)로 관리되며, 읽기 시 offset으로 빠르게 검색
- jffs2_full_dirent: 디렉토리의 유효 엔트리. 연결 리스트로 관리되며, 이름으로 검색하거나 readdir에서 순회
UBIFS 비교
UBIFS(Unsorted Block Image File System)는 JFFS2의 한계를 극복하기 위해 설계된 차세대 Flash 파일시스템입니다. MTD 위에 UBI(Unsorted Block Images) 계층을 추가합니다.
UBI 계층
UBI는 MTD와 파일시스템 사이에 위치하는 볼륨 관리 계층입니다:
- 논리-물리 블록 매핑: LEB(Logical Erase Block) → PEB(Physical Erase Block) 매핑 관리
- Wear Leveling: erase count 기반의 정밀한 wear leveling
- Bad Block 관리: 런타임 bad block을 자동으로 투명하게 처리
- 볼륨 관리: 하나의 MTD 디바이스에서 여러 논리 볼륨 생성
UBIFS 장점
- 빠른 마운트: 인덱스를 Flash에 온디스크로 유지하여 전체 스캔 불필요
- 확장성: 수 GB NAND Flash에서도 효율적 동작
- Write-back 지원: 더 효율적인 쓰기 배치
- 정밀한 Wear Leveling: UBI의 erase count 추적 기반
- 적은 RAM 오버헤드: 인덱스가 Flash에 있으므로 대용량에서도 RAM 사용량이 관리 가능
마이그레이션 고려사항
- JFFS2 → UBIFS 전환 시 UBI 포맷이 필요 (ubiformat, ubimkvol)
- 기존 JFFS2 이미지를 직접 변환하는 도구는 없으므로 데이터를 새로 생성해야 함
- UBI 오버헤드로 인해 사용 가능 공간이 약 5~10% 감소
- 소용량 NOR Flash에서는 UBI 오버헤드가 상대적으로 크므로 JFFS2가 유리할 수 있음
실전 활용
커널 설정 (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/
임베디드 시스템 활용 사례
- 부트로더 환경변수: U-Boot에서 JFFS2 파티션으로 설정 저장
- 루트 파일시스템: 소용량 NOR Flash 기반 임베디드 시스템의 rootfs
- 설정/로그 파티션: 읽기 전용 squashfs rootfs + JFFS2 /data 파티션 조합
- OpenWrt/LEDE: 라우터 펌웨어에서 overlayfs의 읽기-쓰기 레이어로 JFFS2 사용
성능 & 제한사항
확장성 한계
JFFS2는 소용량 Flash에 최적화되어 있으며, 대용량에서는 심각한 확장성 문제가 발생합니다:
- O(n) 마운트: Flash 전체를 스캔해야 하므로 용량에 비례하여 마운트 시간 증가
- O(n) 메모리: 모든 노드 참조를 RAM에 유지해야 하므로 용량에 비례하여 RAM 사용 증가
- GC 부하: Flash가 가득 찰수록 GC 오버헤드가 급격히 증가
마운트 시간 문제
Summary 미사용 시 마운트 시간 예시:
| Flash 크기 | Summary 미사용 | Summary 사용 |
|---|---|---|
| 16 MB | ~2초 | <0.5초 |
| 64 MB | ~8초 | <1초 |
| 128 MB | ~20초 | <2초 |
| 256 MB | ~45초 | ~4초 |
RAM 오버헤드
대안 파일시스템
| 시나리오 | 권장 파일시스템 |
|---|---|
| 소용량 NOR Flash (<64MB) | JFFS2 (여전히 합리적) |
| 대용량 NAND Flash (>64MB) | UBIFS |
| SSD / eMMC (FTL 있음) | F2FS, ext4 |
| MCU / 초소형 임베디드 | LittleFS |
| 읽기 전용 rootfs | squashfs + JFFS2 overlay |
JFFS2 / UBIFS / YAFFS2 / LittleFS 비교
| 항목 | JFFS2 | UBIFS | YAFFS2 | LittleFS |
|---|---|---|---|---|
| 기반 계층 | MTD 직접 | UBI → MTD | MTD 직접 / 자체 NAND | 블록 디바이스 추상화 |
| Flash 지원 | NOR + NAND | NAND (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, rtime | lzo, zlib, zstd | 없음 | 없음 |
| XIP | 가능 (NOR) | 불가 | 불가 | 불가 |
| 라이선스 | GPL v2 | GPL v2 | GPL v2 / 상용 | BSD-3 |
| Linux 메인라인 | 2.4.10+ | 2.6.27+ | 미포함 (out-of-tree) | 미포함 (RTOS) |
| 주요 용도 | 소용량 NOR 임베디드 | 대용량 NAND 임베디드 | Android (과거) | MCU / RTOS |
- 소용량 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의 동적 웨어 레벨링은 로그 구조 특성에서 자연스럽게 발생합니다:
- 순차 쓰기: 새 데이터는 항상 현재 쓰기 블록(nextblock)에 append. 블록이 가득 차면 다음 free 블록으로 이동
- 순환 사용: free_list에서 블록을 순서대로 사용하므로 모든 블록이 골고루 쓰기 대상이 됨
- GC dirty 선택: 99% 확률로 dirty 비율이 높은 블록을 GC 대상으로 선택하여 공간 효율 최적화
이 메커니즘은 자주 변경되는 데이터(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 풀로 자동 대체 |
로그 구조 쓰기 심화
JFFS2의 로그 구조는 전통적인 LFS(Log-structured File System)와 유사하지만, Flash 메모리의 고유한 제약에 맞게 변형되었습니다. 여기서는 순차 쓰기 경로, 노드 배치 정책, 그리고 CRC 기반 무결성 보호 메커니즘을 커널 코드 수준에서 분석합니다.
쓰기 경로 상세
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에 공존할 때 최신 데이터를 결정하는 규칙:
- version 비교: 같은 inode 번호의 노드들 중
version필드가 가장 높은 것이 최신 메타데이터 - 범위 겹침 해결: 파일 데이터는
[offset, offset+dsize)범위별로 독립 관리. 같은 범위에 대해 version이 높은 노드가 우선 - fragtree 통합: 인메모리에서 RB-tree 기반의 fragment tree로 관리하여 O(log n) 검색
- 전원 차단 복구: CRC 검증 실패한 불완전 노드는 무시되고, 이전 유효 버전이 자동으로 사용됨
JFFS2 vs UBIFS 비교 심화
JFFS2와 UBIFS는 동일한 Flash 하드웨어를 대상으로 하지만, 설계 철학이 근본적으로 다릅니다. 이 섹션에서는 아키텍처, 확장성, 마운트 시간, 메모리 사용량의 차이를 정량적으로 비교합니다.
아키텍처 차이
| 측면 | JFFS2 | UBIFS |
|---|---|---|
| 인덱싱 | 인메모리 전체 (해시 테이블 + fragtree) | 온디스크 B+tree (TNC: Tree Node Cache) |
| Wear Leveling | GC 기반 확률적 (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 RAM | UBIFS 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 |
JFFS2가 여전히 유리한 경우
- 소용량 NOR Flash (1~32MB): UBI 오버헤드(~5~10%)가 상대적으로 크고, JFFS2의 마운트 시간이 허용 범위 내
- XIP 필요: NOR Flash에서 코드를 직접 실행해야 하는 경우 UBIFS는 XIP 미지원
- 단순한 스택: UBI 계층 없이 MTD에 직접 마운트하므로 디버깅과 복구가 단순
- 레거시 호환: 기존 JFFS2 이미지와의 호환이 필요한 경우
GC 알고리즘 심화
JFFS2의 GC(Garbage Collection)는 로그 구조 파일시스템의 공간 회수 메커니즘으로, victim 블록 선택, 유효 노드 이동, wbuf 연동까지의 전체 과정을 상세히 분석합니다.
Victim 블록 선택 전략
GC의 효율은 어떤 블록을 선택하느냐에 크게 좌우됩니다. JFFS2는 다음 우선순위로 victim 블록을 선택합니다:
- very_dirty_list: dirty 공간이 블록 크기의 50% 이상인 블록. 유효 노드가 적어 복사 비용이 낮고 공간 회수 효율이 높음
- dirty_list: dirty 공간이 있지만 50% 미만인 블록
- 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가 복사하는 노드도 wbuf에 버퍼링되어 다른 정상 쓰기와 동일한 경로를 거침
- GC 중 전원 차단 시: 원본 블록은 아직 삭제되지 않았으므로 데이터 손실 없음
- GC 완료 후 블록 삭제 시:
jffs2_erase_pending_trigger()가 비동기 erase를 예약
노드 타입 심화
JFFS2의 모든 온디스크 데이터는 타입별 노드(typed node)로 구성됩니다. 각 노드 타입의 내부 구조와 역할, 그리고 노드 간 관계를 상세히 분석합니다.
클린마커(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에 저장 */
- NOR Flash: 블록의 첫 12바이트에 cleanmarker 노드 기록
- NAND Flash: 블록의 첫 페이지 OOB 영역에 기록 (데이터 영역을 낭비하지 않음)
- 역할: 마운트 시 이 마커가 없으면 불완전 삭제로 판단하여 해당 블록을 재삭제 예약
--no-cleanmarkers: mkfs.jffs2 옵션으로 NAND에서 OOB cleanmarker 생략 (일부 NAND 컨트롤러와의 호환을 위해)
노드 타입 플래그
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 마운트는 Flash의 물리적 상태를 인메모리 자료구조로 재구성하는 핵심 과정입니다. 전체 스캔, inode 빌드, summary 노드를 통한 최적화까지의 내부 흐름을 커널 코드 수준에서 분석합니다.
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 단위로 정리합니다:
- inode 캐시 순회: 각 inode에 연결된 모든
raw_node_ref를 확인 - 최신 버전 결정: 같은 offset 범위의 노드들 중 version이 가장 높은 것을 유효로 판정
- obsolete 마킹: 이전 버전의 노드를 obsolete로 표시하고 해당 블록의 dirty_size 증가
- fragtree 구성: 유효 데이터 노드들을 RB-tree 기반의 fragment tree로 구축
- 블록 리스트 갱신: 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]; /* 가변 길이 요약 배열 */
};
CONFIG_JFFS2_SUMMARY=y로 빌드하세요. Summary가 없으면 모든 블록의 모든 노드를 바이트 단위로 읽고 CRC를 검증해야 합니다. 16MB Flash에서 ~2초 vs <0.5초, 128MB에서는 ~20초 vs <2초로 차이가 극적입니다.
압축 심화
JFFS2의 압축 서브시스템은 Flash 공간 효율을 극대화하기 위해 설계되었습니다. 알고리즘 선택 전략, miniLZO 최적화, 압축 모드별 성능 특성을 상세히 분석합니다.
압축 서브시스템 내부
/* 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 제한 |
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, 해시 테이블 간의 연결 구조와 데이터 흐름을 분석합니다.
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) 기반 자료구조입니다:
- 키: 파일 내 byte offset
- 값:
jffs2_node_frag— 해당 범위의 유효 데이터 노드 참조 - 검색: 특정 offset의 데이터를 O(log n)으로 찾아 해당 Flash 노드에서 읽기
- 삽입/삭제: 새 노드가 추가되면 기존 범위를 분할하거나 병합
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 패턴을 관찰합니다.
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 통합, 파티션 설계 등의 실전 지침을 다룹니다.
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 부하 최소 |
| 읽기 전용 rootfs | squashfs + 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
Storage=volatile로 설정하여 RAM에만 유지하는 것을 권장합니다.
관련 문서
JFFS2와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.