首页 > 系统相关 >文件系统(八):Linux JFFS2文件系统工作原理、优势与局限

文件系统(八):Linux JFFS2文件系统工作原理、优势与局限

时间:2024-06-24 09:00:18浏览次数:3  
标签:file1 jffs2 文件系统 Linux 数据 节点 JFFS2

liwen01 2024.06.23

前言

在嵌入式Linux设备中,经常使用jffs2文件系统来作为参数区的文件系统格式。至于为什么要使用jffs2来作为参数区的文件系统,我猜大部分人都没有做过多的思考。

jffs2在2021年被设计出来,距今已过二十多年,现在在嵌入式设备中它还在被大量使用、说明这套设计本身是没有问题。

但是,你是否有思考过,你的jffs2文件系统使用是否正确、合理?如果你存储文件某天突然不见了,你要怎么分析?是flash有坏块,还是被jffs2垃圾回收处理掉了?亦或是应用程序误删除了?又要怎样才能把它恢复回来?

先问几个问题:

  1. 如果jffs2系统中数据频繁更新会有什么影响?
  2. 如果jffs2系统分区比较大会有什么影响?
  3. 如果分区全部写满有什么影响?
  4. 如果出现文件或是数据丢失,可以恢复回来不?

(一)闪存文件系统分类

图1.1 闪存文件系统层次结构

我们前面介绍的FAT32、exFAT、ext4文件系统,在闪存存储设备中,它们是通过FTL中间层使它们适用于闪存。

但是在嵌入式设备开发中,我们有时候是直接基于闪存来使用,比如上面提到的,在flash中划分为一个分区来用存储参数。

jffs 有三个版本,jffs1出来后一两年就被jffs2替代了,而jff3好像是有被定义,但是还未实现。

jffs1与jffs2 并不兼容,基本上属于重新实现,它们都是基于linux操作系统,flash存储介质的一种文件系统。虽然支持移植,但并未看到Linux系统之外的其它系统有在使用jffs文件系统。

关于存储介质、文件系统、分区、格式化等内容,可以查看前面的文章。

文件系统(一):存储介质、原理与架构 文件系统(二):分区、格式化数据结构 文件系统(三):嵌入式、计算机系统启动流程与步骤 文件系统(四):FAT32文件系统实现原理 文件系统(五):exFAT 文件系统原理详解 文件系统(六):一文看懂linux ext4文件系统工作原理 文件系统(七):文件系统崩溃一致性、方法、原理与局限

(二)JFFS1介绍

JFFS 文件系统是2000年由 Axis Communications针对nor flash 设计的一个日志文件系统(Log-structured File System)。

它是基于日志文件系统(LFS)原理设计的一款文件系统,关于LFS可以查看文章:《文件系统(七):文件系统崩溃一致性、方法、原理与局限

(1)数据存储

图2.1 jffs1数据分布

第一版本的JFFS是一个纯日志结构文件系统,包含数据和元数据的节点,按顺序存储在闪存芯片上,严格线性地遍历可用的存储空间。

挂载时系统会扫描整个存储介质,读取并解释每个节点。原始节点中存储的数据提供了足够的信息来重建整个目录层次结构和每个inode在介质上的数据范围的物理位置的完整映射。

(2)垃圾回收

采用日志文件系统,随着数据的增、删、改操作,jffs文件系统的空间会被慢慢使用完,这个时候就需要启动垃圾回收机制了。

图2.2 jffs1数据回收

在jffs1系统中,垃圾回收也是完全按照线性规则来回收,大致步骤如图2.2 jffs1数据回收:

状态1:数据按序存储。这个时候并未开始垃圾回收

状态2:开始垃圾回收,将最早节点中的有效数据移动到后面,标记原来数据为无效数据(脏数据)

状态3:重复状态2操作,直到脏数据空间达到可擦除的最小单位。

状态4:将脏数据擦除,标记为空。

(3)缺点

从上面的数据分布和垃圾回收机制,我们可以看出jffs v1版本的实现存在一些严重缺陷:

  • 垃圾回收线性进行,通过写入新节点以允许它擦除日志中最旧的块,即使被垃圾回收的块仅包含干净的节点。
  • 如果文件系统中存在大量的静态数据、垃圾回收的时候也会移动所有的静态数据,虽然每个块被擦除的次数完全相同、但这也意味着块被擦除的次数比实际需要的要多。
  • JFFS 不支持压缩,在资源紧张的嵌入式系统中,这是一个比较重要的需求

针对jffs1中的缺陷,就有了jffs的第二个版本,也就是jffs2。

(三)JFFS2 数据布局

(1)制作jff2镜像文件

  1. 创建测试目录和文件,在file1-4中随意输入一些数据
  1. 制作镜像文件
mkfs.jffs2 -s 0x100 -e 0x10000 -p 0x100000 -d jffs2_fs -o jffs2.img
  • s表示页大小,一般页大小为256Byte,即0x100
  • e表示擦除块大小,一般块大小为64KB,即0x10000
  • p表示分区大小,在生成时会擦除分区大小的flash,x100000表示1MB

(2)加载jffs2镜像文件到PC机上

  1. 加载MTD块设备模块,使得MTD设备能够通过块设备接口进行访问
sudo modprobe mtdblock
  1. 加载内存设备模块,并配置虚拟的内存技术设备
sudo modprobe mtdram total_size=1024 erase_size=64
  • modprobe: 加载指定的内核模块,并自动处理模块之间的依赖关系。
  • mtdram: 需要加载的内核模块名称,表示内存设备的模拟。
  • total_size=1024: 指定虚拟MTD设备的总大小,单位KB,这里总大小为1MB。
  • erase_size=64: 指定虚拟MTD设备的擦除块大小,单位KB,这里擦除块大小为64KB。
  1. 将镜像文件复制到mtdblock0节点上
sudo dd if=jffs2.img of=/dev/mtdblock0
  1. 将镜像文件以jffs2类型挂载到指定目录
sudo mount -t jffs2 /dev/mtdblock0 /home/biao/test/jffs2/jffs2_simulator/

(3)查看分区信息

  1. 使用jffs2dump查看jffs镜像信息
  1. 查看mtdblock0设备节点上的信息

可以看到/dev/mtdblock0设备节点上的数据与jffs2.img镜像文件上的数据信息是一致的。

不同的是,mtdblock0是实际分配的1M空间,除了有效数据空间外,其它都是空闲地址(Empty space)

(四)JFFS2数据解析

(1)查看block数据

  1. 使用hexdump查看mtdblock0节点前面2KB的数据,数据如下(...中为省略部分)

(2)数据结构定义

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中的源码中找到

(3)目录、文件解析

按上面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个字节。

(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中的节点。

(5)修改数据

使用echo写数据到文件file1

echo"cccc" > file1

数据变化:

从上面我们可以看到:

  1. file1 的目录项是没有变的
  2. file1 的数据项是有3个修改记录,也就是version1 - version4

为什么会有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

图4.5.3数据对比

0xC000的定义如下:

/* Compatibility flags. */
#define JFFS2_COMPAT_MASK 0xc000      /* What do to if an unknown nodetype is found */

(6)数据压缩

下面我们往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字节。

(7)垃圾回收

在最前面制作镜像文件挂载虚拟MTD设备的时候,我们分配的大小是1M空间,理论上我们操作的数据记录超过1M就一定会进行垃圾回收,实际是不是这样呢?

上面我们写的全0数据是很容易压缩,所以实际保存的数据要比文件小很多。这里我们写入随机数,让数据记录快速写满整个分区空间,看jffs2是如何进行垃圾回收的。

dd if=/dev/urandom of=file1 bs=1K count=20

执行第一次,数据是按序分布

数据4.7.1 第一次执行

执行第二次,数据开始跳跃分布,数据分配到0x0000fa00地址就直接跳到0x000d000c位置开始存储,中间间隔了0xc060c 个地址,也就是769K地址,实际是直接跳到了分区的后半段去分配。

数据4.7.2 第二次执行

第5遍写入20K数据的时候,记录数据分布在0x000c000c-0x000dfc24的地址空间

数据4.7.3 第五次执行

执行第6次写入20K随机数据之后,数据空间分布在0x000c000c-0x000ccc6c 的地址

数据4.7.4 第六次执行

从第5次到第6次数据写入的时候,我们看到,第6次数据写入的时候,已经对第5次写入数据的空间进行了回收,所以在第6次写完数据之后,可以看到实际剩余的空闲块比第5次写完数据还多。

(8)数据结论

从上面的几个简单测试中我们可以看出下面几点:

  1. 存储的数据是可以被压缩的
  2. 数据记录不是线性存储
  3. 垃圾回收时静态数据(没有被修改的文件数据)不会被移动
  4. 垃圾回收有可能发生在空余空间还有很多的情况

实际官方的说法是:

  • 100次中有99次是从脏列表中选择一个块进行垃圾回收,以获得最佳性能
  • 剩下的1次是从干净块中选择一个块,以确保数据在介质上移动并实现磨损均衡

具体详细的实现逻辑,可以去jffs2的源码中查找。

(五)JFFS2数据恢复

如果在开发或是在设备使用过程中发现jffs2中的文件丢失了,或者是里面的数据丢失了,首先进行的第一步操作就是:停止往文件系统中写入任何数据

假设丢失的文件是上面测试file1文件

一般的操作流程为:

  1. 停止往jffs2系统中写入任何数据
  2. 将jffs2文件系统所在的分区全部备份一份
  3. 分析备份jffs2中的数据,看是否能找到file1的目录节点
  4. 查看file1文件inode的操作版本,按最大版本号开始分析
  5. 看最后版本的操作时间,分析设备在该时间段有做什么操作
  6. 分析倒数第二版本与最后版本,看最后版本是什么操作

通过上面方法,可以分析出数据丢失的大概原因,只有最后没办法的时候才去怀疑是否flash的扇区损坏了,因为分析flash是否损坏会破坏掉问题现场

如果数据丢失后想恢复回来,在数据还没有被覆盖的前提下,理论上是可以被恢复回来,恢复的难度就需要看丢失文件的具体数据和大小以及被修改的次数了。

(六)JFFS2使用注意事项

通过上面分析,我们大概的了解了jffs2文件系统的工作机制和原理,有几个使用注意事项需要留意:

  1. jffs2有磨损平衡,但磨损平衡比较随机。
  2. 因为数据是通过节点串起来的,所以它并不适合做大容量的文件系统,一般不建议超过32M的文件系统使用jff2
  3. 尽量避免频繁地更新jffs2文件系统里的数据,一是磨损平衡问题、二是每次修改都会产生新数据记录(version),不管修改的数据是多是少。少量数据的修改还会存在写放大的问题。
  4. 对于低功耗设备,关机前最好先正确卸载jffs2文件系统,提高文件系统一致性的保障
  5. 虽然jffs2是日志文件系统,数据丢失或是文件系统异常有可能被修复,但是对于嵌入式设备,一般没有足够的资源去做修复动作,所以对于关键数据的备份显得尤为重要。

结尾

这里介绍了嵌入式Linux系统中非常常用的jffs2文件系统,jffs2文件系统经过二十多年的验证是没有问题的,只是大家在使用的时候需要留意一下它的特性和局限性,避免造成关键数据的丢失。

---------------------------End---------------------------
如需获取更多内容
请关注 liwen01 公众号

标签:file1,jffs2,文件系统,Linux,数据,节点,JFFS2
From: https://www.cnblogs.com/liwen01/p/18259447

相关文章

  • Linux gdb lldb面试题及参考答案(万字长文)
    什么是GDB?简述其主要功能。GDB(GNUDebugger)是GNU项目的一部分,是一个功能强大的源代码级别的调试器,主要用于C、C++和其他多种编程语言的程序调试。GDB提供了丰富的功能来帮助开发者理解程序内部的工作方式,诊断并修复代码中的错误。其主要功能包括但不限于:启动程序:可以在GDB......
  • 在Linux中,如何统计ip访问情况?分析 nginx 访问日志?如何找出访问页面数量在前十位的ip?
    在Linux中统计IP访问情况并分析Nginx访问日志,以找出访问页面数量在前十位的IP地址,可以通过一系列命令行工具高效完成。这里以一个典型的工作流程为例,说明如何进行这一分析:1.确定日志文件位置首先,需要知道Nginx的访问日志文件位置。通常,这个文件位于/var/log/nginx/access.log,但......
  • 在Linux中,如何实时抓取并显示当前系统中tcp 80 端口的网络数据信息?
    在Linux中,实时抓取并显示当前系统中TCP80端口的网络数据信息,可以使用tcpdump这个强大的命令行工具。以下是详细的步骤和命令:打开终端:首先,打开一个具有足够权限的终端窗口。通常,需要使用sudo或以root身份执行tcpdump,因为它需要访问底层网络接口。确定网络接口:使用ifconfig(在......
  • 在Linux中,如何将本地 80 端口的请求转发到 8080 端口?当前主机 IP 为10.0.0.104。
    在Linux系统中,将本地80端口的请求转发到8080端口,可以通过使用iptables命令来实现。当前主机IP为10.0.0.104,具体命令如下:iptables-tnat-APREROUTING-d10.0.0.104-ptcp--dport80-jDNAT--to-destination10.0.0.104:8080解析:iptables:iptables命令用于配置Linux内核......
  • 在Linux中,如何使用 tcpdump 监听主机为 192.168.1.1,tcp 端⼝为 80 的数据,并将将输出结
    在Linux中,要使用tcpdump监听目标为主机IP为192.168.1.1,且TCP端口为80的数据包,并将输出结果保存到名为tcpdump.log的文件中,可以按照以下步骤操作:打开终端:首先,你需要在具有足够权限的用户账户下打开一个终端窗口。通常,root权限是必要的,因为普通用户可能无法监听网络接口的所有流......
  • 在Linux中,服务器开不了机怎么解决⼀步步的排查?
    在Linux中,当服务器无法开机时,可以按照以下步骤进行详细的排查和解决:1.检查电源和硬件连接电源检查:确保电源线正确连接到服务器和电源插座。检查电源插座是否有电,并尝试使用其他插座或电源线。硬件连接检查:检查所有硬件组件(如内存条、硬盘、CPU、显卡等)是否正确安装并......
  • Linux上vi命令
    vi(vim)是上Linux非常常用的编辑器,很多Linux发行版都默认安装了vi(vim)。vi(vim)命令繁多但是如果使用灵活之后将会大大提高效率。vi是“visualinterface”的缩写,vim是viIMproved(增强版的vi)。在一般的系统管理维护中vi就够用,如果想使用代码加亮的话可以使用vim。下面vps侦探整......
  • Linux文本编辑器 - vim
    1.vim的基本概念Vim(ViImproved)是一款功能强大的文本编辑器,是Unix/Linux系统中广泛使用的编辑器之一。它源于上世纪70年代开发的Vi编辑器,它不仅兼容vi的所有指令,而且还有一些新的特性在里面。例如语法加亮,可视化操作不仅可以在终端运行,也可以运行于xwindow、macos、windo......
  • Linux环境ftp搭建及问题总结
     环境:centos7.0一、安装软件vsftpd-服务器端rpm-qa|grepvsftpd      yum -y  install vsftpdftp客户端登录ftp服务器或者lftpyum-y installftp二、启动服务systemctl restart vsftpd默认共享目录为 /var/ftp/pub三、配置ftp默认配置......
  • 最全Redis数据库Linux安装
    概念所有的I/O操作全在内存中进行,速度非纯快,性能非常搞。如果断电或停止服务,数据就会消失,而内存型数据库恰好可以弥补类似于MySQL等关系型数据库在硬盘当中进行I/O操作的速度上的局限。redis是key-values键值对的存储格式,非关系型安装过程安装redis数据库之前确保在Linux......