进程控制

206 阅读6分钟

进程标识

每个进程都有一个非负整数表示的唯一进程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 前后发生的情况。

11150.png

12127.png

Copy On Write
Copy-on-write

文件共享

fork 的一个特性是父进程的所有打开文件描述符都被复制到子进程中,对每个文件描述符来说,就像是执行了 dup 函数,父进程和子进程每个相同的打来描述符共享一个文件表项。

image.png

fork 失败的两个主要原因:

  • 系统中已经有太多的进程(通常意味着某个方面出现了问题)
  • 该实际用户ID的进程总数超过了系统限制。

fork 有以下两种用法:

  • 父进程希望复制自己,是父进程和子进程同时执行不同的代码段。(在网络服务进程中,父进程等待客户端服务请求,当请求到达时,父进程调用 fork,使子进程处理此请求,父进程继续等待下一个服务请求)。
  • 一个进程要执行一个不同的程序。

函数 exit

5 中正常终止进程的方式:

  1. main 函数内执行 return 语句。
  2. 调用 exit 函数。
  3. 调用 _exit 函数 或 _Exit 函数。
  4. 进程的最后一个线程在其启动例程中执行 return 语言。
  5. 进程的最后一个线程调用 pthread_exit 函数。

3 种异常终止方式:

  1. 调用 abort,它产生 SIGABRT 信号。
  2. 当进程接收到某种信号时,信号可由进程自身(如调用 abort 函数)、其它进程或内核产生。
  3. 最后一个线程对“取消”请求作出响应。

不管进程如何终止,最后都会执行内核中的同一段代码,这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

对于父进程已经终止的所有进程,它们的父进程都改变为 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 并未改变,只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

iShot2023-01-22 22.02.33.png