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

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

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. KRPANO太阳光插件

    KRPano太阳光插件可以在全景项目中添加太阳光特效,如下图所示: 同时,该插件支持可视化编辑 使用说明 1.下载插件,把插件放入skin文件夹里面 2.在tour.xml文件中,添加下面的插件引用 ...

  2. 使用Springboot+SpringCloud+Seata1.3.0+Nacos1.2.1进行全局事务管理

    一.官方文档网址 http://seata.io/zh-cn/docs/overview/what-is-seata.html Seata1.3.0开发组提供的开发文档 二.常见问题 2.1:网址: ...

  3. Blazor Server 发起HttpPost请求,但是多参数

    一.介绍 今天突然想起之前工作上遇到的一个问题,在做Blazor 开发时后端给的一个接口请求方式是Post ,但是他需要携带多个参数,新建一个公共类又觉得麻烦,我就尝试着怎么在Post请求中携带多个参 ...

  4. java开发面试笔记

    目录 1.hashMap hashmap源码分析-逐行注释版: 2.线程池 3.MySQL数据库引擎.事务.锁机制 1. 引擎 2. 事务 索引 3. 锁: 4. 调优问题怎么回答? 4.SQL语句 ...

  5. VS Code SSH

    VS Code SSH 连接需要下载 VS Code Server,这是因为 VS Code Server 是在远程服务器上运行的,而不是在本地计算机上运行的.每次连接到不同的远程服务器时,都需要下载 ...

  6. 软件开发人员 Kubernetes 入门指南|Part 2

    在第 1 部分中,我们讲解了 Kubernetes 的核心组件,Kubernetes 是一种开源容器编排器,用于在分布式环境中部署和扩展应用程序:我们还讲解了如何在集群中部署一个简单的应用程序,然后更 ...

  7. Redis 6 学习笔记 4 —— 通过秒杀案例,学习并发相关和apache bench的使用,记录遇到的问题

    背景 这是某硅谷的redis案例,主要问题是解决计数器和人员记录的事务操作 按照某硅谷的视频敲完之后出现这样乱码加报错的问题 乱码的问题要去tomcat根目录的conf文件夹下修改logging.pr ...

  8. CSP-2023 复赛游记

    10.15 决定以后每天晚上都来. 洛天依也是. 10.16 想住 首旅京伦. 大巴车要求车况良好,保险齐全,进校后限速 20 km是什么鬼啊,新型速度单位. 距离最远的考区相距4公里 懂了,大巴车开 ...

  9. Chromium Canvas工作流

    blink 中实现了2种 canvas,分别是 blink::HTMLCanvasElement 和 blink::OffscreenCanvas ,前者对应 html/dom 中的 canvas,后 ...

  10. Qt 迭代器

    目录 (一) java风格迭代器 1. QListIterator类 1. 初始化 2. findNext() 3. findPrevious() 4. hasNext() 5. hasPreviou ...