Linux 内核如何检测内存地址是否被修改以实现 COW?
How does the Linux kernel detect if a memory address was modified to implement COW?
源代码在这里:
#include <stdio.h>
#include <stdlib.h>
void main() {
int *a = malloc(sizeof(int));
*a = 11;
int b = 22;//on the stack
int pid = fork();
if (pid == 0) {
printf("pid=%d, a = %d, &a=%p\n", getpid(), *a, a);
printf("pid=%d, b = %d, &b=%p\n", getpid(), b, &b);
getchar();
*a = 33;// ===========cow=========happend here=====
b = 44;
printf("pid=%d, a = %d, &a=%p\n", getpid(), *a, a);
printf("pid=%d, b = %d, &b=%p\n", getpid(), b, &b);
} else {
printf("pid=%d, a = %d, &a=%p\n", getpid(), *a, a);
printf("pid=%d, b = %d, &b=%p\n", getpid(), b, &b);
}
pause();
}
这是将 33 写入 a 的行的 gdb 反汇编,
我在这里设置了一个断点。并启动这个程序。然后使用crash查看a
的物理地址
>│0x40073a <main+154> movl [=12=]x2c,-0x20(%rbp) //copy on write happend here │
│0x400741 <main+161> mov -0x18(%rbp),%rax │
│0x400745 <main+165> mov (%rax),%ebx
a的线性地址是0x602010,所以使用vtop,我得到了这个:
我们可以看到它们都指向同一个物理地址 2a683010
PID: 6468
COMMAND: "a.out"
TASK: ffff88007c317300 [THREAD_INFO: ffff880016728000]
CPU: 0
STATE: TASK_TRACED|TASK_WAKEKILL
crash> vtop 0x602010
VIRTUAL PHYSICAL
602010 2a683010
PML: 24e6f000 => 2409c067
PUD: 2409c000 => 7144067
PMD: 7144018 => 19847067
PTE: 19847010 => 800000002a683065
PAGE: 2a683000
PID: 6464
COMMAND: "a.out"
TASK: ffff880036992280 [THREAD_INFO: ffff880014e38000]
CPU: 0
STATE: TASK_TRACED|TASK_WAKEKILL
crash> vtop 0x602010
VIRTUAL PHYSICAL
602010 2a683010
PML: 36df9000 => 3a654067
PUD: 3a654000 => 1a71a067
PMD: 1a71a018 => 18f2a067
PTE: 18f2a010 => 800000002a683065
PAGE: 2a683000
在 gdb 中键入 ni 后(将 a 的值更改为 33),再次使用 vtop。我可以看到其中一个进程的物理地址发生了变化。
crash> vtop 0x602010
VIRTUAL PHYSICAL
602010 5d755010
PML: 24e6f000 => 2409c067
PUD: 2409c000 => 7144067
PMD: 7144018 => 19847067
PTE: 19847010 => 800000005d755067
PAGE: 5d755000
crash> vtop 0x602010
VIRTUAL PHYSICAL
602010 2a683010
PML: 36df9000 => 3a654067
PUD: 3a654000 => 1a71a067
PMD: 1a71a018 => 18f2a067
PTE: 18f2a010 => 800000002a683065
PAGE: 2a683000
我的问题是 cpu 执行
时发生了什么
movl [=15=]x2c,-0x20(%rbp)
内核如何知道它正在更改共享内存,因此需要在写入之前进行处理?我猜它正在使用类似页面错误中断的东西。但是我没有发现任何与此相关的中断。
如果内核负责,请提供内核源代码。
My question is what happend when cpu execute
movl [=10=]x2c,-0x20(%rbp)
How did the kernel know that it is changing a shared memory, so a coping is needed to perform before writing? I am guessing it is using something like page fault interrupt. But I did not found any interrupt related about this.
这是通过处理器和 OS 的共同努力实现的。
处理器端:
当CPU执行这样一条指令时:
movl [=11=]x2c,-0x20(%rbp)
即获取存储在 %rbp 寄存器中的地址并向其添加偏移量 -x20,然后发出对其的内存访问(movl)。
提交内存访问后,处理器将遍历硬件页面table(好吧,在大多数情况下,访问TLB是捷径,但我在这里只谈基本原理)。当然,页面 table 应该由 OS 预先设置。
假设处理器进入最后一级页面 table,并发现相应的页面 table 条目(将其称为 pte这个答案的其余部分的简称)对于那个地址
建议包含该地址内容的页面不在内存中!(它只是查询该 pte 的特定页面标志),然后,根据处理器体系结构,会引发硬件异常!根据 Intel 的术语,它把这种类型的异常归类为 fault,你一定经常听到 'page fault'(一种可以修复的异常并且可以恢复执行,就好像根本没有发生过这样的异常一样!)
OS方:
然后我们向上移动堆栈进入 OS 域。在启动过程中,OS 将设置一个异常和中断处理程序 table(在 x86 术语中我们称之为 IDT),并将其注册到处理器。
然后在这个页面错误发生时,预设置处理程序由处理器执行(从技术上讲,处理器应首先保存 CPU 上下文,如压入 cs 和 rip 寄存器,rflags 寄存器,等)。
handler 可以分为 arch-specific 部分(其中 OS 将进一步做一些硬件相关的工作,比如保存更多的寄存器,调用 arch-specific hook,判断是否允许页面错误? 等)和架构独立部分(页面错误逻辑),因此处理程序入口点依赖于架构也就不足为奇了。
对于 x86 上的 Linux,特定于 arch 的部分位于 arch/x86/entry/entry_64.S(对于 64 位)和 do_page_fault() arch/x86/mm/fault.c中的C函数。然后在do_page_fault()中,会调用arch-independent C函数handle_mm_fault(),它位于核心MM代码中mm/memory.c.
对于这道题,在handle_mm_fault()中,do_wp_page()处理COW逻辑。基本上,handle_mm_fault() 只是遍历错误地址的页面 table,发现它是一个写保护页面(存在,但未设置写标志),因此它调用 do_wp_page() 分配一个新页面。
源代码在这里:
#include <stdio.h>
#include <stdlib.h>
void main() {
int *a = malloc(sizeof(int));
*a = 11;
int b = 22;//on the stack
int pid = fork();
if (pid == 0) {
printf("pid=%d, a = %d, &a=%p\n", getpid(), *a, a);
printf("pid=%d, b = %d, &b=%p\n", getpid(), b, &b);
getchar();
*a = 33;// ===========cow=========happend here=====
b = 44;
printf("pid=%d, a = %d, &a=%p\n", getpid(), *a, a);
printf("pid=%d, b = %d, &b=%p\n", getpid(), b, &b);
} else {
printf("pid=%d, a = %d, &a=%p\n", getpid(), *a, a);
printf("pid=%d, b = %d, &b=%p\n", getpid(), b, &b);
}
pause();
}
这是将 33 写入 a 的行的 gdb 反汇编, 我在这里设置了一个断点。并启动这个程序。然后使用crash查看a
的物理地址 >│0x40073a <main+154> movl [=12=]x2c,-0x20(%rbp) //copy on write happend here │
│0x400741 <main+161> mov -0x18(%rbp),%rax │
│0x400745 <main+165> mov (%rax),%ebx
a的线性地址是0x602010,所以使用vtop,我得到了这个:
我们可以看到它们都指向同一个物理地址 2a683010
PID: 6468
COMMAND: "a.out"
TASK: ffff88007c317300 [THREAD_INFO: ffff880016728000]
CPU: 0
STATE: TASK_TRACED|TASK_WAKEKILL
crash> vtop 0x602010
VIRTUAL PHYSICAL
602010 2a683010
PML: 24e6f000 => 2409c067
PUD: 2409c000 => 7144067
PMD: 7144018 => 19847067
PTE: 19847010 => 800000002a683065
PAGE: 2a683000
PID: 6464
COMMAND: "a.out"
TASK: ffff880036992280 [THREAD_INFO: ffff880014e38000]
CPU: 0
STATE: TASK_TRACED|TASK_WAKEKILL
crash> vtop 0x602010
VIRTUAL PHYSICAL
602010 2a683010
PML: 36df9000 => 3a654067
PUD: 3a654000 => 1a71a067
PMD: 1a71a018 => 18f2a067
PTE: 18f2a010 => 800000002a683065
PAGE: 2a683000
在 gdb 中键入 ni 后(将 a 的值更改为 33),再次使用 vtop。我可以看到其中一个进程的物理地址发生了变化。
crash> vtop 0x602010
VIRTUAL PHYSICAL
602010 5d755010
PML: 24e6f000 => 2409c067
PUD: 2409c000 => 7144067
PMD: 7144018 => 19847067
PTE: 19847010 => 800000005d755067
PAGE: 5d755000
crash> vtop 0x602010
VIRTUAL PHYSICAL
602010 2a683010
PML: 36df9000 => 3a654067
PUD: 3a654000 => 1a71a067
PMD: 1a71a018 => 18f2a067
PTE: 18f2a010 => 800000002a683065
PAGE: 2a683000
我的问题是 cpu 执行
时发生了什么movl [=15=]x2c,-0x20(%rbp)
内核如何知道它正在更改共享内存,因此需要在写入之前进行处理?我猜它正在使用类似页面错误中断的东西。但是我没有发现任何与此相关的中断。
如果内核负责,请提供内核源代码。
My question is what happend when cpu execute
movl [=10=]x2c,-0x20(%rbp)
How did the kernel know that it is changing a shared memory, so a coping is needed to perform before writing? I am guessing it is using something like page fault interrupt. But I did not found any interrupt related about this.
这是通过处理器和 OS 的共同努力实现的。
处理器端:
当CPU执行这样一条指令时:
movl [=11=]x2c,-0x20(%rbp)
即获取存储在 %rbp 寄存器中的地址并向其添加偏移量 -x20,然后发出对其的内存访问(movl)。
提交内存访问后,处理器将遍历硬件页面table(好吧,在大多数情况下,访问TLB是捷径,但我在这里只谈基本原理)。当然,页面 table 应该由 OS 预先设置。
假设处理器进入最后一级页面 table,并发现相应的页面 table 条目(将其称为 pte这个答案的其余部分的简称)对于那个地址 建议包含该地址内容的页面不在内存中!(它只是查询该 pte 的特定页面标志),然后,根据处理器体系结构,会引发硬件异常!根据 Intel 的术语,它把这种类型的异常归类为 fault,你一定经常听到 'page fault'(一种可以修复的异常并且可以恢复执行,就好像根本没有发生过这样的异常一样!)
OS方:
然后我们向上移动堆栈进入 OS 域。在启动过程中,OS 将设置一个异常和中断处理程序 table(在 x86 术语中我们称之为 IDT),并将其注册到处理器。
然后在这个页面错误发生时,预设置处理程序由处理器执行(从技术上讲,处理器应首先保存 CPU 上下文,如压入 cs 和 rip 寄存器,rflags 寄存器,等)。
handler 可以分为 arch-specific 部分(其中 OS 将进一步做一些硬件相关的工作,比如保存更多的寄存器,调用 arch-specific hook,判断是否允许页面错误? 等)和架构独立部分(页面错误逻辑),因此处理程序入口点依赖于架构也就不足为奇了。
对于 x86 上的 Linux,特定于 arch 的部分位于 arch/x86/entry/entry_64.S(对于 64 位)和 do_page_fault() arch/x86/mm/fault.c中的C函数。然后在do_page_fault()中,会调用arch-independent C函数handle_mm_fault(),它位于核心MM代码中mm/memory.c.
对于这道题,在handle_mm_fault()中,do_wp_page()处理COW逻辑。基本上,handle_mm_fault() 只是遍历错误地址的页面 table,发现它是一个写保护页面(存在,但未设置写标志),因此它调用 do_wp_page() 分配一个新页面。