关注

Linux 固件升级方案

升级方案概述

该方案采用以下关键设计原则:

  • 双分区设计(活跃分区 / 备用分区)
  • 升级包校验与签名验证
  • 完整的回滚机制
  • 原子操作确保系统一致性
  • 详细的升级日志记录

实现方案

1. 系统分区布局

建议的分区布局:

/dev/mmcblk0p1  - 启动分区(包含U-Boot配置)
/dev/mmcblk0p2  - 系统分区A(活跃)
/dev/mmcblk0p3  - 系统分区B(备用)
/dev/mmcblk0p4  - 数据分区(持久化数据)
/dev/mmcblk0p5  - 升级包存储分区

2. 升级脚本实现

方案说明

1. 升级流程详解

该方案采用双分区策略实现安全升级:

  1. 准备阶段

    • 验证升级包的完整性和签名
    • 挂载相关分区(活跃分区、备用分区、升级分区)
  2. 升级阶段

    • 将新内核、文件系统和应用程序安装到备用分区
    • 保留关键配置文件
    • 执行应用程序的安装后脚本(如有)
  3. 切换阶段

    • 更新启动配置,设置下次从备用分区启动
    • 记录升级信息用于验证
  4. 验证阶段

    • 系统重启后,验证新系统是否正常工作
    • 验证成功则标记升级完成
    • 验证失败则自动回滚到之前的系统

2. 关键特性

  • 双分区设计:保证升级失败时可以回滚到稳定版本
  • 签名验证:防止安装未授权或被篡改的升级包
  • 原子操作:确保系统不会处于半升级状态
  • 自动回滚:系统异常时自动恢复到之前的工作状态
  • 配置保留:升级过程中保留关键系统配置

3. 使用方法

  1. 创建升级包

    create_upgrade_package.sh

    #!/bin/bash
    # 用于创建固件升级包的脚本

    set -e

    # 默认参数
    KERNEL_IMG="kernel.img"
    ROOTFS_TAR="rootfs.tar.gz"
    APPS_TAR="apps.tar.gz"
    VERSION_FILE="version.txt"
    OUTPUT_PACKAGE="firmware_upgrade.tar.gz"
    SIGN_PACKAGE=true
    PRIVATE_KEY="private_key.pem"

    # 显示帮助信息
    usage() {
        echo "用法: $0 [选项]"
        echo "选项:"
        echo "  -k <文件>  指定内核镜像文件 (默认: $KERNEL_IMG)"
        echo "  -r <文件>  指定根文件系统tar包 (默认: $ROOTFS_TAR)"
        echo "  -a <文件>  指定应用程序tar包 (默认: $APPS_TAR)"
        echo "  -v <文件>  指定版本信息文件 (默认: $VERSION_FILE)"
        echo "  -o <文件>  指定输出升级包文件名 (默认: $OUTPUT_PACKAGE)"
        echo "  -n         不签名升级包"
        echo "  -p <文件>  指定私钥文件 (默认: $PRIVATE_KEY)"
        echo "  -h         显示帮助信息"
        exit 1
    }

    # 解析命令行参数
    parse_args() {
        while getopts "k:r:a:v:o:np:h" opt; do
            case $opt in
                k)
                    KERNEL_IMG=$OPTARG
                    ;;
                r)
                    ROOTFS_TAR=$OPTARG
                    ;;
                a)
                    APPS_TAR=$OPTARG
                    ;;
                v)
                    VERSION_FILE=$OPTARG
                    ;;
                o)
                    OUTPUT_PACKAGE=$OPTARG
                    ;;
                n)
                    SIGN_PACKAGE=false
                    ;;
                p)
                    PRIVATE_KEY=$OPTARG
                    ;;
                h)
                    usage
                    ;;
                \?)
                    echo "无效的选项: -$OPTARG" >&2
                    usage
                    ;;
            esac
        done
    }

    # 检查必要文件
    check_files() {
        echo "检查必要文件..."
        
        local REQUIRED_FILES=("$KERNEL_IMG" "$ROOTFS_TAR" "$APPS_TAR" "$VERSION_FILE")
        for file in "${REQUIRED_FILES[@]}"; do
            if [ ! -f "$file" ]; then
                echo "错误: 必要文件不存在 - $file" >&2
                exit 1
            fi
        done
        
        # 如果需要签名,检查私钥
        if [ "$SIGN_PACKAGE" = true ] && [ ! -f "$PRIVATE_KEY" ]; then
            echo "错误: 私钥文件不存在 - $PRIVATE_KEY" >&2
            exit 1
        fi
        
        echo "文件检查通过"
    }

    # 创建升级包
    create_package() {
        echo "创建升级包 $OUTPUT_PACKAGE..."
        
        # 创建临时目录
        local TEMP_DIR=$(mktemp -d)
        
        # 复制文件到临时目录
        cp "$KERNEL_IMG" "$TEMP_DIR/"
        cp "$ROOTFS_TAR" "$TEMP_DIR/"
        cp "$APPS_TAR" "$TEMP_DIR/"
        cp "$VERSION_FILE" "$TEMP_DIR/"
        
        # 如果存在安装后脚本,也一并加入
        if [ -f "post_install.sh" ]; then
            cp "post_install.sh" "$TEMP_DIR/"
        fi
        
        # 打包文件
        tar -zcf "$OUTPUT_PACKAGE" -C "$TEMP_DIR" .
        
        # 清理临时目录
        rm -rf "$TEMP_DIR"
        
        echo "升级包创建完成"
    }

    # 签名升级包
    sign_package() {
        if [ "$SIGN_PACKAGE" = false ]; then
            return 0
        fi
        
        echo "签名升级包..."
        
        # 使用私钥签名
        openssl dgst -sha256 -sign "$PRIVATE_KEY" -out "${OUTPUT_PACKAGE}.sig" "$OUTPUT_PACKAGE"
        
        echo "升级包签名完成: ${OUTPUT_PACKAGE}.sig"
    }

    # 显示版本信息
    show_version() {
        echo "升级包版本信息:"
        echo "-------------------------"
        cat "$VERSION_FILE"
        echo "-------------------------"
    }

    # 主函数
    main() {
        parse_args "$@"
        check_files
        show_version
        create_package
        sign_package
        
        echo "操作完成,升级包已准备就绪:"
        echo "升级包: $OUTPUT_PACKAGE"
        if [ "$SIGN_PACKAGE" = true ]; then
            echo "签名文件: ${OUTPUT_PACKAGE}.sig"
        fi
    }

    main "$@"
     

    bash

    ./create_upgrade_package.sh -k kernel.img -r rootfs.tar.gz -a apps.tar.gz -v version.txt -o firmware_v2.1.tar.gz
    
  2. 执行升级

    firmware_upgrade.sh

    #!/bin/bash
    # 主固件升级脚本
    # 支持内核、文件系统和应用程序升级

    set -e

    # 配置参数
    ACTIVE_PART="/dev/mmcblk0p2"
    PASSIVE_PART="/dev/mmcblk0p3"
    BOOT_PART="/dev/mmcblk0p1"
    DATA_PART="/dev/mmcblk0p4"
    UPGRADE_DIR="/mnt/upgrade"
    LOG_FILE="/var/log/firmware_upgrade.log"
    MOUNT_POINT_ACTIVE="/mnt/active"
    MOUNT_POINT_PASSIVE="/mnt/passive"
    UPGRADE_PACKAGE=""
    VERIFY_SIGNATURE=true
    REBOOT_AFTER_UPGRADE=true

    # 初始化日志
    init_log() {
        echo "=== 固件升级开始: $(date) ===" >> $LOG_FILE
        echo "升级脚本版本: 1.0.0" >> $LOG_FILE
    }

    # 显示使用帮助
    usage() {
        echo "用法: $0 [选项] <升级包路径>"
        echo "选项:"
        echo "  -n    升级后不自动重启"
        echo "  -s    不验证签名"
        echo "  -h    显示帮助信息"
        exit 1
    }

    # 解析命令行参数
    parse_args() {
        while getopts "nsh" opt; do
            case $opt in
                n)
                    REBOOT_AFTER_UPGRADE=false
                    ;;
                s)
                    VERIFY_SIGNATURE=false
                    ;;
                h)
                    usage
                    ;;
                \?)
                    echo "无效的选项: -$OPTARG" >&2
                    usage
                    ;;
            esac
        done
        
        shift $((OPTIND-1))
        if [ $# -ne 1 ]; then
            echo "请指定升级包路径" >&2
            usage
        fi
        UPGRADE_PACKAGE=$1
        
        if [ ! -f "$UPGRADE_PACKAGE" ]; then
            echo "升级包不存在: $UPGRADE_PACKAGE" >&2
            exit 1
        fi
    }

    # 验证升级包签名
    verify_package() {
        echo "验证升级包签名..." >> $LOG_FILE
        
        if [ "$VERIFY_SIGNATURE" = false ]; then
            echo "跳过签名验证" >> $LOG_FILE
            return 0
        fi
        
        # 假设升级包包含签名文件
        if [ ! -f "${UPGRADE_PACKAGE}.sig" ]; then
            echo "签名文件不存在: ${UPGRADE_PACKAGE}.sig" >> $LOG_FILE
            return 1
        fi
        
        # 使用openssl验证签名
        if ! openssl dgst -sha256 -verify /etc/upgrade/public_key.pem \
           -signature "${UPGRADE_PACKAGE}.sig" "$UPGRADE_PACKAGE"; then
            echo "签名验证失败" >> $LOG_FILE
            return 1
        fi
        
        echo "签名验证成功" >> $LOG_FILE
        return 0
    }

    # 挂载分区
    mount_partitions() {
        echo "挂载分区..." >> $LOG_FILE
        
        mkdir -p $MOUNT_POINT_ACTIVE $MOUNT_POINT_PASSIVE $UPGRADE_DIR
        
        mount $ACTIVE_PART $MOUNT_POINT_ACTIVE || {
            echo "挂载活跃分区失败" >> $LOG_FILE
            return 1
        }
        
        mount $PASSIVE_PART $MOUNT_POINT_PASSIVE || {
            echo "挂载备用分区失败" >> $LOG_FILE
            umount $MOUNT_POINT_ACTIVE
            return 1
        }
        
        mount /dev/mmcblk0p5 $UPGRADE_DIR || {
            echo "挂载升级分区失败" >> $LOG_FILE
            umount $MOUNT_POINT_ACTIVE
            umount $MOUNT_POINT_PASSIVE
            return 1
        }
        
        return 0
    }

    # 卸载分区
    umount_partitions() {
        echo "卸载分区..." >> $LOG_FILE
        
        umount $UPGRADE_DIR || echo "卸载升级分区失败" >> $LOG_FILE
        umount $MOUNT_POINT_PASSIVE || echo "卸载备用分区失败" >> $LOG_FILE
        umount $MOUNT_POINT_ACTIVE || echo "卸载活跃分区失败" >> $LOG_FILE
        
        rmdir $MOUNT_POINT_ACTIVE $MOUNT_POINT_PASSIVE $UPGRADE_DIR 2>/dev/null
    }

    # 检查升级包内容
    check_package() {
        echo "检查升级包内容..." >> $LOG_FILE
        
        # 解压升级包到临时目录
        TEMP_DIR=$(mktemp -d)
        if ! tar -zxf "$UPGRADE_PACKAGE" -C $TEMP_DIR; then
            echo "解压升级包失败" >> $LOG_FILE
            rm -rf $TEMP_DIR
            return 1
        fi
        
        # 检查必要文件
        REQUIRED_FILES=("kernel.img" "rootfs.tar.gz" "apps.tar.gz" "version.txt")
        for file in "${REQUIRED_FILES[@]}"; do
            if [ ! -f "$TEMP_DIR/$file" ]; then
                echo "升级包缺少必要文件: $file" >> $LOG_FILE
                rm -rf $TEMP_DIR
                return 1
            fi
        done
        
        # 记录版本信息
        echo "升级包版本: $(cat $TEMP_DIR/version.txt)" >> $LOG_FILE
        echo "当前系统版本: $(cat $MOUNT_POINT_ACTIVE/etc/version 2>/dev/null || echo '未知')" >> $LOG_FILE
        
        # 保存临时目录路径供后续使用
        export TEMP_DIR
        return 0
    }

    # 升级内核
    upgrade_kernel() {
        echo "升级内核..." >> $LOG_FILE
        
        # 备份当前内核
        cp $MOUNT_POINT_ACTIVE/boot/kernel.img $MOUNT_POINT_PASSIVE/boot/kernel.img.bak || {
            echo "备份当前内核失败" >> $LOG_FILE
            return 1
        }
        
        # 安装新内核到备用分区
        cp $TEMP_DIR/kernel.img $MOUNT_POINT_PASSIVE/boot/kernel.img || {
            echo "安装新内核失败" >> $LOG_FILE
            return 1
        }
        
        # 更新设备树(如果有)
        if [ -f "$TEMP_DIR/*.dtb" ]; then
            cp $TEMP_DIR/*.dtb $MOUNT_POINT_PASSIVE/boot/ || {
                echo "安装设备树失败" >> $LOG_FILE
                return 1
            }
        fi
        
        echo "内核升级完成" >> $LOG_FILE
        return 0
    }

    # 升级文件系统
    upgrade_rootfs() {
        echo "升级文件系统..." >> $LOG_FILE
        
        # 解压新的文件系统到备用分区
        if ! tar -zxf $TEMP_DIR/rootfs.tar.gz -C $MOUNT_POINT_PASSIVE; then
            echo "解压文件系统失败" >> $LOG_FILE
            return 1
        }
        
        # 保留必要的配置文件
        if [ -d $MOUNT_POINT_ACTIVE/etc/network ]; then
            cp -r $MOUNT_POINT_ACTIVE/etc/network $MOUNT_POINT_PASSIVE/etc/ || {
                echo "保留网络配置失败" >> $LOG_FILE
                # 非致命错误,继续执行
            }
        fi
        
        # 复制版本信息
        cp $TEMP_DIR/version.txt $MOUNT_POINT_PASSIVE/etc/version || {
            echo "写入版本信息失败" >> $LOG_FILE
            return 1
        }
        
        echo "文件系统升级完成" >> $LOG_FILE
        return 0
    }

    # 升级应用程序
    upgrade_applications() {
        echo "升级应用程序..." >> $LOG_FILE
        
        # 解压应用程序包
        if ! tar -zxf $TEMP_DIR/apps.tar.gz -C $MOUNT_POINT_PASSIVE/usr/local; then
            echo "解压应用程序失败" >> $LOG_FILE
            return 1
        }
        
        # 运行应用程序升级后脚本(如果存在)
        if [ -f "$TEMP_DIR/post_install.sh" ]; then
            cp $TEMP_DIR/post_install.sh $MOUNT_POINT_PASSIVE/tmp/
            chmod +x $MOUNT_POINT_PASSIVE/tmp/post_install.sh
            chroot $MOUNT_POINT_PASSIVE /tmp/post_install.sh >> $LOG_FILE 2>&1 || {
                echo "应用程序安装后脚本执行失败" >> $LOG_FILE
                # 非致命错误,继续执行
            }
            rm -f $MOUNT_POINT_PASSIVE/tmp/post_install.sh
        fi
        
        echo "应用程序升级完成" >> $LOG_FILE
        return 0
    }

    # 更新启动配置,切换到新分区
    update_boot_config() {
        echo "更新启动配置..." >> $LOG_FILE
        
        # 备份当前启动配置
        cp $BOOT_PART/uEnv.txt $BOOT_PART/uEnv.txt.bak || {
            echo "备份启动配置失败" >> $LOG_FILE
            return 1
        }
        
        # 修改U-Boot环境变量,下次从备用分区启动
        # 假设使用uEnv.txt配置启动分区
        sed -i "s/^bootpart=.*/bootpart=3/" $BOOT_PART/uEnv.txt || {
            echo "修改启动配置失败" >> $LOG_FILE
            # 恢复备份
            mv $BOOT_PART/uEnv.txt.bak $BOOT_PART/uEnv.txt
            return 1
        }
        
        # 记录分区切换信息,供首次启动验证
        echo "UPGRADED=1" > $BOOT_PART/upgrade_flag.txt
        echo "PREVIOUS_PART=2" >> $BOOT_PART/upgrade_flag.txt
        echo "NEW_PART=3" >> $BOOT_PART/upgrade_flag.txt
        
        echo "启动配置更新完成,下次将从分区3启动" >> $LOG_FILE
        return 0
    }

    # 清理临时文件
    cleanup() {
        echo "清理临时文件..." >> $LOG_FILE
        if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then
            rm -rf $TEMP_DIR
        fi
    }

    # 主升级流程
    main() {
        init_log
        
        # 解析参数
        parse_args "$@"
        
        # 验证升级包
        if ! verify_package; then
            echo "升级包验证失败,终止升级" >> $LOG_FILE
            exit 1
        fi
        
        # 挂载必要的分区
        if ! mount_partitions; then
            echo "分区挂载失败,终止升级" >> $LOG_FILE
            exit 1
        fi
        
        # 检查升级包内容
        if ! check_package; then
            echo "升级包内容检查失败,终止升级" >> $LOG_FILE
            umount_partitions
            cleanup
            exit 1
        fi
        
        # 执行升级步骤
        if ! upgrade_kernel; then
            echo "内核升级失败,终止升级" >> $LOG_FILE
            umount_partitions
            cleanup
            exit 1
        fi
        
        if ! upgrade_rootfs; then
            echo "文件系统升级失败,终止升级" >> $LOG_FILE
            umount_partitions
            cleanup
            exit 1
        fi
        
        if ! upgrade_applications; then
            echo "应用程序升级失败,终止升级" >> $LOG_FILE
            umount_partitions
            cleanup
            exit 1
        fi
        
        # 更新启动配置
        if ! update_boot_config; then
            echo "启动配置更新失败,终止升级" >> $LOG_FILE
            umount_partitions
            cleanup
            exit 1
        fi
        
        # 清理工作
        umount_partitions
        cleanup
        
        echo "所有升级步骤完成" >> $LOG_FILE
        echo "=== 固件升级成功: $(date) ===" >> $LOG_FILE
        
        # 重启系统
        if [ "$REBOOT_AFTER_UPGRADE" = true ]; then
            echo "3秒后将重启系统..."
            sleep 3
            reboot
        else
            echo "升级完成,请手动重启系统以应用更新"
        fi
    }

    # 执行主函数
    main "$@"
     

    bash

    ./firmware_upgrade.sh firmware_v2.1.tar.gz
    
  3. 系统会自动重启并验证升级结果

verify_upgrade.sh

#!/bin/bash
# 系统启动后验证升级是否成功,失败则回滚

LOG_FILE="/var/log/firmware_verify.log"
BOOT_PART="/dev/mmcblk0p1"
MOUNT_POINT_BOOT="/mnt/boot"
ACTIVE_PART="/dev/mmcblk0p2"
PASSIVE_PART="/dev/mmcblk0p3"

# 初始化日志
init_log() {
    echo "=== 升级验证开始: $(date) ===" >> $LOG_FILE
}

# 挂载启动分区
mount_boot() {
    mkdir -p $MOUNT_POINT_BOOT
    if ! mount $BOOT_PART $MOUNT_POINT_BOOT; then
        echo "挂载启动分区失败" >> $LOG_FILE
        return 1
    fi
    return 0
}

# 卸载启动分区
umount_boot() {
    umount $MOUNT_POINT_BOOT || echo "卸载启动分区失败" >> $LOG_FILE
    rmdir $MOUNT_POINT_BOOT 2>/dev/null
}

# 验证系统状态
verify_system() {
    echo "验证系统状态..." >> $LOG_FILE
    
    # 检查关键服务是否运行
    CRITICAL_SERVICES=("sshd" "ntpd" "main_app")
    for service in "${CRITICAL_SERVICES[@]}"; do
        if ! systemctl is-active --quiet $service; then
            echo "关键服务未运行: $service" >> $LOG_FILE
            return 1
        fi
    done
    
    # 检查网络连接
    if ! ping -c 1 8.8.8.8 >/dev/null 2>&1; then
        echo "网络连接失败" >> $LOG_FILE
        return 1
    fi
    
    # 检查应用程序功能
    if ! /usr/local/bin/main_app --check >/dev/null 2>&1; then
        echo "应用程序功能检查失败" >> $LOG_FILE
        return 1
    fi
    
    echo "系统状态验证成功" >> $LOG_FILE
    return 0
}

# 执行回滚操作
rollback() {
    echo "执行回滚操作..." >> $LOG_FILE
    
    # 读取升级信息
    if [ ! -f "$MOUNT_POINT_BOOT/upgrade_flag.txt" ]; then
        echo "未找到升级标记文件,无法回滚" >> $LOG_FILE
        return 1
    fi
    
    source $MOUNT_POINT_BOOT/upgrade_flag.txt
    
    if [ -z "$PREVIOUS_PART" ] || [ -z "$NEW_PART" ]; then
        echo "升级标记文件格式错误" >> $LOG_FILE
        return 1
    fi
    
    # 恢复启动配置
    if [ -f "$MOUNT_POINT_BOOT/uEnv.txt.bak" ]; then
        mv $MOUNT_POINT_BOOT/uEnv.txt.bak $MOUNT_POINT_BOOT/uEnv.txt || {
            echo "恢复启动配置失败" >> $LOG_FILE
            return 1
        }
    else
        # 直接修改启动分区配置
        sed -i "s/^bootpart=.*/bootpart=$PREVIOUS_PART/" $MOUNT_POINT_BOOT/uEnv.txt || {
            echo "修改启动配置失败" >> $LOG_FILE
            return 1
        }
    fi
    
    # 删除升级标记
    rm -f $MOUNT_POINT_BOOT/upgrade_flag.txt
    
    echo "回滚操作完成,将重启到原系统" >> $LOG_FILE
    return 0
}

# 标记升级成功
mark_success() {
    echo "标记升级成功..." >> $LOG_FILE
    
    # 删除升级标记
    rm -f $MOUNT_POINT_BOOT/upgrade_flag.txt
    
    # 记录成功信息
    echo "SYSTEM_UPGRADED_SUCCESSFULLY=1" > $MOUNT_POINT_BOOT/upgrade_success.txt
    echo "UPGRADE_DATE=$(date)" >> $MOUNT_POINT_BOOT/upgrade_success.txt
    
    echo "升级成功标记已设置" >> $LOG_FILE
    return 0
}

# 主函数
main() {
    init_log
    
    # 检查是否是升级后的首次启动
    if ! mount_boot; then
        echo "无法挂载启动分区,无法验证升级状态" >> $LOG_FILE
        exit 1
    fi
    
    if [ -f "$MOUNT_POINT_BOOT/upgrade_flag.txt" ]; then
        echo "检测到刚完成升级,开始验证系统状态" >> $LOG_FILE
        
        # 验证系统状态
        if verify_system; then
            # 验证成功,标记升级完成
            mark_success
        else
            # 验证失败,执行回滚
            if rollback; then
                # 回滚配置已完成,重启系统
                umount_boot
                echo "系统验证失败,已触发回滚,将重启系统" >> $LOG_FILE
                reboot
            else
                echo "回滚操作失败,系统可能处于不稳定状态" >> $LOG_FILE
            fi
        fi
    else
        echo "未检测到新升级,无需验证" >> $LOG_FILE
    fi
    
    umount_boot
    echo "=== 升级验证完成: $(date) ===" >> $LOG_FILE
    exit 0
}

main "$@"
 

4. 注意事项

  • 确保系统有足够的存储空间
  • 升级过程中不要断电
  • 首次使用前需生成密钥对并配置公钥
  • 测试环境验证通过后再用于生产环境
  • 定期备份重要数据

该方案适用于嵌入式 Linux 设备,可根据具体硬件和软件环境进行调整。

转载自CSDN-专业IT技术社区

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/shanstellar/article/details/151799857

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--