【Linux】进程控制

张开发
2026/5/11 12:25:54 15 分钟阅读

分享文章

【Linux】进程控制
本篇文章主要讲解进程控制相关系统调用包括子进程创建、进程退出、进程等待与进程替换等目录1 创建子进程2 进程退出3 进程等待4 进程替换5 总结1 创建子进程创建子进程主要是通过 fork 函数来创建的之前我们已经讲解过了这里我们再回顾以及补充一下。fork 系统调用#include sys/types.h #include unistd.h pid_t fork(void); 功能为调用的当前进程创建一个子进程 返回值 创建失败返回 -1; 创建成功子进程返回0, 父进程返回子进程的pid示例代码#include stdio.h #include unistd.h #include sys/types.h int main() { printf(before create child: %d\n, getpid()); //创建子进程 pid_t id fork(); if (id -1) { perror(fork); return 1; } printf(after create child: %d\n, getpid()); return 0; }这里对于 fork 有那么几个问题问题1为什么要给父进程返回子进程的 pid解答因为父进程可以创建多个子进程父进程:子进程1:n而 pid 是进程标识符有了 pid 父进程就可以控制子进程了比如回收进程各种内核数据结构所以给父进程返回子进程 pid 是方便父进程来控制子进程。问题2一个函数是如何返回两个值的解答函数的返回值一般都是放在CPU寄存器中函数调用结束后主函数会从寄存器中拿取函数的返回值。而调用 fork 执行 return 语句之前子进程实际上已经创建成功了拥有了独立的进程地址空间进程地址空间也就是虚拟内存到后面会进行讲解这里可以理解为每个进程都拥有独立的虚拟地址空间注意是虚拟并不是真实的物理内存空间此时只需要修改子进程对应返回值的寄存器将其值设为0。父进程返回值的寄存器值依然是子进程的 pid这样就可以做到给不同进程返回不同的值了。问题3一个变量 id 是如何同时拥有两个值的解答这个问题跟进程地址空间有关这里我们先浅浅理解一下。子进程创建之后呢子进程与父进程拥有独立的虚拟地址空间注意是虚拟地址空间不是物理地址空间。所以 id 地址相同实际上是虚拟地址空间但是真正存放 id 变量的实际上是两个不同的物理地址空间所以 id 看上去是一个变量实际上在父子进程中是用不同的物理地址空间存放的。fork 之后的流程在 fork 之后会依次进行如下过程1 内核首先会分配新的内核数据结构比如 task_struct给子进程2 将父进程的部分数据结构的内容拷贝给子进程3 将子进程的状态设为就绪状态R为其分配一个 pid4 复制父进程的进程上下文数据以至于子进程可以从 fork 代码之后正常执行为子进程设置 return 返回值5 fork 返回开始调度器调度进程。注意父子进程谁先运行完是由调度器决定的并不是谁先创建谁就先运行完。其余与 fork 相关的内容都与进程地址空间相关在进程地址空间中我们再进行讲解。2 进程退出退出码一个进程有没有正常退出我们是通过其退出码来确定的这里的退出码也就是我们之前了解过的进程的错误码我们可以使用 strerror 函数将错误码转换为字符串描述#include stdio.h #include string.h int main() { for (int i 0; i 200; i) printf(%d: %s\n, i, strerror(i)); return 0; }上面的各种错误信息前面的数字正是其对应的错误码。一般我们如果输入某个命令出错了会打印出一些错误信息这些错误信息正是根据其错误码打印出来的。例如我们使用 ls 命令查看一个不存在的目录可以发现 ls 报出的错误是 No such file or directory正是 2 号错误码转成的字符串描述那是不是呢我们可以在命令行中输入如下命令来查看上一个程序的退出码echo $?之前我们说过命令也是可执行程序所以其也是有退出码的。所以我们可以通过 echo $? 来查看 ls 的退出码查看之后果然是2那么如果我们查看一个存在的目录或者文件ls 会执行成功按照上面的退出码ls 的退出码应该是 0当然上面的退出码是系统默认默认的退出码我们也可以通过别的方法来自己设定程序的退出码比如成功返回1发生某种错误返回2可以自己设置退出码这样就可以根据退出码做出相应的判断了。退出情况对于一个程序来说其退出情况一共有三种1 程序正常结束且结果正确退出码一般为 02 程序正常结束但是结果不正确退出码一般不为 03 程序由于发生某种错误比如野指针访问、除0等异常退出退出信号不为 0所以对于一个程序来说程序正常还是异常退出其实看的是退出信号如果程序异常退出了退出码是没有意义的而一个程序正常退出之后结果对还是不对看的是退出码。进程退出的三种方式1 main 函数 return 返回第一种进程退出的方式就是我们直接在 main 函数里面 return 返回一个值。之前我们总喜欢在 main 函数最后写一句 return 0其实就是在返回我们当前程序的退出码#include stdio.h int main() { //... return 0; }那如果我们改成 return 1那么退出码其实就会变成1#include stdio.h int main() { //... return 1; }那么为什么我们习惯都会写成 return 0 呢很简单就是因为系统默认成功的退出码就是0是一种默认写法便于与系统或者其他程序协作。2 _exit 系统调用退出在 Linux 中也有一个系统调用是跟进程退出相关的那就是 _exit 系统调用#include unistd.h void _exit(int status); 参数 status: 进程的退出码示例#include stdio.h #include unistd.h int main() { sleep(3); _exit(0); }但是当我们将 status 设为 -1 时进程的退出码竟然变成了 255#include stdio.h #include unistd.h int main() { sleep(3); _exit(-1); }这是因为虽然 status 是一个 int 值但是系统在获取退出码时只会取其低 8 位并且将其视为一个unsigned char所以虽然传入的是 -1但是在内存中存储的补码所以 -1 其实是 11111111 11111111 11111111 11111111取其低 8 位就是 11111111作为 unsigned char其实就是 255 了。那么如果是 status -2 呢其实就是 254 了#include stdio.h #include unistd.h int main() { sleep(3); _exit(-2); }3 exit 库函数退出在 C 语言中也有一个库函数 exit 是用来进行进程退出的#include stdlib.h void exit(int status); 参数 status: 进程的退出码这里的 status 与 _exit 中的 status 相同系统只会取其低 8 位并且当做 unsigned char 输出。其实在 main 函数中使用 return 退出如果 return -1退出码也是 255。所以不管是使用哪种方式进行进程退出其退出码都是 0 ~ 255。示例#include stdio.h #include stdlib.h #include unistd.h int main() { sleep(3); exit(-1); }exit 与 _exit 区别进程退出内核会进行一系列的清理操作比如清理进程的 task_struct。但是 exit 是一个库函数而进程是系统资源所以 exit 库函数想要让进程退出必须要通过内核而访问内核又必须通过系统调用所以 exit 的底层必然是封装了 _exit 系统调用的这样才能完成进程退出功能。但是 exit 函数呢又不仅仅是调用了 _exit 系统调用他还会完成一些别的工作。1 调用用户定义的清理函数这里我们使用 C 中类的析构函数来展示//test.cpp #include iostream #include string #include cstdlib #include unistd.h class A { public: A(const std::string name):_name(name){} ~A() { std::cout _name ~A() std::endl; } private: std::string _name; }; //创建全局对象 A ga(全局对象); int main() { //创建局部对象 A a(局部对象); //创建静态对象 static A sa(静态对象); exit(0); return 0; }在这里呢我们构建了全局、局部与静态三个类对象我们在 main 函数结束之前调用了 exit 函数我们可以看到全局和静态对象被析构了但是局部对象没有析构这是因为调用了 exit 之后就会进入到 exit 函数的逻辑执行完之后进程直接退出了并不会返回 main 函数继续执行后续代码那么 main 函数中的局部对象也就没有等到 main 函数的作用域结束所以并不会发生析构而全局对象和静态对象的生命周期是随进程的在进程正常退出之前回去调用他们的析构函数进行资源的清理工作。那么如果我们将 exit 换为 _exit 系统调用呢#include iostream #include string #include cstdlib #include unistd.h class A { public: A(const std::string name):_name(name){} ~A() { std::cout _name ~A() std::endl; } private: std::string _name; }; //创建全局对象 A ga(全局对象); int main() { //创建局部对象 A a(局部对象); //创建静态对象 static A sa(静态对象); _exit(0); return 0; }我们可以看到并没有进行资源的清理工作所以 _exit 并不会进行资源清理工作。2 刷新用户语言级别的缓冲区我们直接来看下面这两段代码的对比#include stdio.h #include unistd.h #include stdlib.h int main() { printf(hello); exit(0); return 0; }#include stdio.h #include unistd.h #include stdlib.h int main() { printf(hello); _exit(0); return 0; }可以看到调用 exit 会输出 hello但是 _exit 就没有。产生这种情况的原因就是因为语言级缓冲区的存在#include stdio.h int main() { printf(hello world\n); printf(hello world); sleep(2); return 0; }运行一下这个程序发现第一个 hello world 一下就刷出来了第二个 hello world 要等上 2 秒钟才会刷出来。其实我们在使用 printf 库函数的时候其实这个函数里面是会有一个缓冲区的一块内存空间这个缓冲区就称为用户级别缓冲区或者语言级别缓冲区。而由于 printf 函数是打印到屏幕上对于屏幕采用的是行刷新策略。所以一旦我们在后面加上 \n会将用户级缓冲区中的内容立即刷新到内核也就看到了第一个 hello world但是第二个 hello world 没有加 \n所以需要等待进程结束才会进行刷新所以要等 2 s 才能刷新出来。而 exit 与 _exit 的区别就是 exit 退出时会刷新这个用户级别缓冲区而 _exit 不会。所以为了进程退出时能够清理资源并且刷新缓冲区我们推荐进程退出时使用 exit 函数。3 进程等待进程等待的必要性进程等待是指父进程通过系统调用进入阻塞状态并且主动等待一个或者多个子进程运行结束之后回收子进程剩余资源的过程。进程等待是一定要做的那么为什么父进程要等待子进程呢主要有两点原因也是进程等待主要完成的两件事情1 回收子进程的僵尸状态之前我们在进程状态中讲解过进程运行结束后会先进入一种僵尸状态被称之为僵尸进程僵尸进程在某种意义上已经死了我们是无法通过 kill -9 杀死他的而且僵尸进程会占用系统内存资源造成内存泄露问题。所以如果系统中有大量的僵尸进程系统可能会因为内存资源不足影响运行。而父进程一旦通过进程等待检查到了子进程的僵尸状态就会回收子进程的各种信息然后回收子进程避免子进程一直为僵尸进程。2 获取子进程的退出状态我们在退出码小节讲解过进程是正常还是异常退出是通过退出信号来看的退出信号为 0 代表正常退出退出信号不为 0 代表异常退出。而正常退出后运行结果是否正确是根据退出码来看的一般退出码为 0代表结果正确不为 0代表结果错误。那么子进程将任务完成的怎么样我们是需要知道的这个退出状态是保存在子进程的 task_struct 中的所以父进程就要通过进程等待来获取子进程的退出状态以便于我们做出判断。所以父进程为了进行进程等待一般都是最先创建最后退出的。进程等待的方法我们主要通过两个系统调用来进行进程等待wait 与 waitpid 系统调用。wait 系统调用#include sys/types.h #include sys/wait.h pid_t wait(int *wstatus); 参数 wstatus: 输出型参数主要是获取子进程的退出状态不关心可以设为 NULL 返回值成功返回子进程的 pid失败返回 -1等待单个子进程#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h int main() { //创建子进程 pid_t id fork(); if (id 0) { //child printf(I am child, pid: %d\n, getpid()); sleep(2); exit(0); } //使用 wait 来等待子进程 pid_t rid wait(NULL); if (rid -1) { perror(wait); return 1; } printf(子进程等待成功, pid: %d\n, rid); return 0; }等待多个子进程上面是等待一个子进程的情况那么我们要是等待多个子进程呢如果我们只 wait 一次就会出现如下情况#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h const int childnum 10; int main() { // 创建多个子进程 for (int i 0; i childnum; i) { pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); sleep(2); exit(0); } } pid_t rid wait(NULL); if (rid -1) { perror(wait); return 1; } printf(子进程等待成功, pid: %d\n, rid); return 0; }我们可以看到父进程只等待了一个子进程就直接退出了所以 wait 函数每次只会等待一个子进程当等待到一个子进程之后直接就会退出了所以我们需要循环等待多个子进程#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h const int childnum 10; int main() { // 创建多个子进程 for (int i 0; i childnum; i) { pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); sleep(2); exit(0); } } //使用 wait 来等待子进程 for (int i 0; i childnum; i) { pid_t rid wait(NULL); if (rid -1) { perror(wait); return 1; } printf(子进程等待成功, pid: %d\n, rid); } return 0; }waitpid 系统调用上面的 wait 是随机等待任意一个子进程的那么如果我们想要等待一个特定的子进程呢这时候我们就需要使用 waitpid 接口了#include sys/types.h #include sys/wait.h pid_t waitpid(pid_t pid, int *wstatus, int options); 参数 pid: 等待进程的 pid设为 -1 表示等待任意一个子进程等价于 wait wstatus: 与 wait 参数相同等待进程的退出状态 options: 设为 0 表示阻塞等待设为 WNOHANG 表示非阻塞等待 返回值成功时返回等待进程的 pid如果失败返回 -1; 如果设置了 WNOHANG, 那么父进程发现没有子进程在等待那就会返回 0等待单个子进程#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h const int childnum 10; int main() { // 创建子进程 pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); sleep(2); exit(0); } // 使用 waitpid 来等待子进程第一个可以设为-1或者id0 表示阻塞等待 pid_t rid waitpid(id, NULL, 0); if (rid -1) { perror(waitpid); return 1; } printf(子进程等待成功, pid: %d\n, rid); return 0; }等待多个子进程在等待多个子进程时我们可以将 waitpid 中的第一个参数改为 -1表示等待任意一个子进程而且 waitpid 与 wait 相同只会等待一个子进程所以我们依然需要循环来等待所有子进程#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h const int childnum 10; int main() { // 创建多个子进程 for (int i 0; i childnum; i) { pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); sleep(2); exit(0); } } // 使用 waitpid 来等待子进程 for (int i 0;i childnum; i) { pid_t rid waitpid(-1, NULL, 0); if (rid -1) { perror(waitpid); return 1; } printf(子进程等待成功, pid: %d\n, rid); } return 0; }waitpid 的功能比 wait 更全wait 相当于 waitpid 的子功能。waitpid 还可以等待指定的一个子进程并且可以选择阻塞或者非阻塞等待所以以后在进程等待时我们就使用 waitpid不使用 wait 了。获取子进程的退出状态在 waitpid 与 wait 系统调用中有一个参数叫做 wstatus这个参数就是用来获取子进程退出状态的。wstatus 是一个输出型参数类型是 int*。当我们定义一个整形变量 status 传递进去地址之后等待成功之后会将进程的退出状态写入到我们传入 status 变量中我们就可以获取子进程的退出状态了。*wstatus 变量循环进程的退出信息是这样的*wstatus 为 int 类型变量共有 32 位。但是会用到的只有低 16 位高 16 位是用不到的。在这低 16 位中低 7 位表示退出信号第 8 位表示 coredump 标志位次低 8 位表示退出码。所以进程有没有正常退出我们看的是 *wstatus 中的低 7 位而正常退出后运行结果正不正确我们用的是次低 8 位。而那个 coredump 标志位我们的等到进程信号时会进行讲解。所以要想获得进程的退出信息我们需要进行的是位操作退出码: (status 8) 0xFF 退出信号: status 0x7F#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h const int childnum 10; int main() { // 创建子进程 pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); sleep(2); exit(0); } //使用 waitpid 来等待子进程 int status 0; pid_t rid waitpid(-1, status, 0); if (rid -1) { perror(waitpid); return 1; } //打印子进程退出状态 printf(子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n, rid, ((status 8) 0xFF), (status 0x7F)); return 0; }我们可以将退出码设为 1#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h const int childnum 10; int main() { // 创建子进程 pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); sleep(2); exit(1); } //使用 waitpid 来等待子进程 int status 0; pid_t rid waitpid(-1, status, 0); if (rid -1) { perror(waitpid); return 1; } //打印子进程退出状态 printf(子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n, rid, ((status 8) 0xFF), (status 0x7F)); return 0; }让子进程异常退出#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h const int childnum 10; int main() { // 创建子进程 pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); sleep(2); int* ptr NULL; *ptr 10; exit(1); } //使用 waitpid 来等待子进程 int status 0; pid_t rid waitpid(-1, status, 0); if (rid -1) { perror(waitpid); return 1; } //打印子进程退出状态 printf(子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n, rid, ((status 8) 0xFF), (status 0x7F)); return 0; }所以我们就可以通过 status 来获取进程的退出状态了我们就可以通过 status 变量来知道子进程的运行结果从而做出相应判断了。但是我们提取子进程状态时每次都需要进行位操作很麻烦所以系统中位我们提供了两个宏方便我们进行操作WIFEXITED(status): 用来查看进程是否正常退出如果正常退出返回真异常退出返回假 WEXITSTATUS(status): 提取子进程的退出码我们可以查看一下这两个宏在系统中的定义需要注意的就是 WIFEXITED(status) 返回的是进程是否正确退出返回的是一个逻辑值并不是真正的退出信号。#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h int main() { // 创建子进程 pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); sleep(2); int *ptr NULL; *ptr 1; exit(1); } // 使用 waitpid 来等待子进程 int status 0; pid_t rid waitpid(-1, status, 0); if (rid -1) { perror(waitpid); return 1; } if (WIFEXITED(status)) { printf(进程正常退出\n); printf(子进程等待成功, pid: %d, exit code: %d\n, rid, WEXITSTATUS(status)); } else { printf(进程异常退出, exit signal: %d\n, (status 0x7F)); } return 0; }阻塞等待与非阻塞等待在 waitpid 中还有一个参数就是第三个参数 options选择是阻塞等待还是非阻塞等待。4 进程替换我们在创建子进程小节中讲过fork 之后父子进程执行的是父进程 fork 代码之后相同的代码那么子进程要是想要执行别的代码该怎么办呢这时候我们就需要使用进程替换了。进程替换原理之前我们在 C 语言阶段了解过内存的分布包括代码段、数据段、堆取、栈区其实在堆栈之间还有一个共享区所谓的进程替换只不过就是把子进程中的代码段和数据段替换成磁盘中的别的代码和数据所以进程替换是不需要修改内核中进程管理的其他数据数据结构的只需要将磁盘中新的代码和数据替换掉内存中跟需要进行进程替换的进程的代码和数据即可完成程序替换。上面的图片是为了讲解进程替换原理画出的一张图在我们学习完进程地址空间也就是虚拟内存之后程序替换的真实原理其实是不过这个需要我们到了虚拟内存讲解时才能了解了。进程替换接口使用对于进程替换来说其接口都是以 exec 开头的所以称为 exec 系列函数其中有六个库函数#include unistd.h int execl(const char *pathname, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *pathname, const char *arg, ...); int execv(const char *pathname, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);还有一个系统调用#include unistd.h int execve(const char *pathname, char *const argv[], char *const envp[]);别看 exec 系列函数很多但其实学会了一个再结合命名很快就可以学会全部的函数了。1 execl 函数execl 函数中的 l 是 list 的意思意思是我们需要将后面的参数像列表一样一个一个列出来。下面是这个函数的具体使用方法#include unistd.h int execl(const char *pathname, const char *arg, ...); 参数 pathname: 需要替换的二进制文件名称注意需要带路径 arg: 是如何执行二进制程序注意最后必须以 NULL 结尾。 这里替换的时候我们命令行怎么执行这里就怎么传递参数就可以。 返回值成功时没有返回值失败时返回 -1exec 系列函数如果替换成功了是不会有返回值的。因为一旦替换成功就会去执行替换后的代码当前代码就不会执行了那么返回值其实也就没什么用了。替换为自己的程序//exec.c #include stdio.h int main() { printf(hello world\n); return 0; } //test.c #include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h int main() { // 创建子进程 pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); //使用 execl 将子进程替换为 ls -a -l 程序 //注意以 NULL 结尾 //程序必须带路径 int n execl(./exec, ./exec, NULL); //这里可以判断也可以不判断因为程序替换成功就不会执行后续代码了 //if (n 0) perror(execl); exit(1); } printf(我是父进程, pid: %d\n, getpid()); //父进程执行完需要等待子进程 int status 0; int rid waitpid(-1, status, 0); if (rid 0) { perror(waitpid); return 1; } printf(子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n, rid, WEXITSTATUS(status), (status 0x7F)); return 0; }注意在这里为了清晰看到程序替换的效果故意将 exec.c 中的退出码写为了 1替换为系统程序各种命令#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h int main() { // 创建子进程 pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); //使用 execl 将子进程替换为 ls -a -l 程序 //注意以 NULL 结尾 //ls 必须带路径 int n execl(/usr/bin/ls, ls, -a, -l, NULL); //这里可以判断也可以不判断因为程序替换成功就不会执行后续代码了 //if (n 0) perror(execl); exit(1); } printf(我是父进程, pid: %d\n, getpid()); //父进程执行完需要等待子进程 int status 0; int rid waitpid(-1, status, 0); if (rid 0) { perror(waitpid); return 1; } printf(子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n, rid, WEXITSTATUS(status), (status 0x7F)); return 0; }2 其他 exec 系列函数的使用有了 execl 的使用经历之后结合命名就可以很快掌握其他函数了exec 命名规律符号全称规律llist(列表)传入的参数用列表的形式vvector(数组)传入的参数使用指针数组注意以NULL结尾。一般与命令行参数 argv[] 数组配合使用pPATH(路径)会自动搜索环境变量PATH第一个参数就不用带路径了eenvironment(环境变量)一般是自己传入环境变量结合上面的命名规律就很好再结合之前的 execl 使用很容易就上手了。比如execlp#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h int main() { // 创建子进程 pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); //使用 execlp 将子进程替换为 ls -a -l 程序 //注意以 NULL 结尾 //这里的 ls 就不必带路径了因为 PATH 中带有 /usr/bin所以会自动搜索 int n execlp(ls, ls, -a, -l, NULL); //这里可以判断也可以不判断因为程序替换成功就不会执行后续代码了 //if (n 0) perror(execlp); exit(1); } printf(我是父进程, pid: %d\n, getpid()); //父进程执行完需要等待子进程 int status 0; int rid waitpid(-1, status, 0); if (rid 0) { perror(waitpid); return 1; } printf(子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n, rid, WEXITSTATUS(status), (status 0x7F)); return 0; }但是需要注意的是虽然其会自动搜索 PATH 环境变量但是我们替换为我们自己的程序时依然需要带上路径因为 PATH 环境变量中并没有我们自己的路径。如果想要不带路径那就必须将我们自己程序的路径添加到 PATH 环境变量中。execvp#include stdio.h #include unistd.h #include stdlib.h #include sys/types.h #include sys/wait.h int main() { // 创建子进程 pid_t id fork(); if (id 0) { // child printf(I am child, pid: %d\n, getpid()); //使用 execvp 将子进程替换为 ls -a -l 程序 //注意以 NULL 结尾 //这里的 ls 就不必带路径了因为 PATH 中带有 /usr/bin所以会自动搜索 //execvp 注意第二个参数传入的是一个数组 char* argv[] {ls, -a, -l, NULL}; int n execvp(ls, argv); //这里可以判断也可以不判断因为程序替换成功就不会执行后续代码了 //if (n 0) perror(execlp); exit(1); } printf(我是父进程, pid: %d\n, getpid()); //父进程执行完需要等待子进程 int status 0; int rid waitpid(-1, status, 0); if (rid 0) { perror(waitpid); return 1; } printf(子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n, rid, WEXITSTATUS(status), (status 0x7F)); return 0; }通过上面两个示例相信大家已经懂得 p、v 这两个如何使用了e 在日常中使用的并不多这里就不进行讲解了接下来大家可以自行组合了。注意在 exec 系列函数中只有一个系统调用 execve其余的都是库函数。所以除了 execve其余的函数底层都是调用了 execve 系统调用的5 总结本篇文章中我们讲解了进程控制的四大模块包括创建子进程、进程退出、进程等待与进程替换对应的最佳接口分别是 fork、exit、waitpid 以及 exec 系列函数。创建子进程后父子进程共享 fork 之后的代码如果想要替换子进程中的代码就需要使用 exec 系列函数来进行进程替换。而子进程一旦退出如果父进程不回收他那么子进程就会变成僵尸进程造成内存泄露现象所以父进程需要等待子进程防止这种现象的发生并且回收子进程的退出状态。下一篇文章中我们会利用进程控制与环境变量、命令行参数来写一个小的 shell 程序通过这个程序我们不仅可以熟悉接口还可以了解命令行解释器 bash 是如何运行的。

更多文章