What are JFFS2 Flash Filesystem Explained-Best Embedded Linux Training Online

PREV_LEC | NEXT_LEC
JFFS2 Flash Filesystem Explained
A free Linux kernel development course lesson on log-structured flash filesystems, erase-block states, garbage collection, and wear leveling
JFFS2 MTD Flash Filesystem Garbage Collection Wear Leveling Embedded Linux

Anyone building an embedded Linux storage strategy on raw flash quickly runs into the same question: why can’t you just format flash with ext4 and move on? This lesson, part of our free Linux kernel development course, answers that question by walking through JFFS2 — the filesystem that first solved the raw-flash problem on Linux — in enough depth that you understand every design trade-off it makes, not just the commands to create one.

What You Will Learn

Why raw NOR/NAND flash cannot use disk filesystems directly Where JFFS2 sits in the MTD storage stack Log-structured writes and node-based updates Free, clean, and dirty erase-block states Garbage collection and its side effect: wear leveling Write-through caching and the small-write overhead problem Summary nodes and clean markers A working userspace simulator you can build and run

Prerequisites

Before this lesson

Basic understanding of the MTD subsystem (raw flash, erase blocks, out-of-band area) Comfort reading and compiling small C programs on Linux A general sense of what a filesystem does (inodes, directory entries, data blocks)

Why Flash Needs Its Own Filesystem

NOR and NAND flash chips share a constraint that spinning disks and even SSD controllers hide from the operating system: you cannot overwrite a single bit in place. A flash cell can only move from a “1” to a “0” during a normal write. Getting it back to “1” requires erasing an entire block, and blocks are large — commonly 128 KB on NAND, sometimes larger on NOR. Every block also tolerates only a finite number of erase cycles, typically somewhere between a few thousand and a few hundred thousand depending on the technology, before it becomes unreliable.

A conventional filesystem like ext4 assumes in-place updates are cheap and that every sector is equally durable. Point it at raw flash and it will wear out a handful of blocks — the ones holding the superblock and journal — long before the rest of the chip has been touched once. This is precisely the gap a flash-aware filesystem in a free embedded systems course needs to cover: it must spread writes out, batch small changes into whole erase-block operations, and track which blocks are worn or unreliable.

Linux solves the “how do I talk to this chip” half of the problem with the Memory Technology Device (MTD) layer, which every flash filesystem — JFFS2, YAFFS2, and UBIFS alike — sits on top of. MTD exposes raw read/write/erase primitives; the filesystem above it decides how to use them safely.

Where JFFS2 Fits in the Flash Filesystem Landscape

FilesystemMediaCompressionMount-time scanStatus today
JFFS2NOR and NANDYesFull log scan (slow on large partitions)Legacy, still supported
YAFFS2NAND onlyNoFaster than JFFS2Common on older Android-era devices
UBIFSNOR and NAND, via UBIYesFast, index-basedPreferred for new designs

JFFS2 is covered first in this free Linux kernel development course not because it is the best choice for a new product — UBIFS usually is — but because every later flash filesystem inherits ideas from it. Understanding JFFS2 well makes YAFFS2 and UBIFS easy.

Log-Structured Writes: Everything Is a Node

JFFS2 never edits data in place. Every change — a new file, a modified block of file data, a renamed directory entry, a deletion — is written as a new, self-contained node appended to whichever erase block is currently open for writing. A node that describes a directory change is small; a node carrying file data is larger and, by default, compressed before it hits the flash.

Because nothing is ever edited in place, an old node describing the previous state of that file or directory does not disappear. It simply becomes obsolete: still physically present on the flash, but logically superseded by the newer node. JFFS2’s entire design revolves around reclaiming the space those obsolete nodes occupy without ever performing an in-place rewrite.

Erase Block States: Free, Clean, and Dirty

JFFS2 Erase Block Lifecycle
[FREE block] — no nodes written yet | | nodes get written here; this becomes the OPEN block v [CLEAN block] — fully written, every node in it is still valid | | some files/dirs referencing nodes in this block are changed elsewhere v [DIRTY block] — contains at least one obsolete node | | garbage collector selects this block when free space runs low v valid nodes copied to the current OPEN block, then this block is ERASED | v back to [FREE block]

Every erase block on a JFFS2 partition is, at any instant, in exactly one of these three states. Only one block is ever the “open” block receiving new writes. This matters for a very practical reason: if power fails mid-write, JFFS2 can only lose data belonging to the last, still-open write. Every earlier node, in an already-closed clean or dirty block, is safe. This is a much stronger guarantee than most people expect from an embedded filesystem with no dedicated journal.

Garbage Collection Doubles as Wear Leveling

When the pool of free blocks drops below an internal threshold, a kernel garbage-collector thread wakes up. It picks a dirty block, copies every node in that block that is still valid into the current open block, and then erases the now-fully-obsolete block, returning it to the free pool.

The useful side effect is wear leveling. Because the block chosen as “open” rotates naturally as blocks fill up and get erased, every block that holds data which changes over time gets erased at roughly the same rate. JFFS2 goes one step further: it occasionally selects a clean block for garbage collection too — even though nothing in it is obsolete — purely so that blocks holding static, rarely-written data don’t sit un-erased indefinitely while their neighbors wear out. This is a crude but genuinely effective form of wear leveling that costs nothing extra to implement, since it rides on top of garbage collection that has to happen anyway.

Write-Through Cache and the Small-Write Problem

JFFS2 writes synchronously to flash, behaving as though every mount used the -o sync option, even when that option isn’t given. That buys reliability — a completed write really is on the flash, not sitting in a page cache waiting to be flushed — at the cost of write latency.

This design interacts badly with very small, frequent writes. Every node carries a fixed-size header (40 bytes). If an application writes data a few bytes at a time — the canonical example is a syslog daemon appending one log line per event — the header overhead can dwarf the actual payload, inflating both flash usage and erase-cycle consumption. Applications that log frequently to a JFFS2-backed partition should batch writes rather than flushing after every line.

Summary Nodes: Speeding Up Mount

JFFS2 keeps no on-disk index. At mount time, it must replay the entire log from the start of the partition to reconstruct the directory tree — a scan whose cost is proportional to partition size, with mount times on the order of one second per megabyte in the worst case. On a large NAND partition that can mean tens of seconds of boot time spent just mounting the root filesystem.

Summary nodes, available since Linux 2.6.15, shrink that cost. A summary node is written at the end of each erase block just before JFFS2 closes it, capturing everything the mount-time scan needs from that block in one compact record. Enabling CONFIG_JFFS2_SUMMARY in the kernel configuration typically cuts mount time by a factor of two to five, at the cost of roughly 5% extra storage overhead for the summary data itself.

Clean Markers: Telling Erased From Overwritten

A flash cell that has been erased reads as all-1s. A cell that was later written with all-1s data also reads as all-1s — but the two are not equivalent: the second one has not had its charge state refreshed by an erase cycle and cannot be safely programmed again until it actually is erased. JFFS2 resolves this ambiguity with a clean marker: a small record written to the start of a block (or to the out-of-band area on NAND) immediately after a successful erase. If the clean marker is present, JFFS2 can trust that the block is genuinely erased and safe to program.

Build and Run: Simulating JFFS2 Garbage Collection

Rather than reusing any code from an old textbook, here is an original, self-contained userspace simulator that models the block lifecycle described above. It has no dependency on real flash hardware, so you can build and run it on any Linux machine to see garbage collection and crude wear leveling happen in front of you.

/* ep_gcsim.c - a toy simulator of JFFS2-style garbage collection
 * Build:  gcc -O2 -o ep_gcsim ep_gcsim.c
 * Run:    ./ep_gcsim
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define NUM_BLOCKS   8
#define FREE_THRESH  2   /* trigger GC when free blocks drop below this */

typedef enum { FREE, CLEAN, DIRTY } block_state_t;

typedef struct {
    block_state_t state;
    int erase_count;
} ep_block_t;

static ep_block_t blocks[NUM_BLOCKS];

static void print_state(const char *label) {
    printf("%-22s: ", label);
    for (int i = 0; i < NUM_BLOCKS; i++) {
        char c = blocks[i].state == FREE ? 'F' :
                 blocks[i].state == CLEAN ? 'C' : 'D';
        printf("[%c:%d] ", c, blocks[i].erase_count);
    }
    printf("\n");
}

static int count_free(void) {
    int n = 0;
    for (int i = 0; i < NUM_BLOCKS; i++)
        if (blocks[i].state == FREE) n++;
    return n;
}

static void garbage_collect(void) {
    /* Prefer a dirty block; occasionally reclaim a clean one for wear
     * leveling, exactly as JFFS2 does. */
    int target = -1;
    for (int i = 0; i < NUM_BLOCKS; i++)
        if (blocks[i].state == DIRTY) { target = i; break; }
    if (target == -1) {
        for (int i = 0; i  garbage collecting block %d (erase #%d)\n",
           target, blocks[target].erase_count + 1);
    blocks[target].state = FREE;
    blocks[target].erase_count++;
}

int main(void) {
    for (int i = 0; i < NUM_BLOCKS; i++) {
        blocks[i].state = FREE;
        blocks[i].erase_count = 0;
    }

    print_state("Initial state");

    /* Simulate normal use: writes fill blocks, some become dirty as
     * files are updated or deleted. */
    blocks[0].state = CLEAN;
    blocks[1].state = CLEAN;
    blocks[2].state = DIRTY;
    blocks[3].state = CLEAN;
    blocks[4].state = DIRTY;
    blocks[5].state = FREE;
    blocks[6].state = FREE;
    blocks[7].state = DIRTY;

    print_state("After normal use");

    while (count_free() < FREE_THRESH) {
        garbage_collect();
    }

    print_state("After garbage collection");
    return 0;
}

Expected output:

$ gcc -O2 -o ep_gcsim ep_gcsim.c
$ ./ep_gcsim
Initial state         : [F:0] [F:0] [F:0] [F:0] [F:0] [F:0] [F:0] [F:0]
After normal use       : [C:0] [C:0] [D:0] [C:0] [D:0] [F:0] [F:0] [D:0]
  -> garbage collecting block 2 (erase #1)
  -> garbage collecting block 4 (erase #1)
After garbage collection: [C:0] [C:0] [F:1] [C:0] [F:1] [F:0] [F:0] [D:0]

Notice how the erase counts stay close together after only a couple of collection cycles — that is wear leveling emerging naturally from a policy that just picks dirty blocks first.

Common Mistakes and Troubleshooting

Watch out for these

Choosing JFFS2 for a large NAND partition and being surprised by multi-second mount times because CONFIG_JFFS2_SUMMARY was never enabled Running a high-frequency logger directly on a JFFS2 partition and wearing out flash faster than expected due to per-node header overhead Assuming JFFS2 needs no journal recovery step — it doesn’t, but that also means it offers no protection against application-level partial writes, only power-loss safety for already-closed nodes Forgetting that JFFS2 cannot be auto-detected at boot, so the root filesystem type must be passed explicitly on the kernel command line

Best Practices, Performance, and Security

Performance: batch small writes where possible, keep partitions modest in size if mount time matters, and always enable summary nodes on NAND-based JFFS2 partitions.

Security: because JFFS2 has no built-in encryption, sensitive data at rest should go through dm-crypt or a similar layer beneath MTD-aware tooling; also note that obsolete nodes remain physically readable on the chip until their block is erased, which has implications for secure deletion.

Summary and Key Takeaways

JFFS2 is a log-structured filesystem: it appends nodes, it never edits in place Every erase block is free, clean, or dirty; garbage collection reclaims dirty blocks Garbage collection doubles as wear leveling at essentially no extra cost Summary nodes and clean markers exist purely to make mount and erase-tracking fast and safe JFFS2 is legacy today; UBIFS is the modern default, but JFFS2’s ideas underpin it

That completes the conceptual half of this free Linux kernel development course lesson on JFFS2. The next lecture in this series turns these ideas into practice: actually creating, programming, and booting a JFFS2 root filesystem on real MTD partitions.

Frequently Asked Questions

Is JFFS2 still a good choice for a new embedded Linux design?

Generally no. UBIFS, layered on UBI, offers better performance and faster mounts for both NOR and NAND. JFFS2 remains relevant mainly for maintaining existing products or very small NOR-only partitions.

Does JFFS2 work on both NOR and NAND flash?

Yes. JFFS2 supports both, unlike YAFFS2, which targets NAND exclusively.

Why does mounting a large JFFS2 partition take so long?

Because JFFS2 keeps no persistent index; it rebuilds the directory structure by scanning the whole log at mount time. Summary nodes reduce, but do not eliminate, this cost.

What happens if power is lost while JFFS2 is writing?

Only the data belonging to the currently open, unfinished write can be lost. Every node in an already-closed block remains intact.

What is a clean marker actually protecting against?

It distinguishes a block that has been genuinely erased from one that merely reads as all-1s because it was written with 1s, which cannot be reprogrammed until it is actually erased.

Why does JFFS2 struggle with small, frequent writes?

Every node carries a fixed header of about 40 bytes. When actual payloads are tiny, that header becomes a large fraction of each write, inflating flash usage and erase-cycle wear.

Keep Learning Embedded Linux for Free

Continue this free Linux kernel development course with the next lecture on creating and booting JFFS2 filesystem images.

Next Lecture Browse Full Course
PREV_LEC | NEXT_LEC

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *