【内核】kernel 热升级-1:kexec 机制
内核热升级是指,预先准备好需要升级的内核镜像文件,在秒级时间内,完成内核切换,追求用户服务进程无感知。
欧拉操作系统提供了一套比较成熟的解决方案,该解决方案提供了用户态程序和内核态程序两部分:
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:解析内核文件
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:加载内核段数据
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 的执行,切换到新的内核地址上去。下面是该命令的逻辑:
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 机制的更多相关文章
- kernel 3.10内核源码分析--hung task机制
kernel 3.10内核源码分析--hung task机制 一.相关知识: 长期以来,处于D状态(TASK_UNINTERRUPTIBLE状态)的进程 都是让人比较烦恼的问题,处于D状态的进程不能接 ...
- 升级CentOS内核 - 2.6升级到3.10/最新内核
##记得切换到root用户执行升级操作. [root@localhost ~]# uname -a ##旧版 Linux localhost.localdomain 2.6.32-279.el6.i6 ...
- PHP服务器脚本 PHP内核探索:新垃圾回收机制说明
在5.2及更早版本的PHP中,没有专门的垃圾回收器GC(Garbage Collection),引擎在判断一个变量空间是否能够被释放的时候是依据这个变量的zval的refcount的值,如果refco ...
- .NET插件技术-应用程序热升级
今天说一说.NET 中的插件技术,即 应用程序热升级.在很多情况下.我们希望用户对应用程序的升级是无感知的,并且尽可能不打断用户操作的. 虽然在Web 或者 WebAPI上,由于多点的存在可以逐个停用 ...
- PHP内核之旅-6.垃圾回收机制
回收PHP 内核之旅系列 PHP内核之旅-1.生命周期 PHP内核之旅-2.SAPI中的Cli PHP内核之旅-3.变量 PHP内核之旅-4.字符串 PHP内核之旅-5.强大的数组 PHP内核之旅-6 ...
- rpm包安装的nginx热升级
文章目录一.本地环境基本介绍二.yum升级命令说明三.升级好nginx后如何不中断业务切换3.1.nginx相关的信号说明3.2.在线热升级nginx可执行文件程序一.本地环境基本介绍本次测试环境,是 ...
- Nginx热升级流程,看这篇就够了
在之前做过 Nginx 热升级的演示,他能保证nginx在不停止服务的情况下更换他的 binary 文件,这个功能非常有用,但我们在执行 Nginx 的 binary 文件升级过程中,还是会遇到很多问 ...
- 老版本nginx存在安全漏洞,不停服务热升级
1.场景描述 安全部通知:nginx存在"整数溢出漏洞",经测试2017年4月21日之后的版本无问题,将openresty升级到最新版本,Nginx升级到1.13.2之后的版本. ...
- nginx 安装第三方模块(lua)并热升级
需求: nginx上将特定请求拒绝,并返回特定值. 解决办法: 使用lua脚本,实现效果. 操作步骤: 安装Luajit环境 重新编译nginx(目标机器上nginx -V 配置一致,并新增两个模块n ...
- Beego开启热升级
1.打开配置 beego.BConfig.Listen.Graceful = true 2.写入pid 程序入口main()函数里写入pid func writePid() { fileName := ...
随机推荐
- SQL多表查询指南
SQL多表查询指南 在实际的数据库应用中,通常需要查询涉及多个表的数据.SQL提供了多种方法来执行这种多表查询操作. 内连接(INNER JOIN) 内连接是将多个表中满足连接条件的行组合在一起的操作 ...
- C#开发的基础工具类集合 - 开源研究系列文章
今天发布一个基础工具类代码集合. 以前有发布过一个类似的类库(见博文: Magical平台类库代码分享 ),不过那个版本有点久了,也没有这次这个全面,这次发布的是一个很多地方用到的基础类库代码. 1. ...
- Spring Bean 的作用域(Bean Scope)
前言 大家好,我是 god23bin,今天我们来聊一聊 Spring 框架中的 Bean 作用域(Scope). 什么是 Bean 的作用域? 我们在以 XML 作为配置元数据的情况下,进行 Bean ...
- 文心一言 VS 讯飞星火 VS chatgpt (92)-- 算法导论8.3 5题
五.用go语言,在本节给出的第一个卡片排序算法中,为排序 d 位十进制数,在最坏情况下需要多少轮排序?在最坏情况下,操作员需要记录多少堆卡片? 文心一言: 你提到的第一个卡片排序算法可能是指的基数排序 ...
- Arrays.asList():使用指南
Arrays.asList() 是一个 Java 的静态方法,它可以把一个数组或者多个参数转换成一个 List 集合.这个方法可以作为数组和集合之间的桥梁,方便我们使用集合的一些方法和特性.本文将介绍 ...
- TiDB的简单介绍以及进行资源限制的方式与方法
TiDB的简单介绍以及进行资源限制的方式与方法 TiDB的简介 TiDB是一个分布式数据库, 简介为: TiDB 是一个开源的分布式关系型数据库,它兼具了分布式数据库的水平扩展性和传统关系型数据库的 ...
- 算法打卡|Day1 数组part01
Day1 数组part01 今日任务:数组理论基础,704. 二分查找,27. 移除元素 目录 Day1 数组part01 今日任务:数组理论基础,704. 二分查找,27. 移除元素 Part1: ...
- Java虚拟机(JVM):第四幕:自动内存管理 - 经典垃圾收集器
前言:如果说收集算法是内存回收的方法论,那么垃圾收集器则是内存回收的实践者.整哥Java堆 :Full GC. 1.Serial收集器:最基础.历史最悠久的收集器,这是一个单线程工作的收集器. 2.P ...
- UVA10702 Travelling Salesman 题解
UVA10702 Travelling Salesman 题解 题面: 有个旅行的商人,他每到一个的新城市,便卖掉所有东西再购买新东西,从而获得利润.从某城市 A 到某城市 B 有固定利润(B 到 ...
- 从基础到实践,回顾Elasticsearch 向量检索发展史
本文分享自华为云社区<Elasticsearch向量检索的演进与变革:从基础到应用>,作者: 汀丶. 1.引言 向量检索已经成为现代搜索和推荐系统的核心组件. 通过将复杂的对象(例如文本. ...