进程标识
每个进程都有一个非负整数表示的唯一进程ID。
- 进程 ID 为 0,调度进程,常被称为交换进程(swapper),是内核的一部分,不执行任何磁盘上的程序,也被称为系统进程。
- 进程 ID 为 1,通常是 init 进程,init 进程绝不会停止,它是一个普通的用户进程,但是它以超级用户特权运行。
#include <unistd.h>
pid_t getpid(void); // 返回值:调用进程的进程 ID
pid_t getppid(void); // 返回值:调用进程的父进程 ID
uid_t getuid(void); // 返回值:调用进程的实际用户 ID
uid_t geteuid(void); // 返回值:调用进程的有效用户 ID
gid_t getgid(void); // 返回值:调用进程的实际组 ID
gid_t getegid(void); // 返回值:调用进程的有效组 ID
函数 fork
一个现有的进程可以调用 fork 函数创建一个新进程。
#include <unistd.h>
pid_t fork(void);
返回值:
- 子进程返回 0
- 父进程返回子进程 ID
- 错误返回 -1
子进程是父进程的副本,子进程获的父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本,父进程和子进程并不共享这些存储空间部分。
Copy-On-Write
写时复制(Copy-On-Write, COW)是一种资源管理技术。它的主要用途之一是实现 fork 系统调用,它共享操作系统的虚拟内存(pages)。
如果一个资源被复制但没有被修改,则没有必要创建一个新的资源,资源可以在副本和原件之间共享,但是修改仍然需要创建一个副本,因此复制操作可推迟到第一次写入时进行。 通过这种方式共享资源,可以减少未修改副本的资源消耗和资源修改操作的开销。
在执行 fork 系统调用时,写时复制主要用于共享操作系统进程的虚拟内存。 通常情况下,该进程不会修改任何内存,并立即执行一个新的进程,完全取代地址空间。因此,在 fork 期间复制进程的所有内存是一种浪费,而是使用写时复制技术。
写时复制背后的想法是当父进程创建子进程时,这两个进程最初共享内存中相同的页,并且这些共享页被标记为写时复制,意味着如果有任一进程修改共享页,只会创建这些页的副本,修改将由该进程在页副本上完成,不会影响其他进程。
假设,有一个进程 P 创建了一个新的进程 Q,然后进程 P 修改了页 3。下图显示了进程 P 修改页 3 前后发生的情况。
文件共享
fork 的一个特性是父进程的所有打开文件描述符都被复制到子进程中,对每个文件描述符来说,就像是执行了 dup 函数,父进程和子进程每个相同的打来描述符共享一个文件表项。
fork 失败的两个主要原因:
- 系统中已经有太多的进程(通常意味着某个方面出现了问题)
- 该实际用户ID的进程总数超过了系统限制。
fork 有以下两种用法:
- 父进程希望复制自己,是父进程和子进程同时执行不同的代码段。(在网络服务进程中,父进程等待客户端服务请求,当请求到达时,父进程调用 fork,使子进程处理此请求,父进程继续等待下一个服务请求)。
- 一个进程要执行一个不同的程序。
函数 exit
5 中正常终止进程的方式:
- main 函数内执行 return 语句。
- 调用 exit 函数。
- 调用 _exit 函数 或 _Exit 函数。
- 进程的最后一个线程在其启动例程中执行 return 语言。
- 进程的最后一个线程调用 pthread_exit 函数。
3 种异常终止方式:
- 调用 abort,它产生 SIGABRT 信号。
- 当进程接收到某种信号时,信号可由进程自身(如调用 abort 函数)、其它进程或内核产生。
- 最后一个线程对“取消”请求作出响应。
不管进程如何终止,最后都会执行内核中的同一段代码,这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
对于父进程已经终止的所有进程,它们的父进程都改变为 init 进程,称这些进程由 init 进程收养。
在 UNIX 术语中,一个已经终止、但其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程称为僵尸进程(zombie)。
一个由 init 进程收养的进程终止时,init 就会调用一个 wait 函数取得其终止状态。这样也就防止了在系统中塞满僵尸进程。
函数 wait 和 waitpid
当一个进程正常或异常终止时,内核会向其父进程发送 SIGCHLD 信号(系统默认动作是忽略它)。
用 wait 或 waitpid 的进程可能会发生什么:
- 如果其所有子进程都还在运行,则阻塞。
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
- 如果它没有任何子进程,则立即出错返回。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
- wait 使其调用者阻塞,waitpid 可使调用者不阻塞。
- waitpid 可以控制它所等待的进程。
参数 statloc 是一个整型指针,终止进程的终止状态就存放在它所指向的单元内。
对于 waitpid 函数中 pid 参数的作用:
- pid == -1 等待任一子进程,与 wait 等效
- pid > 0 等待进程 ID 与 pid 相等的子进程
- pid == 0 等待组 ID 等于调用进程组 ID 的任一子进程
- pid < -1 等待组 ID 等于 pid 绝对值的任一子进程
实例
#include <unistd.h>
#include <printf.h>
#include <stdlib.h>
int main(void) {
pid_t pid;
if ((pid = fork()) < 0) {
printf("fork error");
} else if (pid == 0) { /* first child */
if ((pid = fork()) < 0)
printf("fork error");
else if (pid > 0)
// pid > 0, first child
exit(0); /* parent from second fork == first child */
// pid == 0, second child
sleep(5);
printf("second child, parent pid = %ld\n", (long) getppid());
exit(0);
}
if (waitpid(pid, NULL, 0) != pid) /* wait for first child */
printf("waitpid error");
exit(0);
}
second child, parent pid = 1
函数 exec
fork 函数创建新的子进程后,子进程往往需要调用一种 exec 函数以执行另一个程序,当进程调用一种 exec 函数时,该进程执行的程序完全替换为新程序,而新程序则从其 main 函数开始执行。
调用 exec 并不创建新进程,前后进程 ID 并未改变,只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。