exec函数族
exec函数族的用途
fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,复制了父进程的的数据段、代码段和堆栈段。虽然可以通过fork返回值的不同来区分父子进程,并为其分别编写执行逻辑。但是父进程和子进程对应存储空间都保存了对方的代码处理逻辑,显然这样是非常不合理的,一方面对内存是极大的浪费,另一方面将父子进程的代码写在一起看起来很不直观,逻辑结构不够清晰。
exec函数族就提供了一个在进程中启动另一个程序执行的方法,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。在exec函数执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。
优点:这样便完美解决了上面我们所说的两大痛点。在需要创建子进程并执行与父进程不同的程序时,fork
和exec
这种组合非常常见。它们的配套使用不仅保证了资源的继承和灵活的进程控制,还可以在父进程和子进程之间实现清晰的职责分离。
exec函数族语法
实际上,在Linux中并没有exec函数,而是有6个以exec开头的函数族,下表列举了exec函数族的6个成员函数的语法。
头文件:<unistd.h>
主要功能:给进程加载指定的程序,如果成功,进程的整个内存空间都被覆盖。
后缀字母含义:
- l : list 以列表的方式将程序需要的命令行参数依次列出来,逐个传递
- v: vector 将所有参数整体构造成指针数组传递,然后将该数组的首地址当做参数传给它,数组中的最后一个指针要求是NULL。
- e:exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execle、execve就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所以环境变量。
- p: 专指PATH环境变量,
execlp
和execvp
会自动在环境变量PATH
指定的路径下搜索要执行的程序。这使得你可以只指定程序的名称而不必提供程序完整的路径。所以我们也可以看到这两个函数需要的第一个参数是file(程序名)而不是程序路径(path)。返回值:函数执行成功不返回,执行错误返回-1,失败原因记录在error中
execl(const char *path, const char *arg, ...) 为例,介绍参数含义。
path 指向要执行的程序的路径。这个路径可以是绝对路径(如 /bin/ls
)或相对路径(如 ./myprogram
)。
arg 则是该程序需要的命令行参数,值得注意的是,命令行参数包括程序路径,并且全部是字符串。且列表的最后一个参数必须是 (char *) NULL
,用来标识参数列表的结束。
例如:execl("./a.out", "./a.out", "123", "abc", NULL);
arg的进一步解释:
- 第一个命令行参数通常保存的是可执行文件的名称,即运行程序时使用的命令。这个名称可以是完整的路径,也可以是相对路径。在 Linux 命令行终端,即使你不传递任何命令行参数给程序,系统仍然会自动将程序的路径,作为
argv[0]
传递给命令行参数。这是由操作系统的进程创建机制所决定的。exec系列函数需要我们手动将这个命令行参数加进去。所以上面execl中看似传递了一模一样两个字符串,实则含义用途不同。 - 在 Linux 命令行执行程序时,同样不需要手动传递最后一个命令行参数
NULL
,系统会自动处理这一部分。exec系列函数需要我们手动将NULL命令行参数加进去。
execl和execv使用示例
parent_process
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv) {
pid_t pid = fork();
if (pid > 0) {
printf("parent PID: %d\n", getpid());
}
if (pid == 0){
//execl("./child_process", "./child_process", "abc", NULL);
char *child_arg[]={"./child_process", "abc", NULL};
execv("./child_process",child_arg);
printf("%s","xxxx");
}
return 0;
}
child_process
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv2) {
printf("parent PID: %d\n", getpid());
for(int i=0; i<argc; i++){
printf("argv2[%d]: %s\n", i, argv2[i]);
}
return 0;
}
./parent_process运行结果
- 我们发现parent_process中的printf("%s","xxxx");没有执行,因为 execv("./child_process",child_arg);执行成功的话,子进程执行的代码就变了,execv函数执行失败的时候才会继续执行父进程代码。
- 对于使用execv/execl来启动另一个程序,本质上方法一样,只不过,execv将参数打包为了一个数组。
- 执行./parent_process的时候,我们发现貌似程序没有执行完毕,终端就打印了提示字符串,这是因为终端只关心程序parent_process的执行,当程序parent_process执行完毕就会告诉终端我执行完了,然后终端打印提示字符。至于子进程代码是否执行完毕这是终端不关心的。
execlp使用示例
exel一族函数不仅可以让子进程执行自定义函数,执行系统函数也是不在话下。对于ls程序,ls只是这个程序的名字,/bin/ls才是这个程序的路径。
execlp/execvp函数可以在环境变量 PATH
指定的路径下搜索要执行的程序,此时对于系统程序ls,我们只用指定一下程序名字就可以执行它。想用execl/execv执行ls程序的话,那就需要写程序的路径了。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv) {
pid_t pid = fork();
if (pid > 0) {
printf("parent PID: %d\n", getpid());
}
if (pid == 0){
execlp("ls", "ls", "-l", NULL);
//execl("/bin/ls", "/bin/ls", "-l", NULL);
printf("%s","xxxx");
}
return 0;
}
exec函数族使用注意
注意:在使用exec函数族时,一定要加上错误判断语句!!!因为exec函数族很容易执行失败,其中最常见的原因有:
① 找不到文件或路径,此时errno被设置为ENOENT。
② 忘记加上NULL,此时errno被设置为EFAULT。
③ 没有对应可执行文件的运行权限,此时errno被设置为EACCES。
wait函数
wait函数介绍
wait 是用于进程控制的系统调用,用于报告子进程的终止状态,它允许一个进程(通常是父进程)等待一个子进程的终止,然后收集和处理子进程的终止状态信息,wait在父子进程的同步与管理中非常重要。
同步父进程和子进程:指协调父进程和子进程的执行顺序,这种同步是父进程等待子进程终止再接着执行父进程,保证父进程不会在子进程完成任务之前执行与子进程结果相关的操作。(wait
函数会阻塞调用它的进程,直到任意一个子进程结束。进程处于阻塞状态的时候会被操作系统挂起,让这个进程不占用cpu资源)
为什么需要同步?(蛮重要的,信息量十足)
确保任务按预期顺序完成,让父进程能及时拿到子进程运行结果并进行下一步操作,且及时释放子进程资源。
- 子进程终止后,会进入一种称为“僵尸进程”的状态。虽然僵尸进程的资源(如占用运行内存、文件描述符)会在进程终止时自动释放,但是对应的进程表条目(僵尸进程的记录)不会被自动清理。
- 僵尸进程的进程表条目保存了终止状态、终止信号、进程ID 和父进程ID 等信息。这个信息让父进程能够获取子进程的退出码和终止原因,以便采取适当的后续操作。
- 它的父进程通过调用
wait
或waitpid
来获取子进程的终止状态,同时释放进程条目。如果父进程不这样做,僵尸进程对应的进程条目会一直存在。进程表能保存的条目数量有限(从进程PID需要循环利用就可以知道),僵尸进程占用的PID是不能被其他进程使用的,大量僵尸进程占用进程表条目,当进程表条目耗尽时,系统将无法创建新的进程,系统可能崩溃。- 为了避免无用进程表项的积累,我们应该确保父进程及时调用
wait
或waitpid
函数来处理子进程的终止状态,从而避免僵尸进程的积累。
头文件 :
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>函数原型:
pid_t wait(int *status);
参数:
status
:这是一个指向整数的指针,用于存储子进程的终止状态。父进程可以通过这个状态来判断子进程的退出情况。如果不需要退出状态,可以传递NULL
。返回值:
成功:
wait
函数返回终止的子进程的进程 ID(PID)失败:返回
-1
,并设置errno
以指示错误类型
wait函数的使用示例
parent_wait_process
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char **argv) {
pid_t pid = fork();
if (pid > 0) {
printf("parent PID: %d\n", getpid());
int status;
wait(&status);//阻塞等待子进程终止
printf("child exit code: %d\n",WEXITSTATUS(status));
}
if (pid == 0){
execl("./child_process", "./child_process", "aaa", NULL);
printf("%s","xxxx");
}
return 0;
}
child_wait_process
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char **argv2) {
printf("parent PID: %d\n", getpid());
for(int i=0; i<argc; i++){
printf("argv2[%d]: %s\n", i, argv2[i]);
}
exit(88);
}
对于wait函数的使用,我们通常都是将int型变量status传入函数,获取到的status包含进程的综合属性。和之前文件属性中的st_mode(文件类型,权限)类似。我们直接输出这个status值是无意义的,status中不同的位保存了进程的不同信息,我们需要按位将信息分离出来,具体可以通过函数宏。
这里使用了WEXITSTATUS(status)得到了子进程的退出状态
即 exit
或 return
的返回值,88。
status
的组成
status
是一个整数,但它包含了多个不同的状态信息,这些信息可以通过以下函数宏进行解码:
-
WIFEXITED(status)
:- 判断子进程是否正常退出(即调用了
exit
或return
)。 - 如果返回真(非零),则表示子进程正常退出。
- 判断子进程是否正常退出(即调用了
-
WEXITSTATUS(status)
:- 在
WIFEXITED(status)
为真时使用,返回子进程的退出状态(即exit
或return
的返回值)。 - 该值通常是一个 0 到 255 之间的整数。
- 在
-
WIFSIGNALED(status)
:- 判断子进程是否因未捕获的信号而终止。
- 如果返回真,表示子进程因信号而终止。
-
WTERMSIG(status)
:- 在
WIFSIGNALED(status)
为真时使用,返回导致子进程终止的信号编号。
- 在
-
WIFSTOPPED(status)
:- 判断子进程是否由于信号而暂停(如使用
kill
发送SIGSTOP
)。 - 如果返回真,表示子进程当前处于暂停状态。
- 判断子进程是否由于信号而暂停(如使用
-
WSTOPSIG(status)
:- 在
WIFSTOPPED(status)
为真时使用,返回导致子进程暂停的信号编号。
- 在
-
WIFCONTINUED(status)
:- 判断子进程是否由于
SIGCONT
信号而继续执行(从暂停状态恢复)。 - 如果返回真,表示子进程已经继续执行。
- 判断子进程是否由于
waitpid函数
waitpid函数介绍
waitpid
(三个参数)是比 wait
(一个参数)更加灵活的函数,用于等待特定的子进程终止,也可以选择不阻塞地轮询子进程的状态。
- 它可以等待特定子进程的终止,这一点和wait函数不同,wait函数等到随便任何一个子进程结束后就停止阻塞了,waitpid可以指定等待的子进程。
- waitpid可以选择不阻塞地轮询子进程的状态。对比:
wait
函数会阻塞调用它的进程,直到任意一个子进程结束。 而waitpid可以选择不阻塞模式,此时进程A调用waitpid函数,如果waitpid等待释放的目标进程没有结束,那么waitpid将不会阻塞进程A,进程A代码继续向下执行。(当然由于我们要避免僵尸进程产生,即使当下还不能回收目标进程,我们肯定是需要后面定时检测目标进程是否结束,从而在进程结束后,尽快(调用wait/waitpid)回收进程,文章后半部分关于定期检查有详细介绍)
头文件
#include <sys/types.h>
#include <sys/wait.h>函数原型
pid_t waitpid(pid_t pid, int *status, int options);
函数参数pid:指定需要等待的子进程的进程 ID(PID)
status:这是一个指向整数的指针,用于存储子进程的终止状态。父进程可以通过这个状态来判断子进程的退出情况。如果不需要退出状态,可以传递
NULL
。options:用于修改
waitpid
的行为,常用选项包括:
0
:阻塞等待,直到指定的子进程终止。WNOHANG
:非阻塞模式,如果没有子进程终止,则立即返回。WUNTRACED
:返回暂停状态的子进程的状态信息。WCONTINUED
:返回收到SIGCONT
信号后继续执行的子进程的状态信息。options 参数是一个位掩码,这意味着可以通过按位或操作(|)来组合多个选项。例如,WNOHANG | WUNTRACED 可以在非阻塞模式下等待子进程的终止或暂停状态。
返回值
成功:
- 返回终止的子进程的 PID。
- 如果使用了
WNOHANG
选项且没有子进程终止,则返回0
。失败:返回
-1
,并设置errno
以指示错误类型。
关于参数options详细说明
0
(阻塞等待)
- 描述:这是
options
的默认值。waitpid
会阻塞父进程执行,挂起父进程,直到指定的子进程终止。 - 用途:父进程希望一直等待子进程的结束,并且不需要急切执行其他任务时使用,这和wait函数的行为一样,等待子进程执行完毕之后,才继续执行父进程。阻塞等待适用于没有高响应需求并且子进程的任务量比较小,或者必须拿到子进程结果父进程才能继续的的程序,父进程可以接受等待子进程执行完再继续执行自己。
2. WNOHANG
(非阻塞模式)
- 描述:
waitpid
不会阻塞执行。如果没有子进程终止,函数会立即返回,并返回0
以表示尚无子进程结束。 - 用途:在父进程不希望被子进程阻塞,可以继续执行其他任务的场景中使用。适用于高响应需求的程序或者子进程的任务量较大的程序。麻烦点:需要父进程周期性地调用
waitpid
以检查子进程是否已经终止。
3. WUNTRACED
(报告暂停状态)
- 描述:如果子进程因接收到
SIGSTOP
信号而暂停执行,waitpid
将返回子进程的 PID,并且status
中的相应位会被设置,以表明子进程已暂停。 - 用途:当父进程希望捕捉并处理子进程的暂停状态时使用。这通常用于调试目的,比如调试程序的时候,我们给进程发送暂停执行信号之后,父进程也能识别到这个进程暂停了,waitpid返回进程对应的信息。
WUNTRACED
选项用于让waitpid
不仅报告子进程的终止状态,还报告子进程的暂停状态。主要是为了在调试过程中更好地控制和监视子进程的状态
4. WCONTINUED
(报告继续执行状态)
- 描述:如果子进程在暂停后收到了
SIGCONT
信号并恢复执行,waitpid
将返回子进程的 PID,并且status
中的相应位会被设置,以表明子进程已恢复执行。 - 用途:当父进程希望监控子进程的继续执行状态时使用。这通常用于调试目的。
WCONTINUED
选项用于让waitpid
不仅报告子进程的终止状态,还报告子进程的恢复状态。
waitpid非阻塞定期检查子进程终止方式(略看即可)
父进程使用 waitpid
的 WNOHANG
选项以非阻塞方式等待子进程终止时,父进程不会被阻塞,可以继续执行其他操作。为了防止此时的子进程成为僵尸进程,我们必须定期检查子进程是否终止,父进程通常会在主循环中定期调用 waitpid
,检查子进程的状态。这个定期检查可以通过多种方式实现,下面是一些常见的实现方式:
- 使用主循环和睡眠:父进程可以在主循环中定期调用
waitpid
并在每次调用之间休眠一段时间。这是一种简单的实现方式,通过定期检查子进程状态而不会占用过多的 CPU 资源。这种方式是chatgpt告诉我的,我觉得这种方式完全就是多次一举。为什么呢?因为正常使用waitpid的阻塞等待子进程下,父进程不是也会被挂起吗?也不占用cpu资源,这里的代码循环也是在检查,发现没有之后sleep一会,使得挂起父进程从而不占用cpu,这本质上不一样吗?甚至多此一举,真这样搞的话不如直接使用阻塞方式。 - 使用定时器来定时检测,这样可以一边执行其他代码的时候抽空用waitpid检查一下,这样子才能满足有些程序高相应的需求,或者子进程执行的时候顺便执行接下来代码。而且代码实现应该比较简单。
我觉得真的想要认真考虑waitpid非阻塞定期,还需要实际做项目的时候遇到这个问题,再来具体写对应解决措施,可能这里说的不太准确,这里只是非常粗浅地聊了一下。对于简单的子进程任务,我们通常使用阻塞调用waitpid就够了,我觉得可能一般项目应该不缺这点效率。
后面有机会遇到需要waitpid非阻塞定期检查的实际例子的话,我再回来填充这一块内容。没有例子我觉得这一块真的很难谈。
阻塞等待和非阻塞等待的对比和总结
阻塞等待 (wait
或 waitpid
不带 WNOHANG
)
非阻塞等待 (waitpid
带 WNOHANG
)
是否选择非阻塞等待取决于父进程的具体需求和应用场景。以下是关于使用阻塞等待和非阻塞等待的详细解释,以帮助你决定在不同情况下的选择:
阻塞等待 (wait
或 waitpid
不带 WNOHANG
)
优点
-
简单直观:
阻塞等待是最简单的处理方式,当调用wait
或waitpid
时,父进程会被挂起,直到子进程结束。这样可以确保父进程能够在子进程终止后立即获取其退出状态。 -
确保资源清理:
使用阻塞等待可以确保父进程在子进程终止时及时处理其退出状态,防止子进程成为僵尸进程。这样可以有效地管理系统资源,避免进程表条目耗尽。 -
适合简单任务:
对于简单的应用程序或脚本,阻塞等待可能是一个合适的选择,因为它不需要额外的复杂处理。
缺点
- 阻塞:父进程在调用
wait
或waitpid
时会被挂起,这可能会导致程序暂停,影响程序的响应性和性能。如果父进程需要同时处理多个任务,阻塞等待可能不适合。
非阻塞等待 (waitpid
带 WNOHANG
)
优点
-
提高响应性:
非阻塞等待允许父进程在子进程未结束时立即返回,从而可以继续执行其他任务。这对于需要同时处理多个任务或需要高响应性的程序非常有用。 -
灵活性:
父进程可以在等待子进程退出的同时进行其他操作,或者周期性地检查子进程状态。这种方式提供了更多的控制选项,可以更灵活地管理进程。 -
避免阻塞:
避免了父进程因为等待子进程结束而被完全阻塞,使得程序可以在等待的过程中继续执行其他逻辑或处理其他事件。
缺点
-
复杂性增加:
非阻塞等待需要额外的逻辑来定期检查子进程状态,并处理可能的状态变化。这增加了程序的复杂性。 -
资源管理:
父进程需要自行管理进程状态的检查和处理,确保在适当的时候进行进程清理,以防止僵尸进程的积累。
选择非阻塞等待的情况
- 并发处理:当父进程需要同时处理多个任务时,例如在事件驱动或多线程应用中,非阻塞等待可以提高系统的整体响应性。
- 高性能要求:在性能要求较高的场景中,非阻塞等待可以避免因等待而造成的程序停滞。
- 异步处理:在需要异步处理子进程退出的情况下,非阻塞等待能够让父进程在子进程退出后继续处理其他事件。
总结
- 阻塞等待 适合于简单应用或脚本,当父进程能够接受等待的情况下,能够确保子进程的状态被正确处理。
- 非阻塞等待 更适合复杂的应用程序,特别是那些需要高响应性和同时处理多个任务的程序。它提供了更多的控制选项,但也增加了处理逻辑的复杂性。