liwen01 2024.06.23
在嵌入式Linux设备中,经常使用jffs2文件系统来作为参数区的文件系统格式。至于为什么要使用jffs2来作为参数区的文件系统,我猜大部分人都没有做过多的思考。
jffs2在2021年被设计出来,距今已过二十多年,现在在嵌入式设备中它还在被大量使用、说明这套设计本身是没有问题。
但是,你是否有思考过,你的jffs2文件系统使用是否正确、合理?如果你存储文件某天突然不见了,你要怎么分析?是flash有坏块,还是被jffs2垃圾回收处理掉了?亦或是应用程序误删除了?又要怎样才能把它恢复回来?
先问几个问题:
我们前面介绍的FAT32、exFAT、ext4文件系统,在闪存存储设备中,它们是通过FTL中间层使它们适用于闪存。
但是在嵌入式设备开发中,我们有时候是直接基于闪存来使用,比如上面提到的,在flash中划分为一个分区来用存储参数。
jffs 有三个版本,jffs1出来后一两年就被jffs2替代了,而jff3好像是有被定义,但是还未实现。
jffs1与jffs2 并不兼容,基本上属于重新实现,它们都是基于linux操作系统,flash存储介质的一种文件系统。虽然支持移植,但并未看到Linux系统之外的其它系统有在使用jffs文件系统。
关于存储介质、文件系统、分区、格式化等内容,可以查看前面的文章。
文件系统(一):存储介质、原理与架构 文件系统(二):分区、格式化数据结构 文件系统(三):嵌入式、计算机系统启动流程与步骤 文件系统(四):FAT32文件系统实现原理 文件系统(五):exFAT 文件系统原理详解 文件系统(六):一文看懂linux ext4文件系统工作原理 文件系统(七):文件系统崩溃一致性、方法、原理与局限JFFS 文件系统是2000年由 Axis Communications针对nor flash 设计的一个日志文件系统(Log-structured File System)。
它是基于日志文件系统(LFS)原理设计的一款文件系统,关于LFS可以查看文章:《文件系统(七):文件系统崩溃一致性、方法、原理与局限》
第一版本的JFFS是一个纯日志结构文件系统,包含数据和元数据的节点,按顺序存储在闪存芯片上,严格线性地遍历可用的存储空间。
挂载时系统会扫描整个存储介质,读取并解释每个节点。原始节点中存储的数据提供了足够的信息来重建整个目录层次结构和每个inode在介质上的数据范围的物理位置的完整映射。
采用日志文件系统,随着数据的增、删、改操作,jffs文件系统的空间会被慢慢使用完,这个时候就需要启动垃圾回收机制了。
在jffs1系统中,垃圾回收也是完全按照线性规则来回收,大致步骤如图2.2 jffs1数据回收:
状态1:数据按序存储。这个时候并未开始垃圾回收
状态2:开始垃圾回收,将最早节点中的有效数据移动到后面,标记原来数据为无效数据(脏数据)
状态3:重复状态2操作,直到脏数据空间达到可擦除的最小单位。
状态4:将脏数据擦除,标记为空。
从上面的数据分布和垃圾回收机制,我们可以看出jffs v1版本的实现存在一些严重缺陷:
针对jffs1中的缺陷,就有了jffs的第二个版本,也就是jffs2。
mkfs.jffs2 -s 0x100 -e 0x10000 -p 0x100000 -d jffs2_fs -o jffs2.img
sudo modprobe mtdblock
sudo modprobe mtdram total_size=1024 erase_size=64
modprobe
: 加载指定的内核模块,并自动处理模块之间的依赖关系。mtdram
: 需要加载的内核模块名称,表示内存设备的模拟。total_size=1024
: 指定虚拟MTD设备的总大小,单位KB,这里总大小为1MB。erase_size=64
: 指定虚拟MTD设备的擦除块大小,单位KB,这里擦除块大小为64KB。sudo dd if=jffs2.img of=/dev/mtdblock0
sudo mount -t jffs2 /dev/mtdblock0 /home/biao/test/jffs2/jffs2_simulator/
可以看到/dev/mtdblock0设备节点上的数据与jffs2.img镜像文件上的数据信息是一致的。
不同的是,mtdblock0是实际分配的1M空间,除了有效数据空间外,其它都是空闲地址(Empty space)
magic: 魔术数字,用来标识是一个有效的JFFS2项
nodetype: 节点类型,在jffs2.h中有定义7种类型
nodetype对应的值如下:
详细的定义可以查看mtd-utils/include/linux/jffs2.h
数据结构
所有的节点,都是以jffs2_unknown_node数据结构开始:幻数、类型、长度、CRC校验,定义如下:
struct jffs2_unknown_node
{
/* All start like this */
jint16_t magic;
jint16_t nodetype;
jint32_t totlen; /* So we can skip over nodes we don't grok */
jint32_t hdr_crc;
} __attribute__((packed));
另外:
jffs2_raw_dirent
结构体中jffs2_raw_inode
结构体中结构体的详细定义,可以在mtd-utils中的源码中找到
按上面hexdump查看的mtdblock0 RAW数据进行解析,可以发现,解析的数据与jffs2dump中查看的信息是一致的。
下面分析的这个test1目录,是在我们最开始制作镜像文件的时候创建的目录。
这里需要注意的一点是,在分区的最开始,是一个jffs2_unknown_node数据结构头,它的节点类型是JFFS2_NODETYPE_CLEANMARKER,表示清理标记节点,用于指示块已被擦除,可以写入新数据。
接下来的是test1目录的目录项节点和它的inode节点。它的inode节点里面据段的数值是空,并没有携带数据块。
其它的几个目录test2、test3、test4数据结构也是类似。
文件解析
下面分析的这个文件,是制作镜像文件时创建的file1文件,里面存有18个字节的a字符串,文件信息如下:
下面根据hexdump中查看的mtdblock0 RAW数据对file1文件进行解析,如下表
与test1目录不同,file1 有携带数据。上面表格中compr 字段表示数据的压缩类型。
数据压缩类型定义如下:
#define JFFS2_COMPR_NONE 0x00
#define JFFS2_COMPR_ZERO 0x01
#define JFFS2_COMPR_RTIME 0x02
#define JFFS2_COMPR_RUBINMIPS 0x03
#define JFFS2_COMPR_COPY 0x04
#define JFFS2_COMPR_DYNRUBIN 0x05
#define JFFS2_COMPR_ZLIB 0x06
#define JFFS2_COMPR_LZO 0x07
与文件中实际数据对比可以看到,这里记录的数据,是将18Byte字节的18个a压缩成了4个字节。
使用echo添加数据到file1
echo"bbbbbbbbbbbbbbbbbbb" >> file1
查看数据变化:
我们看到inode 6 新增加了一个版本记录version2:
Inode node at 0x0000042c, totlen 0x0000004e, #ino 6, version 2, isize 38, csize 10, dsize 22, offset 16
而inode 6 表示的就是file1文件。实际追加的数据是记录在version2 节点中,而原来的18个字节a数据,还是存在原来version1中的节点。
使用echo写数据到文件file1
echo"cccc" > file1
数据变化:
从上面我们可以看到:
为什么会有3个修改记录?实际上是执行上面一个操作它是两个步骤完成的,也就是一个操作中有了两个记录。
version 1 :原始数据,未进行修改
version 2 :是上面执行echo "bbbbbbbbbbbbbbbbbbb" >> file1
命令在file1文件末尾追加的数据
version 3 :执行echo "cccc" > file1
命令时,是先把file1文件数据全部清空
version 4 :执行echo "cccc" > file1
命令时,把cccc字符写入到file1文件中的记录
标记数据无效上面我们看到执行4个记录之后,最后文件中的数据是"cccc",但是之前的数据要怎么处理呢?是直接删除回收还是怎么处理呢?
我们看到version 1 - version 3 的前面,有标记为 Obsolete Inode,它表示为一个过时的节点,也就是一个未知的节点,这个节点是不能够被挂载解析的。
它在flash中实际的数据又有哪些变化呢?
使用hexdump查看version 1 中的RAW数据
对比原始数据,只有一个字节改变,nodetype
由原来的0xE002 改为了0xC002
0xC000的定义如下:
/* Compatibility flags. */
#define JFFS2_COMPAT_MASK 0xc000 /* What do to if an unknown nodetype is found */
下面我们往file1中一次写入256K的0数据,看数据分布会怎么变化
dd if=/dev/zero of=file1 bs=256K count=1
执行第一遍结果如下:
执行到第二十遍结果如下:
实际写入数据有256K*20 = 5120KB = 5M
为什么实际写入数据5M,但是MTD的空间只使用到0x0005ff64,也就是383K的空间呢?
因为我们使用的是dd if=/dev/zero of=file1 bs=256K count=1
命令写入的数据都是0,全是0的数据是很容易压缩的,这338K空间实际是压缩后的使用空间。
看数据:
csize 32, dsize 4096,
实际数据4096字节,压缩后变成了32字节。
在最前面制作镜像文件挂载虚拟MTD设备的时候,我们分配的大小是1M空间,理论上我们操作的数据记录超过1M就一定会进行垃圾回收,实际是不是这样呢?
上面我们写的全0数据是很容易压缩,所以实际保存的数据要比文件小很多。这里我们写入随机数,让数据记录快速写满整个分区空间,看jffs2是如何进行垃圾回收的。
dd if=/dev/urandom of=file1 bs=1K count=20
执行第一次,数据是按序分布
执行第二次,数据开始跳跃分布,数据分配到0x0000fa00地址就直接跳到0x000d000c位置开始存储,中间间隔了0xc060c 个地址,也就是769K地址,实际是直接跳到了分区的后半段去分配。
第5遍写入20K数据的时候,记录数据分布在0x000c000c-0x000dfc24的地址空间
执行第6次写入20K随机数据之后,数据空间分布在0x000c000c-0x000ccc6c 的地址
从第5次到第6次数据写入的时候,我们看到,第6次数据写入的时候,已经对第5次写入数据的空间进行了回收,所以在第6次写完数据之后,可以看到实际剩余的空闲块比第5次写完数据还多。
从上面的几个简单测试中我们可以看出下面几点:
实际官方的说法是:
具体详细的实现逻辑,可以去jffs2的源码中查找。
如果在开发或是在设备使用过程中发现jffs2中的文件丢失了,或者是里面的数据丢失了,首先进行的第一步操作就是:停止往文件系统中写入任何数据
假设丢失的文件是上面测试file1文件
一般的操作流程为:
通过上面方法,可以分析出数据丢失的大概原因,只有最后没办法的时候才去怀疑是否flash的扇区损坏了,因为分析flash是否损坏会破坏掉问题现场
如果数据丢失后想恢复回来,在数据还没有被覆盖的前提下,理论上是可以被恢复回来,恢复的难度就需要看丢失文件的具体数据和大小以及被修改的次数了。
通过上面分析,我们大概的了解了jffs2文件系统的工作机制和原理,有几个使用注意事项需要留意:
这里介绍了嵌入式Linux系统中非常常用的jffs2文件系统,jffs2文件系统经过二十多年的验证是没有问题的,只是大家在使用的时候需要留意一下它的特性和局限性,避免造成关键数据的丢失。