这是机器未来的第58篇文章
原文首发地址:https://robotsfutures.blog.csdn.net/article/details/126924015
【01】imx8qxp yocto工程构建指南
【02】Yocto工程repo源码gitee加速配置方法
【03】imx8qxp一键独立编译指南
【04】嵌入式Linux设备掉电数据容错研究
写在开始:
- 博客简介:专注AIoT领域,追逐未来时代的脉搏,记录路途中的技术成长!
- 博主社区:AIoT机器智能, 欢迎加入!
- 专栏简介:imx8qxp小白从拿到板子到完成项目的过程记录
- 面向人群:嵌入式工程师
本文针对嵌入式设备掉电应用数据丢失的问题,研究了掉电数据容错的一些措施。
如果是自有平台上开发的话,存储介质很好确认,如果是基于第三方平台开发应用,例如AG35 Open版这样的使用场景,如何判断存储介质的类型呢?
/usrdata # df -T Filesystem Type 1K-blocks Used Available Use% Mounted on /dev/root squashfs 41600 41600 0 100% / tmpfs tmpfs 76008 28 75980 0% /run tmpfs tmpfs 76008 1292 74716 2% /var/volatile tmpfs tmpfs 64 4 60 6% /dev tmpfs tmpfs 76008 1292 74716 2% /var/lib /dev/ubiblock1_0 squashfs 30720 30720 0 100% /firmware /dev/ubi2_0 ubifs 7912 3564 4348 45% /systemrw /dev/ubi2_0 ubifs 7912 3564 4348 45% /data /dev/ubi2_0 ubifs 7912 3564 4348 45% /etc /dev/ubiblock3_0 squashfs 128 128 0 100% /oemapp /dev/ubi4_0 ubifs 139112 1592 137520 1% /usrdata /dev/ubi5_0 ubifs 9516 40 9476 0% /persist
这里需要验证/usrdata所在分区在什么存储介质上,通过df -T可以看到其文件系统为ubifs。ubifs应用在裸flash上,因此可以判断存储flash为nandflash。
ubifs **write-back 支持:**回写(写入到page cache即认为写入完成),同JFFS2的write-through(透写:立即写入内存)相比可以显著的提高文件系统的吞吐量。(**译者注:**write-back即回写,写入到page cache,就认为数据写入完成,而write-through即透写,只有将数据写入存储设备才认为写入成功;write-back相对于write-through,无需磁盘IO,因此具有更好的系统IO,但是数据一致性是得不到保证的)
write-back支持,需要应用程序注意及时同步重要的文件。否则掉电会导致这些文件的损坏和消失,掉电对于嵌入式系统而言是很常见的。
缓存是用来减少高速设备访问低速设备所需平均时间的组件,文件读写涉及到计算机内存和磁盘,内存操作速度远远大于磁盘,如果每次调用read,write都去直接操作磁盘,一方面速度会被限制,一方面也会降低磁盘使用寿命,因此不管是对磁盘的读操作还是写操作,操作系统都会将数据缓存起来。
页缓存(Page Cache)是位于内存和文件之间的缓冲区,它实际上也是一块内存区域,所有的文件IO(包括网络文件)都是直接和页缓存交互,操作系统通过一系列的数据结构,比如inode, address_space, struct page,实现将一个文件映射到页的级别,这些具体数据结构及之间的关系我们暂且不讨论,只需知道页缓存的存在以及它在文件IO中扮演着重要角色,很大一部分程度上,文件读写的优化就是对页缓存使用的优化
页缓存对应文件中的一块区域,如果页缓存和对应的文件区域内容不一致,则该页缓存叫做脏页(Dirty Page)。对页缓存进行修改或者新建页缓存,只要没有刷磁盘,都会产生脏页。
LInux中有两种方式查看Page Cache
/usrdata/QuecOpen # free total used free shared buffers cached Mem: 152656 148696 3960 1352 41992 28580 -/+ buffers/cache: 78124 74532 Swap: 0 0 0
/usrdata/QuecOpen # cat /proc/meminfo MemTotal: 152656 kB MemFree: 3188 kB MemAvailable: 77660 kB Buffers: 41992 kB Cached: 28584 kB SwapCached: 0 kB Active: 66176 kB Inactive: 34832 kB Active(anon): 31644 kB Inactive(anon): 136 kB Active(file): 34532 kB Inactive(file): 34696 kB Unevictable: 0 kB Mlocked: 0 kB SwapTotal: 0 kB SwapFree: 0 kB Dirty: 0 kB Writeback: 0 kB AnonPages: 30468 kB Mapped: 10196 kB Shmem: 1356 kB Slab: 25820 kB SReclaimable: 11076 kB SUnreclaim: 14744 kB KernelStack: 2936 kB PageTables: 2116 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 76328 kB Committed_AS: 1660112 kB VmallocTotal: 781312 kB VmallocUsed: 20520 kB VmallocChunk: 729084 kB
关注Cached和Dirty即可。
Linux有几个内核参数可以用来调整write-back
/usrdata/QuecOpen # sysctl -a 2>/dev/null | grep dirty vm.dirty_background_bytes = 0 vm.dirty_background_ratio = 10 vm.dirty_bytes = 0 vm.dirty_expire_centisecs = 3000 vm.dirty_ratio = 20 vm.dirty_writeback_centisecs = 500
vm.dirty_background_ratio
内存可以填充脏页的百分比(dirty数据与全部内存的最大百分比),当脏页总大小达到这个比例后,系统后台进程就会开始将脏页刷磁盘(vm.dirty_background_bytes类似,只不过是通过字节数来设置)
vm.dirty_ratio
绝对的脏数据限制,内存里的脏数据百分比不能超过这个值。如果脏数据超过这个数量,新的IO请求将会被阻挡,直到脏数据被写进磁盘
vm.dirty_writeback_centisecs
指定多长时间做一次脏数据写回操作,单位为百分之一秒,linux周期性write-back线程写出dirty数据的周期,这个机制可以确保所有的脏数据在某个时间点都可以写入介质
vm.dirty_expire_centisecs
指定脏数据能存活的时间,单位为百分之一秒,比如这里设置为30秒,在操作系统进行写回操作时,如果脏数据在内存中超过30秒时,就会被写回磁盘.
这些参数可以通过 sudo sysctl -w vm.dirty_background_ratio=5 这样的命令来修改,需要root权限,也可以在root用户下执行如下命令修改
echo 5 > /proc/sys/vm/dirty_background_ratio
在有了页缓存和脏页的概念后,我们再来看文件的读写流程
/usrdata/QuecOpen # sysctl -a 2>/dev/null | grep dirty vm.dirty_background_bytes = 0 vm.dirty_background_ratio = 10 vm.dirty_bytes = 0 vm.dirty_expire_centisecs = 3000 vm.dirty_ratio = 20 vm.dirty_writeback_centisecs = 500
/usrdata/QuecOpen # cat /dev/null > ag35.config # 此时在3s内断电 /usrdata/QuecOpen # client_loop: send disconnect: Broken pipe zhoushimin@zsm:~$ ./login_fouter.sh spawn ssh root@198.18.1.1 root@198.18.1.1's password: root@frouter:~# mkdir -p /home/root/.android/;echo 0x2c7c > /home/root/.android/adb_usb.ini;export PATH=$PATH:/mnt/tmp/adb root@frouter:~# root@frouter:~# # adb shell root@frouter:~# ls app init.sh ko root@frouter:~# adb shell * daemon not running. starting it now on port 5037 * * daemon started successfully * / # ls WEBSERVER cache firmware mnt run sys tmp bin data home oemapp sbin system usr boot dev lib persist sdcard systemrw usrdata build.prop etc media proc share target var / # cd /usrdata/QuecOpen/ /usrdata/QuecOpen # ls ag35.config app.config ecm_call factory ota_result.dat ag35_app.md5 app_monitor.sh ecm_call-0.csv log_index.dat upgrade /usrdata/QuecOpen # ls -la total 1592 drwxrwxr-x 3 root root 864 Oct 9 05:40 . drwxr-xr-x 3 root root 376 Sep 1 09:19 .. -rw-r--r-- 1 root root 10 Oct 9 05:40 ag35.config # 发现文件内容未被清空 -rw-r--r-- 1 root root 0 Oct 9 05:40 ag35_app.md5
执行写入为空后,快速断电,还是很容易复现文件信息未生效的现象。
在嵌入式系统中为了保证系统的可靠性,至少要保证内核、rootfs数据的可靠性,保证系统可以运行起来,可以根据数据的类型分为系统和数据,为了考虑数据掉电容错,可以将系统分区设计为只读分区和可读写分区,例如squashfs+overlayfs/ext4。
详情参见:https://blog.csdn.net/toradexsh/article/details/109737842
详见4.4 Dirty Page脏页WriteBack参数
如果你想切换到同步模式,在mount文件系统是使用 -o 同步选项。然而,要注意文件系统性能将会下降。此外,要记住UBIFS mount为同步模式仍然不如JFFS2提供更多的保证
你也可以在open()调用时是一年个O_SYNC标志;这使得这个文件所得修改的data(不包括meta-data)都会在write()操作返回前写入media;通常来说,最好使用fsync(), 因为O_SYNC使得每个写都是同步的,而fsync允许多个累积的写
对于一些频次低,数据小,重要的文件建议采用这种方案。
可以使一定数目inodes为同步模式,通过设置inode的sync标志,一旦应用程序对这个文件执行了写操作,使系统立刻把修改的结果写到磁盘
在shell中执行chattr +S
chattr +S /usrdata/QuecOpen/ag35.config
在C程序中,则可以在ioctl命令使用FS_IOC_SETFLAGS配置同步 标志FS_SYNC_FL
int attr; fd = open("pathname", ...); ioctl(fd, FS_IOC_GETFLAGS, &attr); /* Place current flags in aqattraq */ attr |= FS_SYNC_FL; ioctl(fd, FS_IOC_SETFLAGS, &attr); /* Update flags for inode referred to by aqfdaq */
注意, mkfs.ubifs工具会检查原始的FS树,如果文件在原始文件树是同步的,那么在UBIFS image也会是同步的
要强调的是,上面的方法对于任何文件系统都是可行的,包括JFFS2
fsync()可能包括目录 - 它同步目录inode的meta-data。 “sync” flag也可以用在目录上,使得目录inode变成同步的。但是"sync" flag是可继承的, 意味这这个目录下的所有新节点都有这个标志。 这个目录的新文件和新子目录也就变成同步的,子目录的child也是如此。 这个功能是非常有用的,如果想创建一个需要整个目录树都同步的目录。
fdatasync()调用对 UBIFS的目录是不起作用的, 因为UBIFS对目录项的操作都是同步的,当然不是所有文件系统都如此。类似的, “dirsync” inode 标志对UBIFS没有作用
以上提到的功能都是作用于文件描述符,而不是文件stream(FILE *)。 同步一个stream,你应该通过libc的fileno()取得文件描述符,首先使用fflush()来flush stream ,然后调用fsync或者fdatasync. 你也可以使用其他的同步方法,但是记得在同步文件前要先flush stream. fflush() 和sync(), fsync,fdatasync的区别在于前者仅仅同步libc-level的buffer,而后者则是kernel-level buffers。
增加超级电容或电池,掉电时做退出处理。这种成本相对较低,但是仍然需要考虑超级电容或电池的使用寿命问题,是否在产品的生命周期之内。
如果设备作为从设备挂载在主机上,主机在断电前,先通知从机即将断电,做一个断电通知,让从机正常做退出处理。
COW(copy-on-write 的简称),是一种计算机设计领域的优化策略,其核心思想是:如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源(摘自维基百科**)。**
对于重要文件,每次操作前,先备份副本并同步后再操作,如果正式版本出错,还可以从备份版本恢复。
参考文献:
— 博主热门专栏推荐—