fork()系统调用创建一个子进程,是父进程的一个副本,父子进程仅有pid的区别。
子进程拥有与父进程相同的进程虚拟地址空间,但如果在fork()时复制父进程的整个地址空间,虽然实现了创建副本的目的,但这种做法不太聪明,因为直接复制父进程的所有内存页是非常耗费资源的,特别是当父进程占用了大量内存时。
1.当子进程对地址空间上的数据进行读操作时,没必要重新创建一个副本供子进程来读,直接读父进程的地址可以达到同样的效果
2.当子进程对地址空间上的某一页进行写(修改)操作时,由于逻辑上父子进程拥有独立的地址空间,此时修改的必须是子进程自己的地址空间,此时再分配给子进程一页地址空间,这一页空间才是真正意义上属于子进程自己的
fork()
的实现细节当父进程调用 fork()
时,操作系统会进行以下操作:
fork()
时,内核不会复制父进程的所有内存,而是只复制父进程的页表,使子进程的页表指向相同的物理内存页。fork()
时,子进程会共享父进程的所有内存页,这些内存页都会被标记为只读。这样,只有试图写入的内存页会被复制,其他未被修改的内存页依然是共享的和只读的。
假设有一个进程 P
,其内存布局如下:
虚拟地址 | 物理地址 | 内容 |
---|---|---|
0x1000 | 0xA000 | Data1 |
0x2000 | 0xB000 | Data2 |
调用 fork()
:
C
,复制页表并共享内存页。进程 | 虚拟地址 | 物理地址 | 内容 |
---|---|---|---|
P | 0x1000 | 0xA000 | Data1 |
P | 0x2000 | 0xB000 | Data2 |
C | 0x1000 | 0xA000 | Data1 |
C | 0x2000 | 0xB000 | Data2 |
C
修改 0x1000
地址的内容,触发页面保护异常。0xC000
。0xA000
页的内容复制到 0xC000
。C
的页表,使 0x1000
虚拟地址指向 0xC000
。0xC000
设置为可写。进程 | 虚拟地址 | 物理地址 | 内容 |
---|---|---|---|
P | 0x1000 | 0xA000 | Data1 |
P | 0x2000 | 0xB000 | Data2 |
C | 0x1000 | 0xC000 | Data1 (Modified) |
C | 0x2000 | 0xB000 | Data2 |
fork()
调用时立即复制整个地址空间,提高了系统调用的性能。推荐学习 https://xxetb.xetslk.com/s/p5Ibb