嵌入式 Linux 系统在线升级策略

对于运行 Linux 系统的嵌入式产品,很多时候我们发现了当前版本内核、驱动、或者应用程序的 bug 并对之修复之后,或者研发出了功能更丰富、性能更突出的应用软件时,想要对当前运行的设备进行相应程序替换和升级。很多人的做法是通过对每一台设备烧写新版软件的方式进行软件版本更新,如果产品数量少且分布地点比较集中,这种方案具有一定的实效性。但是当设备数量庞大且地点分散时,这种本地烧写的升级方式将会变得非常难以操作,且升级结果可视化具有一定难度,需要通过串口等终端才能确认。

针对采用 Linux 系统且具有互联网接入能力的嵌入式设备,不论这种接入方式是有线网络、wifi、2G 或者 4G,本文将为其提供一种通过服务端后台对在线的所有或者部分设备进行远程批量升级的高效、可靠、直观的升级策略。升级内容可以是内核、驱动、文件系统、应用程序或者某些配置文件。接下来,将首先展示该方案的架构图,紧接着一步步讲述各个功能或者逻辑模块的细节。

方案概述

此升级方案由后台服务端程序、web 页面、终端升级程序三部分组成。如图 1 展示了升级方案 的架构图。

图 1. 升级方案架构图

服务端程序

服务端程序用来监测终端设备状态,管理升级包,升级流程控制并且提供 web 端响应以及数据库访问。本策略中的服务端为 apache-tomcat,程序采用 java servlet,数据库为 MySQL,web 页面为 JSP 编写。您可以使用任何一种后台语言(如 php、python 等)实现本文所描述的服务端功能。

服务端功能有:

  • 终端消息处理
  • 升级包管理
  • 升级指令处理

终端消息处理

服务端程序通过 getParameter("version")获得终端软件版本号,通过 queryLatestVersion()查询数据库中最新软件版本号,然后将二者进行对比。如果相同,则证明该终端设备软件版本已经是最新,返回 latest 指令;如果不同且服务端没有收到 web 端用户的升级指令,则通过 queryAddress()从数据库中查询最新升级包的地址,将之返回给终端,以便终端设备从该地址下载升级包,另外,如果此时用户在 web 界面执行了升级命令,则返回 update 指令给终端,终端设备执行升级操作。详细请查看清单 1。

清单 1. 终端消息处理代码片段
public void doGet(HttpServletRequest request, HttpServletResponse response)\
throws ServletException, IOException {
    String msg = null;
    String version_latest = null;
    String address_latest = null;
    String version = request.getParameter("version");

    PrintWriter out = response.getWriter();
    version_latest = queryLatestVersion();
    if(version.equals(version_latest)){ 
        msg = "|latest|null|null|";
    }else if(UpdateServlet.update_status){
        msg = "|update|"+version_latest+"|null|";
        UpdateServlet.update_status = false;
    }else{
        address_latest = queryAddress();
        msg = "|download|172.x.x.x"+address_latest+"|"+MD5+"|";
    }
    out.print(msg);
    out.flush();
    out.close();
}

升级包管理

服务端程序处理 web 端上传的升级包,首先确认存放升级包的路径是否存在,没有则创建。升级包接收完成之后,从升级包文件名中截取版本号,然后将文件名、版本号、升级包在服务端的存放路径信息插入到数据库中。类似的,服务端程序也响应 web 端用户对升级包的更改、删除等操作。详细的升级包管理请查看清单 2。

清单 2. 升级包管理代码片段
protected void doPost(HttpServletRequest request,\
    HttpServletResponse response) throws ServletException, IOException {
    String uploadPath = "/xx/xx";
    File uploadDir = new File(uploadPath);
    if (!uploadDir.exists()) {
        uploadDir.mkdir();
    }
    try {
    List<FileItem> formItems = upload.parseRequest(request);
    if (formItems != null && formItems.size() > 0) {
         for (FileItem item : formItems) {
             if (!item.isFormField()) {
                 String fileName = new File(item.getName()).getName();
                 Patternp=Pattern.compile("update_package-(.*?).tar.gz");
                 Matcherm=p.matcher(fileName);
                 while(m.find()){
                     version = m.group(1);
                 }
                String filePath = uploadPath + File.separator + fileName;
                sql = "INSERT INTO package(name,version,address)\
                VALUES('"+fileName+"','"+version+"','"+filePath+"');";
                dbOperate(sql);
                File storeFile = new File(filePath);
                item.write(storeFile);
                request.setAttribute("message",\
                "Package Has beed uploaded successfully!");
            }
        }
   }
   } catch (Exception ex) {
        request.setAttribute("message","error info: " + ex.getMessage());
    }
}

升级指令处理

如果用户从 web 端选择了升级设备并且点击了升级按钮,服务端程序则会记录该指令,当下一次收到终端设备的 POST 消息时,则会对指定的终端下发 update 升级指令,终端收到 update 命令后执行升级程序。升级完成之后终端会再次周期性上报其版本号,通过 web 端设备列表即可查看所有设备升级结果,做到升级流程、结果的可视化。

终端升级程序

终端升级程序由升级管理程序和升级执行程序两部分组成。本文所描述的升级策略先决条件是构建合理的磁盘、Flash 分区,以便支持本策略中各种程序的正常运行。

终端磁盘分区示例

图 2. 终端设备磁盘分区图

图 2 是一个针对本策略的基本 Flash 分区示例。Flash 的总容量为 128M,第一个分区为启动分区,用来存放启动 Linux 系统的引导程序,容量 2M;第二个分区为 Linux 内核分区,用来存放 Linux 内核镜像文件,容量 8M;第三个分区为根文件系统分区,用来存放根文件系统镜像文件且作为运行时用户操作空间,容量 100M;第四个为备份分区,用来存放想要备份的内容,以便升级完成后被拷贝到新的文件系统中,容量 16M;最后一个为固化信息分区,用来存放设备软件版本号、设备类型、设备 id 等信息,容量 2M。该分区信息仅作为参考,分区数量、大小需要根据具体项目做相应修改。当然如果项目没有特殊性,且硬件资源与该示例匹配,此分区方案亦可直接被沿用。

升级管理程序

升级管理程序功能如下:

  • 管理软件版本信息
  • POST 设备信息给服务端
  • 从服务端下载升级包
  • 校验,管理升级包
  • 启动升级执行程序

升级管理程序随着系统开机启动且作为守护进程运行。第一次运行时从宏定义中读取软件版本号并固化到 info 分区中,每隔一段时间从 info 分区中读取设备类型、设备 id、软件版本号。并将这些信息通过 HTTP POST 给服务端。服务端收到设备信息之后解析出其中的软件版本号,并和数据库中的最新升级包版本号进行对比。如果升级包版本号高于设备版本号,则返回 download 指令以及升级包地址、升级包 MD5 码给终端设备。

表 1. 终端设备信息消息格式

设备 id

设备类型

软件版本

000001

家庭网关

1.0.1

000002

摄像头

1.0.2

表 2. 服务端返回消息格式

指令

参数 1

参数 2

download

<ip>/dir/update_package.<version>.tar.gz

<MD5 code>

update

<update package version>

null

latest

null

null

表 1、表 2 分别展示了终端设备发送、服务端返回的消息格式以及内容。

升级管理程序收到服务端返回消息对其解析,根据不同指令做如下响应:

  • 指令为 download,则根据参数 1 提供的地址下载对应的升级包到终端设备本地的 tmp 目录中。下载完成之后取得升级包的 MD5 码和参数 2 中的进行对比,完成升级包校验。
  • 指令为 update,则把参数 1 中的版本号和本地 tmp 目录中的升级包版本号进行对比,如果相同才会启动升级执行程序进行升级。
  • 指令为 latest,则证明当前终端设备的软件版本和服务端中的最新升级包版本相同,已经是最新版本,不予理会。

升级执行程序

升级执行程序功能如下:

  • 解压升级包
  • 备份文件
  • 格式化内核、文件系统分区
  • 加载升级包中的文件到内核、文件系统分区
  • 重启操作系统
  • 拷贝备份文件到文件系统中

当升级执行程序被升级管理程序启动之后,首先解压升级包,并对之校验、检测。如果检测通过则开始备份用户文件,需要说明的是 backup 分区挂载在文件系统根目录 backup 文件夹上,因此备份方式是将需要备份的文件拷贝到 backup 文件夹中且记录其原始路径。下一步进行内核、文件系统分区格式化操作,此后将升级包中新版的内核镜像、文件系统镜像写到内核、根文件系统分区中,完成新老替换。然后自动重启操作系统,启动成功之后,将备份文件拷贝到对应的文件系统路径中。此时的终端设备升级完毕,运行新版系统和软件。如果升级内容仅仅为应用程序或者配置文件,则只需进行相应文件的替换即可。

设备和服务端的交互

终端设备通过 HTTP 协议与服务端进行交互。终端程序每隔 10 秒向服务端 HTTP POST 发送一次设备信息,服务端根据版本号对比结果以及 web 端升级指令状态返回三种不同指令给终端设备。终端通过解析指令做出相应响应。其中下载功能调用 libcurl 库,具有断点续传能力。10 秒的请求频率可根据具体项目应用场景做出调整,如果终端数量比较少且服务端能够承受连接压力,想要响应更加快速、及时,可考虑将 HTTP 改为 socket 长连接的通信方式。

web 端

Web 端提供用户进行升级操作的人机接口,显示、接收、跟踪整个升级过程。采用 JSP 编写。其功能如下:

  • 显示设备状态。显示设备在线、离线状态、设备类型、设备 id、软件版本号。
  • 升级包管理。显示所有升级包,对已有的升级包进行修改、删除等操作。上传新的升级包。
  • 升级操作管理。用户可通过设备列表多选、全选设备,点击升级按钮生成升级指令。

总结

本文提供了一种远程在线方式对嵌入式 Linux 设备进行批量升级的策略,升级内容包括内核、驱动、文件系统、应用程序、配置文件等。能够快速、稳定完成升级操作。描述了服务端程序、终端设备升级程序、web 端功能、设备和服务端交互方式,完整地展示了升级流程的细节,供开发者参考。

需要注意的是,该策略的实施过程中,需要确保升级设备具有足够电量以保证升级程序的顺利执行。该策略仅仅提供功能性的描述,为了确保可靠性和适应更加复杂的环境,开发者需要增加双分区启动备份机制。此外,由于升级包存放在 tmp 目录中,因此可支持的升级包大小受限于内存物理空间,开发者可将升级包存放在指定磁盘分区对该功能进行优化。