在 Linux 中执行和验证成功的进程执行





5.00/5 (8投票s)
本文介绍如何在一个进程中启动一个子进程,并验证启动是否成功。
引言
在 Linux 中,可以使用 fork 和 exec 来启动进程。然而,由于 exec 会完全替换进程映像,因此通常无法通知父进程子进程已成功启动。我们将使用管道来帮助我们。如果 exec 调用成功,管道将自动关闭;如果发生错误,我们将通过管道写入错误码,父进程会读取错误码并相应地打印错误消息。
使用代码
第一步是包含必要的头文件
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <fcntl.h>
fork、exec 调用以及管道创建函数都在 unistd.h 中,管道的标志在 fcntl.h 中。其余的头文件仅用于方便的输入输出方法。
下一步是声明我们的进程变量。
int pipe_status, exec_status, fork_status, pipe_descriptors[2];
现在我们创建管道,为此我们使用 pipe2 函数。它接受一个指向 int 的指针(本质上是指向我们数组的第一个参数)以及管道创建标志作为第二个参数。在我们的情况下,我们将标志设置为 CLOSE-ON-EXEC,这意味着管道将在 exec 调用成功后自动关闭。
pipe_status = pipe2(pipe_descriptors, O_CLOEXEC);
出错时,pipe2 返回 -1,并设置 errno 以显示导致创建失败的错误。
下一步是创建一个我们现有进程的克隆,我们将使用 fork 来完成。请注意,fork 调用之后的代码似乎会执行两次(每个进程一次),因此我们在 fork 之前创建管道,否则将为父进程和子进程创建两个独立的管道,从而导致它们之间无法通信。
fork_status = fork();
失败时,fork 返回 -1,并设置 errno。成功时,会创建一个现有进程的克隆,fork 将子进程的 PID 返回给父进程,将 0 返回给子进程。因此,测试 fork_status 的值对于确定哪个是父进程哪个是子进程非常重要。
if (fork_status == 0){ //Child Process } if (fork_status > 0){ //Parent Process }
子进程仅在 fork_status 等于 0 时返回 true,父进程仅在 fork_status 大于 0 时返回 true。
现在子进程和父进程已经识别出来,让我们看看如何运行新进程并检查其是否成功完成。
1) 使用 execlp 执行目标进程,它将用我们正在执行的程序的进程映像替换我们 fork 的子进程的进程映像,并且我们之前打开的管道将自动关闭。
2) 如果发生错误,execlp 将返回 -1,并设置 errno 以显示失败原因。
3) 我们将简单地将 errno 的值写入管道,父进程可以读取它并相应地打印错误消息。
4) 如果父进程从管道读取的字节数为 0,则表示管道已关闭,这表示 exec 调用成功。如果父进程从管道读取的字节数大于 0,则表示子进程已将某些内容写入管道,在这种情况下将是错误号。
让我们开始做吧。
exec_status = execlp("gedit", "gedit", NULL);
execlp 函数将要执行的程序名称作为第一个参数。在我们的例子中是 gedit。第二个参数是执行程序时要传递给程序的参数,它必须始终是程序本身的名称。从终端启动程序时,终端本身会隐式完成此操作,但在使用 exec 系列调用时,我们需要自己完成。
Gedit 位于 /usr/bin,但我们不需要在此处指定完整路径,因为 execlp 会复制 PATH 变量中添加的所有路径。如果我们使用了 execl
方法,则需要传递完整路径。
目标进程执行成功后,管道将自动关闭,并且 execlp 调用之后的任何代码都不会在子进程中运行,因为其整个进程映像已被目标进程替换。
失败时,execlp 将返回 -1,然后我们将通过使用 sprintf 将 errno 转换为字符数组来将其写入管道。
if (exec_status == -1) { char error_buffer[4] = {0}; int error = errno; sprintf(error_buffer, "%d", error); close(pipe_descriptors[0]); write(pipe_descriptors[1], error_buffer, sizeof(int)); close(pipe_descriptors[1]); }
在写入管道之前,最好关闭管道的读取端(位于管道描述符数组的索引 0 处)。这样做是为了防止进程超出允许打开的描述符的最大数量。 写入管道后,出于同样的原因关闭管道的写入端。
现在我们需要在进程中从管道读取数据。
if (fork_status > 0) { char buffer[4] = {0}; close(pipe_descriptors[1]); int bytes_read = read(pipe_descriptors[0], buffer, sizeof(int)); if (bytes_read == 0) cout << "HOORAH!!!" << endl; if (bytes_read > 0) { int error = atoi(buffer); cout << "EXECLP Failed because: " << strerror(error) << endl; } }
我们将错误作为字符数组写入,我们也同样将其作为字符数组读取。在从管道读取之前,请务必关闭管道的写入端。这样做是为了避免浪费允许的打开描述符的限制,同时也允许父进程接收 EOF,从而防止父进程在子进程完成向管道写入数据后无限期地等待传入的字节。
read 调用将导致父进程阻塞,直到有数据写入管道。read 的返回值基本上是从管道读取的字节数。如果返回 0,则表示管道已关闭,这意味着 execlp 调用成功。如果返回值大于 0,则表示子进程已将某些内容写入管道,在本例中将是错误号。
摘要
在 win32 API 中,有一个名为 CreateProcess 的方法,它返回一个零值或非零值,表示进程创建成功或失败。而在 Linux 中,你只能通过在屏幕上看到进程的出现来知道它是否成功启动,但是 exec 仅在失败时返回。使用管道是通知父进程子进程成功执行 exec 的一种方法。
干杯。