在Unix/Linux系统下进程创建时需要进行如下系统调用:fork/exec
fork()把一个进程复制成二个进程:parent (old PID), child (new PID)
exec()用新程序来重写当前进程:PID没有改变
接下来就重点学习这两个系统调用:
当我们fork() 创建一个继承的子进程将会发生如下事情:复制父进程的所有变量和内存,复制父进程的所有CPU寄存器(有一个寄存器例外)(这个寄存器是用来区分父进程和子进程的PID)
fork()的返回值:调用fork()函数成功时,将会有两个返回值。子进程的fork()返回0,父进程的fork()返回子进程标识符。fork() 返回值可方便后续使用,子进程可使用getpid()获取PID。
fork()执行过程对于子进程而言,是对父进程地址空间的一次复制。下面我们通过图示来看一下这个复制过程:
注意了,图示中的两个childPID的值是不同的,对于父进程中的childPID当然是子进程的PID,而子进程中的childPID的值为0。
fork()使用示例:
int main() { pid_t pid; int i; for(i=0; i<LOOP; i++) { /* 创建新进程*/ pid = fork(); if (pid < 0) { /*创建失败 */ fprintf(stderr, “Fork Failed”); exit(-1); } else if (pid == 0) { /* 子进程 */ fprintf(stdout, “i=%d, pid=%d, parent pid=%d\n”,I, getpid() ,getppid()); } } wait(NULL); exit(0); }
了解了fork()之后我们再来看看exec()系统调用:系统调用exec( )加载新程序取代当前运行进程,也就是说exec调用成功时,它是相同的进程,但是运行了不同的程序,代码段、堆栈和堆(heap)等也都完全重写了。它允许进程“加载”一个完全不同的程序,并从main开始执行,而我们的fork()创建出来的子进程是从fork()之后的代码段处开始执行的。
我们接下来讨论一下fork()的实现开销:当我们使用fork()系统调用时,首先要对子进程分配内存,然后复制父进程的内存和CPU寄存器到子进程里。开销昂贵!!
然而在99%的情况里,我们在调用fork()之后调用exec(),因为在大多数情况下我们都是不希望和父进程执行同样的代码,不然的话我们创建一个新的进程也就没有多大的意义了。这时候我们就该考虑一下这个问题了:既然不想使用父进程中的代码,在fork()操作中内存复制就是没有作用的。那么为什么不能结合它们在一个调用中?
此后,就产生了vfork():创建进程时,不再创建一个同样的内存映像。一些时候称之为轻量级fork(),子进程几乎立即调用exec()。而在子进程调用exec()之前,暂时与父进程共享地址空间。
我们注意到了,前面fork()的示例代码中使用了wait()函数,那么这个函数是干什么用的呢?这就要引出一个新的概念了:父进程等待子进程
wait()系统调用用于父进程等待子进程的结束:子进程结束时通过exit()向父进程返回一个值,父进程通过wait()接受并处理返回值
wait()系统调用的功能:有子进程存活时,父进程进入等待状态,等待子进程的返回结果。当某子进程调用exit()时,唤醒父进程,将exit()返回值作为父进程中wait的返回值
既然子进程结束时是通过exit()向父进程返回一个值,那么我们又要学习一下exit()系统调用了:
进程结束执行时调用exit(),完成进程资源回收
exit()系统调用的功能:
- 将调用参数作为进程的“结果”
- 关闭所有打开的文件等占用资源
- 释放内存
- 释放大部分进程相关的内核数据结构
- 检查是否父进程是存活着的,如存活,保留结果的值直到父进程需要它,进入僵尸(zombie/defunct)状态。如果没有,它释放所有的数据结构,进程结果
- 清理所有等待的僵尸进程
说到这里,大家可能对僵尸进程有点疑惑:所谓的僵尸进程,就是父进程没有调用wait收集子进程的退出状态,子进程就已经退出了,此时这个退出的子进程就成为了僵尸进程,它的很多进程资源都还没有释放(例如PID在进程表中仍然存在)。
版权声明:本文为博主原创文章,未经博主允许不得转载。