前言
熟悉 linux 进程机制的人都知道 linux 中新建进程是以 fork + exec 的形式创建的进程
fork 的时候复制了父进程的相关数据结构, 然后更新了待执行的 binary, 去执行
然后 父子进程之间 内存管理是 基于 copy on write 的
对于某块物理页, fork 之后内存设置为 只读, 然后 当出现写的请求的时候 复制该物理页, 然后 进行操作
测试用例
用例主要是 申请了一块空间, 然后设置为 aaa
然后 fork 了一下, 单个进程 变成了 父子进程, 然后 父进程将 空间设置为 bbbb, 子进程将 空间设置为 ccccc
这里 就是调试 我们上面的 cow 的流程
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
void processHandler(int pid, char *p1, char *processName) {
printf(" %s - %d, p1 : 0x%x, %s \n", processName, pid, p1, p1);
}
int main(int argc, char **argv) {
char *p1 = (char *) malloc(20);
memset(p1, 'a', 16);
printf(" %s - %d, p1 : 0x%x, %s \n", "parent", getpid(), p1, p1);
int childPid = fork();
sleep(rand() % 5);
if (childPid == 0) {
memset(p1, 'b', 16);
processHandler(getpid(), p1, "child1");
} else {
memset(p1, 'c', 16);
processHandler(getpid(), p1, "parent");
}
}
cow 父进程 和 子进程 页表项设置为只读
首先禁用掉 ASLR, 在这种 比较小的 case, malloc 调用 brk 申请第一个块空间, 首地址会是 0x602000
将父进程 和 子进程的页表项设置为只读的地方在这里
在 copy_pte_range 中可以看到这个 vma 的区间, [6299648 - 6434816] 即为 [0x602000 - 0x6230000] 我们 malloc 申请的这部分的空间 属于这个 vma
当 父进程 / 子进程 修改了对应的页的内容, cow 复制物理页
按照我们这里的目前的规则, 一次执行会生成两个进程, 不会产生其他的进程
父进程为 奇数, 子进程为 偶数
这里便是 父/子 进程尝试修改 [0x602000 - 0x6230000] 所在的空间, 然后 kernel 的相关处理
先修改的进程 复制原来的物理页, 并设置为 可读写, 然后 对应的进程 再去操作给定的物理页
后修改的进程 发现可以只有当前进程使用该物理页, 直接复用该物理页
物理页的复制是基于 kmap_atomic + memcpy 来实现的
这里的 先操作的进程号是 313, 对应的是 父进程
后修改的进程 发现可以只有当前进程使用该物理页, 直接复用该物理页
(initramfs) ./Test23MemoryCowChildFirst
parent - 313, p1 : 0x602010, aaaaaaaaaaaaaaaa
child1 - 314, p1 : 0x602010, bbbbbbbbbbbbbbbb
parent - 313, p1 : 0x602010, cccccccccccccccc
这里 先操作的是子进程 先修改对应的只读页 的时候
(initramfs) ./Test23MemoryCowChildFirst
parent - 319, p1 : 0x602010, aaaaaaaaaaaaaaaa
child1 - 320, p1 : 0x602010, bbbbbbbbbbbbbbbb
parent - 319, p1 : 0x602010, cccccccccccccccc
完
参考
详细讲解Linux内核写时复制技术COW机制(手撕源代码)