创建子进程 Fork()
在Unix操作系统中,fork()函数是一个创建新进程的系统调用,调用fork()函数会导致当前进程(称为父进程)创建出一个新的子进程。
PID:在类Unix系统中每个进程都有唯一一个正数的进程ID,这个ID通常称为PID(process ID)
先从一个fork示例中来了解fork方法
创建一个子进程
1 | int main(){ |
运行这段代码的输出如下
1 | child : x=2 |
调用一次,返回两次:fork函数被父进程调用后会返回两次,一次返回是在父进程中,一次返回是在子进程中
如何区分父子进程呢?通过fork的返回值pid = Fork();
当pid等于-1表示这次创建子进程失败了, 当pid等于0表示这次返回是子进程,而父进程中的fork返回的是子进程的PID。
调用一次fork的进程图
并发执行:我们可以看到上面代码的输出结果是先输出child后输出parent,但是实际情况无法预测,父进程和子进程是两个独立的进程,CPU会以任意方式交替执行逻辑控制流中的指令,不同的系统执行结果不一定相同。
相同但是独立的地址空间:在fork函数调用时,父子进程拥有相同地址空间、用户栈、本地变量值、堆、全局变量值和相同的代码。但是在返回之后父子进程拥有不同的地址空间,子进程复制了父进程的所有资源。这也是导致了上述示例中父子进程x的输出不一致。
Copy on Write:fork的资源复制过程在最新的Linux系统中会推迟资源的拷贝,会使用写时复制(Copy on Write)。假如进程只有读的事件,那么父子进程使用相同的代码段数据段,只有发生写事件时,系统才会拷贝给子进程一份资源。fork()的实际开销就是复制父进程的一个页表和为子进程创建一个进程描述符,使用COW会提高程序运行效率。
共享文件:当运行示例程序时,可以看到父子进程都把他们的输出显示在屏幕上。这是因为子进程继承了父进程打开的所有文件,当父进程调用fork时,stdout文件是打开的,并指向屏幕。子进程也继承了这个文件,因此它的输出也是指向屏幕的。
嵌套子进程
连着调用两次fork函数会发生什么呢?它会创建几个子进程?
1 | int main(){ |
下图是调用两次Fork()的进程图,第一次fork新创建了一个进程,子进程中也会保存了Fork()函数的调用代码。所以第二次fork父进程和子进程各自又创建了一个进程。
vfork
vfork()
系统调用也用于创建一个新进程,但是它有一些特殊的语义,主要用于创建一个新进程,该进程将与父进程共享地址空间(但有重要的限制)。
特点:
vfork()
保证子进程会先运行,在它调用exec
系列函数或者exit()
之前,父进程处于阻塞状态,直到子进程运行完或者调用了exec
函数。- 子进程与父进程共享地址空间,因此子进程对数据的修改可能会影响到父进程。
- 在
vfork()
中,通常子进程应该立即调用exec
系列函数来加载一个新的程序映像,而不是在子进程中做复杂的计算或者改变大量数据,这是因为修改数据可能会影响到父进程。
fork和vfork比较
- 内存管理:
fork()
创建子进程时会复制父进程的地址空间,而vfork()
则共享父进程的地址空间,因此vfork()
更加高效,但需要注意数据安全性。 - 执行顺序:
vfork()
保证子进程会先运行,在执行完毕或者调用exec
系列函数后,父进程才会继续执行。 - 适用场景:
fork()
适合一般的进程创建,而vfork()
适合需要快速创建并执行新程序映像的场景,如在子进程中立即执行exec
来加载新程序。 vfork()
比fork()
更为轻量级,但在使用时需要特别注意共享地址空间可能引起的数据安全性问题
《深入理解计算机系统》异常控制流—进程控制