内核热升级是指,预先准备好需要升级的内核镜像文件,在秒级时间内,完成内核切换,追求用户服务进程无感知。

欧拉操作系统提供了一套比较成熟的解决方案,该解决方案提供了用户态程序内核态程序两部分:

kexec -e 执行代码追踪

用户态通过reboot系统调用,传入LINUX_REBOOT_CMD_KEXEC参数,触发热升级流程,其核心还在于内核态的处理。

// 用户态  file: kexec.c
my_exec()
|-> reboot(LINUX_REBOOT_CMD_KEXEC);
// 内核态 file: kernel/reboot.c
SYSCALL_DEFINE(reboot, ...)
case LINUX_REBOOT_CMD_KEXEC:
|-> kernel_kexec(); // file: kernel/kexec_core.c

ELF 文件的内部结构

在分析kexec -l前,有必要来研究一下 ELF 形式的文件内部结构。

ELF 文件主要分为两大部分,ELF 头和程序节段。其中程序节段分别被节头表程序头表所指向。

详见此博客:【内核】ELF 文件执行流程

实际的 ELF 文件,除 ELF 头外,其他部分常有不同。其他部分(主要是两个头表)的声明和定义,是在 ELF 头中确定的。ELF 头的代码结构如下:

#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT]; // 16 字节 ELF 文件声明,由固定信息组成,用来表示是 ELF 文件
Elf32_Half e_type; // 标识 elf 文件类型: 0. 未知, 1. 可重定位文件, 2. 可执行文件, 3. 共享目标文件, 4. core 文件
Elf32_Half e_machine; // 程序运行的硬件体系结构,80386 体系为 3
Elf32_Word e_version; // 文件版本号
Elf32_Addr e_entry; // 程序入口地址
Elf32_Off e_phoff; // Program header table 在文件中的偏移量(字节数
Elf32_Off e_shoff; // Section header table 在文件中的偏移量(字节数
Elf32_Word e_flags; // 文件标识符,IA32 汇编为 0
Elf32_Half e_ehsize; // ELF header 的字节数
Elf32_Half e_phentsize; // Program header table 中每个条目的字节数
Elf32_Half e_phnum; // Program header table 中条目数
Elf32_Half e_shentsize; // Section header table 中每个条目的字节数
Elf32_Half e_shnum; // Section header table 中条目数
Elf32_Half e_shstrndx; // 包含节名称的字符串表是第几个节
} Elf32_Ehdr;

由上可知,从 ELF 头可以定位到 程序头表节头表 的位置中。

节头表 中的每个条目 Section Header 都描述了 ELF 文件中 Sections 区域中一个节的信息,结构如下:

typedef struct {
Elf32_Word sh_name; // 节区名,是节区头部字符串表节区(Section Header String Table Section)的索引,名字是一个 NULL 结尾的字符串
Elf32_Word sh_type; // 该节类型
Elf32_Word sh_flags; // 节区标志
Elf32_Addr sh_addr; // 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置,否则,此字段为 0
Elf32_Off sh_offset; // 该节区首个字节的偏移
Elf32_Word sh_size; // 该节长度
Elf32_Word sh_link; // 该节头部表索引,具体内容依赖于节类型
Elf32_Word sh_info; // 节头部表附加信息,具体内容依赖于节类型
Elf32_Word sh_addralign; // 地址对齐约束
Elf32_Word sh_entsize; // 该节固定表项长度
} Elf32_Shdr;

下面这个图清晰的表现了节头表如何映射到各个节地址:

程序头表的结构和寻址也如出一辙。程序头表中的每一个 Program Header 是与程序执行直接相关的,他描述了一个即将被载入内存的段在文件中的位置、大小以及它被载入内存后所在的位置和大小。结构如下所示:

typedef struct {
Elf32_Word p_type; // 当前 Program header 所描述的段的类型
Elf32_Off p_offset; // 该段首地址在文件中的偏移量(字节数)
Elf32_Addr p_vaddr; // 该段被载入内存后,首个字节的虚拟地址
Elf32_Addr p_paddr; // 该段被载入内存后,首个字节的物理地址(对于使用虚拟地址的系统来说,该项为 0)
Elf32_Word p_filesz; // 段长度(字节数)
Elf32_Word p_memsz; // 段在内存中的长度
Elf32_Word p_flags; // 段标志位
Elf32_Word p_align; // 段在文件内和内存中的对齐方式
} Elf32_Phdr;

程序头表描述了可执行文件中有哪几个段,每个段需要被载入到内存的哪个位置。于是,通过 ELF header 中的字段,找到 Program Header Table,然后读取每个 Program Header,将对应的段载入到内存指定的位置,然后跳转,即可实现 ELF 可执行文件的执行了。

kexec 原理分析:kexec 加载

kexec -l 命令会触发内核加载动作,最终使要快速切换的新内核,加载到内存中。

kexec 处理内核加载机制分为两个阶段:解析内核文件(用户态)和加载内核段数据(内核态),下文分别描述这两个过程。

STEP-1:解析内核文件
sequenceDiagram
participant kexec.c
participant kexec_arm64.c
participant kexec_syscall.h

Note left of kexec.c : kexec -l
kexec.c ->> kexec.c : main():解析参数
kexec.c ->> kexec.c : my_load():核心函数,以下过程皆在此函数中
kexec.c ->> kexec.c : slurp_decompress_file():解压内核文件
kexec.c ->> kexec_arm64.c : file_type[i].probe():调用对应内核镜像的 probe() 函数,执行校验
kexec.c ->> kexec_arm64.c : file_type[i].load():调用对应内核镜像的 load() 函数

kexec_arm64.c->> kexec_arm64.c : elf_arm64_load():此函数为 elf 格式的 load 函数
kexec_arm64.c->> kexec_arm64.c : build_elf_exec_info():解析 elf 文件头
kexec_arm64.c->> kexec_arm64.c : arm64_process_image_header():解析 elf 文件 program 头表
kexec_arm64.c->> kexec_arm64.c : elf_exec_load():加载解析 elf 文件的 program segment
kexec_arm64.c->> kexec_arm64.c : arm64_load_other_segments():加载其他段,例如传递给新内核的参数、purgatory 炼狱空间

kexec_arm64.c->> kexec.c : result
kexec.c ->> kexec.c : 系统调用前的若干校验
kexec.c ->> kexec_syscall.h : 系统调用 kexec_load()

执行kexec_load()传入的参数为:

  • info.entry:修改于arm64_load_other_segments(),指向purgatory的起始地址。
  • info.nr_segments:program 段的数量
  • info.segment:指向 program 段的起始地址
  • info.kexec_flags:标志位图
STEP-2:加载内核段数据
sequenceDiagram
participant my_load()
participant kexec
participant kexec_core
autonumber

my_load() ->> kexec : kexec_load():系统调用
kexec ->> kexec : do_kexec_load():主要处理函数,以下过程皆在此函数中
kexec ->> kexec : kimage_alloc_init():初始化函数,提取用户传入的镜像数据
Note right of kexec : control_code_page 在这里被分配
kexec ->> kexec : machine_kexec_prepare()
Loop foreach nr_segments:
kexec ->> kexec_core: kimage_load_segment():将段数据分配到内存页中
alt type == DEFAULT
kexec_core ->> kexec_core : kimage_load_normal_segment()
else type == CRASH<br/>type == QUICK
kexec_core ->> kexec_core : kimage_load_special_segment()
end
end

kexec ->> kexec : kimage_terminate(image)
kexec ->> kexec : 将 image 写入 dest_image
kexec ->> my_load() : result=0

kexec_load()执行之后,image 的一个状态:

  • image->start:修改于kimage_alloc_init(),指向purgatory的起始地址。
  • image->nr_segments:修改于kimage_alloc_init(),即 program 段的数量。
  • image->segment:修改于kimage_alloc_init(),即 program 段起始地址。
  • image->control_code_page:刚刚初始化
  • image->entry:修改于kimage_load_normal_segment(),存一个地址,指向 segment 的实际地址,entry页实际上是程序头表
  • image->last_entry:修改于kimage_load_normal_segment()永远指向新 entry 页的末尾。

kexec 原理分析:kexec 执行

kexec -e 命令会触发 kexec 的执行,切换到新的内核地址上去。下面是该命令的逻辑:

sequenceDiagram
participant kexec.c
participant reboot.c
participant kexec_core
participant machine_kexec
participant cpu_reset.S
participant relocate_kernel.S
Note left of kexec.c : kexec -e

kexec.c ->> kexec.c : my_exec()
kexec.c ->> reboot.c : reboot(LINUX_REBOOT_CMD_KEXEC)
reboot.c ->> kexec_core : kernel_kexec()
activate kexec_core
kexec_core ->> kexec_core : kernel_restart_prepare()<br/>内核重启准备工作
kexec_core ->> kexec_core : migrate_to_reboot_cpu()<br/>将任务迁移到重启的特定 CPU 上
kexec_core ->> kexec_core : cpu_hotplug_enable()<br/>重新启用 CPU 热插拔功能
kexec_core ->> kexec_core : machine_shutdown() <br/>关闭机器,触发硬件重启
Note right of kexec_core : cpu_park 是在此处陷入
kexec_core ->> machine_kexec : machine_kexec(kexec_image)<br/>进行kexec模式重启
deactivate kexec_core

machine_kexec ->> machine_kexec : 将 arm64_relocate_new_kernel 代码<br/>拷贝到 reboot_code_buffer,即<br/>control_page 起始处
Note right of machine_kexec : 这里更新 control_code_page
machine_kexec ->> machine_kexec : 准备工作:flush
machine_kexec ->> cpu_reset.S : cpu_soft_restart()<br/>传入control_code_page 地址
Note right of cpu_reset.S : 跳转到 control_code_page
cpu_reset.S ->> relocate_kernel.S : arm64_relocate_new_kernel
Note right of relocate_kernel.S : 准备进入 purgatory 空间
relocate_kernel.S ->> relocate_kernel.S : 跳转到 kimage->start 开始执行

kexec 在内核加载阶段,于内存中创建了一张 控制表 control_code_page,用于存放重定向新内核地址的控制代码。这段控制代码名为arm64_relocate_new_kernel,位于/arch/arm64/kernel/relocate_kernel.S汇编文件中。

sys_reboot 系统调用简要分析

为了研究 kexec -e 内核切换时调用的 reboot 流程与正常系统 reboot 的区别,需要对 sys_reboot 系统调用有一个代码上的认识。

sys_reboot 系统调用实现于kernel/reboot.c文件中,函数签名如下:

SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd, void __user *, arg) {
...
}

第三个参数 cmd 为调用此 sys_call 时传入的参数,表示重启方式,内核中定义了以下若干种重启方式:

 /*
2: * Commands accepted by the _reboot() system call.
3: *
4: * RESTART Restart system using default command and mode.
5: * HALT Stop OS and give system control to ROM monitor, if any.
6: * CAD_ON Ctrl-Alt-Del sequence causes RESTART command.
7: * CAD_OFF Ctrl-Alt-Del sequence sends SIGINT to init task.
8: * POWER_OFF Stop OS and remove all power from system, if possible.
9: * RESTART2 Restart system using given command string.
10: * SW_SUSPEND Suspend system using software suspend if compiled in.
11: * KEXEC Restart system using a previously loaded Linux kernel
12: */
13:
14: #define LINUX_REBOOT_CMD_RESTART 0x01234567
15: #define LINUX_REBOOT_CMD_HALT 0xCDEF0123
16: #define LINUX_REBOOT_CMD_CAD_ON 0x89ABCDEF
17: #define LINUX_REBOOT_CMD_CAD_OFF 0x00000000
18: #define LINUX_REBOOT_CMD_POWER_OFF 0x4321FEDC
19: #define LINUX_REBOOT_CMD_RESTART2 0xA1B2C3D4
20: #define LINUX_REBOOT_CMD_SW_SUSPEND 0xD000FCE2
21: #define LINUX_REBOOT_CMD_KEXEC 0x4558454

解释如下:

方式 魔数 说明
RESTART 0x01234567 正常的重启,也是我们平时使用的重启。执行该动作后,系统会重新启动。
HALT 0xCDEF0123 停止操作系统,然后把控制权交给其它代码(如果有的话)。具体的表现形式,依赖于系统的具体实现。
CAD_ON 0x89ABCDEF 开启:通过Ctrl-Alt-Del组合按键触发重启(RESTART)动作
CAD_OFF 0x00000000 禁止:通过Ctrl-Alt-Del组合按键触发重启(RESTART)动作
POWER_OFF 0x4321FEDC 正常的关机。执行该动作后,系统会停止操作系统,并去除所有的供电。
RESTART2 0xA1B2C3D4 重启的另一种方式。可以在重启时,携带一个字符串类型的cmd,该cmd会在重启前,发送给任意一个关心重启事件的进程,同时会传递给最终执行重启动作的machine相关的代码。内核并没有规定该cmd的形式,完全由具体的machine自行定义。
SW_SUSPEND 0xD000FCE2 Hibernate操作
KEXEC 0x4558454 Kexec操作,重启并执行已经加载好的其它Kernel Image

具体的调用关系如图:

kernel_restart、kernel_halt 和 kernel_power_off 分别代表内核重启、内核停机和内核下电,这三个函数的实现过程大致相同,分别是:

  • kernel_xxxx_prepare():执行前的准备工作

    • blocking_notifier_call_chain():向关心reboot事件的进程,发送SYS_RESTART、SYS_HALT或者SYS_POWER_OFF事件。对RESTART来说,还要将cmd参数一并发送出去。
    • 将系统状态设置为相应的状态(SYS_RESTART、SYS_HALT或SYS_POWER_OFF)。
    • usermodehelper_disable():禁止User mode helper。
    • device_shutdown():关闭所有的设备。
  • migrate_to_reboot_cpu():将当前的进程 迁移到 reboot cpu 上
    • 该函数执行后,只有 reboot CPU 在运行了
  • syscore_shutdown():将系统核心回调函数列表一一唤起
  • pr_emerg():打印对应日志
  • kmsg_dump():同上,留下临别遗言
  • machine_restart()/machine_halt()/machine_power_off():执行重启/停机/下电(此过程基于不同的硬件架构,默认以 ARM 架构为例)
    • 禁用中断
    • 停CPU
    • 各自处理逻辑

machine_restart()/machine_halt()/machine_power_off()代码很简单,罗列此处,对比观摩。

// Restart 函数要求:在 主CPU 重置系统时,从CPU 需要停下当前的任何工作;并且还需要提供一种机制,可以将所有的 从CPU 同时拉起
// 这样就保证了 CPU 任务的一致性,避免出现新环境已经起来了还有 CPU 运行古早任务的情况
void machine_restart(char *cmd)
{
local_irq_disable(); // 禁用中断
smp_send_stop(); // 停 从CPU if (efi_enabled(EFI_RUNTIME_SERVICES))
efi_reboot(reboot_mode, NULL); // 不同架构下的 restart 过程,执行到这里一般就结束了,不会继续往下走了
if (arm_pm_restart)
arm_pm_restart(reboot_mode, cmd);
else
do_kernel_restart(cmd); // 若执行到这里,说明出了大问题,Reboot 失败
printk("Reboot failed -- System halted\n");
while (1);
}
// Halt 停机只要求 从CPU 停机即可
// 停机的过程十分简单粗暴:先禁用中断,再停下当前任务,再 while(1),如此三板斧,大罗神仙也难救回来
void machine_halt(void)
{
local_irq_disable();
smp_send_stop();
while (1);
}
// 下电函数仅在 Halt 函数的基础上加了一条逻辑:下电时把停机的 CPU 带走
void machine_power_off(void)
{
local_irq_disable();
smp_send_stop();
if (pm_power_off)
pm_power_off();
}

对比上面调用关系的图,kernel_kexec()与前三者不同的是,kernel_kexec()在调用machine_shundown()之前,并没有关闭系统核心(syscore)。这是因为,在后续切换新内核的过程中,需要就内核的系统核心保持运行,以提供必要的支持和服务(内存管理)。

machine_shundown()与上面三个 machine 函数区别较大,其在内核 kexec 最初的实现中,有这样一段耐人寻味的描述:

/*
* Called by kexec, immediately prior to machine_kexec().
*
* This must completely disable all secondary CPUs; simply causing those CPUs
* to execute e.g. a RAM-based pin loop is not sufficient. This allows the
* kexec'd kernel to use any and all RAM as it sees fit, without having to
* avoid any code or data used by any SW CPU pin loop. The CPU hotplug
* functionality embodied in smpt_shutdown_nonboot_cpus() to achieve this.
*/
void machine_shutdown(void)
{
smp_shutdown_nonboot_cpus(reboot_cpu);
}

【内核】kernel 热升级-1:kexec 机制的更多相关文章

  1. kernel 3.10内核源码分析--hung task机制

    kernel 3.10内核源码分析--hung task机制 一.相关知识: 长期以来,处于D状态(TASK_UNINTERRUPTIBLE状态)的进程 都是让人比较烦恼的问题,处于D状态的进程不能接 ...

  2. 升级CentOS内核 - 2.6升级到3.10/最新内核

    ##记得切换到root用户执行升级操作. [root@localhost ~]# uname -a ##旧版 Linux localhost.localdomain 2.6.32-279.el6.i6 ...

  3. PHP服务器脚本 PHP内核探索:新垃圾回收机制说明

    在5.2及更早版本的PHP中,没有专门的垃圾回收器GC(Garbage Collection),引擎在判断一个变量空间是否能够被释放的时候是依据这个变量的zval的refcount的值,如果refco ...

  4. .NET插件技术-应用程序热升级

    今天说一说.NET 中的插件技术,即 应用程序热升级.在很多情况下.我们希望用户对应用程序的升级是无感知的,并且尽可能不打断用户操作的. 虽然在Web 或者 WebAPI上,由于多点的存在可以逐个停用 ...

  5. PHP内核之旅-6.垃圾回收机制

    回收PHP 内核之旅系列 PHP内核之旅-1.生命周期 PHP内核之旅-2.SAPI中的Cli PHP内核之旅-3.变量 PHP内核之旅-4.字符串 PHP内核之旅-5.强大的数组 PHP内核之旅-6 ...

  6. rpm包安装的nginx热升级

    文章目录一.本地环境基本介绍二.yum升级命令说明三.升级好nginx后如何不中断业务切换3.1.nginx相关的信号说明3.2.在线热升级nginx可执行文件程序一.本地环境基本介绍本次测试环境,是 ...

  7. Nginx热升级流程,看这篇就够了

    在之前做过 Nginx 热升级的演示,他能保证nginx在不停止服务的情况下更换他的 binary 文件,这个功能非常有用,但我们在执行 Nginx 的 binary 文件升级过程中,还是会遇到很多问 ...

  8. 老版本nginx存在安全漏洞,不停服务热升级

    1.场景描述 安全部通知:nginx存在"整数溢出漏洞",经测试2017年4月21日之后的版本无问题,将openresty升级到最新版本,Nginx升级到1.13.2之后的版本. ...

  9. nginx 安装第三方模块(lua)并热升级

    需求: nginx上将特定请求拒绝,并返回特定值. 解决办法: 使用lua脚本,实现效果. 操作步骤: 安装Luajit环境 重新编译nginx(目标机器上nginx -V 配置一致,并新增两个模块n ...

  10. Beego开启热升级

    1.打开配置 beego.BConfig.Listen.Graceful = true 2.写入pid 程序入口main()函数里写入pid func writePid() { fileName := ...

随机推荐

  1. SQL多表查询指南

    SQL多表查询指南 在实际的数据库应用中,通常需要查询涉及多个表的数据.SQL提供了多种方法来执行这种多表查询操作. 内连接(INNER JOIN) 内连接是将多个表中满足连接条件的行组合在一起的操作 ...

  2. C#开发的基础工具类集合 - 开源研究系列文章

    今天发布一个基础工具类代码集合. 以前有发布过一个类似的类库(见博文: Magical平台类库代码分享 ),不过那个版本有点久了,也没有这次这个全面,这次发布的是一个很多地方用到的基础类库代码. 1. ...

  3. Spring Bean 的作用域(Bean Scope)

    前言 大家好,我是 god23bin,今天我们来聊一聊 Spring 框架中的 Bean 作用域(Scope). 什么是 Bean 的作用域? 我们在以 XML 作为配置元数据的情况下,进行 Bean ...

  4. 文心一言 VS 讯飞星火 VS chatgpt (92)-- 算法导论8.3 5题

    五.用go语言,在本节给出的第一个卡片排序算法中,为排序 d 位十进制数,在最坏情况下需要多少轮排序?在最坏情况下,操作员需要记录多少堆卡片? 文心一言: 你提到的第一个卡片排序算法可能是指的基数排序 ...

  5. Arrays.asList():使用指南

    Arrays.asList() 是一个 Java 的静态方法,它可以把一个数组或者多个参数转换成一个 List 集合.这个方法可以作为数组和集合之间的桥梁,方便我们使用集合的一些方法和特性.本文将介绍 ...

  6. TiDB的简单介绍以及进行资源限制的方式与方法

    TiDB的简单介绍以及进行资源限制的方式与方法 TiDB的简介 TiDB是一个分布式数据库, 简介为: TiDB 是一个开源的分布式关系型数据库,它兼具了分布式数据库的水平扩展性和传统关系型数据库的 ...

  7. 算法打卡|Day1 数组part01

    Day1 数组part01 今日任务:数组理论基础,704. 二分查找,27. 移除元素 目录 Day1 数组part01 今日任务:数组理论基础,704. 二分查找,27. 移除元素 Part1: ...

  8. Java虚拟机(JVM):第四幕:自动内存管理 - 经典垃圾收集器

    前言:如果说收集算法是内存回收的方法论,那么垃圾收集器则是内存回收的实践者.整哥Java堆 :Full GC. 1.Serial收集器:最基础.历史最悠久的收集器,这是一个单线程工作的收集器. 2.P ...

  9. UVA10702 Travelling Salesman 题解

     UVA10702 Travelling Salesman 题解 题面: 有个旅行的商人,他每到一个的新城市,便卖掉所有东西再购买新东西,从而获得利润.从某城市 A 到某城市 B 有固定利润(B 到 ...

  10. 从基础到实践,回顾Elasticsearch 向量检索发展史

    本文分享自华为云社区<Elasticsearch向量检索的演进与变革:从基础到应用>,作者: 汀丶. 1.引言 向量检索已经成为现代搜索和推荐系统的核心组件. 通过将复杂的对象(例如文本. ...