[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
RISC-V 休眠实现分析 2 -- 加载 swap 镜像
Corrector: TinyCorrect v0.1 - [spaces header codeblock codeinline pangu] Author: sugarfillet sugarfillet@yeah.net Date: 2023/05/27 Revisor: Falcon falcon@tinylab.org Project: RISC-V Linux 内核剖析 Proposal: RISC-V Linux SMP 技术调研与分析 Sponsor: PLCT Lab, ISCAS
前言
上文介绍了 RISC-V Linux 中休眠流程,本文紧随其后介绍休眠的唤醒流程。
说明:
- 本文的 Linux 版本采用
Linux v6.4-rc1
- 下文用到的术语:
- 休眠镜像 - 表示休眠触发过程中,记录要保存的页的镜像
- swap 镜像 - 表示休眠镜像写入到 swap 设备中的镜像
- 唤醒镜像 - 表示从 swap 镜像加载到内存中的镜像
唤醒入口 – software_resume
Linux 的休眠唤醒可通过在内核命令行中指定 resume=/dev/swappart
休眠镜像所在的 swap 分区。系统启动时在 late_initcall
阶段调用 software_resume
触发休眠唤醒流程。
software_resume
函数前期用于处理 swap 分区和 swap 文件,保存在 swsusp_resume_device
变量中,如果没有通过内核命令行指定的话,会以第一个发现的 swap 设备执行唤醒操作,之后跳转到 Check_image
标签。你可能会看到类似这样的日志:
[ 3.481250] PM: hibernation: Checking hibernation image partition /dev/vdb2
[ 3.483419] PM: hibernation: Hibernation image partition 254:18 present
[ 3.484130] PM: hibernation: Looking for hibernation image.
[ 3.487953] PM: hibernation: resume from hibernation
Check_image
首先校验 swap 头 struct swsusp_header
,之后执行一些准备工作(比如:准备挂起控制台、冻结进程),调用 load_image_and_restore
用于加载休眠镜像并恢复系统状态,后续的逻辑(比如:解冻进程)用于在休眠唤醒失败后恢复到现有的系统。
load_image_and_restore
函数主要涉及两个过程:
swsusp_read
加载 swap 镜像并构建唤醒镜像,获取之前保存的系统状态hibernation_restore
根据之前保存的系统状态恢复系统
// kernel/power/hibernate.c : 914
late_initcall_sync(software_resume);
software_resume()
//... 处理 swap 分区和文件
swsusp_resume_device = name_to_dev_t(resume_file);
Check_image:
swsusp_check(); // 校验 swap 头
pr_info("resume from hibernation\n");
pm_prepare_console();
freeze_processes();
freeze_kernel_threads();
load_image_and_restore()
pm_pr_dbg("Loading hibernation image.\n");
swsusp_read(&flags);
hibernation_restore(flags & SF_PLATFORM_MODE);
thaw_processes();
pm_restore_console();
pr_info("resume failed (%d)\n", error);
pm_pr_dbg("Hibernation image not present or could not be loaded.\n");
加载 swap 镜像 – swsusp_read
swsusp_read
从 swap 设备中读取 swap 镜像,并加载为唤醒镜像。其中涉及两个关键的数据结构及其相关结构:
struct swap_map_handle
用于跟踪对 swap 设备的以页为单位的读写操作,get_swap_reader
用于初始化读句柄,swap_read_page
用于从 swap 中读取一页到 buf
参数,release_swap_reader
用于关闭读句柄;写句柄也是类似的接口,不做赘述。
// kernel/power/swap.c : 96
struct swap_map_handle {
struct swap_map_page *cur;
struct swap_map_page_list *maps;
sector_t cur_swap;
sector_t first_sector;
unsigned int k;
unsigned long reqd_free_pages;
u32 crc32;
};
struct swap_map_page_list {
struct swap_map_page *map;
struct swap_map_page_list *next;
};
struct swap_map_page {
sector_t entries[MAP_PAGE_ENTRIES];
sector_t next_swap;
};
static int get_swap_reader(struct swap_map_handle *handle, unsigned int *flags_p);
static int swap_read_page(struct swap_map_handle *handle, void *buf, struct hib_bio_batch *hb);
static void release_swap_reader(struct swap_map_handle *handle);
struct snapshot_handle
结构用于管理对休眠/唤醒镜像的读写,并提供 snapshot_read_next
、snapshot_write_next
两个接口可以用于读写操作。比如:在从 swap 中读取镜像时,调用 snapshot_write_next
接口会从系统休眠镜像中返回一页,后续可以调用 swap_read_page
接口对该页填充数据;当往 swap 中写入镜像时,调用 snapshot_read_next
接口从系统休眠镜像中获取一页(已有数据填充),后续可以调用 swap_write_page
接口将该页写入 swap。
// kernel/power/power.h : 134
struct snapshot_handle {
unsigned int cur; /* number of the block of PAGE_SIZE bytes the
* next operation will refer to (ie. current)
*/
void *buffer; /* address of the block to read from
* or write to
*/
int sync_read; /* Set to one to notify the caller of
* snapshot_write_next() that it may
* need to call wait_on_bio_chain()
*/
};
int snapshot_write_next(struct snapshot_handle *handle);
在 swsusp_read
函数中,就是重复的调用 snapshot_write_next
和 swap_read_page
两个接口处理 swap 镜像。swap_read_page
相对简单,就是负责把读到的页填充到 buf
中,而 snapshot_write_next
由于休眠/唤醒镜像的基本结构(如下),则需要进行更加复杂的处理。
// hibernation/resuming image
| snapshot header | meta pages | data pages |
// kernel/power/swap.c : 1619
int swsusp_read(unsigned int *flags_p)
get_swap_reader(&handle, flags_p); // get swap handle
error = snapshot_write_next(&snapshot); // get header buffer
header = (struct swsusp_info *)data_of(snapshot); // data_of(snapshot) => snapshot->buffer
swap_read_page(&handle, header, NULL); // read a page from swap to header
load_image(&handle, &snapshot, header->pages - 1) : // now we have header->page
for ( ; ; ) {
snapshot_write_next(snapshot);
swap_read_page(handle, data_of(*snapshot), &hb);
swap_reader_finish(&handle); // release swap handle
pr_debug("Image successfully loaded\n");
构建唤醒镜像 – snapshot_write_next
处理镜像头信息
第一次调用 snapshot_write_next()
(下文代码中标记为 A):会调用 get_image_page
申请一个用于唤醒镜像的页,记录在 snapshot->buffer
中,handle->cur++
用于记录镜像当前分配的页数;之后调用 swap_read_page(&handle, header, NULL);
从 swap 中读取一页,这一页表示为一个 struct swsusp_info
结构,其中记录了如下信息。
// kernel/power/power.h : 10
struct swsusp_info { // 镜像头信息
struct new_utsname uts;
u32 version_code;
unsigned long num_physpages;
int cpus;
unsigned long image_pages; // swap 镜像中的数据页
unsigned long pages; // 此 swap 镜像中的总页数 = 数据页 + meta 页
unsigned long size;
} __aligned(PAGE_SIZE);
struct new_utsname {
char sysname[__NEW_UTS_LEN + 1];
char nodename[__NEW_UTS_LEN + 1];
char release[__NEW_UTS_LEN + 1];
char version[__NEW_UTS_LEN + 1];
char machine[__NEW_UTS_LEN + 1];
char domainname[__NEW_UTS_LEN + 1];
};
通过解析镜像头信息,我们知道了 swap 镜像中的总页数,之后调用 load_image(&handle, &snapshot, header->pages - 1)
,此函数只是循环地调用 snapshot_write_next()
和 swap_read_page()
从 swap 中读入镜像页。
第二次调用 snapshot_write_next()
(下文代码中标记为 B):对当前 snapshot->buffer
调用 load_header
校验并加载头信息:
- 调用
arch_hibernation_header_restore
处理架构定义的头信息,以struct arch_hibernate_hdr *
结构强制转换struct swsusp_info* buffer
- 对比内核版本信息
init_utsname()->version
–uname -v
保证唤醒与休眠在同一个内核上执行 - 设置并启动(如果需要的话)
sleep_cpu
,保证从休眠的 CPU 上唤醒 - 保存
struct arch_hibernate_hdr
信息到静态变量resume_hdr
,以供后续流程使用,比如:.saved_satp
为镜像保存的根页表地址,restore_cpu_addr
为镜像保存的用于跳转到swsusp_arch_suspend
else 分支的函数(后面会对这两个成员做详细介绍)
- 对比内核版本信息
- 设置
nr_copy_pages
变量用来保存 swap 镜像中的数据页数,nr_meta_pages
变量用来保存 swap 镜像中的 meta 页数
RISC-V 支持
CONFIG_ARCH_HIBERNATION_HEADER
选项,故而可以处理架构定义的头信息
// arch/riscv/kernel/hibernate.c : 59
static struct arch_hibernate_hdr {
struct arch_hibernate_hdr_invariants invariants;
unsigned long hartid;
unsigned long saved_satp;
unsigned long restore_cpu_addr;
} resume_hdr;
struct arch_hibernate_hdr_invariants {
char uts_version[__NEW_UTS_LEN + 1];
};
// arch/riscv/kernel/hibernate.c : 152
int arch_hibernation_header_restore(void *addr)
arch_hdr_invariants(&invariants);
memcmp(&hdr->invariants, &invariants, sizeof(invariants))
sleep_cpu = riscv_hartid_to_cpuid(hdr->hartid);
ret = bringup_hibernate_cpu(sleep_cpu);
resume_hdr = *hdr;
处理镜像体
第三次以及后续调用 snapshot_write_next()
(下文代码中标记为 C):用于处理 swap 镜像中的 meta page,调用 unpack_orig_pfns
函数在 copy_bm
bitmap 中对 buffer
中描述的页置位;当 meta page 处理完后,调用 prepare_image
为 data pages 分配内存,之后调用 get_buffer
为 orig_bm
中标记的页获取一个唤醒镜像地址。
// kernel/power/snapshot.c : 2629
snapshot_write_next(&snapshot); // Get the address to store the next image page.
if (!handle->cur) { // 为 header page 分配空间 // --------- A
buffer = get_image_page(GFP_ATOMIC, PG_ANY);
handle->buffer = buffer;
} else if (handle->cur == 1) // 处理 header,创建 copy_bm 用于记录 // ------------ B
load_header(buffer);
check_header(info)
check_image_kernel(info)
arch_hibernation_header_restore(info) // RISC-V restore struct arch_hibernate_hdr
nr_copy_pages = info->image_pages;
nr_meta_pages = info->pages - info->image_pages - 1;
memory_bm_create(©_bm, GFP_ATOMIC, PG_ANY); // create copy_bm for restore meta page
else if (handle->cur <= nr_meta_pages + 1) // 处理 meta pages // ----------- C
unpack_orig_pfns(buffer, ©_bm); // set the pfn from meta pages in copy_bm
if (handle->cur == nr_meta_pages + 1) { // image meta pages 读取结束
prepare_image(&orig_bm, ©_bm); // 为 data pages 分配内存
handle->buffer = get_buffer(&orig_bm, &ca); // 从分配的内存中获取一页,以供 swap 写入
return PTR_ERR(handle->buffer);
else {
handle->buffer = get_buffer(&orig_bm, &ca); // 从分配的内存中获取一页,以供 swap 写入
return PTR_ERR(handle->buffer);
handle->cur++;
return PAGE_SIZE;
prepare_image
为 data pages 分配内存,关键过程如下:
- 复制
copy_bm
到free_pages_map
,用来表示 meta pages 中记录的页 - 复制
copy_bm
到orig_bm
,并释放copy_bm
- 为 data pages 分配内存
- 调用
get_image_page
分配safe_pages_list
链表,在get_buffer
中预分配内存不足时提供内存分配空间 - 调用
get_zeroed_page
为 data pages 预分配内存 safe_pages_list
与预分配内存都记录到forbidden_pages_map
、free_pages_map
bitmap 中,用来记录那些用于唤醒镜像的页
- 调用
// kernel/power/snapshot.c : 2500
prepare_image(&orig_bm, ©_bm); // Make room for loading hibernation image. 创建 orig_bm,分配 safe_pages_list 以及预分配内存
duplicate_memory_bitmap(free_pages_map, bm); // free_pages_map 中标记 meta pages 中记录的页
duplicate_memory_bitmap(new_bm, bm); // copy copy_bm to orig_bm
memory_bm_free(bm, PG_UNSAFE_KEEP);
// Reserve some safe pages for potential later use.
nr_pages = nr_copy_pages - nr_highmem - allocated_unsafe_pages;
nr_pages = DIV_ROUND_UP(nr_pages, PBES_PER_LINKED_PAGE); // 需要多少个页来管理 pbe(对应一页 nr_copy_pages)
while (nr_pages > 0) {
lp = get_image_page(GFP_ATOMIC, PG_SAFE); // 这里基于 `free_pages_map` 保证用于唤醒镜像的内存页不与 swap 镜像中页冲突
lp->next = safe_pages_list;
safe_pages_list = lp;
// Preallocate memory for the image // 为唤醒镜像预分配内存
nr_pages = nr_copy_pages - nr_highmem - allocated_unsafe_pages; // allocated_unsafe_pages:之前分配到的页是 meta pages 中的页,这里就不用分配了
while (nr_pages > 0) {
lp = (struct linked_page *)get_zeroed_page(GFP_ATOMIC);
swsusp_set_page_forbidden(virt_to_page(lp));
swsusp_set_page_free(virt_to_page(lp));
// kernel/power/snapshot.c : 177
static void *get_image_page(gfp_t gfp_mask, int safe_needed)
{
void *res;
res = (void *)get_zeroed_page(gfp_mask);
if (safe_needed)
while (res && swsusp_page_is_free(virt_to_page(res))) { // 当前分配的页,与 meta pages 中描述的页冲突了
/* The page is unsafe, mark it for swsusp_free() */
swsusp_set_page_forbidden(virt_to_page(res)); // 标记该页
allocated_unsafe_pages++;
res = (void *)get_zeroed_page(gfp_mask); // 为 safe_pages_list 重新分配
}
if (res) {
swsusp_set_page_forbidden(virt_to_page(res));
swsusp_set_page_free(virt_to_page(res));
}
return res;
}
get_buffer
函数用于获取唤醒镜像下一页地址,首先从 orig_bm
中获取一个 meta pages 中的页,如果该页已经在预分配内存中,则直接返回该页的地址(该页会被 swap 中的页直接覆盖),否则从 safe_pages_list
链表中分配。分配时,需要有个结构来记录 1. 当前页在当前系统的地址(orig_address
). 临时页用来存放 swap 的页数据 (address
),这个结构就是 struct pbe
,该结构通过 chain_alloc
函数从 safe_pages_list
链表中分配,设置其 orig_address
, address
成员,并链接到 struct pbe *restore_pblist;
链表中,最后返回 pbe->address
以供 swap 填充。
// kernel/power/snapshot.c : 2762
get_buffer(&orig_bm, &ca); // Get the address to store the next image data page.
pfn = memory_bm_next_pfn(bm);
page = pfn_to_page(pfn);
if swsusp_page_is_forbidden(page) && swsusp_page_is_free(page) // 命中预分配内存,直接返回该页的地址
return page_address(page);
pbe = chain_alloc(ca, sizeof(struct pbe)); // 从 `safe_pages_list` 链表中分配
pbe->orig_address = page_address(page); // 保存该页在当前系统中的地址
pbe->address = safe_pages_list; safe_pages_list = safe_pages_list->next; // pbe->address 被 swap 填充,也就休眠时的内存页
pbe->next = restore_pblist; restore_pblist = pbe; // 插入 restore_pblist
return pbe->address;
// include/linux/suspend.h : 616
struct pbe {
void *address; /* address of the copy */
void *orig_address; /* original address of a page */
struct pbe *next;
};
struct pbe *restore_pblist;
以上内容可能比较晦涩,这里举个例子来说明:
假设 meta pages 中记录了 4 个页帧 – {1,2,6,7}
,步骤 1 调用 get_image_page
进行内存分配,第一次得到页帧 1
,与 meta pages 中的页冲突,再次分配得到页帧 2
同样冲突,再次分配得到页帧 5
,则 safe_pages_list
中记录页帧 5
。步骤 2 则只需要分配 2 个页帧,比如为页帧 6
、页帧 8
。在 get_buffer
时,页帧 1
、页帧 2
、页帧 6
存在于 forbidden_pages_map
,直接返回当前页帧的虚拟地址,以供 swap 填充,但页帧 7
就需要在页帧 5
中分配 struct pbe
,设置 pbe.orig_address
为页帧 7
的虚拟地址、设置 pbe.address
为一个来自于 safe_pages_list
的页并返回,以供 swap 填充。为了对页帧 7
中数据进行恢复,则需要后续操作在 restore_pblist
链表中,拷贝 pbe.address
到 pbe.orig_address
。
小结
本节主要介绍 swap 镜像的加载过程,整个过程主要使用 snapshot_write_next
和 swap_read_page
两个接口进行唤醒镜像的构建。meta pages 中的页帧,一部分采用预分配内存交由 swap 直接覆盖,剩余的页帧则需要后续流程中对 retore_pblist
链表进行页拷贝。
RISC-V 架构开启 CONFIG_ARCH_HIBERNATION_HEADER
选项,提供架构定义的休眠镜像头结构 struct arch_hibernate_hdr
,在第二次调用 snapshot_write_next()
处理镜像头信息的过程中对其进行恢复,并保存到静态变量 resume_hdr
。
在 swsusp_read
的后续流程中,如何使用 resume_hdr
以及如何对 retore_pblist
链表进行页拷贝,我们放在下篇文章进行介绍。
参考资料
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |
Read Album:
- TinyBPT 和面向 buildroot 的二进制包管理服务(3):服务端说明
- TinyBPT 和面向 buildroot 的二进制包管理服务(2):客户端说明
- TinyBPT 和面向 buildroot 的二进制包管理服务(1):设计简介与框架
- RISC-V Linux 内核及周边技术动态第 118 期
- RISC-V Linux 内核及周边技术动态第 117 期