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