fork
是Unix和类Unix操作系统中用于创建进程的系统调用。fork
会创建一个子进程,该子进程几乎是父进程的完全拷贝,包括代码段、数据段、堆和栈。然而,为了提高效率,fork
使用了一种叫做写时拷贝(Copy-On-Write, COW)的技术。
写时拷贝(Copy-On-Write, COW)
基本原理
写时拷贝的基本思想是:在创建子进程时,不立即复制父进程的内存空间,而是让父子进程共享同一块物理内存。只有当其中一个进程尝试修改这块内存时,操作系统才会在写入时进行拷贝,为修改的那块内存创建一个新的物理内存页。这种方式节省了内存并提高了性能,特别是在很多情况下,子进程会很快调用exec
类函数替换进程镜像,而无需实际修改父进程的内存。
内存方面的详细工作机制
-
内存页标记为只读: 当
fork
系统调用创建子进程时,父进程的内存页会被标记为只读(read-only)。父子进程共享这些只读的内存页。 -
页表复制: 父进程的页表(page table)会被复制到子进程中,但是这些页表指向的是相同的物理内存页。因为页表项是只读的,所以两个进程都不能修改这些内存页。
-
页故障(Page Fault)处理: 当父进程或子进程尝试写入某个内存页时,会触发页故障(page fault)。操作系统的页故障处理程序会检查这个页是否标记为写时拷贝。
-
实际拷贝: 在触发页故障后,操作系统会为该进程分配一个新的物理内存页,并将原来的内容复制到这个新的页中。然后,页表会被更新,指向新的物理内存页,并将这个页标记为可写(writable)。只有在这一时刻,实际的物理内存才会被复制。
-
内存保护更新: 新的页表项指向了新分配的物理内存页后,内存保护会被更新,允许对新内存页的写操作。
举个例子
假设有一个进程P,它的内存布局如下:
+-----------+
| 代码段 | -> 共享且只读
+-----------+
| 数据段 | -> 写时拷贝
+-----------+
| 堆 | -> 写时拷贝
+-----------+
| 栈 | -> 写时拷贝
+-----------+
当进程P调用fork
创建子进程C时,初始情况下,父子进程共享所有的物理内存页,并且这些页都被标记为只读。其页表情况如下:
P 页表:
+-----------+ +-----------+
| 代码段 | --> | 物理页1 |
+-----------+ +-----------+
| 数据段 | --> | 物理页2 |
+-----------+ +-----------+
| 堆 | --> | 物理页3 |
+-----------+ +-----------+
| 栈 | --> | 物理页4 |
+-----------+ +-----------+C 页表:
+-----------+ +-----------+
| 代码段 | --> | 物理页1 |
+-----------+ +-----------+
| 数据段 | --> | 物理页2 |
+-----------+ +-----------+
| 堆 | --> | 物理页3 |
+-----------+ +-----------+
| 栈 | --> | 物理页4 |
+-----------+ +-----------+
当子进程C尝试写入堆时(假设是物理页3),会触发页故障,操作系统执行写时拷贝:
- 分配新的物理页(物理页5)。
- 将物理页3的内容复制到物理页5。
- 更新子进程C的页表,使其堆指向物理页5,并标记为可写。
更新后的页表情况如下:
P 页表:
+-----------+ +-----------+
| 代码段 | --> | 物理页1 |
+-----------+ +-----------+
| 数据段 | --> | 物理页2 |
+-----------+ +-----------+
| 堆 | --> | 物理页3 |
+-----------+ +-----------+
| 栈 | --> | 物理页4 |
+-----------+ +-----------+C 页表:
+-----------+ +-----------+
| 代码段 | --> | 物理页1 |
+-----------+ +-----------+
| 数据段 | --> | 物理页2 |
+-----------+ +-----------+
| 堆 | --> | 物理页5 | -> 新分配的页,可写
+-----------+ +-----------+
| 栈 | --> | 物理页4 |
+-----------+ +-----------+
此时,子进程C对堆的修改不会影响到父进程P,确保了两个进程的内存隔离。
优点
-
节省内存:
- 使用写时拷贝技术,父进程和子进程在
fork
后共享相同的内存页,直到需要写入时才进行实际的物理页拷贝。这种方式减少了内存使用,尤其在子进程迅速调用exec
系列函数时优势明显。
- 使用写时拷贝技术,父进程和子进程在
-
提高效率:
- 避免了
fork
时对所有内存页进行立即拷贝的昂贵操作,提高了进程创建的效率。
- 避免了