glibc 知:手册26:进程

1. 前言

The GNU C Library Reference Manual for version 2.35

2. 进程

Processes

进程是分配系统资源的原始单位。每个进程都有自己的地址空间和(通常)一个控制线程。一个进程执行一个程序;您可以让多个进程执行同一个程序,但每个进程在其自己的地址空间内都有自己的程序副本,并独立于其他副本执行它。

进程是分层组织的。每个进程都有一个明确安排创建它的父进程。给定父进程创建的进程称为其子进程。子进程从父进程继承了许多属性。

本章描述程序如何创建、终止和控制子进程。实际上,涉及到三个不同的操作:创建一个新的子进程,使新进程执行一个程序,以及协调子进程与原程序的完成。

系统函数为运行另一个程序提供了一种简单、可移植的机制;它会自动完成所有三个步骤。如果您需要更多地控制如何完成此操作的细节,您可以使用原始函数来单独执行每个步骤。

2.1. 运行命令

Running a Command

运行另一个程序的简单方法是使用系统函数。这个函数完成了运行子程序的所有工作,但它不能让你对细节有太多的控制:你必须等到子程序终止才能做任何其他事情。

函数:int system (const char *command)

Preliminary: | MT-Safe | AS-Unsafe plugin heap lock | AC-Unsafe lock mem | See POSIX Safety Concepts.

此函数将命令作为 shell 命令执行。在 GNU C 库中,它总是使用默认的 shell sh 来运行命令。特别是,它搜索 PATH 中的目录以查找要执行的程序。如果无法创建 shell 进程,则返回值为 -1,否则为 shell 进程的状态。有关如何解释此状态代码的详细信息,请参阅进程完成

如果命令参数是空指针,则返回值为零表示没有可用的命令处理器。

这个函数是多线程程序中的一个取消点。如果线程在调用系统时分配了一些资源(如内存、文件描述符、信号量或其他),则会出现问题。如果线程被取消,这些资源将保持分配状态,直到程序结束。为了避免这种对系统的调用应该使用取消处理程序来保护。

系统函数在头文件 stdlib.h 中声明。

可移植性注意:一些 C 实现可能没有任何可以执行其他程序的命令处理器的概念。可以通过执行system(NULL)来判断命令处理器是否存在;如果返回值非零,则命令处理器可用。

popen 和 pclose 函数(参见管道到子进程)与系统函数密切相关。它们允许父进程与正在执行的命令的标准输入和输出通道进行通信。

2.2. 进程创建概念

Process Creation Concepts

本节概述了进程以及创建进程并使其运行另一个程序所涉及的步骤。

当调用函数 posix_spawn、fork、_Fork 或 vfork 之一时,将创建一个新进程。(system 和 popen 也在内部创建新进程。)由于 fork 函数的名称,创建新进程的行为有时称为 fork 进程。每个新进程(子进程或子进程)都分配有一个进程 ID,与父进程的进程 ID 不同。请参阅进程标识

在 fork 一个子进程之后,父进程和子进程都继续正常执行。如果您希望您的程序在继续之前等待子进程完成执行,则必须在 fork 操作之后通过调用 wait 或 waitpid 显式执行此操作(请参阅进程完成)。这些函数为您提供有关子进程终止原因的有限信息——例如,它的退出状态代码。

新派生的子进程在 fork 或 _Fork 调用返回的位置继续执行与其父进程相同的程序。您可以使用 fork 或 _Fork 的返回值来判断程序是在父进程中运行还是在子进程中运行。

让多个进程运行同一个程序只是偶尔有用。但是孩子可以使用其中一个 exec 函数执行另一个程序;请参阅执行文件。进程正在执行的程序称为它的进程映像。开始执行一个新程序会导致进程忘记它之前的进程映像;当新程序退出时,进程也退出,而不是返回到之前的进程映像。

2.3. 进程识别

Process Identification

每个进程都由一个进程 ID 号命名,一个 pid_t 类型的值。进程 ID 在创建时分配给每个进程。进程 ID 会随着时间的推移而重复使用。当相应进程的父进程在进程终止后等待进程 ID 时,进程的生命周期结束。请参阅进程完成。(父进程可以隐式地安排这种等待。)进程 ID 仅在进程的生命周期内唯一标识进程。根据经验,这意味着该进程必须仍在运行。

进程 ID 还可以表示进程组和会话。请参阅作业控制

在 Linux 上,由 pthread_create 创建的线程也接收一个线程 ID。初始(主)线程的线程 ID 与整个进程的进程 ID 相同。随后创建的线程的线程 ID 是不同的。它们是从与进程 ID 相同的编号空间分配的。进程 ID 和线程 ID 有时也统称为任务 ID。与进程相比,线程永远不会被显式等待,因此一旦线程退出或被取消,线程 ID 就可以重用。即使对于可连接的线程也是如此,而不仅仅是分离的线程。线程被分配给一个线程组。在 Linux 上运行的 GNU C 库实现中,进程 ID 是进程中所有线程的线程组 ID。

您可以通过调用 getpid 来获取某个进程的进程 ID。getppid函数返回当前进程的父进程ID(这也称为父进程ID)。您的程序应包含头文件 unistd.h 和 sys/types.h 以使用这些功能。

数据类型:pid_t

pid_t 数据类型是有符号整数类型,能够表示进程 ID。在 GNU C 库中,这是一个 int。

函数:pid_t getpid (void)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

getpid 函数返回当前进程的进程 ID。

函数:pid_t getppid (void)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

getppid 函数返回当前进程的父进程的进程 ID。

函数:pid_t gettid (void)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

gettid 函数返回当前线程的线程 ID。返回值是从Linux内核获取的,不受缓存。请参阅上面对线程 ID 的讨论,特别是关于重用已退出线程的 ID。

这个函数是 Linux 特有的。

2.4. 创建进程

Creating a Process

fork 函数是创建进程的原语。它在头文件 unistd.h 中声明。

函数:pid_t fork (void)

Preliminary: | MT-Safe | AS-Unsafe plugin | AC-Unsafe lock | See POSIX Safety Concepts.

fork 函数创建一个新进程。

如果操作成功,那么父进程和子进程都有,都看到fork返回,但是值不同:在子进程中返回值0,在父进程中返回子进程ID。

如果进程创建失败,则 fork 在父进程中返回值 -1。为 fork 定义了以下 errno 错误条件:

EAGAIN

没有足够的系统资源来创建另一个进程,或者用户已经有太多的进程在运行。这意味着超过 RLIMIT_NPROC 资源限制,通常可以增加;请参阅限制资源使用

ENOMEM

该过程需要的空间多于系统所能提供的空间。

子进程与父进程不同的具体属性有:

  • 子进程有自己唯一的进程 ID。
  • 子进程的父进程ID是其父进程的进程ID。
  • 子进程获取自己的父进程打开文件描述符的副本。随后更改父进程中文件描述符的属性不会影响子进程中的文件描述符,反之亦然。请参阅对文件的控制操作。但是,与每个描述符关联的文件位置由两个进程共享;请参阅文件位置
  • 子进程经过的处理器时间设置为零;请参阅处理器时间查询
  • 子进程不继承父进程设置的文件锁。请参阅对文件的控制操作
  • 子进程不继承父进程设置的警报。请参阅设置警报
  • 子进程的待处理信号集(请参阅如何传递信号)被清除。(子进程从父进程继承其阻塞信号和信号动作的掩码。)

函数:pid_t _Fork (void)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

_Fork 函数与 fork 类似,但它不调用任何使用 pthread_atfork 注册的回调,也不重置任何内部状态或锁(例如 malloc 锁)。在新的子进程中,只能调用异步信号安全函数,例如 dup2 或 execve。

_Fork 函数是 fork 的异步信号安全替代。它是一个 GNU 扩展。

函数:pid_t vfork (void)

Preliminary: | MT-Safe | AS-Unsafe plugin | AC-Unsafe lock | See POSIX Safety Concepts.

vfork 功能类似于 fork 但在某些系统上它更有效;但是,您必须遵守一些限制才能安全使用它。

虽然 fork 制作了调用进程地址空间的完整副本并允许父进程和子进程独立执行,但 vfork 不会制作此副本。相反,使用 vfork 创建的子进程共享其父进程的地址空间,直到它调用 _exit 或其中一个 exec 函数。同时,父进程暂停执行。

您必须非常小心,不要让使用 vfork 创建的子进程修改任何全局数据,甚至是与父进程共享的局部变量。此外,子进程不能从调用 vfork! 的函数返回(或跳出)。这会使父进程的控制信息非常混乱。如果有疑问,请改用 fork。

一些操作系统并没有真正实现 vfork。GNU C 库允许您在所有系统上使用 vfork,但如果 vfork 不可用,则实际执行 fork。如果您遵循使用 vfork 的正确预防措施,即使系统使用 fork 代替,您的程序仍然可以运行。

2.5. 执行文件

Executing a File

本节介绍 exec 系列函数,用于将文件作为进程映像执行。您可以使用这些函数使子进程在分叉后执行新程序。

要从被调用程序的角度查看 exec 的效果,请参阅基本程序/系统接口。

该系列中的函数在指定参数的方式上有所不同,但除此之外它们都做同样的事情。它们在头文件 unistd.h 中声明。

函数:int execv (const char *filename, char *const argv[])

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

execv 函数将 filename 命名的文件作为新的进程映像执行。

argv 参数是一个以 null 结尾的字符串数组,用于为要执行的程序的主函数提供 argv 参数的值。该数组的最后一个元素必须是空指针。按照惯例,这个数组的第一个元素是程序的文件名,没有目录名。有关程序如何访问这些参数的完整详细信息,请参阅程序参数

新进程映像的环境取自当前进程映像的环境变量;有关环境的信息,请参阅环境变量

函数:int execl (const char *filename, const char *arg0, ...)

Preliminary: | MT-Safe | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.

这类似于 execv,但 argv 字符串是单独指定的,而不是作为数组指定的。必须将空指针作为最后一个此类参数传递。

函数:int execve (const char *filename, char *const argv[], char *const env[])

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

这类似于 execv,但允许您将新程序的环境显式指定为 env 参数。这应该是与环境变量格式相同的字符串数组;请参阅环境访问

函数:int fexecve (int fd, char *const argv[], char *const env[])

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

这类似于 execve,但不是通过路径名来识别可执行程序,而是使用文件描述符 fd。必须使用 O_RDONLY 标志或(在 Linux 上)O_PATH 标志打开描述符。

在 Linux 上,如果 /proc 尚未安装并且内核缺少对底层 execveat 系统调用的支持,fexecve 可能会失败并返回 ENOSYS 错误。

函数:int execle (const char *filename, const char *arg0, ..., char *const env[])

Preliminary: | MT-Safe | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.

这类似于 execl,但允许您明确指定新程序的环境。environment 参数在标记最后一个 argv 参数的空指针之后传递,并且应该是与 environ 变量格式相同的字符串数组。

函数:int execvp (const char *filename, char *const argv[])

Preliminary: | MT-Safe env | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.

execvp 函数与 execv 类似,不同之处在于它搜索 PATH 环境变量(请参阅标准环境变量)中列出的目录,以在 filename 不包含斜杠的情况下从 filename 中找到文件的完整文件名。

此功能对于执行系统实用程序很有用,因为它会在用户选择的位置查找它们。Shell 使用它来运行用户键入的命令。

函数:int execlp (const char *filename, const char *arg0, ...)

Preliminary: | MT-Safe env | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.

此函数与 execl 类似,不同之处在于它执行与 execvp 函数相同的文件名搜索。

参数列表和环境列表加在一起的大小不得大于 ARG_MAX 字节。请参阅一般容量限制。在 GNU/Hurd 系统上,对于每个字符串,大小(与 ARG_MAX 进行比较)包括字符串中的字符数,加上 char * 的大小,再加一,四舍五入到 char 大小的倍数*。其他系统可能有一些不同的计数规则。

这些函数通常不会返回,因为执行新程序会导致当前正在执行的程序完全消失。如果发生故障,则返回值 -1。除了通常的文件名错误(请参阅文件名错误)之外,还为这些函数定义了以下 errno 错误条件:

E2BIG

新程序的参数列表和环境列表的组合大小大于 ARG_MAX 字节。GNU/Hurd 系统对参数列表大小没有特定限制,因此不会出现此错误代码,但如果参数对于可用内存而言太大,您可能会得到 ENOMEM。

ENOEXEC

无法执行指定的文件,因为它的格式不正确。

ENOMEM

执行指定的文件需要比可用存储更多的存储空间。

如果新文件的执行成功,它会更新文件的访问时间字段,就像文件已被读取一样。有关文件访问时间的更多详细信息,请参阅文件时间

没有指定文件再次关闭的时间点,而是在进程退出之前或执行另一个进程映像之前的某个时间点。

执行一个新的进程映像会完全改变内存的内容,只将参数和环境字符串复制到新位置。但是该过程的许多其他属性没有改变:

如果设置了进程映像文件的 set-user-ID 和 set-group-ID 模式位,则会影响进程的有效用户 ID 和有效组 ID(分别)。这些概念在进程角色中进行了详细讨论。

在现有过程映像中设置为忽略的信号也将在新过程映像中设置为忽略。所有其他信号都设置为新过程映像中的默认操作。有关信号的更多信息,请参阅信号处理

在现有进程映像中打开的文件描述符在新进程映像中保持打开状态,除非它们设置了 FD_CLOEXEC (close-on-exec) 标志。保持打开的文件从现有进程映像继承打开文件描述符的所有属性,包括文件锁。文件描述符在底层输入/输出中讨论。

相比之下,流无法通过 exec 函数生存,因为它们位于进程本身的内存中。新的过程映像没有流,除了它重新创建的流。pre-exec 进程映像中的每个流在其中都有一个描述符,并且这些描述符确实通过 exec 存在(前提是它们没有设置 FD_CLOEXEC)。新的过程映像可以使用 fdopen 将这些重新连接到新的流(请参阅描述符和流)。

2.6. 进程完成

Process Completion

本节描述的函数用于等待子进程终止或停止,并确定其状态。这些函数在头文件 sys/wait.h 中声明。

函数:pid_t waitpid (pid_t pid, int *status-ptr, int options)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

waitpid 函数用于向进程 ID 为 pid 的子进程请求状态信息。通常,调用进程被挂起,直到子进程通过终止使状态信息可用。

pid 参数的其他值有特殊解释。值 -1 或 WAIT_ANY 请求任何子进程的状态信息;值 0 或 WAIT_MYPGRP 请求与调用进程在同一进程组中的任何子进程的信息;和任何其他负值 - pgid 请求其进程组 ID 为 pgid 的任何子进程的信息。

如果子进程的状态信息立即可用,则此函数立即返回,无需等待。如果多个符合条件的子进程具有可用的状态信息,则随机选择其中一个,并立即返回其状态。要从其他符合条件的子进程中获取状态,您需要再次调用 waitpid。

options 参数是一个位掩码。它的值应该是零个或多个 WNOHANG 和 WUNTRACED 标志的按位或(即“|”运算符)。您可以使用 WNOHANG 标志来指示父进程不应等待;和 WUNTRACED 标志从停止的进程以及已终止的进程请求状态信息。

来自子进程的状态信息存储在 status-ptr 指向的对象中,除非 status-ptr 是空指针。

这个函数是多线程程序中的一个取消点。如果线程在调用 waitpid 时分配了一些资源(如内存、文件描述符、信号量或其他),则会出现问题。如果线程被取消,这些资源将保持分配状态,直到程序结束。为了避免这种对 waitpid 的调用应该使用取消处理程序来保护。

返回值通常是上报状态的子进程的进程ID。如果有子进程但没有一个等待被注意到,waitpid 将阻塞直到有一个。但是,如果指定了 WNOHANG 选项,waitpid 将返回零而不是阻塞。

如果将要等待的特定 PID 分配给 waitpid,它将忽略所有其他子项(如果有)。因此,如果有孩子等待被注意,但指定 PID 的孩子不是其中之一,waitpid 将阻塞或返回零,如上所述。

如果发生错误,则返回值 -1。为此函数定义了以下 errno 错误条件:

EINTR

该函数因向调用进程传递信号而中断。请参阅被信号中断的原语

ECHILD

没有要等待的子进程,或者指定的 pid 不是调用进程的子进程。

EINVAL

为选项参数提供了无效值。

这些符号常量被定义为 waitpid 函数的 pid 参数的值。

WAIT_ANY

此常量宏(其值为 -1)指定 waitpid 应返回有关任何子进程的状态信息。

WAIT_MYPGRP

此常量(值为 0)指定 waitpid 应返回有关与调用进程相同的进程组中的任何子进程的状态信息。

这些符号常量被定义为 waitpid 函数的选项参数的标志。您可以对这些标志进行按位或运算,以获得用作参数的值。

WNOHANG

此标志指定如果没有准备好被注意的子进程,waitpid 应该立即返回而不是等待。

WUNTRACED

此标志指定 waitpid 应报告任何已停止以及已终止的子进程的状态。

函数:pid_t wait (int *status-ptr)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

这是 waitpid 的简化版本,用于等待任何一个子进程终止。来电:

wait (&status)

完全等同于:

waitpid (-1, &status, 0)

这个函数是多线程程序中的一个取消点。如果线程在调用 wait 时分配了一些资源(如内存、文件描述符、信号量或其他),则会出现问题。如果线程被取消,这些资源将保持分配状态,直到程序结束。为了避免这种调用等待应该使用取消处理程序来保护。

函数:pid_t wait4 (pid_t pid, int *status-ptr, int options, struct rusage *usage)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

如果usage 是空指针,wait4 等价于waitpid (pid, status-ptr, options)。

如果usage 不为null,wait4 将子进程的使用数据存储在*rusage 中(但仅当子进程已终止时,而不是已停止时)。请参阅资源使用情况

这个函数是一个 BSD 扩展。

这是一个示例,说明如何使用 waitpid 从所有已终止的子进程中获取状态,而无需等待。该函数设计为 SIGCHLD 的处理程序,该信号指示至少一个子进程已终止。

void
sigchld_handler (int signum)
{
  int pid, status, serrno;
  serrno = errno;
  while (1)
    {
      pid = waitpid (WAIT_ANY, &status, WNOHANG);
      if (pid < 0)
        {
          perror ("waitpid");
          break;
        }
      if (pid == 0)
        break;
      notice_termination (pid, status);
    }
  errno = serrno;
}

2.7. 进程完成状态

Process Completion Status

如果子进程的退出状态值(见程序终止)为零,那么waitpid或wait上报的状态值也为零。您可以使用以下宏测试在返回的状态值中编码的其他类型的信息。这些宏在头文件 sys/wait.h 中定义。

宏:int WIFEXITED (int status)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

如果子进程以 exit 或 _exit 正常终止,则此宏返回非零值。

宏:int WEXITSTATUS (int status)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

如果 WIFEXITED 的状态为真,则该宏从子进程返回退出状态值的低 8 位。请参阅退出状态

宏:int WIFSIGNALED (int status)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

如果子进程因收到未处理的信号而终止,则此宏返回非零值。请参阅信号处理

宏:int WTERMSIG (int status)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

如果 WIFSIGNALED 的状态为真,则此宏返回终止子进程的信号的信号编号。

宏:int WCOREDUMP (int status)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

如果子进程终止并产生核心转储,则此宏返回非零值。

宏:int WIFSTOPPED (int status)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

如果子进程停止,此宏将返回一个非零值。

宏:int WSTOPSIG (int status)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

如果 WIFSTOPPED 的状态为真,则此宏返回导致子进程停止的信号的信号编号。

2.8. BSD 进程等待函数

BSD Process Wait Function

GNU C 库还提供了与 BSD 兼容的 wait3 函数。该函数在 sys/wait.h 中声明。是wait4的前身,比较灵活。wait3 现在已过时。

函数:pid_t wait3 (int *status-ptr, int options, struct rusage *usage)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

如果usage 是空指针,wait3 等价于waitpid (-1, status-ptr, options)。

如果usage 不为null,wait3 将子进程的使用数据存储在*rusage 中(但仅当子进程已终止时,而不是已停止时)。请参阅资源使用情况

2.9. 进程创建示例

Process Creation Example

这是一个示例程序,展示了如何编写类似于内置系统的函数。它使用等效的“sh -c command”执行其命令参数。

#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

/* Execute the command using this shell program.  */
#define SHELL "/bin/sh"

int
my_system (const char *command)
{
  int status;
  pid_t pid;

  pid = fork ();
  if (pid == 0)
    {
      /* This is the child process.  Execute the shell command. */
      execl (SHELL, SHELL, "-c", command, NULL);
      _exit (EXIT_FAILURE);
    }
  else if (pid < 0)
    /* The fork failed.  Report failure.  */
    status = -1;
  else
    /* This is the parent process.  Wait for the child to complete.  */
    if (waitpid (pid, &status, 0) != pid)
      status = -1;
  return status;
}

在此示例中,您应该注意几件事。

请记住,提供给程序的第一个 argv 参数表示正在执行的程序的名称。这就是为什么在对 execl 的调用中,一次提供 SHELL 来命名要执行的程序,第二次提供 argv[0] 的值。

如果成功,子进程中的 execl 调用不会返回。如果它失败了,你必须做一些事情来使子进程终止。仅仅返回一个错误的状态码就会留下两个运行原始程序的进程。相反,正确的行为是让子进程向其父进程报告失败。

调用 _exit 来完成此操作。使用 _exit 而不是 exit 的原因是为了避免刷新完全缓冲的流,例如 stdout。这些流的缓冲区可能包含由 fork 从父进程复制的数据,这些数据最终将由父进程输出。在子进程中调用 exit 会输出两次数据。请参阅端接内部

3. 参考

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值