我最近了解到 fork 甚至在寄存器上也应用 COW(写时复制)。
我玩了一个简单的玩具盒来验证:
(gdb) info inferiors
Num Description Connection Executable
* 1 process 59316 1 (native) /home/Drew/mycode/a.out
2 process 59386 1 (native) /home/Drew/mycode/a.out
(gdb) print( $rdi )
$1 = 18874385
(gdb) set $rdi=42
(gdb) print( $rdi )
$2 = 42
(gdb) inferior 2
[Switching to inferior 2 [process 59386] (/home/Drew/mycode/a.out)]
[Switching to thread 2.1 (process 59386)]
#0 0x00002aaaab090291 in fork () from /lib64/libc.so.6
(gdb) print( $rdi )
$3 = 18874385
正如你们看到的,修改父进程中的 rdi 不会干扰子进程。
我想知道它是怎么做到的?现代 CPU 是否专门为多进程提供多个相同的寄存器?
编辑:
感谢各位的评论。我会在这里集体回答评论中的问题,希望这有助于澄清。
我正在使用的参考资料是这份讲稿。
我一般想弄清楚:
- 无论是否是 COW,Linux 是否会复制寄存器?
- 如果答案是肯定的,那么它是由硬件完成的,即 CPU 制造商提供设计支持,就像他们支持虚拟机一样,或者由软件完成,比如说,操作系统将寄存器值存储在某处(在 RAM 中?),因此,它不是对寄存器本身进行操作,而是有效地修改页面,但以某种方式设法模仿寄存器行为?
- 如果 fork 不以任何方式复制寄存器,为什么我会在 GDB 中看到这种现象?
提供抢占式多任务的系统提供 CPU 虚拟化。这允许每个线程表现得好像它拥有自己的 CPU 核心(及其寄存器)。更具体地说,这允许线程使用寄存器,而不必担心它们在操作过程中被另一个线程(同一进程或不同进程)更改。
无论线程是如何创建的,情况都是如此,因此包括由 创建的线程
fork
。系统如何实现这一点并不重要。但它可以确保rdi
一个线程中对指令指针的任何更改都不会在另一个线程中看到。我不知道
fork
之后寄存器的初始值提供了什么保证fork
。它可能与系统 ABI 涵盖的其他调用提供相同的保证。有趣的事实:我曾经编写了一个根本不使用写时复制的小程序,在产生子进程后,除了寄存器和文件句柄之外,它们是相同的。
这实际上非常简单,只是根本不要写入 RAM。
查看手册页
clone()
,我们甚至可以在父进程和子进程之间共享 RAM。我们可以传递 CLONE_VM 而不是 CLONE_PID 来实现这一点。当您使用 fork()、vfork() 或 clone() 系统调用创建新线程时,寄存器复制只发生一次。
每个进程、每个线程本质上都必须有一个单独的寄存器组;这与进程(旧术语)或线程(新术语)的本质相关。
那么这是如何做到的呢?
每当内核切换线程时,旧的寄存器组都会存储在进程数据结构中。(记住术语;内核使用旧术语。)每当切换回来时,它都会恢复它们。实际上还有一点;进程数据结构实际上有内核的寄存器组;当发生触发上下文切换的中断时,用户空间寄存器组被推送到线程的内核堆栈上。