首页
chatGPT
关于
友链
其它
统计
更多
壁纸
留言
Search
1
cgroup--(4)cgroup v1和cgroup v2的详细介绍
6,537 阅读
2
修改Linux Kernel defconfig的标准方法
6,453 阅读
3
Android系统之VINTF(1)manifests&compatibility matrices
6,057 阅读
4
使用git生成patch和应用patch
3,572 阅读
5
c语言的__attribute__
3,184 阅读
默认分类
文章收集
学习总结
算法
环境配置
知识点
入门系列
vim
shell
Git
Make
Android
Linux
Linux命令
内存管理
Linux驱动
Language
C++
C
Rust
工具
软件工具
Bug
COMPANY
登录
Search
标签搜索
Rust
shell
Linux
c
uboot
Vim
vintf
Linux驱动
Android
device_tree
git
DEBUG
arm64
链表
数据结构
IDR
内核
ELF
gcc
ARM
adtxl
累计撰写
380
篇文章
累计收到
16
条评论
首页
栏目
默认分类
文章收集
学习总结
算法
环境配置
知识点
入门系列
vim
shell
Git
Make
Android
Linux
Linux命令
内存管理
Linux驱动
Language
C++
C
Rust
工具
软件工具
Bug
COMPANY
页面
chatGPT
关于
友链
其它
统计
壁纸
留言
搜索到
380
篇与
的结果
2023-10-10
KASAN的使用
1. 简介KernelAddressSANitizer(KASAN)是一个动态检测内存错误的工具。它为找到use-after-free和out-of-bounds问题提供了一个快速和全面的解决方案。KASAN使用编译时检测每个内存访问,因此您需要GCC 4.9.2或更高版本。检测堆栈或全局变量的越界访问需要GCC 5.0或更高版本。目前KASAN仅支持x86_64和arm64架构(linux 4.4版本合入)。你使用ARM64架构,那么就需要保证linux版本在4.4以上。当然了,如果你使用的linux也有可能打过KASAN的补丁。例如,使用高通平台做手机的厂商使用linux 3.18同样支持KASAN。2.打开KASAN直接通过menuconfig打开KASAN,在Kernel hacking-->Memory Debugging然后编译烧写即可。遇到问题,打开kasan后编译出来的Image比较大,需要增大下boot和recovery分区的size,顺便需要调整下系统运行的memory map// 调整partition --- a/partitions/partition_dynamic.xml +++ b/partitions/partition_dynamic.xml @@ -4,8 +4,8 @@ <user_entry type = "{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7}" name = "brp" size_kb = "31744" /> <user_entry type = "{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7}" name = "dtb" size_kb = "512" /> - <user_entry type = "{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7}" name = "boot" size_kb = "65536" /> - <user_entry type = "{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7}" name = "recovery" size_kb = "65536" /> + <user_entry type = "{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7}" name = "boot" size_kb = "131072" /> + <user_entry type = "{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7}" name = "recovery" size_kb = "131072" /> <user_entry type = "{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7}" name = "misc" size_kb = "8192" /> <user_entry type = "{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7}" name = "super" size_kb = "2113536" /> <user_entry type = "{EBD0A0A2-B9E5-4433-87C0-68B6B72699C7}" name = "cache" size_kb = "262144" /> // 调整uboot -#define CONFIG_SYS_BOOTM_LEN SZ_64M +#define CONFIG_SYS_BOOTM_LEN SZ_128M /* Physical start address of SDRAM */ #define CONFIG_SYS_SDRAM_BASE 0x4000000 /* Link Definitions */ #define FDT_ADDR_R CONFIG_SYS_SDRAM_BASE #define SCRIPT_ADDR_R 0x4200000 -#define KERNEL_ADDR_R 0x8280000 -#define RAMDISK_ADDR_R 0x6280000 +#define KERNEL_ADDR_R 0xc280000 +#define RAMDISK_ADDR_R 0xa280000 // 调整kernel dts - aipu_reserved: memory@e380000 { + aipu_reserved: memory@24000000 { compatible = "shared-dma-pool"; - reg = <0x0 0x0e380000 0x0 0x01000000>; + reg = <0x0 0x24000000 0x0 0x01000000>; no-map; }; - ramoops@f380000 { + ramoops@23000000 { compatible = "ramoops"; - reg = <0x0 0x0f380000 0x0 0x100000>; + reg = <0x0 0x23000000 0x0 0x100000>; console-size = <0x40000>; 3. 测试kasan检测到内存问题,会打印类似下面的log,[2023-10-10 14:16:16] [ 0.347342] -(0)[1:swapper/0->0:swapper/0]BUG: KASAN: slab-out-of-bounds in find_next_bit+0x38/0x9c [2023-10-10 14:16:16] [ 0.347362] -(0)[1:swapper/0->0:swapper/0]Read of size 8 at addr ffff8001642e5ab0 by task swapper/0/1 [2023-10-10 14:16:16] [ 0.347376] -(0)[1:swapper/0->0:swapper/0] [2023-10-10 14:16:16] [ 0.347399] -(0)[1:swapper/0->0:swapper/0]CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.19.176 #46 [2023-10-10 14:16:16] [ 0.347415] -(0)[1:swapper/0->0:swapper/0]Hardware name: ESWIN_LD60 (DT) [2023-10-10 14:16:16] [ 0.347430] -(0)[1:swapper/0->0:swapper/0]Call trace: [2023-10-10 14:16:16] [ 0.347450] -(0)[1:swapper/0->0:swapper/0] dump_backtrace+0x0/0x1e8 [2023-10-10 14:16:16] [ 0.347467] -(0)[1:swapper/0->0:swapper/0] show_stack+0x20/0x2c [2023-10-10 14:16:16] [ 0.347487] -(0)[1:swapper/0->0:swapper/0] dump_stack+0xf4/0x148 [2023-10-10 14:16:16] [ 0.347507] -(0)[1:swapper/0->0:swapper/0] print_address_description+0x84/0x2e8 [2023-10-10 14:16:17] [ 0.347524] -(0)[1:swapper/0->0:swapper/0] __kasan_report+0x16c/0x1cc [2023-10-10 14:16:17] [ 0.347541] -(0)[1:swapper/0->0:swapper/0] kasan_report+0x10/0x18 [2023-10-10 14:16:17] [ 0.347559] -(0)[1:swapper/0->0:swapper/0] __asan_load8+0x84/0x8c [2023-10-10 14:16:17] [ 0.347577] -(0)[1:swapper/0->0:swapper/0] find_next_bit+0x38/0x9c [2023-10-10 14:16:17] [ 0.347595] -(0)[1:swapper/0->0:swapper/0] cpumask_next+0x20/0x28 [2023-10-10 14:16:17] [ 0.347615] -(0)[1:swapper/0->0:swapper/0] pm_qos_update_target+0x30c/0x5c8 [2023-10-10 14:16:17] [ 0.347635] -(0)[1:swapper/0->0:swapper/0] apply_constraint+0x84/0x130 [2023-10-10 14:16:17] [ 0.347653] -(0)[1:swapper/0->0:swapper/0] __dev_pm_qos_add_request+0x104/0x248 [2023-10-10 14:16:17] [ 0.347672] -(0)[1:swapper/0->0:swapper/0] dev_pm_qos_expose_latency_limit+0x88/0x1b0 [2023-10-10 14:16:17] [ 0.347690] -(0)[1:swapper/0->0:swapper/0] register_cpu+0x1b0/0x1cc [2023-10-10 14:16:17] [ 0.347710] -(0)[1:swapper/0->0:swapper/0] topology_init+0x94/0xd4 [2023-10-10 14:16:17] [ 0.347728] -(0)[1:swapper/0->0:swapper/0] do_one_initcall+0xc0/0x37c [2023-10-10 14:16:17] [ 0.347746] -(0)[1:swapper/0->0:swapper/0] do_initcall_level+0xf0/0x200 [2023-10-10 14:16:17] [ 0.347762] -(0)[1:swapper/0->0:swapper/0] do_basic_setup+0x78/0x94 [2023-10-10 14:16:17] [ 0.347780] -(0)[1:swapper/0->0:swapper/0] kernel_init_freeable+0x128/0x1b0 [2023-10-10 14:16:17] [ 0.347797] -(0)[1:swapper/0->0:swapper/0] kernel_init+0x18/0x2ac [2023-10-10 14:16:17] [ 0.347814] -(0)[1:swapper/0->0:swapper/0] ret_from_fork+0x10/0x18 [2023-10-10 14:16:17] [ 0.347828] -(0)[1:swapper/0->0:swapper/0] [2023-10-10 14:16:17] [ 0.347842] -(0)[1:swapper/0->0:swapper/0]Allocated by task 0: [2023-10-10 14:16:17] [ 0.347855] -(0)[1:swapper/0->0:swapper/0](stack is not available) [2023-10-10 14:16:17] [ 0.347868] -(0)[1:swapper/0->0:swapper/0] [2023-10-10 14:16:17] [ 0.347882] -(0)[1:swapper/0->0:swapper/0]Freed by task 0: [2023-10-10 14:16:17] [ 0.347895] -(0)[1:swapper/0->0:swapper/0](stack is not available) [2023-10-10 14:16:17] [ 0.347907] -(0)[1:swapper/0->0:swapper/0] [2023-10-10 14:16:17] [ 0.347925] -(0)[1:swapper/0->0:swapper/0]The buggy address belongs to the object at ffff8001642e5a00 [2023-10-10 14:16:17] [ 0.347925] which belongs to the cache kmalloc-128 of size 128 [2023-10-10 14:16:17] [ 0.347948] -(0)[1:swapper/0->0:swapper/0]The buggy address is located 48 bytes to the right of [2023-10-10 14:16:17] [ 0.347948] 128-byte region [ffff8001642e5a00, ffff8001642e5a80) [2023-10-10 14:16:17] [ 0.347968] -(0)[1:swapper/0->0:swapper/0]The buggy address belongs to the page: [2023-10-10 14:16:17] [ 0.347983] -(0)[1:swapper/0->0:swapper/0]page:ffff7e000590b940 count:1 mapcount:0 mapping:ffff800164c03c00 index:0x0 [2023-10-10 14:16:17] [ 0.348006] -(0)[1:swapper/0->0:swapper/0]flags: 0x4000000000000200(slab) [2023-10-10 14:16:17] [ 0.348031] -(0)[1:swapper/0->0:swapper/0]raw: 4000000000000200 dead000000000100 dead000000000200 ffff800164c03c00 [2023-10-10 14:16:17] [ 0.348053] -(0)[1:swapper/0->0:swapper/0]raw: 0000000000000000 0000000080100010 00000001ffffffff 0000000000000000 [2023-10-10 14:16:17] [ 0.348068] -(0)[1:swapper/0->0:swapper/0]page dumped because: kasan: bad access detected [2023-10-10 14:16:17] [ 0.348081] -(0)[1:swapper/0->0:swapper/0] [2023-10-10 14:16:17] [ 0.348095] -(0)[1:swapper/0->0:swapper/0]Memory state around the buggy address: [2023-10-10 14:16:17] [ 0.348113] -(0)[1:swapper/0->0:swapper/0] ffff8001642e5980: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [2023-10-10 14:16:17] [ 0.348131] -(0)[1:swapper/0->0:swapper/0] ffff8001642e5a00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [2023-10-10 14:16:17] [ 0.348148] -(0)[1:swapper/0->0:swapper/0]>ffff8001642e5a80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [2023-10-10 14:16:17] [ 0.348163] -(0)[1:swapper/0->0:swapper/0] ^ [2023-10-10 14:16:17] [ 0.348179] -(0)[1:swapper/0->0:swapper/0] ffff8001642e5b00: 00 00 00 00 00 00 00 fc fc fc fc fc fc fc fc fc [2023-10-10 14:16:17] [ 0.348197] -(0)[1:swapper/0->0:swapper/0] ffff8001642e5b80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [2023-10-10 14:16:17] [ 0.348211] -(0)[1:swapper/0->0:swapper/0]================================================================== [2023-10-10 14:16:17] [ 0.348225] -(0)[1:swapper/0->0:swapper/0]Disabling lock debugging due to kernel taint
2023年10月10日
192 阅读
0 评论
1 点赞
2023-09-19
kprobe简介
1. kprobe是什么?probe是linux内核的一个重要特性,是一个轻量级的内核调试工具,同时它又是其他一些更高级的内核调试工具(比如perf和systemtap)的“基础设施”,4.0版本的内核中,强大的eBPF特性也寄生于kprobe之上,所以kprobe在内核中的地位就可见一斑了。。利用kprobes技术,内核开发人员可以在内核的绝大多数指定函数中动态的插入探测点来收集所需的调试状态信息而基本不影响内核原有的执行流程。kprobes技术目前提供了3种探测手段:kprobe、jprobe和kretprobe,其中jprobe和kretprobe是基于kprobe实现的,他们分别应用于不同的探测场景中。如何高效地调试内核?printk是一种方法,但是printk终归是毫无选择地全量输出,某些场景下不实用,于是可以试一下tracepoint,使能tracepoint机制的时候才输出。对于傻傻地放置printk来输出信息的方式,tracepoint是个进步,但是tracepoint只是内核在某些特定行为(比如进程切换)上部署的一些静态锚点,这些锚点并不一定是你需要的,所以你仍然需要自己部署tracepoint,重新编译内核。那么kprobe的出现就很有必要了,它可以在运行的内核中动态插入探测点,执行预定义的操作。kprobes技术包括的3种探测手段分别时kprobe、jprobe和kretprobe。首先kprobe是最基本的探测方式,是实现后两种的基础,它可以在任意的位置放置探测点(就连函数内部的某条指令处也可以),它提供了探测点的调用前、调用后和内存访问出错3种回调方式,分别是pre_handler、post_handler和fault_handler,其中pre_handler函数将在被探测指令被执行前回调,post_handler会在被探测指令执行完毕后回调(注意不是被探测函数),fault_handler会在内存访问出错时被调用;jprobe基于kprobe实现,它用于获取被探测函数的入参值;最后kretprobe从名字种就可以看出其用途了,它同样基于kprobe实现,用于获取被探测函数的返回值。kprobes的技术原理并不仅仅包含存软件的实现方案,它也需要硬件架构提供支持。其中涉及硬件架构相关的是CPU的异常处理和单步调试技术,前者用于让程序的执行流程陷入到用户注册的回调函数中去,而后者则用于单步执行被探测点指令,因此并不是所有的架构均支持,目前kprobes技术已经支持多种架构,包括i386、x86_64、ppc64、ia64、sparc64、arm、ppc和mips(有些架构实现可能并不完全,具体可参考内核的Documentation/kprobes.txt)。kprobes的特点与使用限制:1、kprobes允许在同一个被被探测位置注册多个kprobe,但是目前jprobe却不可以;同时也不允许以其他的jprobe回掉函数和kprobe的post_handler回调函数作为被探测点。2、一般情况下,可以探测内核中的任何函数,包括中断处理函数。不过在kernel/kprobes.c和arch/*/kernel/kprobes.c程序中用于实现kprobes自身的函数是不允许被探测的,另外还有do_page_fault和notifier_call_chain;3、如果以一个内联函数为探测点,则kprobes可能无法保证对该函数的所有实例都注册探测点。由于gcc可能会自动将某些函数优化为内联函数,因此可能无法达到用户预期的探测效果;4、一个探测点的回调函数可能会修改被探测函数运行的上下文,例如通过修改内核的数据结构或者保存与struct pt_regs结构体中的触发探测之前寄存器信息。因此kprobes可以被用来安装bug修复代码或者注入故障测试代码;5、kprobes会避免在处理探测点函数时再次调用另一个探测点的回调函数,例如在printk()函数上注册了探测点,则在它的回调函数中可能再次调用printk函数,此时将不再触发printk探测点的回调,仅仅时增加了kprobe结构体中nmissed字段的数值;6、在kprobes的注册和注销过程中不会使用mutex锁和动态的申请内存;7、kprobes回调函数的运行期间是关闭内核抢占的,同时也可能在关闭中断的情况下执行,具体要视CPU架构而定。因此不论在何种情况下,在回调函数中不要调用会放弃CPU的函数(如信号量、mutex锁等);8、kretprobe通过替换返回地址为预定义的trampoline的地址来实现,因此栈回溯和gcc内嵌函数__builtin_return_address()调用将返回trampoline的地址而不是真正的被探测函数的返回地址;9、如果一个函数的调用此处和返回次数不相等,则在类似这样的函数上注册kretprobe将可能不会达到预期的效果,例如do_exit()函数会存在问题,而do_execve()函数和do_fork()函数不会;10、如果当在进入和退出一个函数时,CPU运行在非当前任务所有的栈上,那么往该函数上注册kretprobe可能会导致不可预料的后果,因此,kprobes不支持在X86_64的结构下为__switch_to()函数注册kretprobe,将直接返回-EINVAL。2. kprobe怎么使能?不同kernel版本可能不同,最好根据menuconfig配置需要打开如下kernel defconfig CONFIG_KPROBES=y CONFIG_FUNCTION_TRACER=y CONFIG_FTRACE_SYSCALLS=y 对应的配置项为: CONFIG_KPROBES=y CONFIG_KRETPROBES=y CONFIG_TRACING=y CONFIG_GENERIC_TRACER=y CONFIG_TRACING_SUPPORT=y CONFIG_FTRACE=y CONFIG_FUNCTION_TRACER=y CONFIG_FTRACE_SYSCALLS=y CONFIG_KPROBE_EVENTS=y 3. kprobe怎么使用?--模块加载的方式kprobe主要有两种使用方法,一是通过模块加载;二是通过debugfs接口。模块加载的方式:内核源码下有目录下 samples/kprobes,该目录下有kprobes的例子,可以仿照这些例子写自己的kprobe模块。以kprobe_example.c为例,首先声明一个kprobe结构体,然后定义其中几个关键成员变量,包括symbol_name,pre_handler,post_handler。其中,symbol_name是函数名(kprobe_example.c中该项为_do_fork),告诉内核我的探测点放置在了函数_do_fork处,pre_hander和post_hander分别表示在执行探测点之前和之后执行的钩子函数。然后通过register_kprobe函数注册kprobe即可。第一个例子,使用kprobe打印fork时的pc/* * NOTE: This example is works on x86 and powerpc. * Here's a sample kernel module showing the use of kprobes to dump a * stack trace and selected registers when _do_fork() is called. * * For more information on theory of operation of kprobes, see * Documentation/kprobes.txt * * You will see the trace data in /var/log/messages and on the console * whenever _do_fork() is invoked to create a new process. */ #include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> #define MAX_SYMBOL_LEN 64 static char symbol[MAX_SYMBOL_LEN] = "_do_fork"; module_param_string(symbol, symbol, sizeof(symbol), 0644); /* 程序定义了一个struct kprobe结构体实例kp,并初始化了其中的symbol_name字段为do_fork;在驱动初始化函数中,将注册 * pre_handler,post_handler, fault_handler这三个回调函数为handler_pre,handler_post,handler_fault; * 最后调用register_kprobe注册。 */ /* For each probe you need to allocate a kprobe structure */ static struct kprobe kp = { .symbol_name = symbol, }; /* handler_pre回调函数的第一个入参是注册的struct kprobe,第二个参数是保存的触发断点前的寄存器状态。 * 它在do_fork函数被调用之前被调用,该函数仅仅是打印了被探测点的地址,保存个别寄存器参数。 */ /* kprobe pre_handler: called just before the probed instruction is executed */ static int handler_pre(struct kprobe *p, struct pt_regs *regs) { #ifdef CONFIG_ARM64 pr_info("<%s> pre_handler: p->addr = 0x%p, pc = 0x%lx," " pstate = 0x%lx\n", p->symbol_name, p->addr, (long)regs->pc, (long)regs->pstate); #endif /* A dump_stack() here will give a stack backtrace */ return 0; } // 该函数在do_fork函数调用之后被调用 /* kprobe post_handler: called after the probed instruction is executed */ static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { #ifdef CONFIG_ARM64 pr_info("<%s> post_handler: p->addr = 0x%p, pstate = 0x%lx\n", p->symbol_name, p->addr, (long)regs->pstate); #endif } /* handler_fault回调函数会在执行handler_pre,handler_post或单步执行do_fork出现错误时调用, * 这里的第三个参数是发生错误时的trap number,与架构相关 */ /* * fault_handler: this is called if an exception is generated for any * instruction within the pre- or post-handler, or when Kprobes * single-steps the probed instruction. */ static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) { pr_info("fault_handler: p->addr = 0x%p, trap #%dn", p->addr, trapnr); /* Return 0 because we don't handle the fault. */ return 0; } static int __init kprobe_init(void) { int ret; kp.pre_handler = handler_pre; kp.post_handler = handler_post; kp.fault_handler = handler_fault; ret = register_kprobe(&kp); if (ret < 0) { pr_err("register_kprobe failed, returned %d\n", ret); return ret; } pr_info("Planted kprobe at %p\n", kp.addr); return 0; } static void __exit kprobe_exit(void) { unregister_kprobe(&kp); pr_info("kprobe at %p unregistered\n", kp.addr); } module_init(kprobe_init) module_exit(kprobe_exit) MODULE_LICENSE("GPL"); 将kprobe_example.ko insmod进内核之后,每当系统新启动一个进程,比如执行ls,cat等,都会输出:[11573.615667] -(2)[2535:AudioPileline->1:init]<_do_fork> post_handler: p->addr = 0x0000000014fff252, pstate = 0x80400009 [11573.669603] -(3)[2535:AudioPileline->1:init]<_do_fork> pre_handler: p->addr = 0x0000000014fff252, pc = 0xffff0000080be50c, pstate = 0x80400009 [11573.682456] -(3)[2535:AudioPileline->1:init]<_do_fork> post_handler: p->addr = 0x0000000014fff252, pstate = 0x80400009 [11573.736206] -(3)[2535:AudioPileline->1:init]<_do_fork> pre_handler: p->addr = 0x0000000014fff252, pc = 0xffff0000080be50c, pstate = 0x80400009 [11573.749062] -(3)[2535:AudioPileline->1:init]<_do_fork> post_handler: p->addr = 0x0000000014fff252, pstate = 0x80400009 [11573.802870] -(3)[2535:AudioPileline->1:init]<_do_fork> pre_handler: p->addr = 0x0000000014fff252, pc = 0xffff0000080be50c, pstate = 0x80400009 [11573.815722] -(3)[2535:AudioPileline->1:init]<_do_fork> post_handler: p->addr = 0x0000000014fff252, pstate = 0x80400009第一行是执行pre_handler钩子函数的输出,第二行是执行post_handler钩子函数的输出,当然这些都是内核中案例的写法,你可以写自己的钩子函数。kretprobe例子,打印返回值和函数执行时间kretprobe是基于kprobe实现的,下面是一个例子/* * kretprobe_example.c * * Here's a sample kernel module showing the use of return probes to * report the return value and total time taken for probed function * to run. * * usage: insmod kretprobe_example.ko func=<func_name> * * If no func_name is specified, _do_fork is instrumented * * For more information on theory of operation of kretprobes, see * Documentation/kprobes.txt * * Build and insert the kernel module as done in the kprobe example. * You will see the trace data in /var/log/messages and on the console * whenever the probed function returns. (Some messages may be suppressed * if syslogd is configured to eliminate duplicate messages.) */ #include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> #include <linux/ktime.h> #include <linux/limits.h> #include <linux/sched.h> static char func_name[NAME_MAX] = "_do_fork"; module_param_string(func, func_name, NAME_MAX, S_IRUGO); MODULE_PARM_DESC(func, "Function to kretprobe; this module will report the" " function's execution time"); /* per-instance private data */ struct my_data { ktime_t entry_stamp; }; /* Here we use the entry_hanlder to timestamp function entry */ static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs) { struct my_data *data; if (!current->mm) return 1; /* Skip kernel threads */ data = (struct my_data *)ri->data; data->entry_stamp = ktime_get(); return 0; } /* * Return-probe handler: Log the return value and duration. Duration may turn * out to be zero consistently, depending upon the granularity of time * accounting on the platform. */ static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs) { unsigned long retval = regs_return_value(regs); struct my_data *data = (struct my_data *)ri->data; s64 delta; ktime_t now; now = ktime_get(); delta = ktime_to_ns(ktime_sub(now, data->entry_stamp)); pr_info("%s returned %lu and took %lld ns to execute\n", func_name, retval, (long long)delta); return 0; } static struct kretprobe my_kretprobe = { .handler = ret_handler, .entry_handler = entry_handler, .data_size = sizeof(struct my_data), /* Probe up to 20 instances concurrently. */ .maxactive = 20, }; static int __init kretprobe_init(void) { int ret; my_kretprobe.kp.symbol_name = func_name; ret = register_kretprobe(&my_kretprobe); if (ret < 0) { pr_err("register_kretprobe failed, returned %d\n", ret); return -1; } pr_info("Planted return probe at %s: %p\n", my_kretprobe.kp.symbol_name, my_kretprobe.kp.addr); return 0; } static void __exit kretprobe_exit(void) { unregister_kretprobe(&my_kretprobe); pr_info("kretprobe at %p unregistered\n", my_kretprobe.kp.addr); /* nmissed > 0 suggests that maxactive was set too low. */ pr_info("Missed probing %d instances of %s\n", my_kretprobe.nmissed, my_kretprobe.kp.symbol_name); } module_init(kretprobe_init) module_exit(kretprobe_exit) MODULE_LICENSE("GPL");运行模块,如下,我们监视ion_carveout_heap_allocate这个函数的返回值和运行时间。可以判断出通过ion分配内存所需要的时间console:/ # insmod kretprobe_example.ko func=ion_carveout_heap_allocate [12177.111236] -(1)[2655:HwBinder:2427_3->1:init]ion_carveout_heap_allocate returned 0 and took 10417 ns to execute [12177.123240] -(0)[2655:HwBinder:2427_3->1:init]ion_carveout_heap_allocate returned 0 and took 10583 ns to execute [12177.139424] -(0)[2655:HwBinder:2427_3->1:init]ion_carveout_heap_allocate returned 0 and took 10667 ns to execute [12177.152293] -(1)[2655:HwBinder:2427_3->1:init]ion_carveout_heap_allocate returned 0 and took 10292 ns to execute [12177.164249] -(2)[2655:HwBinder:2427_3->1:init]ion_carveout_heap_allocate returned 0 and took 11667 ns to execute [12177.176606] -(0)[2655:HwBinder:2427_3->1:init]ion_carveout_heap_allocate returned 0 and took 46666 ns to execute [12177.190549] -(3)[2655:HwBinder:2427_3->1:init]ion_carveout_heap_allocate returned 0 and took 27083 ns to execute [12177.202639] -(0)[2655:HwBinder:2427_3->1:init]ion_carveout_heap_allocate returned 0 and took 11292 ns to execute [12177.214691] -(2)[2655:HwBinder:2427_3->1:init]ion_carveout_heap_allocate returned 0 and took 12083 ns to execute 4. kprobe结构体与API介绍先看kprobe这个结构体的成员变量,struct kprobe { struct hlist_node hlist; /* list of kprobes for multi-handler support */ struct list_head list; /*count the number of times this probe was temporarily disarmed */ unsigned long nmissed; /* location of the probe point */ kprobe_opcode_t *addr; /* Allow user to indicate symbol name of the probe point */ const char *symbol_name; /* Offset into the symbol */ unsigned int offset; /* Called before addr is executed. */ kprobe_pre_handler_t pre_handler; /* Called after addr is executed, unless... */ kprobe_post_handler_t post_handler; /* * ... called if executing addr causes a fault (eg. page fault). * Return 1 if it handled fault, otherwise kernel will see it. */ kprobe_fault_handler_t fault_handler; /* Saved opcode (which has been replaced with breakpoint) */ kprobe_opcode_t opcode; /* copy of the original instruction */ struct arch_specific_insn ainsn; /* * Indicates various status flags. * Protected by kprobe_mutex after this kprobe is registered. */ u32 flags; }; 其中各个字段的含义如下:struct hlist_node hlist:被用于kprobe全局hash,索引值为被探测点的地址;struct list_head list:用于链接同一被探测点的不同探测kprobe;kprobe_opcode_t *addr:被探测点的地址;const char *symbol_name:被探测函数的名字;unsigned int offset:被探测点在函数内部的偏移,用于探测函数内部的指令,如果该值为0表示函数的入口;kprobe_pre_handler_t pre_handler:在被探测点指令执行之前调用的回调函数;kprobe_post_handler_t post_handler:在被探测指令执行之后调用的回调函数;kprobe_fault_handler_t fault_handler:在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数;kprobe_break_handler_t break_handler:在执行某一kprobe过程中触发了断点指令后会调用该函数,用于实现jprobe;kprobe_opcode_t opcode:保存的被探测点原始指令;struct arch_specific_insn ainsn:被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数);u32 flags:状态标记。涉及的API函数接口如下:int register_kprobe(struct kprobe *kp) //向内核注册kprobe探测点 void unregister_kprobe(struct kprobe *kp) //卸载kprobe探测点 int register_kprobes(struct kprobe **kps, int num) //注册探测函数向量,包含多个探测点 void unregister_kprobes(struct kprobe **kps, int num) //卸载探测函数向量,包含多个探测点 int disable_kprobe(struct kprobe *kp) //临时暂停指定探测点的探测 int enable_kprobe(struct kprobe *kp) //恢复指定探测点的探测5. kprobe怎么使用?--使用kprobe on ftrace模块加载的终究不是很方便,尤其对于一些不带gcc的嵌入式系统,需要交叉编译ko,将ko拷贝到单板,然后insmod,不太方便。debugfs下(确切地说,应该是ftrace)提供了一套注册、使能、注销kprobe的接口,可以很方便地操作kprobe。用法如下:需要打开如下kernel config,打开"General setup"->"Kprobes",以及"Kernel hacking"->"Tracers"->"Enable kprobes-based dynamic events"。CONFIG_KPROBES=y CONFIG_OPTPROBES=y CONFIG_KPROBES_ON_FTRACE=y CONFIG_UPROBES=y CONFIG_KRETPROBES=y CONFIG_HAVE_KPROBES=y CONFIG_HAVE_KRETPROBES=y CONFIG_HAVE_OPTPROBES=y CONFIG_HAVE_KPROBES_ON_FTRACE=y CONFIG_KPROBE_EVENT=y使能ftrace有些系统没有挂载debugfs,需要先挂载下;然后再使能ftracecd /sys/kernel/debug/tracing mount -t debugfs nodev /sys/kernel/debug echo 1 > tracing_on注册kprobe事件进入到tracing目录,这里就是传说中ftrace的天下了,执行echo "p:sys_write_event ksys_write" > kprobe_events向kprobe_events写入"p:sys_write ksys_write",注册kprobe事件。你会发现,当前目录下的events下,新增一个kprobes目录,该目录下:evb:/sys/kernel/debug/tracing # ls events/kprobes/ enable filter sys_write_event即,我们注册的kprobe事件生效了。那么"p:sys_write_event ksys_write"是什么意思呢?首先p表示我们要注册一个kprobe,如果要注册retprobe,此处应为r;sys_write_event表示这个kprobe叫什么名字;ksys_write表示我们的插入点在哪里。那么,"p:sys_write_event ksys_write"的语义就很明显了:在函数ksys_write处插入一个kprobe点,这个点的名字叫sys_write_event。3.使能kprobe向该events中的enable节点写入1,然后再cat 下trace节点,即可看到evb:/sys/kernel/debug/tracing # echo 1 > events/kprobes/sys_write_event/enable evb:/sys/kernel/debug/tracing # cat trace # tracer: nop # # entries-in-buffer/entries-written: 1276/7596 #P:4 # # _-----=> irqs-off # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / delay # TASK-PID CPU# |||| TIMESTAMP FUNCTION # | | | |||| | | mali-cmar-backe-7239 [002] d... 5020.025131: sys_write_event: (ksys_write+0x0/0xd8) mali-cmar-backe-7239 [002] d... 5020.025149: sys_write_event: (ksys_write+0x0/0xd8) mali-cmar-backe-7239 [002] d... 5020.025157: sys_write_event: (ksys_write+0x0/0xd8) mali-cmar-backe-7239 [002] d... 5020.025342: sys_write_event: (ksys_write+0x0/0xd8) mali-cmar-backe-7239 [002] d... 5020.025410: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.036317: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.041591: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.041673: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.041808: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.049699: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.052805: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.054392: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.054486: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.054580: sys_write_event: (ksys_write+0x0/0xd8) TimerDispatch-2579 [002] d... 5020.067048: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.086188: sys_write_event: (ksys_write+0x0/0xd8) Colors-12808 [002] d... 5020.099746: sys_write_event: (ksys_write+0x0/0xd8) sh-2728 [002] d... 5020.136629: sys_write_event: (ksys_write+0x0/0xd8)输出说明:pid为7239的进程,在自本次开机5020.025131秒的时候,调用了一次函数ksys_write。5.关闭kprobe先关闭kprobeevb:/sys/kernel/debug/tracing # echo 0 > events/kprobes/sys_write_event/enable 再注销kprobe,evb:/sys/kernel/debug/tracing # echo "-:kprobes/sys_write_event" > kprobe_events通过这个例子,我们可以对基于ftrace使用kprobe有个直观的感受。6. kprobe事件配置kprobe事件相关的节点有如下:/sys/kernel/debug/tracing/kprobe_events-----------------------配置kprobe事件属性,增加事件之后会在kprobes下面生成对应目录。 /sys/kernel/debug/tracing/kprobe_profile----------------------kprobe事件统计属性文件。 /sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/enabled-------使能kprobe事件 /sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/filter--------过滤kprobe事件 /sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/format--------查询kprobe事件显示格式新增一个kprobes事件,通过写kprobe_events来设置。kprobe_events文件支持3中格式的输入:p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS]-------------------设置一个probe探测点 r[:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS]------------------------------设置一个return probe探测点 -:[GRP/]EVENT----------------------------------------------------------删除一个探测点解释如下:GRP : Group name. If omitted, use "kprobes" for it.------------设置后会在events/kprobes下创建<GRP>目录。 EVENT : Event name. If omitted, the event name is generated based on SYM+offs or MEMADDR.---指定后在events/kprobes/<GRP>生成<EVENT>目录。 MOD : Module name which has given SYM.--------------------------模块名,一般不设 SYM[+offs] : Symbol+offset where the probe is inserted.-------------被探测函数名和偏移 MEMADDR : Address where the probe is inserted.----------------------指定被探测的内存绝对地址 FETCHARGS : Arguments. Each probe can have up to 128 args.----------指定要获取的参数信息。 %REG : Fetch register REG---------------------------------------获取指定寄存器值 @ADDR : Fetch memory at ADDR (ADDR should be in kernel)--------获取指定内存地址的值 @SYM[+|-offs] : Fetch memory at SYM +|- offs (SYM should be a data symbol)---获取全局变量的值 $stackN : Fetch Nth entry of stack (N >= 0)----------------------------------获取指定栈空间值,即sp寄存器+N后的位置值 $stack : Fetch stack address.-----------------------------------------------获取sp寄存器值 $retval : Fetch return value.(*)--------------------------------------------获取返回值,用户return kprobe $comm : Fetch current task comm.----------------------------------------获取对应进程名称。 +|-offs(FETCHARG) : Fetch memory at FETCHARG +|- offs address.(**)------------- NAME=FETCHARG : Set NAME as the argument name of FETCHARG. FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types (x8/x16/x32/x64), "string" and bitfield are supported.----------------设置参数的类型,可以支持字符串和比特类型 (*) only for return probe. (**) this is useful for fetching a field of data structures.执行如下两条命令就会生成目录/sys/kernel/debug/tracing/events/kprobes/myprobe;第三条命令则可以删除指定kprobe事件,如果要全部删除则echo > /sys/kernel/debug/tracing/kprobe_events。echo 'p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)' > /sys/kernel/debug/tracing/kprobe_events echo 'r:myretprobe do_sys_open ret=$retval' >> /sys/kernel/debug/tracing/kprobe_events-----------------------------------------------------这里面一定要用">>",不然就会覆盖前面的设置。 echo '-:myprobe' >> /sys/kernel/debug/tracing/kprobe_events echo '-:myretprobe' >> /sys/kernel/debug/tracing/kprobe_events参数后面的寄存器是跟架构相关的,%ax、%dx、%cx表示第1/2/3个参数,超出部分使用$stack来存储参数。函数返回值保存在$retval中。对kprobe事件的是能通过往对应事件的enable写1开启探测;写0暂停探测。echo 1 > /sys/kernel/debug/tracing/trace echo 'p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)' > /sys/kernel/debug/tracing/kprobe_events echo 'r:myretprobe do_sys_open ret=$retval' >> /sys/kernel/debug/tracing/kprobe_events echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable echo 1 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable ls echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable echo 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable cat /sys/kernel/debug/tracing/trace然后在/sys/kernel/debug/tracing/trace中可以看到结果。sourceinsight4.-3356 [000] .... 3542865.754536: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd6764a0 filename=0x8000 flags=0x1b6 mode=0xe3afff48ffffffff bash-26041 [001] .... 3542865.757014: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8241 flags=0x1b6 mode=0xe0c0ff48ffffffff ls-18078 [005] .... 3542865.757950: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1 mode=0xc1b7bf48ffffffff ls-18078 [005] d... 3542865.757953: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 ls-18078 [005] .... 3542865.757966: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x6168 mode=0xc1b7bf48ffffffff ls-18078 [005] d... 3542865.757969: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 ls-18078 [005] .... 3542865.758001: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x6168 mode=0xc1b7bf48ffffffff ls-18078 [005] d... 3542865.758004: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 ls-18078 [005] .... 3542865.758030: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1000 mode=0xc1b7bf48ffffffff ls-18078 [005] d... 3542865.758033: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 ls-18078 [005] .... 3542865.758055: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1000 mode=0xc1b7bf48ffffffff ls-18078 [005] d... 3542865.758057: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 ls-18078 [005] .... 3542865.758080: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x19d0 mode=0xc1b7bf48ffffffff ls-18078 [005] d... 3542865.758082: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 ls-18078 [005] .... 3542865.758289: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8000 flags=0x1b6 mode=0xc1b7bf48ffffffff ls-18078 [005] d... 3542865.758297: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 ls-18078 [005] .... 3542865.758339: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x0 mode=0xc1b7bf48ffffffff ls-18078 [005] d... 3542865.758343: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 ls-18078 [005] .... 3542865.758444: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x98800 flags=0x2 mode=0xc1b7bf48ffffffff ls-18078 [005] d... 3542865.758446: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 bash-26041 [001] .... 3542865.760416: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8241 flags=0x1b6 mode=0xe0c0ff48ffffffff bash-26041 [001] d... 3542865.760426: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3 bash-26041 [001] d... 3542865.793477: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3kprobe事件过滤跟踪函数需要通过filter进行过滤,可以有效过滤掉冗余信息。filter文件用于设置过滤条件,可以减少trace中输出的信息,它支持的格式和c语言的表达式类似,支持 ==,!=,>,<,>=,<=判断,并且支持与&&,或||,还有()。echo 'filename==0x8241' > /sys/kernel/debug/tracing/events/kprobes/myprobe/filterkprobe和栈配合使用如果要在显示函数的同时显示其栈信息,可以通过配置trace_options来达到。echo stacktrace > /sys/kernel/debug/tracing/trace_optionskprobe_profile统计信息获取一段kprobe时间之后,可以再kprobe_profile中查看统计信息。后面两列分别表示命中和未命中的次数。cat /sys/kernel/debug/tracing/kprobe_profile myprobe7. do_fork()函数实例8. kprobe原理9. kprobes的特定与使用限制
2023年09月19日
300 阅读
0 评论
0 点赞
2023-09-19
Android平台ebpf初探
版权声明:本文为CSDN博主「内核工匠」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/feelabclihu/article/details/112386672
2023年09月19日
215 阅读
0 评论
0 点赞
2023-09-18
宋宝华:论Linux的页迁移(Page Migration)完整版
版权声明:本文为CSDN博主「宋宝华」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/21cnbao/article/details/108067917对于用户空间的应用程序,我们通常根本不关心page的物理存放位置,因为我们用的是虚拟地址。所以,只要虚拟地址不变,哪怕这个页在物理上从DDR的这里飞到DDR的那里,用户都基本不感知。那么,为什么要写一篇论述页迁移的文章呢?我认为有2种场景下,你会关注这个Page迁移的问题:一个是在Linux里面写实时程序,尤其是Linux的RT补丁打上后的情况,你希望你的应用有一个确定的时延,不希望跑着跑着你的Page正在换位置而导致的延迟;再一个场景就是在用户空间做DMA的场景,尤其是SVA(SharedVirtual Addressing),设备和CPU共享页表,设备共享进程的虚拟地址空间的场景,如果你DMA的page跑来跑去,势必导致设备DMA的暂停,设备的传输性能出现严重抖动。这种场景下,设备的IOMMU和CPU的MMU会共享Page table:1. COW导致的页面迁移1.1 fork典型的CoW(写时拷贝)与fork()相关,当父子兄弟进程共享一部分page,而这些page本身又应该是具备独占属性的时候,这样的page会被标注为只读的,并在某进程进行写动作的时候,产生page fault,其后内核为其申请新的page。比如下面的代码中,把10写成20的进程,在写的过程中,会得到一页新的内存,data原本的虚拟地址会指向新的物理地址,从而发生page的migration。类似的场景还有页面的私有映射引发的CoW:运行这个程序,通过smem观察,可以明显看到,父进程执行新的写后(程序打印“cow is done in parent process”后),父子进程不再共享相关page,他们的USS/PSS都显著增大:由fork衍生的写时拷贝场景相对来说比较基础,在此我们不再赘述。1.2 KSM其他的CoW的场景有KSM(Kernel same-page merging)。KSM会扫描多个进程的内存,如果发现有page的内容是一模一样的,则会将其merge为一个page,并将其标注为写保护的。之后对这个page执行CoW,谁写谁得到新的拷贝。比如,你在用qemu启动一个虚拟机的时候,使用mem-merge=on,就可以促使多个VM共享许多page,从而有利于实现“超卖”。sudo /x86_64-softmmu/qemu-system-x86_64 \ -enable-kvm -m 1G \ -machine mem-merge=on不过这本身也引起了虚拟机的一些安全漏洞,可被side-channel攻击。比如,把下面的代码编译为a.out,并且启动两份a.out进程./a.out&./a.out代码:我们看到这2个a.out的内存消耗情况如下:但是,如果我们把中间的if 0改为if 1,也就是暗示mmap()的这1MB内存可能要merge,则耗费内存的情况发生显著变化:耗费的内存大大减小了。我们可以看看pageshare的情况:Merge发生在进程内部,也发生在进程之间。当然,如果在page已经被merge的情况下,谁再写merge过的page,则会引起写时拷贝,比如如下代码中的p[0]=100这句话。2. 内存规整导致的页面迁移2.1 CMA引起的内存迁移CMA (The Contiguous Memory Allocator)可运行作为dma_alloc_coherent()的后端,它的好处在于,CMA区域的空闲部分,可以被应用程序拿来申请MOVABLE的page。如下图中的一个CMA区域的红色部分已经被设备驱动通过dma_alloc_coherent()拿走,但是蓝色部分目前被用户进程通过malloc()等形式拿走。一旦设备驱动继续通过dma_alloc_coherent()申请更多的内存,则内核必须从别的非CMA区域里面申请一些page,然后把蓝色的区域往新申请的page移走。用户进程占有的蓝色page发现了迁移。CMA在内核的配置选项中依赖于MMU,且会自动使能MIGRATION(Pagemigration)和MEMORY_ISOLATION:2.2 alloc_pages当内核使能了COMPACTION,则Linux的底层buddy分配器会在alloc_pages()中尝试进行内存迁移以得到连续的大内存。COMPACTION这个选项也会使能CMA一节提及的MIGRATION选项。从代码的顺序上来看,alloc_pages()分配order比较高的连续内存的时候,是优先考虑COMPACTION,再次考虑RECLAIM的。2.3 /proc/sys/vm/compact_memory当然,上面alloc_pages所提及的compaction也可以被用户手动的触发,触发方式:echo 1 >/proc/sys/vm/compact_memory将1写入compact_memory文件,则内核会对各个zone进行规整,以便能够尽可能地提供连续内存块。我的Ubuntu已经运行了一段时间,内存稍微有些碎片化了,我们来对比下手动执行compact_memory前后,buddy的情况:可以清晰地看出来,执行compact_memory后,DMA32 ZONE和NORMAL ZONE里面,order比较大的连续page数量都明显增大了。2.4 huge page再次展开内核的COMPACTION选型,你会发现COMPACTION会被透明巨页自动选中:这说明透明巨页是依赖于COMPACTION选项的。所谓透明巨页,无非就是应用程序在运行的时候,神不知鬼不觉地偷偷地就使用到了Hugepage的功能,这个过程对用户是透明的。与透明对应的无非就是不透明的巨页,这种方式下,应用程序需要显示地告诉内核我需要使用巨页。我们先来看看不透明的巨页是怎么玩的?一般用户程序可以这样写,在mmap里面会加上MAP_HUGETLB的Flag,当然这个巨页也必须是提前预设好的,否则mmap就会失败。ptr_ = mmap(NULL, memory_size_, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);比如下面的代码我们想申请2MB的巨页:程序执行的时候会返回错误,打印如下:$ ./a.out Hugetlb: Cannot allocate memory原因很简单,因为现在系统里面2MB的巨页数量和free的数量都是0:我们如何让它申请成功呢?我们首先需要保证系统里面有一定数量的巨页。这个时候我们可以写nr_hugepages得到巨页:我们现在让系统得到了10个大小为2048K的巨页。现在来重新运行a.out,就不在出错了,而且系统里面巨页的数量发生了变化:Free的数量从10页变成了9页。聪明的童鞋应该想到了,当我们尝试预留巨页的时候,它最终还是要走到buddy,假设系统里面没有连续的大内存,系统是否会进行内存迁移以帮忙规整出来巨页呢?这显然符合前面说的alloc_pages()的逻辑。从alloc_buddy_huge_page()函数的实现也可以看出这一点:另外,这种巨页的特点是“预留式”的,不会free给系统,也不会被swap。因此可有效防止用户态DMA的性能抖动。对于DPDK这样的场景,人们喜欢这种巨页分配,减少了页面的数量和TLB的miss,缩短了虚拟地址到物理地址的重定位的转换时间,因此提高了性能。当然,我们在运行时通过写nr_hugepages的方法设置巨页,这种方法未必一定能够成功。所以,工程中也可以考虑通过内核启动的bootargs来设置巨页,这样Linux开机的过程中,就可以直接从bootmem里面分配巨页,而不必在运行时通过order较高的alloc_pages()来获取。这个在内核文档的kernel-parameters.txt说的比较清楚,你可以在bootargs里面设置各种不同hugepagesize有多少个页数:透明巨页听起来是比较牛逼的,因为它不需要你在应用程序里面通过MAP_HUGETLB来显式地指定,但是实际的使用场景则未必这么牛逼。使用透明巨页的最激进的方法莫过于把enabled和defrag都设置为always:echo always >/sys/kernel/mm/transparent_hugepage/enabled echo always >/sys/kernel/mm/transparent_hugepage/defragenabled写入always暗示对所有的区域都尽可能使用透明巨页,defrag写入always暗示内核会激进地在用户申请内存的时候进行内存回收(RECLAIM)和规整(COMPACTION)来获得THP(透明巨页)。我们来前面的例子代码稍微进行更改,mmap16MB内存,并且去掉MAP_HUGETLB:运行这个程序,并且得到它的pmap情况:我们发现从00007f46b0744000开始,有16MB的anon内存区域,显然对应着我们代码里面的mmap(16*1024*1024)的区域。我们进一步最终/proc/15371/smaps,可以得到该区域的内存分布情况:显然该区域是THPeligible的,并且获得了透明巨页。内核文档filesystems/proc.rst对THPeligible的描述如下:"THPeligible" indicates whether the mapping is eligible for allocating THP pages - 1 if true, 0 otherwise. It just shows the current status.透明巨页的生成,显然会涉及到前面的内存COMPACTION过程。透明巨页在实际的用户场景里面,可能反而因为内存的RECLAIM和COMPACTION而降低了性能,比如有些VMA区域的寿命很短申请完使用后很快释放,或者某些使用大内存的进程是短命鬼,进行规整花了很久,而跑起来就释放了这部分内存,显然是不值得的。类似《权力的游戏》中的夜王,花了那么多季进行内存规整准备干夜王这个透明巨页,结果夜王上来就被秒杀了,你说我花了多时间追剧冤不冤?所以,透明巨页在实际的工程中,又引入了一个半透明的因子,就是内核可以只针对用户通过madvise()暗示了需要巨页的区间进行透明巨页分配,暗示的时候使用的参数是MADV_HUGEPAGE:所以,默认情况下,许多系统会把enabled和defrag都设置为madvise:echo madvise >/sys/kernel/mm/transparent_hugepage/enabled echo madvise >/sys/kernel/mm/transparent_hugepage/defrag或者干脆把透明巨页的功能关闭掉:echo never >/sys/kernel/mm/transparent_hugepage/enabled echo never >/sys/kernel/mm/transparent_hugepage/defrag如果我们只对madvise的区域采用透明巨页,则用户的代码可以这么写:既然我都已经这么写代码了,我还透明个什么鬼?所以,我宁可为了某种确定性,而去追求预留式的,非swap的巨页了。3. NUMA Balancing引起的页面迁移在一个典型的NUMA系统中,存在多个NODE,很可能每个NODE都有CPU和Memory,NODE和NODE之间通过某种总线再互联。下面中的NUMA系统有4个NODE,每个NODE有24个CPU和1个内存,NODE之间通过红线互联:在这样的系统中,通常CPU访问本地NODE节点的memory会比较快,而跨NODE访问memory则会慢很多(红色总线慢)。所以Linux的NUMA自动均衡机制,会尝试将内存迁移到正在访问它的CPU节点所在的NODE,如下图中绿色的memory经常被CPU24访问,但是它位于NODE0的memory:则Linux内核可能会将绿色内存迁移到CPU24所在的本地memory:这样CPU24访问它的时候就会快很多。显然NUMA_BALANCING也是依赖MIGRATION机制的:下面我们来写个多线程的程序,这个程序里面有28个线程(一个主线程,26个dummy线程执行死循环,以及一个写内存的线程):我们开那么多线程的目的,无非是为了让write_thread_start对应的线程,尽可能地不被分配到主线程所在的NUMA节点。这个程序的主线程最开始写了64MB申请的内存,30秒后,通过write_done=1来暗示write_thread_start()线程你可以开始写了,write_thread_start()则会把这64MB也写一遍,如果主线程和write_thread_start()线程不在一个NODE节点的话,内存迁移就有可能发生。这是我们刚开始2秒的时候获得的该进程的numastat,可以看出,这64MB内存几乎都在NODE3上面:但是30秒后,我们再次看它的NUMA状态,则发生了巨大的变化:64MB内存跑到NODE1上面去了。由此我们可以推断,write_thread_start()线程应该是在NODE1上面跑,从而引起了这个迁移的发生。当然,我们也可以通过numactl--cpunodebind=2类似的命令来规避这个问题,比如:# numactl --cpunodebind=2 ./a.outNUMA Balancing的原理是通过把进程的内存一部分一部分地周期性地进行unmap(比如每次256MB),在页表里面把扫描的部分的PTE设置为 “no access permission” ,以在其后访问它的时候,强制产生pagefault,进而探测page fault发生在本地NODE还是远端NODE,来获知CPU和memory是否较远的。这说明,哪怕没有真实的迁移发生,NUMA balancing也会导致进程的内存访问出现Page fault。4. Page migration究竟是怎么做的?内存规整和NUMA平衡等引发的Page migration的过程,一言以蔽之,就是把一个page从A位置移动到B位置。正所谓“当官的一动嘴,当差的跑断腿”。这个过程真地是一点都不简单。在一个真实的工程场景中,由于共享内存等原因,一个page可能被多个进程share,被映射到多个进程的VMA里面去,所以这个迁移过程,要伴随多个进程的虚实映射的更改(迁移前后虚拟地址都是不变的)以及相关address_space的radix tree从指向A到指向B的变更。此外,新page对应的一些flags,应该和旧page是一样的。如果时间停止,比如一旦我们开启了迁移page A到page B的过程,进程1,2,3,4都站那里不动了,内核其他人也站那里不动了,这个过程还相对来说比较简单。真实的情况是,在我们把A迁移到B的过程中,进程1,2,3,4应该有可能还会访问迁移中的page对应的虚拟地址。当迁移正在进行中,“树欲静而风不止”,用户进程和内核其他子系统不会因为你“想静静”就给你“静静”。迁移的步骤非常繁杂,我们抓主干放弃细节,主要以clean的page的迁移为分析目标,抛开dirty页面的writeback问题。整个迁移的过程中,除了进程1-4可以访问page里面的内容,内核内存管理子系统其他的代码,也可能访问page对应的元数据。应该避免内核的内存管理的其他子系统进来干扰。比如,我正在迁移pageA,结果你的内存管理子系统还在LRU里面扫描A,比如把pageA从inactiveLRU移到activeLRU。明天新皇帝就要打进京城了,老皇帝今天晚上还在修皇宫,这么无私奉献的皇帝也有?所以,迁移的过程中,我们先要用__isolate_lru_page()把page从LRU里面隔离出来,不要再在明天就要挂的皇帝上面费心思了。因为,你一边在修改老page的元数据,一边老page在灭亡中,这也太乱了吧?还有一种情况,A皇明天就要禅位给B皇,结果你今天晚上把A皇灭了,比如pageA被回收释放了,那么明天的皇帝交接仪式还如何进行?__isolate_lru_page()的核心代码如下,它阻止了Page A被释放,同时也避免LRU的扫描动作: if (likely(get_page_unless_zero(page))) { /* * Be careful not to clear PageLRU until after we're * sure the page is not being freed elsewhere -- the * page release code relies on it. */ ClearPageLRU(page); ret = 0; }它把page的_refcount进行了增加,这样这个pageA就不会在迁移给B的过程中,被内存回收子系统free掉。另外,ClearPageLRU()会清除掉page的PG_lru标记,使得PageLRU(page)为假,从而阻止了很多类似如下的LRU代码路径:static void __activate_page(struct page *page, struct lruvec *lruvec, void *arg) { if (PageLRU(page) && !PageActive(page) && !PageUnevictable(page)) { int lru = page_lru_base_type(page); int nr_pages = hpage_nr_pages(page); del_page_from_lru_list(page, lruvec, lru); SetPageActive(page); lru += LRU_ACTIVE; add_page_to_lru_list(page, lruvec, lru); … } }隔离的准备工作完成,我们开始进行迁移,然后就到了令人痛不欲生的内核函数:int migrate_pages(struct list_head *from, new_page_t get_new_page, free_page_t put_new_page, unsigned long private, enum migrate_mode mode, int reason);如果你看内核的https://www.kernel.org/doc/html/latest/vm/page_migration.html文档,相信看到这个地方,一共有20步,一定懵逼了:这个步骤太多,比较难以理顺,所以我们重点是抓核心矛盾:迁移必须是无缝的,不能因为你的迁移导致进程不能正常运行,迁移过程中,进程还是可能访问正在迁移的page!当页面A正在迁移到B的过程,原先的进程1-4访问对应的虚拟地址的时候,这个访问其实没有什么不合法。所以,我们必须保证它还是可以访问,当然这个访问可能是有延后的。大体可以分为3个阶段:在迁移的早期阶段,进程的页表项还是指向page A的,映射了page A的用户进程还是可以无障碍地访问到A;在迁移的中期阶段,他们访问不到A了,因为A被unmap了,但是这个时候它其实也访问不了B,因为B还没有map。有那么一段时间是一个皇位悬空的状态。但是进程1-4访问这个虚拟地址没有什么不合法啊!我们甚至都不需要让进程1-4感知到我们正在迁移。所以,在这个悬空的期间,进程1-4对虚拟地址进行访问的时候,我们应该能感知到这个访问的发生并让他们稍作等待,以便新皇登基后他们继续访问;在迁移的末期阶段,page B被map,新皇登基完成,先皇被抛弃,我们应该唤醒在“中期阶段”的进程1-4对该虚拟地址的访问,并让进程1-4能访问到page B。第2阶段,根据被迁移的page寻找进程,寻找VMA,以操作他们的PTE,用到了反向映射技术RMAP;而PTE修改后,用户进程继续访问相关page对应的虚拟地址,则会发生page fault,发生page fault后,进程阻塞等待,等待在发生page fault的page的lock上面,这一点从__migration_entry_wait()这个page fault处理函数的核心函数的注释和代码流程可以看出:put_and_wait_on_page_locked()会将进程放入等待队列,等待page的PG_locked标记被清楚,也就是等待第3阶段的完成,从而实现了无缝地交接。5. 如何规避页迁移5.1 mlock可以吗?mlock()的主要作用是是防止swap。我们可以用mlock()锁住匿名页或者有文件背景的页面。如果匿名页被mlock住了,对应的内存不会被写入swap分区;如果文件背景的页面被mlock住了,相关的pagecache不会被reclaim。有文件背景的页面的mlock稍微有点难理解,所以我们用如下代码演示了有文件背景的mmap区域被mlock:由于我们mlock了,所以这4096个字节必须常驻内存!但是mlock()并不能保证这4096个字节对应的物理页面不迁移,它最主要的作用是防止page被swap。这一点从mlock()的manpage上面看地非常清楚:mlock(),mlock2(), and mlockall() lock part or all of the calling process's virtualaddress space into RAM, preventing that memory from being paged to the swaparea.为了实际证明被mlock()的page还是可能被迁移,我们把前面演示NUMA Balance页迁移的程序稍微进行改动,把malloc的64MB内存进行mlock():再运行这个程序,效果如下:我们明显看出,Node0上面的64MB内存,随着write_thread_start()的执行,被迁移到了NODE1。在Linux中,执行mlock()操作的时候,相应的VMA会被设置VM_LOCKED标记。当然,用户mlock()的区域大小,并不一定正好等于mlock()对应区域的VMA大小,所以mlock()的过程中,会进行VMA的拆分或合并(原因是同一个VMA里面的flags应该是一样的)。我们都本着眼见为实的原则,用代码来说话。把前面那段文件背景页面的被mlock()前后进程的VMAdump一下,我们稍微改一下代码,mmap 1M但是只mlock其中的4K:在mlock前后我们dump这个进程的VMA。mlock前,我们看到一段1024K的VMA区域,背景为/home/baohua/file:在mlock()后我们再次观察,我们看到这个1024k被拆分了1020k+4k:开始的4K由于有了单独的VMA特性,1024被拆分为4+1020两个VMA。mlock()锁定区域的page,也会被mark成PG_mlocked(注意不是PG_locked),相关页面会被放入unevictable_list链表,page变成不可回收的page,从LRU剥离。与unevictable相反的page是可回收的,此类page会进入inactive或者activeLRU,内核的swap机制会扫描相关page和LRU决定对最不活跃的page进行回收。根据内核文档https://www.kernel.org/doc/Documentation/vm/unevictable-lru.txt,Linux supports migration of mlocked pages and otherunevictable pages. This involves simplymoving the PG_mlocked and PG_unevictable states from the old page to the newpage.所以,对于mlock()的page,还是可能被迁移,只是迁移后,在迁移完成的目标page上面,保留了原先的PG_mlocked标记,新的page还是LRU剥离的,仍然不会被swap机制扫描到。至于这种不可回收page的内存规整动作是否应该进行,则可以由/proc/sys/vm/compact_unevictable_allowed参数进行控制。如果我们使能了它,内存规整的代码也会扫描unevictableLRU,对不可回收page进行页面迁移。透明巨页是一个神奇的存在,假设我们透明巨页的大小是2MB,如果我们mlock()其中的1MB,则没有被mlock()的1MB是可以被回收的,当内核进行内存回收动作的时候,可以将2MB再次拆分为一个个的4KBpage,从而保证没有被mlock()的部分可以被回收。5.2 GUP可以吗?我们已经发现mlock()不具备防止迁移的功能。那么,如果我们要用用户空间的buffer做DMA,常规的途径是什么呢?Linux内核可以用GUP(get_user_pages的衍生变体),来pin住page,从而避免相关的page被迁移或被swap代码释放。这实际上给针对用户空间的buffer进行DMA提供了某种形式的安全保障。前面我们说过,mlock()可以避免page的swap,但是没法避免page的迁移和old page的释放,所以,如果我们把用户空间的一片被mlock()的区域交给外设去做DMA,比如DMA的方向是DMA_FROM_DEVICE,由于设备感知到的是page A(app通过参数传递的方式给驱动传递了用户空间buffer,传递的时候buffer还对应page A),但是实际在DMA发生前,可能内存已经被迁移到了B,Page A甚至都释放给buddy或者被别的程序申请走了,这个时候设备做DMA,还是往A里面写,那么极有可能导致莫名其妙的崩溃发生。所以,对于用户空间的buffer,直接进行DMA的场景,我们必须保证当我们在app里把buffer传递给driver的时候,假设这个时候buffer对应page A在我们做DMA的时刻,以及DMA进行中的时刻,pageA都必须是一直存在的。这个pageA不能被释放、重新分配给第三者。具体是如何做到的呢?我们可以看看V4L2驱动,如果用户空间传一个指针进来进行DMA,V4L2驱动的内核态是如何处理的,相关代码位于drivers/media/v4l2-core/videobuf-dma-sg.c:videobuf_dma_init_user_locked()根据用户空间buffer的开始地址和大小,计算出page的数量,然后调用pin_user_pages()把这些page在内核pin住。pin_user_pages()这个函数位于mm/gup.c,属于GUP家族函数的一部分。该函数的功能,顾名思义,就是把用户的page pin住。Pin住和mlock()是完全不同的概率,mlock()是针对VMA添加VMA_LOCKED属性,针对VMA映射的page添加PG_mlocked属性,要求page不可reclaim不被swap,但是从pageA到page B的迁移仍然可能发生,pageA仍然可能被释放,但是这个释放也无关紧要,因为我们已经有了B。所以本质上,mlock(p, size)是对虚拟地址p ~ p+size-1常驻内存的一种保障,或者说是对有VM_LOCKED属性的VMA区域有常驻内存的page的一种保障,至于常驻的是A还是B,其实都不违背mlock()的语义。mlock强调长期有人值班,不能掉链子,而不在乎你换不换班。Pin是完全不同的语义,pin强调的是贞节牌坊式的坚守。pin是把buffer对应的page本身让它无法释放,因为pin的过程,会导致这个page的引用计数增加GUP_PIN_COUNTING_BIAS(值为1024,我理解是一种代码的hack,以区分常规的get_user_pages和pin_user_pages,常规的get只是+1,而pin的主要目的是为了用user space的buffer做DMA操作):Pin page并没有改变page所在VMA区域的语义,这是和mlock()之间很大的区别,mlock()会导致VMA区域得到VM_LOCKED,但是我们并没有一个VM_PINNED这样的东西。从用户场景上来看,内核驱动Pin住了USERPTR对应的memory后,进一步,V4L2的代码videobuf_dma_map()会把这个buffer转换为sg进行map_sg操作,之后就可以DMA了。所以pin最主要的作用是保证DMA动作不至于访问内存的时候踏空。如果我们不再需要这些userbuffer来进行DMA,相关的page应该被unpin:Unpin显然会导致一次引用计数减去GUP_PIN_COUNTING_BIAS的动作,如果ref减去GUP_PIN_COUNTING_BIAS的结果成为0,证明我们是最后一个用户,应该调用__put_page()促使page被正确释放:这里还有一个特别值得关注的地方,如果我们进行的是DMA_FROM_DEVICE,也就是设备到内存的DMA,则相关的page可能被设备而不是CPU写dirty了,我们把unpin_user_pages_dirty_lock()的最后一个参数设置为了TRUE,这样unpin_user_pages_dirty_lock()会把相关Page的dirty标记置上,否则Linux内核甚至都不知道设备写过这个page。pin_user_pages()在内核态增加了page的refcount,从而让内存管理的其他子系统不会释放这个page(尽管它没有像mlock那样把page从LRU上面拿下来,但是swap的代码也不可能释放它),也成功地阻止了page migration在这一个page上面的发生,甚至我们想去hot remove被pin的page所在的内存,都不可能成功了。对于我们关注的页面迁移问题,pagemigration的代码会检查page的refcount,refcount不合适的page(pin本身导致了refcount不符合expected_count),不会被迁移:所以,被pin的userspace page总体是DMA安全的。这里还有2个比较tricky的问题:1.就是用户的page,被内核pin住了,用户在unpin之前进行unmap呢?首先我们不欢迎这样的动作,对于pin的主要场景如RDMA、VFIO等,我们强调pin住的page是LONGTERM的,一般用户空间弄好buffer,应该一直复用这个buffer。详见内核文档:core-api/pin_user_pages.rst也可以看include/linux/mm.h的一段注释:实际上,用户空间应该控制这个buffer是indefinite的。如果万一pin住的页面用户层面真地在unpin之前就unmap了呢?pin的refcount其实也阻止了这个page被释放,只是这个page所在对应的虚拟地址由于被unmap了,所以不再对CPU可见了。当然,还有一个madvise(p, size, MADV_DONTNEED),其行为更加诡异,曾经臭名昭著的DirtyCoW漏洞(CVE-2016-5195),就与它息息相关。它并不是unmap,但是暗示内核它对某段地址访问暂时完成了使命很长一段时间不想访问了,并且去掉了页表的映射。这样后期再次访问这个地址p~ p+size-1的时候,如果p底层对应的是一个共享映射,可以获得老page的值;如果底层对应的是一个MAP_PRIVATE的映射,则按需获得一个0填充的页面:MADV_DONTNEED显然也是与pin的语义是违背的,这属于不“under userspace control”的情况了。如果我们真地对私有映射的pin区域执行了MADV_DONTNEED呢?显然pin也阻止了相关page被释放,但是当CPU重新访问page对应的虚拟地址的时候,MADV_DONTNEED却要求“the caller will see zero-fill-on-demand pages upon subsequent page references”,所以你不能指望先用MADV_DONTNEED暗示了自己不要这个memory了,然后再反过来期待DMA后CPU还能获取到自己曾经抛弃的内存,因为“是你当初说分手,分手就分手,现在又要用真爱把我哄回来,爱情不是你想卖,想买就能卖,让我挣开,让我明白,放手你的爱”。下面的代码映射100MB,但是只对其中的前2页执行MADV_DONTNEED:程序运行打印的结果如下:前2页变成了0,第3页还是100。2.如果pin住的区域是一个私有映射的区域(mmap的时候,使用了MAP_PRIVATE),然后在此区域执行了CoW会怎样?在下面的代码中,我们有个假想的设备驱动/dev/kernel-driver-with-pin-support,它支持一个IOCTL来让用户pin userspace的page。此例中,pin的这部分内存是私有映射的,而父进程又fork了子进程,之后父进程尝试写自己pin过的区域,这时会引发写时拷贝(CoW),从而让p对应的底层page发生变化:这种编程方式,实际上也违背了用户空间应该控制这个buffer是indefinite的原则,只是更加隐晦,所以要用userspace buffer做DMA的话,也应该尽可能避免这种编程方式,一般要对用户态buffer做DMA的进程,应该是叶子进程。如果一定要fork,可以考虑对pin住的区域执行madvise(p, size, MADV_DONTFORK),以避免CoW,具体的原理在madvise的man page上说地很清楚:5.3 使用巨页?我们如果用户空间是用非透明的巨页的话,实际上这种巨页是“预留式”的,因此可有效的避免内存的回收、swap和因为memory compaction、Automatic NUMA balancing等导致的page migration等。一般用户这么获得hugetlb内存:base_ptr_ = mmap(NULL, memory_size_, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);它实际上免去了对pin、mlock()等的需求。由于巨页本身可减小TLB miss以及walk page table的路径长度,想必可带来性能提升。一般情况下,当我们想在系统预留n个巨页的时候,若系统有m个NUMA节点,Linux会倾向于每个NUMA节点预留n/m个巨页。如果采用内存本地化的NUMA策略,任务运行在节点A的话,内核会倾向于从节点A预留的巨页向目标任务分配,所以我们最好是对任务进行一下NUMA的绑定。6. 最后的话在对用户空间代码进行轻微控制的情况下,GUP是比较理想的、抗性能抖动的、安全的针对用户空间buffer进行DMA的途径。当然,某些场景下,用户空间程序具有很大的灵活多变性,比如经常要动态malloc、free,我们又要在这样的heap上面去做DMA,每次都去pin和unpin显然是不现实的。这个时候我们可以借用Linux实时编程技术里面常常采用的,让malloc/free在一个存在的堆池发生分配和释放的技术:诀窍就在于先申请一个大堆,通过mallopt(M_TRIM_THRESHOLD, -1UL)避免大堆在free的时候还给内核。之后所有的malloc,free,都是在事先预留好的大堆进行,不再需要每次malloc/free区域单独pin,它带来了类似SVA的编程灵活性。如果用户空间采用无GUP的SVA方案,我们可能需考虑屏蔽如下功能以防止性能抖动:避免使用transparent huge pages (THP);避免使用kernel samepage merging (KSM);避免使用automatic NUMA balancing;避免运行时修改非透明巨页的数量;避免因为CMA而触发的页迁移;对需要做DMA的区域执行mlock()避免其被swap。当然上述问题几乎都可以被用户应用使用非透明巨页来规避,因为非透明巨页天然获得“预留式”内存的特征。当然这种方法绑死了用户应用必须使用hugetlbfs的逻辑,可能要求用户修改bootargs等来在系统启动过程中预留巨页。
2023年09月18日
100 阅读
0 评论
0 点赞
2023-09-18
Linux内存管理--CMA技术原理分析
转载自https://blog.csdn.net/feelabclihu/article/details/129457653?spm=1001.2014.3001.5502前言本文介绍CMA(Contiguous Memory Allocator)技术原理,从源码分析CMA的初始化和分配流程,同时讲解涉及到的页面迁移、LRU(Least Rencntly Used)缓存、PCP(per cpu page)缓存等知识。1. CMA概述CMA是什么?为什么需要CMA?Linux伙伴系统(Buddy)使用 Page 粒度来管理内存,每个页面大小为4K。伙伴系统按照空闲内存块的长度,把内存挂载到不同长度的 free_list链表中。free_list 的单位是以 (2^order个Page) 来递增的,即 1 page、2 page、… 2^n,通常情况下最大 order 为10 对应的空闲内存大小为 4M bytes。我们使用伙伴系统来申请连续的物理页面最大的页面最大小4M bytes,且系统内存碎片化严重的时候,也很难分配到高order的页面。嵌入式系统上一些外设设备,如GPU、Camera,HDMI等都需要预留大量连续内存才能正常工作,且很多情况下仅4M连续内存是不足以满足设备的需求的,当然我们也可以使用memblock预留内存的方法来保留更大的连续内存,但这部分内存只能被设备所使用而Buddy使用不到,会导致内存浪费。CMA由此而生,我们即要能分配连续的大的内存空间给设备使用,平时设备不用时又要把内存给系统用,最大化利用内存。CMA连续内存分配器,主要是为了可用于分配连续的大块内存。系统初始化的时候会通过保留一片物理内存区域,平时设备驱动不用时,内存管理系统将该区域用于分配和管理可移动类型页面,提供给APP或者内核movable页面使用。设备驱动使用时,此时已经分配的页面则进行迁移走,该区域用于连续内存分配。在后续的章节中,我们主要通过阅读源码的方式,来介绍CMA的初始化、分配和页面迁移流程等。注:后续文中所贴源码为kernel5.4版本,代码截图会省略次要代码,只保留关键代码。2. CMA主要数据结构和API2.1 struct cma使用struct cma来描述一个CMA区域:base_pfn:CMA区域物理地址的起始page frame number(页帧号)count: CMA区域的页面数量bitmap:描述cma区域页面的分配情况,1表示已分配,0为空闲。order_per_bit:表示bitmap中一个bit所代表的页面数量(2^order_per_bit)。2.2 cma_init_reserved_mem从保留内存块里面获取一块地址为base、大小为size的内存,用来创建和初始化struct cma。2.3 cma_init_reserved_areascma_init_reserved_areas()-->cma_activate_area()-->init_cma_reserved_pageblock()-->set_pageblock_migratetype(page, MIGRATE_CMA)为了提升内存利用率,该函数用来将这CMA内存标记后归还给 buddy 系统,供 buddy作为可移动页面内存申请。2.4 cma_alloc用来从指定的CMA 区域上分配count个连续的页面,按照align对齐。2.5 cma_release用来释放已经分配count个连续的页面。3. CMA主要流程分析3.1 CMA初始化流程3.1.1 系统初始化:系统初始化过程需要先创建CMA区域,创建方法有:dts的reserved memory或者通过命令行参数。这里我们看经常使用的通过dts的reserved memory方式,物理内存的描述放置在dts中配置,比如:linux,cma 为CMA 区域名称。compatible须为“shared-dma-pool”。resuable 表示 cma 内存可被 buddy 系统使用。size 表示cma区域的大小,单位为字节alignment指定 CMA 区域的地址对齐大小。linux,cma-default 属性表示当前 cma 内存将会作为默认的cma pool 用于cma 内存的申请。在系统启动过程中,内核对上面描述的dtb文件进行解析,从而完成内存信息注册,调用流程为:setup_arch arm64_memblock_init early_init_fdt_scan_reserved_mem __reserved_mem_init_node__reserved_mem_init_node会遍历__reservedmem_of_table section中的内容,检查到dts中有compatible匹配(CMA这里为“shared-dma-pool”)就进一步执行对应的initfn。通过RESERVEDMEM_OF_DECLARE定义的都会被链接到__reservedmem_of_table这个section段中,最终会调到使用RESERVEDMEM_OF_DECLARE定义的函数,如下rmem_cma_setup:3.1.2 rmem_cma_setupcma_init_reserved_mem 从保留内存块里面获取一块地址为base、大小为size的内存,这里用dtb中解析出来的地址信息来初始化CMA,用来创建和初始化struct cma,代码很简单:如果dts指定了linux,cma-default,则将dma_contiguous_set_default指向这个CMA区域,使用dma_alloc_contiguous从CMA分配内存时,默认会从该区域分。执行到此, CMA和其它的保留内存是一样的,都是放在 memblock.reserved 中,这部分保留内存一样没能被 Buddy 系统用到。前面讲过为了提升内存利用率,还需要将CMA这部分内存标记后归还给 Buddy系统,供 Buddy作为可移动页面提供给APP或内核内存申请,由cma_init_reserved_areas来实现。3.1.3 cma_init_reserved_areas在内核初始化的后期会调用core_initcall描述的初始化函数:cma_init_reserved_areas,它直接调用cma_activate_area来实现。cma_activate_area根据cma大小分配bitmap,然后循环调用init_cma_reserved_pageblock来操作CMA区域中所有的页面, 看下源码:@1 CMA区域由一个bitmap来管理各个page的状态,cma_bitmap_maxno计算Bitmap需要多少内存,i变量表示该CMA eara有多少个pageblock(4M)。@2 遍历该CM区域中的所有的pageblock@3 确保CMA区域中的所有page都是在一个zone内@4 最终调用init_cma_reserved_pageblock,以pageblock为单位进行处理,设置migrate type为MIGRATE_CMA,将页面添加到伙伴系统中并更新zone管理的页面总数。如下:@1 将页面已经设置的reserved标志位清除掉。@2 将migratetype设置为MIGRATE_CMA@3 循环调用__free_pages函数,将CMA区域中所有的页面都释放到buddy系统中。@4 更新伙伴系统管理的内存数量。执行到此,后续这部分CMA内存就可以为buddy所申请。在伙伴系统中migratetype为movable并且分配flag带CMA,可以从CMA分配内存:3.2 CMA分配流程在阅读cma分配流程代码前,我们先看下它的函数调用流程,后面将通过源码对流程及各个函数进行分析。3.2.1 cma_alloc@1 bitmap的计算,主要是获取bimap最大的可用bit数(bitmap_maxno),此次分配需要多大的bitmap(bitmap_count)等。@2 根据上面计算得到的bitmap信息,从bitmap中找到一块空闲的位置。@3 一些特别情况(在后面会讲到)经常会导致CMA分配失败,当分配返回EBUSY时,需要msleep(100)再retry,默认会retry 5次。@4 将要分配的页面的对应bitmap先置位为1,表示已经分配了。@5 使用alloc_config_range来进行内存分配,在后面节详细分析。@6 分配失败则清除掉bitmap。3.2.2 内核中的“批处理”:LRU缓存和PCP缓存在分析alloc_config_range之前,先插讲两个知识点LRU缓存和PCP缓存,在阅读内核源码中,我们会发现内核很喜欢使用一些“批处理”的方法来提升效率,减少一些拿锁开销。1.LRU 缓存经典的LRU(Least Rencntly Used)链表算法如下图:注:详细的LRU算法介绍可以参考内核工匠之前的文章:kswapd介绍新分配的页面不断地加入ACTIVE LRU链表中,同时ACTIVE LRU链表也不断地取出将页面放入 INACTIVE LRU链表。链表中的锁(pgdat->lru_lock)竞争力度是非常强烈的,如果页面转移是一个一个进行的,那对锁的竞争将会十分严重。为了改善这种情况,内核加入了一个 PER-CPU的 LRU缓存(用 struct pagevec 表示),页面要加入LRU链表会先放入当前 CPU 的LRU缓存 中,直到LRU缓已经满了(一般为15个页面),再获取lru_lock,一次性将这些页面批量放入LRU链表。2.PCP(PER-CPU PAGES)缓存由于内存页面属于公共资源,系统中频繁分配释放页面,会因为获得释放锁(zone->lock),CPU之间的同步操作产生大量消耗。同样为了改善这种情况,内核加入了per cpu page 缓存(struct per_cpu_pages表示),每个CPU都从Buddy批发申请少量的页面存放在本地。当系统需要申请内存时,优先从PCP缓存拿,用完了再从buddy批发。释放时也优先放回该PCP缓存,缓存满了再放回buddy系统。内核之前只支持order=0的PCP,社区最新已经有补丁可以支持order>0的per-cpu。3.2.3 alloc_config_range函数:继续看cma_alloc流程的alloc_config_range要干哪些事情:简而言之,目的就是想从一块“脏的”连续内存块(已经被各种类型的内存使用),得到一块干净的连续内存块,要么是回收掉,要么是迁移走,最后将这块干净的连续内存返回给调用者使用,如下图:走读下代码:@1 start_isolate_page_range:将目标内存块的pageblock 的迁移类型由MIGRATE_CMA 变更为 MIGRATE_ISOLATE。因为buddy系统不会从 MIGRATE_ISOLATE 迁移类型的pageblock 分配页面,可以防止在cma分配过程中,这些页面又被人从Buddy分走。@2 drain_all_pages:回收per-cpu pages,前面已经有介绍过PCP,回收过程需要先将放在PCP缓存页面归还给Buddy。@3 __alloc_contig_migrate_range:将目标内存块已使用的页面进行迁移处理,迁移过程就是将页面内容复制到其他内存区域,并更新对该页面的引用。@3.1 lru_cache_disable: 因为在LRU缓存的页面是无法迁移的,需要先将pagevec页面刷到LRU,即将准备添加到LRU链表上,却还未加入LRU的页面(还待在LRU缓存)添加到LRU上,并关闭LRU缓存功能。@3.2 isolate_migratepages_range 隔离要分配区域已经被Buddy使用的page,存放到cc的链表中,返回的是最后扫描并处理的页框号。这里隔离主要是防止后续迁移过程,page被释放或者被LRU回收路径使用。@3.3 reclaim_clean_pages_from_list:对于干净的文件页,直接回收即可。@3.4 migrate_pages:该函数是页面迁移在内核态的主要接口,内核中涉及到页面迁移的功能大都会调到,它把可移动的物理页迁移到一个新分配的页面。在下一节详细介绍它。@3.5 lru_cache_enable迁移过程完成,重新使能LRU PAGEVEC@4.undo_isolate_page_range: @1的逆过程pageblock的迁移类型从 MIGRATE_ISOLATE 恢复为 MIGRATE_CMA。最后将这些页面返回给调用者。3.3 CMA释放流程cma_release释放CMA内存的代码很简单,就是把页面重新free给Buddy和清楚到cma的bitmap分配标识,这里直接贴一下代码:4. 页面迁移系统要使用CMA区域的内存,内存上的页面必须是可迁移的,这样子当设备要使用CMA时页面才能迁移走,那么哪些页面可以迁移呢?有两种类型:LRU上的页面,LRU链表上的页面为用户进程地址空间映射的页面,如匿名页和文件页,都是从buddy分配器migrate type为movable的pageblock上分来的。非LRU上,但是是movable页面。非LRU的页面通常是为kernel space分配的page,要实现迁移需要驱动实现page->mapping->a_ops中的相关方法。比如我们常见的zsmalloc内存分配器的页面就支持迁移。migrate_pages()是页面迁移在内核态的主要接口,内核中涉及到页面迁移的功能大都会调到它。如下图,migrate_pages()无非是要分配一个新的页面,断开旧页面的映射关系,重新简历映射到新的页面,并且要拷贝旧页面的内容到新页面、新页面的struct page属性要和旧页面设置得一样,最后释放旧的页面。下面来阅读下它的源码。4.1 migrate_pages:migrate_pages函数和参数:from: 准备迁移页面的链表get_new_page:申请新页面函数的指针putnew_page:释放新页面函数的指针private:传给get_new_page的参数,CMA这里没有使用到传NULLmode:迁移模式,CMA的迁移模式会设置为MIGRATE_SYNC。共有下面几种:reason:迁移原因,记录是什么功能触发了迁移的行为。因为内核许多路径都需要用migrate_pages来迁移比如还有内存规整、热插拔等。CMA传递的为MR_CONTIG_RANG,表示调用alloc_contig_range()分配连续内存。再看migrate_pages代码,它遍历 from链表,对每个page调用unmap_and_move来实现迁移处理。4.2 unmap_and_moveunmap_and_move函数的参数同migrate_pages一模一样,它调用get_new_page分配一个新页面,然后使用__unmap_and_move迁移页面到这个新分配的页面中,我们主要看下__unmap_and_move4.3 __unmap_and_move:@1尝试获取old page的页面锁PG_locked,若页面已经被其它进程持有了锁,则这里会尝试获取锁失败,对于MIGRATE_ASYNC模式的为异步迁移拿不到锁就直接跳过此页面。CMA迁移模式为MIGRATE_SYNC,这里一定使用lock_page一定要等到锁。@2处理正在回写的页面,根据迁移模式判断是否等待页面回写完成。MIGRATE_SYNC_LIGHT和MIGRATE_ASYNC不等待,cma迁移模式为MIGRATE_SYNC,会调用wait_on_page_writeback()函数等待页面回写完成。@3 对于匿名页,为了防止迁移过程anon_vma数据结构被释放了,需要使用page_get_anon_vma增加anon_vma->refcount引用计数。@4 获取new page的页面锁PG_locked,正常情况都能获取到。@5 判断这个页面是否属于非LRU页面,如果页面为非LRU页面,则通过调用move_to_new_page来处理,该函数会回调驱动的miratepage函数来进行页面迁移。如果是LRU页面,继续执行@6 @6 通过page_mapped()判断是否有用户PTE映射了改页面。如果有则调用try_to_unmap(),通过反向映射机制解除old page所有相关的PTE。@7 调用move_to_new_page,拷贝old page的内容和struct page属性数据到new page。对于LRU页面 move_to_new_page是通过调用migrate_page做了两件事:复制struct page的属性和页面内容。@8对页表进行迁移:remove_migration_ptes通过反向映射机制建立new page到进程的映射关系。@9 迁移完成释放old、new页面的PG_locked,当然对于匿名页我们也要put_anon_vma减少的anon_vma->refcount引用计数@10 对于非LRU页面,调用put_page,释放old page引用计数(_refcount减1)对于传统LRU putback_lru_page把newpage添加到LRU链表中。4.4 move_to_new_page在@5和@7中,非LRU和LRU页面都是通过move_to_new_page来复制页面,我们来看下他的实现:1.对于非LRU页面,该函数会回调驱动的miratepage函数来进行页面迁移比如在zsmalloc内存分配器会注册迁移回调函数,迁移流程这里会调用到zsmalloc的zs_page_migrate来迁移其申请的页面。zsmalloc内存分配器这里就不展开讲了,有兴趣的读者可以阅读zsmalloc的源码。2.对于LRU页面,调用migrate_page做了两件事:复制struct page的属性和页面内容。@7.1 struct page属性的复制:migrate_page_move_mapping 要先检查page的refcount是否符合预期,符合后之后会复制页面的映射数据,比如page->index、page->mapping以及PG_swapbacked这里顺带提一下refcount:refcount是struct page中重的引用计数,用来表示内核中引用改页面的次数。当refcount=0,表示该页面为空闲页面或即将要被释放的页面。当refcount的值>0,表示该页面已经被分配了且内核正在使用,暂时不会被释放。内核中使用get_page、pin_user_pages、get_user_pages等函数来增加_refcount的引用计数,可以防止在进行某些操作过程(比如添加入LRU)页面被其它路径释放了,同时他也会导致refcount不符合预期,也就是在这里不能迁移。@7.2 page页面内容的复制:copy_highpage就很简单了,使用kmap映射两个页面,再将旧页面的内存复制到新的页面。@7.3 migrate_page_states用来复制页面的flag,如PG_dirty,PG_XXX等标志位,也是属于struct page属性的复制。4.5 小结:整个迁移过程已经分析完,画出流程图如下,五、总结从上面章节分析,我们可以看到CMA的设计都是围绕这两点来做的:平时设备驱动不用时,CMA内存交给Buddy管理,这是在初始化流程cma_init_reserved_areas()或cma_release()来实现的。2.设备驱动要使用时,通过cma_alloc来申请物理连续的CMA内存。对于已经在Buddy被APP或者内核movable分配走了的页面,要通过回收或迁移将这块内存清理“干净”,最后将这块物理连续“干净”的内存返回给设备驱动使用。核心实现在alloc_config_range()和migrate_pages()函数。
2023年09月18日
176 阅读
0 评论
0 点赞
2023-09-15
[转载]TEE安全系统SMC深入理解
版权声明:本文为CSDN博主「内核工匠」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/feelabclihu/article/details/128157100
2023年09月15日
314 阅读
0 评论
0 点赞
2023-08-16
此内容被密码保护
加密文章,请前往内页查看详情
2023年08月16日
212 阅读
0 评论
0 点赞
2023-08-16
uboot启动流程(3)
暂无简介
2023年08月16日
446 阅读
0 评论
0 点赞
2023-08-15
uboot启动流程(2)
转载自https://zhuanlan.zhihu.com/p/520087511
2023年08月15日
596 阅读
0 评论
0 点赞
2023-08-15
uboot启动流程(1)
转载自https://zhuanlan.zhihu.com/p/520060653
2023年08月15日
633 阅读
0 评论
0 点赞
1
...
5
6
7
...
38