背景

默认的 OTA 方案是基于 recovery 系统完成的。某个产品考虑产品形态和 flash 容量之后,计划去掉 recovery 系统(不考虑掉电安全),这就需要 OTA 方案能支持在只有单个系统的情况下完成升级动作。

默认的 recovery 系统方式

先介绍下默认使用的基于 recovery 系统的升级方式。

主系统由内核和根文件系统组成,分别保存在 flash 上的 kenrelrootfs 分区。另外设置一个 recovery 分区,用于保存 recovery 系统。

此处的 recovery 系统,是一个带 initramfs 的内核,OTA 所需的应用和库都包含在 initramfs 中,因此启动到 recovery 系统之后,可不再依赖 flash 上的其他分区。

当需要进行系统升级时,先设置标志并重启,bootloader 检测到标志后会启动进入 recovery系统。在 recovery 系统中,kernelrootfs 分区都是处于未使用状态,直接将新的数据写入分区中即可。

更新完主系统之后,设置标志,重启到新的主系统即可。

没有 recovery 带来的问题

系统默认是将 flash 上的 rootfs 分区挂载为根文件系统,即系统运行时随时都可能会读写 rootfs 分区的数据。

OTA 不重启到 recovery 系统中,直接在正常系统中,即在 rootfs 分区仍被挂载为根文件系统的情况下,直接从块设备接口将数据写入 rootfs 分区,会有概率导致系统崩溃。

毕竟 OTA 应用和库本身都是放在 rootfs 中的,系统其他活跃进程也随时有可能对文件系统发出请求。

基于 initramfs 的解决方式

问题很明确,不能再挂载着rootfs的时候更新 rootfs,那先考虑下,在挂载 rootfs 之前进行OTA

原本的内核是直接在内核初始化之后挂载 flash 上的 rootfs 分区作为根文件系统。现在 recovery 系统没了,但我们可以借鉴 recovery 系统的形式,为这个内核加上 initramfs,在其中包含 OTA 所需的程序。

存在initramfs的情况下,启动时内核会先挂载 initramfs 并执行 rdinit 指定的程序,到了 initramfsinit 脚本中,就可以判断是正常启动还是 OTA 了,若为正常启动则直接挂载 rootfs 分区,并进行根文件系统切换,后续的流程就跟原方案的主系统启动流程一致了。

若判断到正在进行 OTA,则转而执行 OTA 流程,将新的数据写入 kernelrootfs 分区,此时的环境跟原方案的 recovery 系统是一样的。

这种方案的优点是跟之前的流程较为类似,可复用一些成果。缺点是内核带上 initramfs 之后,不可避免地体积会变大,启动时间会变长。

关于标志传递

如何告知 initramfs 中的启动脚本,当前需要进行 OTA 呢?

方式一:通过自定义分区传递标志,在 flash 上的划定某个分区,例如划定一个 misc 分区,约定好标志,OTA 时更新其中的标志即可

方式二:通过 ubootenv 分区传递标志,uboot 原生提供了可以在 linux 用户空间读写 env 分区的应用,编译后使用 fw_printenvfw_setenv 应用即可。详见 uboot 文档。

方式三:通过cmdline传递标志,initramfs可直接读取方式一和二设置的标志,也可以请 bootloader 约定好,由bootloader检测到方式一和二设置的标志后,修改传递给 kernelcmdline

方式四:通过芯片提供的寄存器传递标志。例如某些芯片的 RTC 模块中,会预留一些寄存器,供用户自定义使用,不掉电重启数据是不会丢的。

基于临时 ramfs 的解决方式

initramfs 是在挂载 rootfs 之前进行 OTA,那有没有办法在挂载 rootfs 之后进行 OTA 呢?也是有的,先把 rootfs 分区卸载掉就可以了。

当然,直接 umount 是不行的,rootfs 分区现在还是尊贵的根文件系统,要想卸载,就得先切换到另一个根文件系统去。那另外的根文件系统从何而来呢?没有现成的,但可以造!

我们看看 openwrt 如何做的。切换根文件之前,先调用 kill_remaining 函数 kill 掉无关进程,这样可以让构造的 ramfs 只需包含 OTA 所需的应用和库。

  1. kill_remaining() { # [ <signal> [ <loop> ] ]
  2. local loop_limit=10
  3. local sig="${1:-TERM}"
  4. local loop="${2:-0}"
  5. local run=true
  6. local stat
  7. local proc_ppid=$(cut -d' ' -f4 /proc/$$/stat)
  8. echo -n "Sending $sig to remaining processes ... "
  9. while $run; do
  10. run=false
  11. for stat in /proc/[0-9]*/stat; do
  12. [ -f "$stat" ] || continue
  13. local pid name state ppid rest
  14. read pid name state ppid rest < $stat
  15. name="${name#(}"; name="${name%)}"
  16. # Skip PID1, our parent, ourself and our children
  17. [ $pid -ne 1 -a $pid -ne $proc_ppid -a $pid -ne $$ -a $ppid -ne $$ ] || continue
  18. local cmdline
  19. read cmdline < /proc/$pid/cmdline
  20. # Skip kernel threads
  21. [ -n "$cmdline" ] || continue
  22. echo -n "$name "
  23. kill -$sig $pid 2>/dev/null
  24. [ $loop -eq 1 ] && run=true
  25. done
  26. let loop_limit--
  27. [ $loop_limit -eq 0 ] && {
  28. echo
  29. echo "Failed to kill all processes."
  30. exit 1
  31. }
  32. done
  33. echo
  34. }

然后拷贝所需文件到 ram 中,构造出所需的 ramfs

  1. switch_to_ramfs() {
  2. # 将一些基础文件拷贝到ram中,构造ramfs
  3. for binary in \
  4. /bin/busybox /bin/ash /bin/sh /bin/mount /bin/umount \
  5. pivot_root mount_root reboot sync kill sleep \
  6. md5sum hexdump cat zcat bzcat dd tar \
  7. ls basename find cp mv rm mkdir rmdir mknod touch chmod \
  8. '[' printf wc grep awk sed cut \
  9. mtd partx losetup mkfs.ext4 nandwrite flash_erase \
  10. ubiupdatevol ubiattach ubiblock ubiformat \
  11. ubidetach ubirsvol ubirmvol ubimkvol \
  12. snapshot snapshot_tool \
  13. # 除了上面列出来的,还可以将自定义的一些文件赋值到 $RAMFS_COPY_BIN 中,这样就无需改动官方的这份文件
  14. $RAMFS_COPY_BIN
  15. do
  16. local file="$(which "$binary" 2>/dev/null)"
  17. [ -n "$file" ] && install_bin "$file"
  18. done
  19. install_file /etc/resolv.conf /lib/*.sh /lib/functions/*.sh /lib/upgrade/*.sh /lib/upgrade/do_stage2 /usr/share/libubox/jshn.sh $RAMFS_COPY_DATA
  20. [ -L "/lib64" ] && ln -s /lib $RAM_ROOT/lib64

接着进行关键的根文件系统切换

  1. supivot $RAM_ROOT /mnt || {
  2. echo "Failed to switch over to ramfs. Please reboot."
  3. exit 1
  4. }

切换后收个尾

  1. #原本的根文件系统,变成挂载在 /mnt 下,现在可以卸载掉
  2. /bin/mount -o remount,ro /mnt
  3. /bin/umount -l /mnt
  4. grep /overlay /proc/mounts > /dev/null && {
  5. /bin/mount -o noatime,remount,ro /overlay
  6. /bin/umount -l /overlay
  7. }
  8. }

最后在 ramfs 中调用真正的 OTA 命令

  1. # Exec new shell from ramfs
  2. exec /bin/busybox ash -c "$COMMAND"

这种做法的好处是,避免了 intiramfs 带来的体积和启动速度问题,且 OTA 过程只有一次重启。

更具体请参考 openwrt 官方的升级脚本(旧版本搜索run_ramfs,新版本搜索 switch_to_ramfs)。

毕竟是 shell 脚本,很容易便可以移植到其他的环境中使用的。

基于 ramfs 的 OTA的更多相关文章

  1. OTA升级详解(三)

    君子知夫不全不粹之不足以为美也, 故诵数以贯之, 思索以通之, 为其人以处之, 除其害者以持养之: 出自荀子<劝学篇> 终于OTA的升级过程的详解来了,之前的两篇文章OTA升级详解(一)与 ...

  2. nrf52——DFU升级USB/UART升级方式详解(基于SDK开发例程)

    摘要:在前面的nrf52--DFU升级OTA升级方式详解(基于SDK开发例程)一文中我测试了基于蓝牙的OTA,本文将开始基于UART和USB(USB_CDC_)进行升级测试. 整体升级流程: 整个过程 ...

  3. Linux设备管理(四)_从sysfs回到ktype

    sysfs是一个基于ramfs的文件系统,在2.6内核开始引入,用来导出内核对象(kernel object)的数据.属性到用户空间.与同样用于查看内核数据的proc不同,sysfs只关心具有层次结构 ...

  4. 深入理解Linux字符设备驱动

    文章从上层应用访问字符设备驱动开始,一步步地深入分析Linux字符设备的软件层次.组成框架和交互.如何编写驱动.设备文件的创建和mdev原理,对Linux字符设备驱动有全面的讲解.本文整合之前发表的& ...

  5. Linux Communication Mechanism Summarize

    目录 . Linux通信机制分类简介 . 控制机制 0x1: 竞态条件 0x2: 临界区 . Inter-Process Communication (IPC) mechanisms: 进程间通信机制 ...

  6. linux概念之分区与文件系统

    分区类型 [root@-shiyan dev]# fdisk /dev/sda WARNING: DOS-compatible mode is deprecated. It's strongly re ...

  7. sysfs - 用于导出内核对象(kobject)的文件系统

    sysfs - _The_ filesystem for exporting kernel objects.sysfs - 用于导出内核对象(kobject)的文件系统Patrick Mochel & ...

  8. Docker背后的容器管理——Libcontainer深度解析

    Libcontainer 是Docker中用于容器管理的包,它基于Go语言实现,通过管理namespaces.cgroups.capabilities以及文件系统来进行容器控制.你可以使用Libcon ...

  9. 使用 /sys 文件系统访问 Linux 内核

    sysfs 与 /sys sysfs 文件系统总是被挂载在 /sys 挂载点上.虽然在较早期的2.6内核系统上并没有规定 sysfs 的标准挂载位置,可以把 sysfs 挂载在任何位置,但较近的2.6 ...

随机推荐

  1. C++字符串转整形、浮点型stof()、atoi()、strtol()等

    头文件:#include<stdlib.h>string str;stof:float val=stof(str);atoi:int val=atoi(str);atol:long val ...

  2. C#算法设计排序篇之06-堆排序(附带动画演示程序)

    堆排序(Heap Sort) 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/685 访问. 堆排序是指利用堆积树(堆)这 ...

  3. Netty 是如何支撑高性能网络通信的?

    作为一个高性能的 NIO 通信框架,Netty 被广泛应用于大数据处理.互联网消息中间件.游戏和金融行业等.大多数应用场景对底层的通信框架都有很高的性能要求,作为综合性能最高的 NIO 框架 之一,N ...

  4. CSS动画实例:一颗躁动的心

    在页面中放置一个类名为container的层作为盛放心心的容器,在该层中再定义一个名为heart的子层,HTML代码描述如下: <div class="container"& ...

  5. windows安装cnpm步骤

    1.首先前往nodejs官网下载nodejs 2.安装nodejs 3.打开cmd,输入npm -v,检查npm是否安装成功.成功返回的话返回输出版本号 4.安装cnpm,输入npm install ...

  6. hge引擎示例教程cmake项目

    hge引擎的示例代码在vs2017不能很好的运行,需要调不少东西,所以我将其重新整理成cmake的项目. 所有示例均在vs2017 msvc 下测试可以正常运行. 由于缺少libhgehelp.a所以 ...

  7. DHCPV6 vs DHCPV4

    原文链接:https://blog.csdn.net/kdb_viewer/article/details/83310904 一.DHCPv4 vs DHCPv6 1. 相同点 使用DHCP clie ...

  8. Java实现树形结构的数据转Json格式

    在项目中难免会用到树形结构,毕竟这是一种常用的组织架构.楼主这里整理了两个实现的版本,可以直接拿来使用,非常方便. 楼主没有单独建项目,直接在以前的一个Demo上实现的.第一种,看下面代码: pack ...

  9. latex:公式的序号

    1.排序单位 在文类book或report中,行间公式是以章为排序单位的,即每一新章节开始,公式序号计数器equation就被清零.比如第1章第3个公式的序号是(1.3),第2章第1个公式的序号是(2 ...

  10. 牛客网PAT练习场-A+B和C

    签到题 .题目地址:https://www.nowcoder.com/pat/6/problem/4077 #include<iostream> #include<cstdio> ...