在Unix操作系统中,每一个进程都具有自己唯一的进程标识号(ID)。大多数Unix操作系统会对进程ID进行重用延迟,当进程结束时,调度程序并不会立即将该进程ID重分配给下一个进程,以免正在运行的进程利用旧ID错误调用新建进程。其中ID为0表示调度进程(swapper),负责进程之间的调度关系。用户进程无法与调度进程直接通信,调度进程由内核直接控制。ID为1表示初始化进程(init process),初始化进程在内核引导流程结束时被调用,用于初始化系统环境。初始化文件是/erc/rc*文件、/etc/inittab文件及/etc/init.d目录下的文件。初始化进程从不退出。getpid系列函数可以获取进程ID。
fork函数用于在已存在进程中新建进程。fork函数调用一次,返回两次。当fork返回值为0时,当前进程为fork函数创建的子进程;当fork函数返回值大于0时,当前进程与fork函数调用前的进程一致,我们称之为父进程;当fork函数返回值为-1时,fork函数建立子进程失败。系统无法保证父进程与子进程的调用顺序,此处,对父进程与子进程的调用顺序做出任何假设都是错的。当需要保证父进程与子进程执行顺序时,需要使用进程间通信(interprocess communication , IPC)方法来调整进程执行流程。fork函数调用失败可能原因如下:
- 系统中进程总数过多
- 该用户所建立的进程总数过多
子进程是父进程的复制品,子进程中的堆(heap)、栈(stack)、数据片段( data space)与父进程完全一致。父进程与子进程仅保持代码片段(text segment)的共享。由于fork函数常用与新建子进程并执行指定程序,这时不需要对父进程进行大量复制操作。Unix操作系统使用了写入时复制(copy-on-write,COW)技巧,当子进程中数据第一次被修改时,内核首先对需要复制的部分内容复制到子进程中,然后再执行相应操作。部分操作系统提供spawn函数作为fork函数后跟exec函数的代替品。在POSIX标准中,子进程仅复制父进程的调用线程(即,由哪一个线程调用了fork函数,则子进程复制该线程),而在Solaris系统中,子进程复制父进程的所有线程。
当子进程先于父进程终止时,父进程需要通过wait或waitpid函数获取子进程的终止状态(termination status),并释放子进程所占用的系统资源。若子进程结束后,父进程尚未释放子进程所占用的资源,则子进程进入僵死(zombie)状态。当父进程先于子进程终止时,初始化进程(init process)将成为子进程的父进程(初始化进程是一切父进程已终止的进程的父进程)。初始化进程会自动释放已僵死进程所占用的系统资源。
当父进程打开了多个文件时,子进程仅复制父进程的文件描述符,其形式类似与dup函数被调用的过程。fork函数执行后父进程与子进程的文件关系如图1所示
图1. 子进程与父进程的文件关系
由于子进程与父进程共享同一个文件列表,当同时在父进程与子进程中操作同一个文件时,需要注意文件内容的同步。
子进程从父进程继承的属性如下:
- 用户ID、用户组ID、有效用户ID、有效用户组ID
- 附加用户组ID
- 进程组ID
- 会话ID
- 控制终端
- 设置用户ID标识(set-user-ID flags)、设置用户组ID标识(set-group-ID flags)
- 当前工作目录
- 根目录
- 文件权限掩码(file mode creation mask)
- 信号掩码和部署(signal mask and dispositions)
- 任意文件描述符的执行时关闭(close-on-exec)标识
- 环境参数
- 共享存储段(attached shared memory segments)
- 内存映射
- 资源限制
父进程与子进程之间的区别在于:
- fork函数返回值
- 当前进程ID
- 父进程ID
- 子进程中用户CPU时间(tms_utime)、系统CPU时间(tms_stime)、用户终止子进程时间(tms_cutime)、系统终止子进程时间(tms_cstime)被清0
- 子进程不继承父进程所设置的文件锁(file locks)
- 子进程中未处理的警告(alarm)被清除
- 子进程中未处理的信号集被设置为空
vfork函数与fork函数类似。vfork函数与fork函数的区别在于,vfork函数子进程不复制父进程的地址空间(address space),对子进程中数据的操作与对父进程数据操作具有同等效果。子进程仅能简单的执行exec函数或exit函数。vfork函数保证子进程调用提前于父进程调用,当子进程以调用exec或exit函数后,父进程才恢复运行。
如果在vfork函数建立的子进程中调用exit函数,由于子进程与父进程使用共同的地址空间,exit函数将关闭所有打开的文件,并清空缓存,子进程结束后父进程中文件操作的行为是不确定的。