基本概念

闪存的好处是无噪音,寻址快,低功耗。但是也有缺点,对于同一个block必须在每次写入前对整个block进行擦除,因此称之为erase block。并且闪存的擦除次数是有限的,最多也就百万次,为了增加闪存寿命,就必须要使得每个地方均匀擦除,防止个别地方擦除次数太多。 NOR flash允许随机逐字节访问,因此CPU可以直接从NOR flash执行代码,对于bootloader来说非常好,不必复制到内存就能执行。而目前市面最流行的是NAND flash,因为价格更低,但是需要专门的控制器访问,所以不能直接从flash执行代码,有时候flash中有坏块,可以通过硬件或软件来识别和去除坏块。目前有两种NAND flash,一种仿真块设备接口,包含一个硬件FTL来维护erase block,像USB flash、EMMC、SSD属于此类,这样有助于降低OS的软件复杂度。另一种是裸flash,操作系统访问flash控制器,直接管理block,这种情况下就可以计算一个块的擦除次数。 Linux内核实现了一个MTD子系统,对访问flash设备提供了一个通用接口。

MTD在Linux内核中的结构如下图所示。

mtd-architecture.png

MTD设备需要分区,并且是采用硬编码的方式。相比于普通块设备, Linux没有提供对MTD整个设备进行访问的接口。分区信息不会保存到flash中,其block可能会在某个时刻坏掉,所以flash不能用来存放重要信息。

NAND flash和NOR flash主要区别如下图所示: nand-nor-flash-cmp.png

常用缩写如下:

MTD Memory Technology Device
DOC DiskOnChip
FTL Flash Translation Layer
NFTL NAND Flash Translation Layer
SSD Solid State Disks
JFFS2 Journaling Flash File System version 2
EBS Erase Block Summary
UBI Unsorted Block Images
wear-leveling 均衡损耗

内核中的MTD

内核配置

CONFIG_MTD
核心功能,一般来说是必须要支持的。
CONFIG_MTD_PARTITIONS
支持分区。
CONFIG_MTD_CHAR
支持/dev/mtdX/dev/mtdrX
CONFIG_MTD_BLOCK
支持dev/mtdblockX
CONFIG_MTD_BLOCK_RO
支持只读/dev/mtdblockX
CONFIG_MTD_MTDRAM
用RAM来测试驱动。
CONFIG_MTD_BLOCK2MTD
用block设备转换为MTD设备。
CONFIG_FTL
支持/dev/ftlSX
CONFIG_NFTL
支持/dev/nftlSX
CONFIG_NFTL_RW
支持对NFTL格式化的设备写入。
CONFIG_BLK_DEV_LOOP
支持loopback设备。

在内核中,MTD_BLOCK和MTD_BLOCK_RO只有一个可以构建到内核中,毕竟二者互斥,当然可以都编译成模块。

CONFIG_JFFS2_FS
支持JFFS2.
CONFIG_JFFS2_LZO
支持LZO压缩。

分区映射

不同于DOC设备,CFI flash不能使用fdisk/pdisk,因为分区信息通常不会保存到CFI设备中,而是直接编码到映射驱动中,在驱动初始化的时候注册到MTD子系统。分区映射的代码如下所示。

static struct mtd_partition tqm8xxl_fs_partitions[] = {
    {
    name:       "cramfs",               // 方便识别
    offset:     0x00000000,             // 起始地址
    size:       0x00200000,             // 分区大小
    mask_flags: MTD_WRITEABLE,          // force writable
    },
    {
    name:       "jffs2",
    offset:     0x00200000,
    size:       0x00200000,
    }
};

相邻分区直接连着即可,没必要对齐。我们可以通过如下命令查看相关信息。

# cat /proc/mtd
dev:    size   erasesize  name
mtd0: 00240000 00001000 "boot"
mtd1: 001c0000 00001000 "rootfs"
mtd2: 00100000 00001000 "jffs2"

分区边界必须以erasesize对齐,erasesize的单位是字节。

设备节点

在dev目录下面可以创建5种类型的MTD设备,如下表所示。

dev entry type major num minor range naming scheme
mtdX char 90 [0, 32, 2] \(X = \frac{minor}{2}\)
mtdrX char 90 [1, 33, 2] \(X = \frac{(minor - 1)}{2}\)
mtdblockX block 31 [0, 16, 1] \(X = minor\)
ftlSX block 44 [0, 255, 16]  
nftlSX block 93 [0, 255, 16]  
SX

S = set(eg: a, b, …)

X = minor - 16(set - 1)

mtdX
每一个分区可以作为独立设备对待。
mtdrX
等同于对应mtdX的只读设备。
mtdblockX
等同于对应mtdX的块设备。
nftlSX
每一个集合可以作为独立设备对待,而每一个集合中的项则视为一个分区,第一个集合项视作整个设备。如nftlb3就是第2个集合的第3个分区。

当然在实际中不需要全部都创建,但是有时候还是需要手动去创建一些节点。可以使用如下脚本来创建字符和块设备节点。

num=3
for i in $(seq 0 $num)
do
    mknod mtd$i c 90 $(expr $i + $i)
    mknod mtdblock$i b 31 $i
done

MTD工具箱

MTD功能不同于其它子系统,因此需要特殊的工具对其进行操作。大多数工具作用于/dev/mtdX设备,也就是通过字符设备操作。

mtdinfo
获取MTD设备的详细信息。
flash_eraseall
完全擦除指定的MTD设备。
flashcp
写入NOR flash。
nandwrite
写入NAND flash。

在使用设备之前,要对设备进行擦除,可以使用如下命令。

flash_eraseall -j /dev/mtd2             # 擦除并格式化为JFFS2
mount -t jffs2 /dev/mtdblock2 /mnt/flash

然后可以在挂载点进行读写。

而设备也只允许在擦除之后写入一次,也就是每次要想写入新的数据,就必须要擦除一次。擦除之后的写入可以用多种方式进行,如下所示。

cat /tmp/initrd.bin > /dev/mtd2
dd of=/dev/mtd2 if=/tmp/ppcboot.img
nandwrite -p /dev/mtd2 rootfs.jffs2
dd if=/dev/mtd2 of=/tmp/ppcboot.img

可以通过mkfs.jffs2准备JFFS2文件,由于写入的数据一般不会占满整个分区,所以读回来的数据就会包含额外的不相关数据。

移植mtd-utils

  • zlib:git clone https://gitshell.com/micky/zlib/
  • lzo: 下载地址

Makefile实际上包括两个文件,一个是Makefile,一个是common.mk,在Makefile中会去包含common.mk文件。首先配置CROSS参数,使用指定编译环境,该参数在common.mk中。

CROSS=rsdk-linux-

设置编译选项,绕过编译错误,直接写到Makefile开头部分即可。

WITHOUT_XATTR=1
WITHOUT_XATTR=1
如果不指定该选项,在编译mkfs.jffs2的时候会报错。
mkfs.jffs2.c:71:21: error: sys/acl.h: No such file or directory
mkfs.jffs2.c: In function 'formalize_posix_acl':
mkfs.jffs2.c:1025: error: 'ACL_USER_OBJ' undeclared (first use in this function)
mkfs.jffs2.c:1025: error: (Each undeclared identifier is reported only once
mkfs.jffs2.c:1025: error: for each function it appears in.)
mkfs.jffs2.c:1026: error: 'ACL_GROUP_OBJ' undeclared (first use in this function)
mkfs.jffs2.c:1027: error: 'ACL_MASK' undeclared (first use in this function)
mkfs.jffs2.c:1028: error: 'ACL_OTHER' undeclared (first use in this function)
mkfs.jffs2.c:1034: error: 'ACL_USER' undeclared (first use in this function)
mkfs.jffs2.c:1035: error: 'ACL_GROUP' undeclared (first use in this function)

如果想要使用XATTR那么就可能需要添加libacl-dev。

jffs2文件系统

基本概念

JFFS2是日志结构文件系统,由Axis Communications AB设计, JFFS2针对NOR flash设计,虽然也可以用到NAND flash上面,但是不够优化,而YAFFS2则是针对NAND flash设计。 JFFS2在2001年加入到linux内核中。

使用JFFS2主要有如下好处:可以永久写入到flash、优化flash内存、提供损耗均衡、支持压缩、使用时不必从flash复制到内存。 JFFS2文件系统实现了坏块探测和管理,并且即使在断电和系统崩溃也能保持一致状态。

每个文件被视为一个node,包括元数据和数据,每个node关联一个version,将最新version的节点写入到其它空闲位置,这让写入变得简洁,但是导致读取很复杂,每次读取都要搜索最新版本的node。 JFFS2的block有三种状态,clean/dirty/free,在后台有一个垃圾收集器,将dirty变为free,其做法是在dirty block中找到valid node,复制到valid block或者free block,然后将dirty block变为free block。为了提高性能,最近使用的文件就会在内存中建立映射,为了建立这样的映射,在mount的时候需要扫描所有的node,这样就使得mount很慢,如果启动时要挂载JFFS2文件系统,就会导致启动很慢。如果配置选项CONFIG_JFFS2_SUMMARY,就可以将映射保存到设备中,可以减少mount时间。

JFFS2的主要优点如下:

  • 支持NAND flash,尽管性能不算好。
  • 支持硬链接。
  • 支持压缩,有4种压缩算法:zlib、rubin、rtime、lzo。

JFFS2的主要缺点如下:

  • 挂载的时候需要扫描所有节点,通过EBS可以提高性能,EBS存储到每个block的尾部,每次写入的时候更新。每次挂载的时候只需要扫描EBS,而不用读取整个block。
  • 写入许多小的数据块会导致负压缩率。
  • 没有办法准确获取剩余空间,因为这取决于压缩率和写入序列。
  • 读写性能差,因为读取的时候需要解压缩,写入的时候需要压缩。
  • 垃圾收集线程占用CPU资源不说还会阻塞对文件系统的访问。

目前至少有三种文件系统被设计用来替代JFFS2,分别是logfs、ubifs、yaffs。

基本用法

创建镜像

不论用那种方法创建镜像都必须指定一个目录,如果想创建ramdisk.gz的镜像,可以用如下方式为ramdisk.gz生成目录ram,然后就能够利用ram目录来制作镜像。

mkdir ram
gunzip ramdisk.gz
mount ramdisk ram -o loop

利用mtd-utils可以将JFFS2 image直接写入到bootloader。制作JFFS2镜像:

mkfs.jffs2 -r <dir> -e <erase_block_size> \
           -s <page_size> [-p] [-n] \
           -o <output_file>
-r
指定用于创建镜像的目录
-e
单位是KiB,最小是8KiB
-n
如果是为NAND flash就要使用该选项
mkfs.jffs2 -r jffs2 -p 0x100000 -e 0x1000 -b -o jffs2.bin

如果想进一步优化镜像,加快挂载速度,可以使用sumtool命令。

另外可以使用tarball建立镜像。进入到用于创建镜像的目录,执行如下命令。

tar czf jffs2.tar.gz ram/*

写入镜像

dd if=filesystem.jffs2 of=/dev/mtdblock0

挂载

mount -t jffs2 /tmp/mtdblock0 /media/jffs2

常用命令

mkfs.jffs2

mkfs.jffs2 [ -p,--pad[=SIZE] ] [ -r,-d,--root directory ]
           [ -s,--page size=SIZE ] [ -e,--eraseblock=SIZE ]
           [ -c,--cleanmarker=SIZE ] [-n,--no-cleanmarkers ]
           [ -o,--output image.jffs2 ] [ -l,--little-endian]
           [ -b,--big-endian ] [ -D,--devtable=FILE ]
           [ -f,--faketime ] [-q,--squash ]
           [ -U,--squash-uids ] [ -P,--squash-perms ]
           [ --with-xattr] [ --with-selinux ]
           [ --with-posix-acl ] [ -m,--compression-mode=MODE]
           [ -x,--disable-compressor=NAME ]
           [ -X,--enable-compressor=NAME ]
           [ -y,--compressor-priority=PRIORITY:NAME ]
           [ -L,--list-compressors ] [-t,--test-compression ]
           [ -h,--help ] [ -v,--verbose ] [ -V,--version ]
           [ -i,--incremental image.jffs2 ]
-p,–pad[=SIZE]
如果没有指定SIZE,就填充到最后一个erase block size,如果指定了SIZE就填充到指定大小。使用0xFF作为填充值。要指定填充大小必须使用--pad,使用-p不接受参数,所以默认填充到64KB大小。
-r,-d,–root directory
指定用于制作镜像的源目录,默认使用当前目录。
-s,–page size=SIZE
默认是4KiB,一个data节点的最大大小。
-e, –eraseblock=SIZE
默认是64KiB,如果指定值小于4096,那么默认单位是KB。这个值应当和目标MTD device的erase block size大小相等,否则就不会工作在最优模式。
-n, –no-cleanmarkers
不在每个block的开头写入CLEANMARKER节点,对NAND flash很有用。
-o, –output=FILE
指定输出文件。
-l, –little-endian
-b, –big-endian
默认和host的大小端相同,所有有些情况需要明确指定。

sumtool

Usage: sumtool [OPTIONS] -i inputfile -o outputfile

将JFFS2镜像转换为概括JFFS2镜像,如果内核支持概括功能就能加快挂载速度。

Options:
  -e, --eraseblock=SIZE     default: 64KiB (16KiB on NAND)
  -c, --cleanmarker=SIZE    size of cleanmarker,
                            default: 12 (16 bytes on NAND)
  -n, --no-cleanmarkers     not add cleanmarker
  -o, --output=FILE         output to FILE
  -i, --input=FILE          input from FILE
  -b, --bigendian           image is big endian
  -l  --littleendian        image is little endian
  -h, --help                display this help text
  -v, --verbose             verbose operation
  -V, --version             display version information
  -p, --pad                 pad with 0xFF to the end

UBIFS文件系统

基本概念

在flash上面除了cramfs、jffs2、yaffs2之外,还有ubifs,它们都是基于文件系统+mtd+flash设备的架构。 ubifs是在linux-2.6.27之后加入的。 flash具有先擦除再写入、坏块、有限读写次数等特性,目前管理flash的方法主要有:

  • 采用MTD+FTL/NFTL+传统文件系统,如:FAT、ext2等。通过软件的方式来实现日志管理、坏块管理、损益均衡等技术。存在效率核知识产权等局限。
  • 采用硬件翻译层+传统文件系统,如:SD卡、U盘等。成本较高。
  • 用MTD+FLASH专用文件系统,如JFFS1/2,YAFFS1/2等。提高了flash的管理能力,并得到广泛应用,也存在技术瓶颈,如内存消耗大,对flash容量、文件系统大小、访问模式等线性依赖,损益均衡能力差。

ubifs是一个新兴的应用于mtd上的有效的文件系统。可以有效的组织flash的坏块和负载平衡,同时提高访问速度,减小内存消耗,具有日志的功能,是JFFS2的后续增强版。相比于JFFS2和YAFFS2的最大的特点是支持写缓存和剩余空间计算。

MTD device代表物理设备,包括好块和坏块,Ubi device代表逻辑设备,对用户而言只有好块, Ubi device提供了对坏块的管理,并且对物理块进行了新的组织。即使用户串行读写,到Ubi device的时候就会进行新的映射,目的在于负载均衡。

MTD初始化的时候将同一类型的flash划分为一个MTD device,接下来从内核启动参数或默认分区表中获得分区信息,最后将每个分区作为一个MTD device添加到mtd_table中,查看/proc/mtd实际上就是查看mtd_table中的信息。每个MTD device可以attach到一个Ubi device,每个Ubi device可以创建很多Ubi volume,每个Ubi volume又被作为一个MTD device保存到mtd_table中。 MTD的type分为:nor、nand、ram、rom、ubivolume。

基本用法

内核配置

Device Drivers —>Memory Technology Device (MTD) support —>UBI - Unsorted block images —>Enable UBI

File systems —>Miscellaneous filesystems —>UBIFS file system support

分区表设置,以uboot内核启动参数为例:

Bootargs=console=ttyS0,115200n8
ubi.mtd=4 root=ubi0:rtfs rootfstype=ubifs rw
mtdparts=mtd_nand:200M(boot),300M(cfg),400M(other),500M(rootfs),-(left)
console
串口参数
ubi.mtd=4
系统的根文件系统在第4个MTD上,系统把mtd4 attach到ubi0。
root=ubi0:rtfs
根文件系统在ubi0中叫rtfs的volumne上。
rootfstype=ubifs
指示rootfs文件系统的类型为ubifs。

最后是分区表的定义,格式为:

mtd_id:[-]size[@offset](name)[mask_flags],...
-
表示剩余所有空间
size
分区大小
offset
分区的起始偏移量
(name)
物理分区的名字
mask_flags
分区的读写属性

挂载使用

flash_eraseall /dev/mtd4
ubiattach /dev/ubi_ctrl -m 4
ubimkvol /dev/ubi0 -N rootfs -s 100MiB  # ubi0_0等价于ubi0:rootfs
mount -t ubifs ubi0_0 /mnt/ubi
mount -t ubifs ubi0:rootfs /mnt/ubi

制作ubifs镜像需要确定如下参数:

  • MTD partition size,可以从/proc/mtd查看。
  • flash physical eraseblock size,可以从/proc/mtd查看。
  • minimum flash input/output unit size,NOR flash通常是一个字节, NAND flash是一个页面。
  • sub-page size(for NAND flashes),从flash手册查看。
  • logical eraseblock size,对于有子页的NAND flash来说,等于物理擦除块大小 - 一页的大小。

这些参数也可以通过工具从内核得到,如mtdinfo -u。也可以通过ubi和mtd连接时产生的信息得到。

modprobe ubi mtd=4
ubiattach /dev/ubi_ctrl -m 4
mkfs.ubifs -r rootfs -m 2048 -e 129024 -c 812 -o ubifs.img
ubinize -o ubi.img -m 2048 -p 128KiB -s 512 /home/lht/omap3530/tools/ubinize.cfg
-r
指定文件系统内容的位置
-m
页面大小
-e
逻辑擦除块大小
-p
物理擦除块大小
-c
最大逻辑擦除块数量
-c
最小硬件输入/输出页面大小

ubinize.cfg的内容如下:

[ubifs]
    mode=ubi
    image=ubifs.img
    vol_id=0
    vol_size=100MiB
    vol_type=dynamic
    vol_name=rootfs
    vol_flags=autoresize