
作者主页
| 现有专栏 | |
| 算法篇 | Linux篇 |
| QT百日筑基篇 | 数据结构篇 |
进程的控制

目录/索引:
目录
进程的创建
进程的创建
深度了解"fork"
fork的返回值:
fork的简单使用
进程在虚拟机的作用:
写时拷贝:
fork的常规用法:
fork调用失败的常见原因:
进程的终止:
进程退出的场景:
退出码的详解:
正常终止(查看进程的退出码)指令: echo $?
基础退出码:
1-133退出码大全:
Linux Shell主要的退出码:
errno:获取进程结束的错误码
进程退出的常见方法:
1.main函数进行返回
2. 调用exit(是C语言方向的接口)
3. _exit(系统级别的接口)
_exit vs exit:
进阶了解exit and _exit(1)
进程的等待:
进程等待必要性
进程等待的方法:
wait()
waitpid()
获取子进程的status(详细,简单易懂)
阻塞等待和非阻塞等待:
进程的替换
替换原理:
替换的操作:
程序的返回:
创建子进程完成替换:
exec家族:
execl
exclp
编辑execv
execvp
execle
execvpe
execve(系统接口)
进程的创建
深度了解"fork"
for在我们的进程中是非常重要的一个函数,它可以进行创建子进程,有利于我们后续的代码的书写。
先看一下fork的文档
man fork

fork的返回值:

成功会将自己的进程返回给父进程(前面说过主要用于父进程对该进程的回收),并且成功子进程返回0, 父进程返回非0(子进程的pid) , <0则代表创建失败。都失败了我还玩什么玩直接return。
fork的简单使用
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 pid_t pi=fork();
8 while(1)
9 {
10 if(pi==0)//子进程
11 {
12 printf("这是一个子进程\\n");
13 sleep(5);
14 }
15
16 else if(pi>0)
17 {
18 printf("这是一个父进程\\n");
19 sleep(5);
20 }
21 else
22 {
23 printf("创建错误\\n");
24 //错误了我还玩啥玩
25 return 1;
26 }
27 }
28 return 0;
29 }
运行结果展示:

这就是我们fork的简单的使用。
调用时进程会给新的子进程分配新的内存块和内核数据结构给子进程。
将父进程对应的数据拷贝给子进程一份,(像我们前面所说的环境变量)。
添加子进程到系统进程列表当中, fork返回调度器进行调度。
进程在虚拟机的作用:
如果创建的子进程的代码是OS(另一台操作系统的代码)的代码那么就相当于我们跑了2台操作系统, 这也就是虚拟机的原理。
写时拷贝:
在进行fork操作的时候我们刚开始的数据段是可以进行写的, 但是fork之后我们的数据段的权限会变成只读的。(fork之后,父子代码共享, 父子将不再进行写入, 数据也是共享的)。当一方进行写入,因为权限的只读的,会发生错误,那么就会触发写时拷贝, 他会在将我们的代码和数据在物理内存中进行拷贝一份, 这样我们父与子就能有各自的物理内存了。
一句话总结:OS以这种错误的方式给子进程进行分配空间(并不是真正的错误)。这样我们能更好的进行空间的利用,等到我们需要分别操作的时候,在进行申请空间。

因为有了写时拷贝技术的存在, 我们的父子进程才能彻底的进行分离, 完成了进程独立性的技术保证! 写时拷贝是一种延时申请技术,可以提高整体内存的使用率。
fork的常规用法:
一个进程希望复制自己, 使父子进程同时执行不同的代码,例如:父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回会调用exec函数。
fork调用失败的常见原因:
这个问题也就是子进程创建失败的原因。
系统中有太多的进程了。
实际用户的进程数超出了限制。
创建虚拟空间和页表失败了。
进程的终止:
进程的终止的本质使释放系统的资源, 就是释放进程释放相关的内核数据结构(PCB)和对应的代码和数据。
进程退出的场景:
1. 代码运行,结果正确。
2. 代码运行完毕,结果不正确。
3. 代码异常退出(进程的信号会进行说明的)。
我们的ctrl+c 可以强制的让信号进行终止。
退出码的详解:
正常终止(查看进程的退出码) 指令: echo $?
作用:查看进程的退出码。
我们对应的退出码也要写道task_struct中
我们在main函数进行调用的时候一般都会返回return 0,这里的0表示进程正常进行退出。
echo $?

我们写一段程序来验证这一功能:
我们能看到这是一个简单的c语言程序。没有任何的毛病,所以这里他会一致的进行执行, 到最后返回一个0。
#include<stdio.h>
int main()
{
printf("你好退出码!");
return 0;
}

像我们在shell(命令行参数)中运行的命令像ls pwd也是一个对应的c语言程序。那我们是不是也能查看对应的进程退出码?那我们就来实验一下。
下图,发现果然如此我们的指令是正确的,他们都正常的进行了对应的退出。

那我们试试不正常的退出
我们发现我们的当前目录不存在add.c这个文件。然后我们打印对应的退出码, 进行查看果然不是0是2,那么这个2代表的是什么呢???别急我们先看下面的一个情况,在进行说明:

那如果我们再次使用echo $?还是2吗???
我们发现是0为啥不是2???注意我们上次使用的是echo $?是正常进行结束的, 所以我们的这次返回值是返回上一次的额结果。 所以我们能得出以下的结论:
我们的指令echo $?是返回上一个进程的退出码的。

我们记得上一次打印出来了一个2这个2是啥我们现在就来看一看。
基础退出码:
我们需要用到一个函数。
strerror()
看一下文档:
man 3 strerror
作用: 这个函数会返回一个指针指向一个字符串,主要进行描述这个错误的过程。
说人话就是我们给他一个错误码, 他会进行对应的解释,将错误码转化为错误描述。

我们编写一个程序来打印这些退出码:
#include <stdio.h>
#include <string.h>
int main() {
for (int i = 0; i < 200; i++)//我们不知道有多少个退出码,先设置200个
{
printf("退出码:%d 对应的解释是%s\\n", i , strerror(i));
}
return 0;
}
我们能发现:
1.果然我们的0这个退出码:后面写了一个success代表成功。
2.我们1号退出码:是操作不被允许。
3.我们最想知道的2号退出码是:没有找到该文件。
4.我们发现我们的退出码:134 往后就打印出Unknown error 134说明没有这个对应的退出码。
所以我们的退出码是0-133。
下面附带一张表来详细的解释这些信号的用途。

1-133退出码大全:
| 1 | 通用未知错误 | 脚本 / 程序通用失败,未细分错误类型(最常见) |
| 2 | shell 内置命令语法错误 | bash 脚本参数、语法写错,命令不存在参数 |
| 3 | 内部数据校验失败 | 程序数据校验、文件格式校验出错 |
| 4 | IO 操作失败 | 读 / 写文件、管道 IO 异常 |
| 5 | 程序自身初始化失败 | 配置加载、资源初始化崩溃 |
| 6 | 无效文件句柄 / 描述符 | 操作已关闭、不存在的 fd |
| 7 | 命令执行失败 | system/exec 调用外部命令返回失败 |
| 8 | 内存分配失败 | malloc/mmap 申请内存不足 |
| 9 | 参数数量不匹配 | 程序传入命令行参数个数错误 |
| 10 | 参数格式非法 | 参数类型、数值范围不符合要求 |
| 11 | 数组 / 指针越界 | 底层程序内存访问越界(C/C++ 常见) |
| 12 | 外部程序调用失败 | 子进程执行外部工具返回异常 |
| 13 | 文件不可写 | 权限不足、只读文件、磁盘满 |
| 14 | 文件不可读 | 无读权限、文件损坏、不存在 |
| 15 | 管道通信断开 | 管道一端提前关闭,SIGPIPE 场景 |
| 16 | 目录访问失败 | cd、opendir 无权限或目录不存在 |
| 17 | 文件已存在 | 创建文件 / 目录时目标已存在 |
| 18 | 文件不存在 | open/stat 找不到指定路径 |
| 19 | 设备不存在 | 操作硬件设备、串口、磁盘设备丢失 |
| 20 | 子进程创建失败 | fork 调用失败,进程数上限 |
| 21 | 用户 / 组 ID 无效 | setuid/setgid 指定 ID 不存在 |
| 22 | 参数超出合法范围 | 数字参数过大 / 过小、越界 |
| 23 | 文件过大 | 文件尺寸超出程序处理上限 |
| 24 | 打开文件数超限 | 超过进程 ulimit 文件句柄限制 |
| 25 | 磁盘空间耗尽 | 写入时磁盘满 |
| 26 | 权限不足 | 执行、读写、创建权限全部拒绝 |
| 27 | 文件类型不匹配 | 预期普通文件,实际是目录 / 链接 / 设备 |
| 28 | 链接数量超限 | 文件硬链接达到系统上限 |
| 29 | 跨设备硬链接禁止 | 不能对不同磁盘分区创建硬链接 |
| 30 | 文件名过长 | 路径 / 文件名超出系统 NAME_MAX 限制 |
| 31 | 信号处理函数异常 | signal 注册、信号回调执行出错 |
| 32~63 | 程序自定义业务错误 | 无统一标准,各软件自行定义(数据库、中间件常用) |
| 64 | EX_USAGE:命令参数使用错误 | POSIX 标准,参数非法、缺失 |
| 65 | EX_DATAERR:输入数据格式错误 | 文件内容、输入数据损坏、格式不对 |
| 66 | EX_NOINPUT:输入文件无法打开 | 源文件丢失、无读权限 |
| 67 | EX_NOUSER:指定用户不存在 | 程序指定运行用户 id 无效 |
| 68 | EX_NOHOST:主机名解析失败 | DNS 解析、hostname 找不到 |
| 69 | EX_UNAVAILABLE:服务不可用 | 依赖服务未启动、端口未监听 |
| 70 | EX_SOFTWARE:程序内部逻辑错误 | 代码 bug、断言失败、逻辑崩溃 |
| 71 | EX_OSERR:操作系统底层错误 | 系统调用异常、内核资源故障 |
| 72 | EX_OSFILE:系统关键文件损坏 | /etc/passwd、lib 库缺失损坏 |
| 73 | EX_CANTCREAT:无法创建输出文件 | 目录只读、权限、磁盘满 |
| 74 | EX_IOERR:文件读写 IO 硬件错误 | 磁盘坏道、存储硬件故障 |
| 75 | EX_TEMPFAIL:临时可重试错误 | 资源临时占用,稍后可重试 |
| 76 | EX_PROTOCOL:网络协议交互错误 | HTTP/TCP/ 自定义协议握手失败 |
| 77 | EX_NOPERM:权限严重不足 | 系统级操作被内核拒绝 |
| 78 | EX_CONFIG:程序配置文件错误 | conf 配置语法、参数非法 |
| 79~127 | 软件自定义业务码 | 各类应用、脚本自定义细分错误 |
退出码 128 ~ 133(信号杀死进程:128 + 信号编号)
| 128 | 128 + 0 | 无 | 极少出现,信号 0 仅检测进程存活,不终止程序 |
| 129 | 128 + 1 | SIGHUP | 终端断开、会话退出;守护进程收到常自动重启 |
| 130 | 128 + 2 | SIGINT | Ctrl+C 手动中断程序,日常调试最常见 |
| 131 | 128 + 3 | SIGQUIT | Ctrl+\\ 退出,程序会生成 core dump 核心转储文件 |
| 132 | 128 + 4 | SIGILL | 非法指令,CPU 无法解析代码,程序编译 / 内存损坏 |
| 133 | 128 + 5 | SIGTRAP | 调试断点触发(gdb/cgdb 调试器断点中断进程) |
退出码:
退出码是告诉我们程序最后一次执行的命令状态, 在命令结束后,我们可以知道命令是成功完成还是错误的形式进行结束, 程序的退出码为0代表成功,没有任何的问题, 代码为1和0以外的任何退出码都视为不成功。
Linux Shell主要的退出码:
基础退出码:
| 0 | 命令执行成功,无错误 | ls、cd、echo 正常运行 |
| 1 | 通用未知错误 / 一般性失败 | 命令语法错误、普通运行失败 |
| 2 | 内置命令参数错误、非法参数 | ls 传入无效参数、脚本传参格式错误 |
| 124 | 命令超时(timeout 命令触发) | timeout 5 sleep 10 |
| 125 | timeout 自身执行失败 | timeout 传非法选项 |
| 126 | 找到文件,但无执行权限 | ./test.sh 但无 x 权限 |
| 127 | 命令未找到,不存在 | 输入不存在的 abc123 命令 |
| 128 | 无效退出码(退出参数非数字) | exit abc |
信号退出码
进程被信号终止时,退出码 = 128 + 信号值
| 129 | 1 | SIGHUP | 终端断开、会话挂断 |
| 130 | 2 | SIGINT | Ctrl+C 中断程序 |
| 131 | 3 | SIGQUIT | Ctrl+\\ 退出并生成 core |
| 134 | 6 | SIGABRT | 程序主动 abort 崩溃 |
| 137 | 9 | SIGKILL | kill -9 强制杀死进程 |
| 143 | 15 | SIGTERM | kill 正常终止进程 |
脚本自定义退出码
| 64 | 命令行参数使用错误(标准 POSIX) |
| 65 | 输入文件不存在 / 读取失败 |
| 66 | 输出文件无法写入、权限不足 |
| 69 | 服务 / 程序不可用 |
| 70 | 内部脚本异常 |
| 77 | 权限不足无法访问配置 |
我们也能编写一个程序来出现错误码“2”号:
我们能看到我已读的形式进行打开, 该文件不存在这个目录所以会失败。
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main() {
FILE* fp=fopen("add.txt","r");
if(fp==NULL) return errno;
//文件正常
//…
return 0;
}
那么就会返回errno,这个函数下面进行了对应的讲解。他就会返回2该文件不存在。

errno:获取进程结束的错误码
man 3 errno

进程退出的常见方法:
1.main函数进行返回
经过上面的演示我们知道了我们的main函数可以通过return进行返回退出码,这个退出码可以被我们自己设置, 也可通过errno的方式来进行让os来进行对应的设置。

2. 调用exit(是C语言方向的接口)
我们在使用之前先看一下对应的文档。
man exit
他的作用主要是返回退出码, 和return的操作几乎是一样的

我们来看一下:
我们能发现和return的操作是一样的。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include<stdlib.h>
int main()
{
FILE* fp = fopen("add.txt","r");
if(fp==NULL) exit(1);
//…文件正常被打开
return 0;
}

3. _exit(系统级别的接口)
man _exit

我们同样编写一个简单的代码进行验证。
我们发现和上面的操作几乎是一模一样。当然他们肯定由不一样的地方。我们下面就介绍。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
FILE* fp = fopen("add.txt","r");
if(fp==NULL) _exit(1);
//…文件正常被打开
return 0;
}

_exit vs exit:
我们通过两个代码来进行查看:
注意:这里文字不使用”\\n“,因为\\n会刷新对应的缓冲区。
我们能看到exit是刷新了对应的缓冲区的。
#include <stdio.h>
#include<stdlib.h>
int main()
{
printf("这是一串文字");
exit(1);
return 0;
}

我们再看看_exit呢?
我们能看到对应的没打印出来文字。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("这是一串文字");
_exit(1);
return 0;
}

结论:这里exit和_exit的主要区别是一个刷新了缓冲区一个没有进行刷新。
所以我们可以知道缓冲区是c语言的并不是OS的, 如果是OS的那么_exit也会刷新的。所以缓冲区是OS级别的概念。

进阶了解exit and _exit(1)
通过下面的代码我们可以知道exit是直接杀死进程的。向我们的return在函数内部只是结束这个函数而不是结束这个进程。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include<stdlib.h>
#include<unistd.h>
void add()
{
_exit(1);
}
int main()
{
add();
return 0;
}

进程的等待:
进程等待必要性
– 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
– 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
– 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
– 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法:
这里提供两个系统方面的接口
一个是wait一个是waitpid
我们查一下对应的文档
man wait

他们的返回值:

wait()
返回值:
成功返回子进程的id, 失败返回-1
![]()
参数是一个输出型参数:

第一个WIFEXITED(wifexited):若为正常的结束状态,则为真(查看进程是否正常的退出)
WEXITSTATUS:查看进程的退出码。
也可以使用NULL: 表示对退出码不关心。
使用:
while :;do ps axj | head -1 && ps axj |grep test; sleep 1;done
我们能发现下面的规律我们刚开始子进程一直在跑,我们对应的父进程没有多少代码应该早就结束了,可是我们在shell中查看他一直处于挂起状态。所以子进程没有进入僵尸,而是父进程一直等待子进程结束,将子进程进行回收。
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pi = fork();
if(pi == 0)
{
//子进程
int cnt=5;
while(cnt–)
{
printf("我是一个子进程我的父亲的id:%d,我的id是%d \\n",getppid(),getpid());
}
sleep(10);
}
else if (pi >0)
{
//父进程
wait(NULL);
}
else
{
//出现异常
printf("error fail!");
exit(-1);
}
return 0;
}

wait失败的情况:
1.没有子进程。
2.id不存在。
有参数的情况
具体的status>>8到后面在进行讲解。
1.写法一:位运算
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pi = fork();
if(pi == 0)
{
//子进程
int cnt=5;
while(cnt–)
{
printf("我是一个子进程我的父亲的id:%d,我的id是%d \\n",getppid(),getpid());
}
sleep(10);
}
else if (pi >0)
{
//父进程
int status =0 ;
wait(&status);
printf("对应的退出码为:%d\\n", (status>>8)&0XFF);
}
else
{
//出现异常
printf("error fail!");
exit(-1);
}
return 0;
}

2.写法二:采用输出型参数
这里的输出型参数就代表将他进行手动的转化。成对应的退出码,这与退出码的存储有关,这个存储一会会详细的给你进行介绍。
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pi = fork();
if(pi == 0)
{
//子进程
int cnt=5;
while(cnt–)
{
printf("我是一个子进程我的父亲的id:%d,我的id是%d \\n",getppid(),getpid());
}
sleep(10);
}
else if (pi >0)
{
//父进程
int status =0;
wait(&status);
printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
}
else
{
//出现异常
printf("error fail!");
exit(-1);
}
return 0;
}

waitpid()
返回值:
waitpid():调用成功时,返回状态发生变化的子进程 PID; 若参数中指定了 WNOHANG 非阻塞标志,且存在匹配 pid 的子进程,但该子进程状态尚未发生改变,则返回 0; 调用出错时,返回 -1。
参数:
这里waitpid有三个参数
pid_t waitpid(pid_t pid, int *wstatus, int options);
pid:
pid==-1 代表等待任意一个子进程, 与wait等效
pid>0 代表等待与进程的pid相等的进程的。
wstatus:
与wait的status相同。
第一个WIFEXITED(wifexited):若为正常的结束状态,则为真(查看进程是否正常的退出)
WEXITSTATUS:查看进程的退出码。
也可以使用NULL: 表示对退出码不关心。
options:(后面会详解进行阻塞等待和不阻塞等待的)
默认是0,表示阻塞等待。
WNOHANG :若子进程的pid没有结束,不与等待, 正常结束, 则返回子进程的pid。
等待我们的id为pi的进程,运行完进行回收。
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pi = fork();
if(pi == 0)
{
//子进程
int cnt=5;
while(cnt–)
{
printf("我是一个子进程我的父亲的id:%d,我的id是%d \\n",getppid(),getpid());
}
sleep(10);
}
else if (pi >0)
{
//父进程
// int status =0;
// wait(&status);
// printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
int status =0;
waitpid(pi, &status, 0);//不进行阻塞等待
printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
}
else
{
//出现异常
printf("error fail!");
exit(-1);
}
return 0;
}

总结:
- 如果子进程已经退出,调用 wait/waitpid 时,wait/waitpid 会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用 wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。

获取子进程的status(详细,简单易懂)
我们之前看过我们的status是一个int类型的变量。
我们一个int是4个byte,1个byte是8个比特位。
对应的我们的int是4×8=32个比特位。
这里我们的status二进制:
0000 0000 0000 0000 0000 0000 0000 0000
这里我们的前16位弃而不用。不存任何的东西。
我们的后16位(15-8)中的次前8位存退出码,剩余的8位存信号。
我们给出的信号是在退出码的地方,如果我们进程退出码是1,那么它对应的status是:
0000 0000 0000 0000 0000 0001 0000 0000
我们想要得到对应的退出码要将我们的status>>8位,然后&上0XFF取出前8位即可。
这里的信号我们会在后续的进程间的信号中进行讲解(但是我们能写一些代码来看看信号).
这里注意的是我们的cor dump只占一个比特位。
这里注意如果程序异常了对应的退出码就没什么作用了。

查看“9”号信号:
这里为啥要&0X7F呢??
因为我只要前7位第8位存储的是core dump
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pi = fork();
if(pi == 0)
{
//子进程
while(1)
{
printf("我是一个子进程我的父亲的id:%d,我的id是%d \\n",getppid(),getpid());
sleep(3);
}
}
else if (pi >0)
{
//父进程
// int status =0;
// wait(&status);
// printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
int status =0;
waitpid(pi, &status, 0);//不进行阻塞等待
printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
printf("信号位:%d\\n", status & 0X7F);
}
else
{
//出现异常
printf("error fail!");
exit(-1);
}
return 0;
}
我们将使用9号信号杀死该进程,
我们看到对应的信号值是9号信号。

我们看看对应的11号信号:
我们运行下面的程序:
我们对空指针进行解引用操作肯定是错误的。
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pi = fork();
if(pi == 0)
{
//子进程
while(1)
{
// printf("我是一个子进程我的父亲的id:%d,我的id是%d \\n",getppid(),getpid());
// sleep(3);
int * ptr =0;//ptr是一个空指针
*ptr=10;//对空指针进行*操作
}
}
else if (pi >0)
{
//父进程
// int status =0;
// wait(&status);
// printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
int status =0;
waitpid(pi, &status, 0);//不进行阻塞等待
printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
printf("信号位:%d\\n", status & 0X7F);
}
else
{
//出现异常
printf("error fail!");
exit(-1);
}
return 0;
}
我们发现是11号信号,11号是什么呢我们可以查看一下:
kill -l
我们发现是段错误。


阻塞等待和非阻塞等待:
像我们的阻塞等待上面已经进行了对应的说明, 就是我们的父进程阻塞在哪里等待我们的子进程运行结束, 就在那里一致的进行等待。
非阻塞等待:“ 在等待我们的子进程是我们的父进程也一直在做自己的工作。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pi = fork();
if (pi == 0) {
// 子进程
int cnt = 2;
while (cnt–) {
printf("我是一个子进程我的父亲的id:%d,我的id是%d \\n", getppid(), getpid());
sleep(3);
}
}
else if (pi > 0) {
// 父进程
// int status =0;
// wait(&status);
// printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
int status = 0;
int ret =0;
while (ret == 0) {
ret = waitpid(pi, &status, WNOHANG); // 非阻塞等待
printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
printf("信号位:%d\\n", status & 0X7F);
sleep(2);
}
printf("对应的退出码为:%d\\n", WEXITSTATUS(status));
printf("信号位:%d\\n", status & 0X7F);
}
else {
// 出现异常
printf("error fail!");
exit(-1);
}
return 0;
}

进程的替换
fork()之后,父子各自执行父进程代码的一部分如果子进程就想执行一个全新的程序呢?进程的程序替换来完成这个功能!
程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!
替换原理:
用 fork 创建子进程后执行的是和父进程相同的程序 (但有可能执行不同的代码分支), 子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。 
在程序替换的过程中并没有创建新的进程,只是把之前的进程的代码和数据进行替换,这里的替换是覆盖式的替换。
我为什么说他没有创建新的进程呢?我们就来见一见把。
我们需要用到:execl这个函数。
替换的操作:
先看一下对应的官方文档:
man execl
参数第一个参数是path:就是要替换的程序的路径在哪里。
第二个参数是agr:我们要怎么进行执行,我们命令行怎么写我们就怎么写。

我们先浅浅的使用一下。
注意最后一个参数要以NULL进行结尾。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("程序开始运行!\\n");
execl("/usr/bin/ls", "ls", "-l","-a",NULL);
printf("程序运行结束\\n");
return 0;
}
我们运行一下:
我们后面还有一句话没有执行,这就说明这个程序把我们当前的程序进行了替换。

当然我们也可以写c++, python,php, shell脚本…..等等进行替换。
我们来测试一下:
我们书写了以一个c++程序:
#include<iostream>
#include<cstdio>
int main()
{
std::cout<<"我是一个c++程序"<< std::endl;
return 0;
}
那我们在c语言进行调用:(要进行提前编译)
c语言程序:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("程序开始运行!\\n");
execl("./ctest", "./ctest",NULL);
printf("程序运行结束\\n");
return 0;
}
我们看到我们真的将c++代码进行了对应的替换。

那么我将进一步的进行验证我们在替换过程中没有创建进程:
我们对应的c++程序:
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<sys/types.h>
int main()
{
std::cout<<"我是一个c++程序我的id是" << getpid()<<std::endl;
return 0;
}
c:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("程序开始运行!\\n");
printf("我是一个c语言程序我的id:%d\\n", getpid());
execl("./ctest", "./ctest",NULL);
printf("程序运行结束\\n");
return 0;
}
我们运行一下:
我们的id一摸一样所以我们的进程是没有创建新进程进行执行替换的程序的。

程序的返回:
我们这里要注意我们替换成功时,execl函数是没有返回值的,因为没必要,有也没用, 为什么这么说呢???因为我们的程序被替换时, 我们的原来的代码已经被覆盖了, 我们后面拿到返回值也就没用了。
这里我们注意我们的return也是没用了。因此我们一般将退出设置位exit(1)这么的进行,表示替换失败, 后续的代码也没有必要执行了。
我们一般会像下面那样进行对应的设置。
#include<stdio.h>
#include<unistd.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
printf("程序开始运行!\\n");
printf("我是一个c语言程序我的id:%d\\n", getpid());
execl("./ctest", "./ctest",NULL);
printf("程序运行结束\\n");
exit(-1);
}
那我们在开发的过程中可以创建子进程来完成替换。
创建子进程完成替换:
我们能看到我们的子进程完成对应的替换,我们的父进程可以执行自己的代码。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/wait.h>
int main() {
printf("程序开始运行!\\n");
pid_t pi = fork();
if (pi == 0) {
execl("./ctest", "./ctest", NULL);
exit(-1);
}
waitpid(pi, NULL, 0);
printf("回收成功\\n");
return 0;
}

为什么子进程进行替换父进程没有影响呢???
1.进程具有独立性。
2.数据和代码会发生写时考呗。
最后我们来进行剩下的接口的使用:
exec家族:
我们刚才看文档的时候还有几个函数:
man execl

execl
语法:
int execl(const char *pathname, const char *arg, …)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/wait.h>
int main() {
printf("程序开始运行!\\n");
pid_t pi = fork();
if (pi == 0) {
execl("/usr/bin/ls", "ls","-l","-a", NULL);
exit(-1);
}
waitpid(pi, NULL, 0);
printf("回收成功\\n");
return 0;
}

exclp
语法:
int execlp(const char *file, const char *arg, …);
特点:这里的 p是PATH的意思说明这里的这个函数自带路径, 只要我们写文件名即可。
使用:
这里的ls代表的意思不一样,第一个ls是代表要执行那个文件, 第二个是我想怎么进行执行。我们在下面的代码可以看到我们不要携带路径只要知道对应的文件名即可。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/wait.h>
int main() {
printf("程序开始运行!\\n");
pid_t pi = fork();
if (pi == 0) {
execlp("ls", "ls","-l","-a", NULL);
exit(-1);
}
waitpid(pi, NULL, 0);
printf("回收成功\\n");
return 0;
}
execv
语法:
int execv(const char *pathname, char *const argv[]);
我们看到他没有p说明要带路径,我们后面的argv代表我们要传入一个字符串数组。
使用:
这里的argv就说明我么要传入一个命令行数组。 注意:这个数组要以NULL进行结尾
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/wait.h>
int main() {
printf("程序开始运行!\\n");
pid_t pi = fork();
char* const arg[10]={
"ls",
"-l",
"-a",
NULL
};
if (pi == 0) {
execv("/usr/bin/ls",arg);
exit(-1);
}
waitpid(pi, NULL, 0);
printf("回收成功\\n");
return 0;
}

execvp
语法:
int execvp(const char *file, char *const argv[]);
通过名字我们能看到他有p可以直进行传路径,有v所以要传入以命令行数组。
使用:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/wait.h>
int main() {
printf("程序开始运行!\\n");
pid_t pi = fork();
char* const arg[10]={
"ls",
"-l",
"-a",
NULL
};
if (pi == 0) {
execvp("ls",arg);
exit(-1);
}
waitpid(pi, NULL, 0);
printf("回收成功\\n");
return 0;
}

execle
语法:
int execle(const char *pathname, const char *arg, …char *const envp[]) 这里我们看到有没有p要传路径,也没有v所以不是传我们的命令行参数表。那我们的e呢?
这里的e代表env要传环境变量,和我们的命令行参数一样这里要传一个数组。不能直接的环境变量。
使用
我们这里写两个程序进行验证:
这个是进行替换的程序:
#include <iostream>
int main(int argc, char *argv[], char *env[]) {
// 打印命令行参数
for (int i = 0; i < argc; i++) {
printf("命令行参数argv[%d]——>%s\\n",i,argv[i]);
}
// 打印环境变量
for (int i = 0; env[i]; i++) {
printf("env[%d]——>%s\\n", i, env[i]);
}
return 0;
}
我们的c语言主要的程序:
解释一下,我替换了这个程序,我们下面的程序和进行调用上面的程序,所以用到的下面的那些命令, 我么都将以argv的形式传给了上面的main函数。env传给了main的env所以我们运行一下能看到对应的结果,这个第二个参数相当与我们自己在命令行进行的输入。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/wait.h>
char* const envp[]={
"zhibu = 1234",
"PATH =1011",
NULL
};
int main() {
printf("程序开始运行!\\n");
pid_t pi = fork();
char* const arg[10]={
"ls",
"-l",
"-a",
NULL
};
if (pi == 0) {
execle("./ccpp","ccpp","-a", "-b",NULL , envp);
exit(-1);
}
waitpid(pi, NULL, 0);
printf("回收成功\\n");
return 0;
}
查看下面的结果,我们的命令行参数没有问题,但是我们的环境变量被替换了。我们如果像以增加试的进行添加给怎么办呢???下面给出对应的两种方式解决这个问题。

1.使用putenv进行添加
man putenv

我们这不使用execle我们使用execl
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
char *const envp[] = {"zhibu = 1234", "PATH =1011", NULL};
int main() {
printf("程序开始运行!\\n");
pid_t pi = fork();
char *const arg[10] = {"ls", "-l", "-a", NULL};
if (pi == 0) {
putenv("zhibu = 1234");
execl("./ccpp", "ccpp", "-a", "-b", NULL);
exit(-1);
}
waitpid(pi, NULL, 0);
printf("回收成功\\n");
return 0;
}

第二中使用环境变量的全局指针:environ
使用之前要提前进行声明extern** eniron
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
extern char **environ;
char *const addenv[] = {"zhibu = 1234", "PATH =1011", NULL};
char *const arg[10] = {"ls", "-l", "-a", NULL};
int main() {
printf("程序开始运行!\\n");
pid_t pi = fork();
if (pi == 0) {
for(int i=0;addenv[i];i++)
{
putenv(addenv[i]);
}
execle("./ccpp", "ccpp", "-a", "-b", NULL,environ);
exit(-1);
}
waitpid(pi, NULL, 0);
printf("回收成功\\n");
return 0;
}

execvpe
语法:
int execvpe(const char *file, char *const argv[],char *const envp[]);
#define _GNU_SOURCE //开启GNU扩展,识别execvpe
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
char *const envp[] = {"zhibu = 1234", "PATH =1011", NULL};
char *const arg[10] = {"ccpp", "-l", "-a", NULL};
int main() {
printf("程序开始运行!\\n");
pid_t pi = fork();
if (pi == 0) {
execvpe("ccpp",arg, envp);
exit(-1);
}
waitpid(pi, NULL, 0);
printf("回收成功\\n");
return 0;
}
当然还有一个接口
execve(系统接口)
man execve
我们查看一下也知道他是一个系统调用。所以我们前面的6个是库函数进行了语言级别的封装,他们6个底层都是在调用我们的execve,所以我们底层传的path,argv,env都是给系统传的。所以我们如果没传我们的env,我们库函数的内部会进行传的。

下篇预告:shell的编写
完结

网硕互联帮助中心







评论前必须登录!
注册