<Linux>进程控制
进程控制

文章目录
- 进程控制
- 一、进程创建
- 1.fork函数认识
- 2.写时拷贝
- 3.fork常规用法
- 4.fork调用失败的原因
- 二、进程终止
- 1.进程退出场景
- 2.进程退出码
- 3.进程退出的方式
- 三、进程等待
- 1.进程等待是什么?
- 2.进程等待的必要性
- 3.进程等待的方法
- 3.1.wait函数
- 3.2.waitpid函数
- 4.如何获取子进程status
- 4.1.如何理解status参数?
- 4.2.获取退出码和退出信号
- 3.阻塞等待和非阻塞等待
- 四、进程替换
- 4.1 替换原理
- 4.2 替换函数
- 4.4 exec*接口介绍
- 4.5.替换函数使用实例
一、进程创建
1.fork函数认识
在Linux中fork函数非常的重要,它的作用是在一个已经存在的进程中创建一个新进程。新进程叫做子进程,原来的进程叫做父进程。
| 函数名称 | fork |
|---|---|
| 函数功能 | 创建子进程 |
| 头文件 | #include<unistd.h> |
| 函数原型 | pid_t fork(void); |
| 参数 | 无 |
| 返回值 | >-1:成功(其中子进程返回0,父进程返回子进程的id) =-1:失败 |
进程调用fork,当控制转移到内核中的fork代码后,内核要做的是:
- 分配新内存和数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程中
- 添加子进程到系统进程列表中
- fork返回,调度器开始调度

当一个进程调用了fork之后,父子进程代码是共享的,虽然他们都运行到了相同的地方,但是每个进程都可以开始自己的旅程:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
int main()
{printf("Before:pid is: %d\n",getpid());pid_t pid = fork();if(pid == -1){perror("fork()");exit(1);}printf("after:pid is: %d,return is %d\n",getpid(), pid);sleep(1);return 0;
}
结果展示:

我们可以看到,第一行输出是fork之前,只有父进程在执行,打印了before信息,fork创建子进程后,打印了两行after信息,分别由父子进程打印,注意到,进程29404打印了before的pid,而另外一个after却没有打印,这是为啥呢?

所以,fork之前父进程独立执行,fork之后,两个父子进程执行流分别执行。
注意:fork之后谁先执行,完全由调度器决定。(父子都有可能先执行)
2.写时拷贝
通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式各自一份副本(在物理内存中)。

3.fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
4.fork调用失败的原因
以下两种原因可能会导致fork调用失败:
- 系统中有太多的进程
- 实际用户的进程数超过了限制
我们写一个死循环创建子进程的程序测试我们当前操作系统最多能创建多少个进程:

注意:上面这个程序可能会导致服务器或虚拟机直接挂掉,虚拟机的话,大家只需要使用shotdown命令关机重启即可,服务器则需要去服务器控制台进行重启。
二、进程终止
1.进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止(vs下叫做程序崩溃)
2.进程退出码
我们先前写C/C++代码的时候,都会在入口函数main函数开始写,我们总是喜欢在结尾的时候给上一个return 0,继而引发出了如下的两个问题:
- return 0,给谁return?
- 为何是0?其它值可以吗?
下面一次解决:
- 1、return 0,给谁return?
给父进程,具体理由在下面会有讲解。
- 2、为何是0?其它值可以吗?
返回值代表的是进程代码跑完,结果是否正确,如果是0,则成功,非零则失败。所以我们在写一个程序的时候,如果测试结果正确,这里我们可以给上return返回值0,可如果不正确,我们return的应该是其他值以此表示结果失败,只不过我们平时都无脑return 0了,准确说是不太正确的。
此外,失败虽是用非零值表示,可也是有讲究的,结果成功都是用0表示,结果失败反倒用不同的数字来表示,以此表示失败的不同原因。所以我们把main函数的return返回值称之为进程退出码!!进程退出码表征了进程退出的信息,此信息是要给父进程去读取的。
示例:
我们可以通过如下的指令查看退出码:
echo $?
$?表示在bash中,最近一次执行完毕时,对应进程的退出码!(说的简单点就是上一条指令执行完毕后的退出码)

再比如我们平时在命令行输入的指令,诸如ls、cd……类的,其退出码均为0,表示结果正确,可是当你随便输入一条错误指令的时候,其退出码则是某一数字表示结果错误:

问:一般而言,失败的的非零值我该如何设置呢?以及默认表达的含义?
- C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息。
- **总结:**错误码退出码可以对应不同的错误原因,方便定位问题!
- 这里就可以提出我们退出码的意义?
它能够表示结果的正确与否,正确用0表示,因为那么多个数字,0只有一个,但是错误却有多个,用非0数字表示,错误的原因也是有多种可能的
退出码也是不能够随意乱写的,每一个退出码对应的数字,代表不同的错误,我们可以利用函数接口strerror观察有哪些错误码
#include<stdio.h>
#include<string.h>
int main()
{for(int i=0;i<150;i++){printf("%d:%s\n",i,strerror(i));}return 0;
}
结果展示部分错误码对应的错误原因:

3.进程退出的方式
正常终止:
-
1、
main函数中return返回,代表退出进程;而非main函数中return返回表示的就是普通的函数返回/结束调用。 -
2、调用
exit函数,它在程序的任意地方调用都是代表终止进程,参数是退出码,exit函数会完成一些收尾工作,例如资源的清理和释放,刷新缓冲区等。 -
3、调用
_exit函数,它的作用是强制终止进程,不要进行后续收尾工作,比如刷新缓冲区(用户级别的缓冲区!)
异常终止:
- 【ctrl + c】信号终止
介绍return退出、exit函数和_exit函数
return退出
return退出是一种最为常见的一种退出进程的方法,执行return n等于执行exit(n),因为调用main函数运行时,会将main的返回值当做exit的参数。

exit函数
exit函数是标准C库中的一个库函数。
| 函数名称 | exit |
|---|---|
| 函数功能 | 正常终止一个进程 |
| 头文件 | #inlcude<stdlib.h> |
| 函数原型 | void exit(int status) |
| 参数 | status:程序退出的状态 |
| 返回值 | 无 |

执行并查看退出码:

在调用exit之前,还会做一些其他的工作,
- 执行用户通过atexit或者on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓冲数据均被写入。
- 调用_exit函数。

结果展示:
_ exit函数
_ exit也是标准C库中的一个库函数,它和_ Exit函数调用同义。
| 函数名称 | _exit |
|---|---|
| 函数功能 | 正常终止一个进程 |
| 头文件 | #include<unistd.h> |
| 函数原型 | void _exit(int status) |
| 参数 | status 定义了进程的终止状态,父进程通过wait来获取该值 |
| 返回值 | 无 |
_exit是强制退出进程,并不进行后续的收尾工作!
注意:status定义了进程的终止状态,父进程通过wait来获取该值,虽然status是int,但是仅有低八位可以被父进程所用。所以_exit(-1)时,在终端执行echo $?时,发现返回值是255。
_exit和exit的作用都是终止进程,但是还是有点区别的,区别如下:
结果演示:
exit和_exit函数的区别:
exit函数退出进程前,exit函数会执行用户定义的清理函数、刷新缓冲区,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。

三、进程等待
1.进程等待是什么?
让父进程fork之后,需要通过wait或者waitpid等待子进程退出,父进程想要知道子进程完成的任务情况如何了。
2.进程等待的必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏问题,需要通过父进程
wait,释放子进程占用的资源。 - 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程需要获得子进程的退出状态。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息(status),能够得知子进程的执行结果。
- 可以保证“时序问题”,那就是保证子进程先退出,父进程后退出。
3.进程等待的方法
利用系统级别的函数wait和waitpid
3.1.wait函数
| 函数名称 | wait |
|---|---|
| 函数功能 | 暂停当前进程,直至子进程结束,并取回子进程结束时的状态 |
| 头文件 | #include<sys/wait.h> |
| 函数原型 | pid_t wait(int *status) |
| 参数 | status:子进程终止状态的地址 |
| 返回值 | 等待成功返回被等待进程pid,失败返回-1 |
说明:输出型参数status,获取子进程状态,不关心时可以设置为NULL
作用:等待任意子进程
我们写一段代码来看一看,fork之后我们先让子进程运行5秒,之后子进程退出,而让父进程一直在等待(调用wait函数),我们就能看到进程等待的现象。
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>int main()
{pid_t id = fork();if (id == 0){//childint cnt = 5;while (cnt)//5秒后子进程退出{printf("child is running!,pid:%d, cnt:%d\n", getpid(), cnt);cnt--;sleep(1);}exit(0);//退出子进程}printf("father wait begin!\n");sleep(5);//父进程休眠5秒pid_t ret = wait(NULL);if (ret > 0){printf("father wait:%d,sucess\n", ret);}else{printf("father wait failed!\n");}sleep(5);//子进程被回收之后,让父进程再运行5秒钟return 0;
}
我们可以使用以下监控脚本对进程进行实时监控:
while :; do ps axj | head -1 && ps axj | grep proc1 | grep -v grep;echo "—————————————————————————————————————————————————————————————————";sleep 1;done
运行结果如下:
这时我们可以看到,当子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了。 由此得知我们可以通过wait()的方案解决回收子进程Z状态,让子进程进入X。
3.2.waitpid函数
| 函数名称 | waitpid |
|---|---|
| 函数功能 | 获取子进程结束时的状态 |
| 头文件 | #include<sys/wait.h> |
| 函数原型 | pid_t waitpid(pid_t pid, int *status, int options) |
| 参数 | pid:指定的子进程PID status:子进程终止状态的地址 options:控制操作方式的选项 |
| 返回值 | 1、> 0 等待子进程成功,当正常返回的时候waitpid返回收集到的子进程的进程ID; 2、 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; 3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; |
对于三个参数进一步说明:
pid:
1、pid<-1等待进程组识别码为pid绝对值的任何子进程.
2、pid=-1 等待任何子进程,相当于wait().
3、pid=0 等待进程组识别码与目前进程相同的任何子进程.
4、pid>0 等待任何子进程识别码为pid的子进程.
status:
用下面的常用的两个宏:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
- 0:默认行为,阻塞等待(父进程什么都不做,就是等待子进程退出)
WNOHANG:设置等待方式为非阻塞等待
注意:当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
下面再来强调下第二个参数:status
status是一个输出型参数,通过调用该函数,从函数内部拿出来特定的数据,也就是从操作系统拿到特定数据。
子进程退出的时候会将自己的退出信息写入自己的task_struct,随后变成Z状态,随后父进程调用wait / waitpid接口,通过status把子进程的退出码拿到。
4.如何获取子进程status
4.1.如何理解status参数?
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

解释上图:
- 在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。
我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F; //退出信号
对于此,系统中提供了两个宏来获取退出码和退出信号。
- WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
- WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status);//是否正常退出
exitCode = WEXITSTATUS(status);//获取退出码
- 需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。
4.2.获取退出码和退出信号
我们通过位运算可以,根据status得到退出码和退出信号:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{pid_t id=fork();if(id==0){//childint cnt=5;while(cnt){printf("child is running! pid:%d, cnt:%d\n",getpid(), cnt);cnt--;sleep(1);}exit(10);}printf("father wait begin!\n");sleep(10);//pid_t ret =wait(NULL);int status=0;pid_t ret=waitpid(id,&status,0);if(ret>0){printf("father wait:%d sucess,status wait code:%d,status exit signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);}else{printf("father wait failed!\n");}sleep(10);return 0;
}
结果如下:
status的最低7位表示进程退出时收到的信号,进程如果异常退出,是因为这个进程收到了特定的信号,我们先前kill -9 pid就是在进程异常时退出而发出的信号。下面来模拟下进程的异常退出:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{pid_t id=fork();if(id==0){//childwhile(1){printf("child is running! pid:%d, cnt:%d\n",getpid(), cnt);sleep(1);}exit(10);}printf("father wait begin!\n");sleep(10);//pid_t ret =wait(NULL);int status=0;pid_t ret=waitpid(id,&status,0);if(ret>0){printf("father wait:%d sucess,status wait code:%d,status exit signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);}else{printf("father wait failed!\n");}sleep(10);return 0;
}
这里子进程是在无限循环的,父进程只能阻塞等待,现在我们把子进程kill掉,结果如下:

如果kil -3 pid,退出信号就是3……当进程收到信号的时候,就代表进程异常了。

综上:退出信号代表进程是否异常,退出码代表程序跑完后的结果正确与否。
问:一个进程退出的时候,父进程会拿到退出码和退出信号,那到底先看谁呢?
- 一旦进程出现异常,只关心退出信号,退出码没有任何意义。
强调系统中的两个宏:
status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
我们对于位操作是不是有点太过复杂和麻烦了,我们可以用上文提到的宏来代替位操作:
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{pid_t id = fork();if (id == 0){//childint cnt = 5;while (cnt){printf("child[%d] is running! cnt is %d\n", getpid(), cnt);cnt--;sleep(1);}exit(11);}printf("father wait begin!\n");sleep(10);//pid_t ret =wait(NULL);int status = 0;pid_t ret = waitpid(id, &status, 0);if (ret>0){if (WIFEXITED(status))//没有收到任何退出信号{ //正常结束,获取对应的退出码printf("exit code:%d\n", WEXITSTATUS(status));}else{printf("error get a signal!\n");}}/*if (ret > 0){printf("father wait:%d sucess,status wait code:%d,status exit signal:%d\n", ret, (status >> 8) & 0xFF, status & 0x7F);}else{printf("father wait failed!\n");}*/sleep(10);return 0;
}
结果展示:

3.阻塞等待和非阻塞等待
再来强调下waitpid函数中的最后一个参数options,当其值为0时,就是阻塞等待,当为WNOHANG时,就是非阻塞等待。下面展开讨论:
阻塞等待:
-
如果子进程就是不退出(如死循环),怎么办呢?我的父进程只能阻塞等待。
-
当我们调用某些函数的时候,因为条件不就绪(可能是任意的软硬件条件),需要我们阻塞等待,本质就是当前进程自己变成阻塞状态,当条件就绪的时候再被唤醒。
详细说明:
- 就是一个进程在系统层面上因为要等待某种事件发生,如果这个事件并没有发生,那么当前进程的代码和数据将无法运行,此时就要进入阻塞状态,也就是将父进程的task_struct的状态由R->S,从运行队列投入到等待队列,等待子进程退出,当子进程退出了,本质就是条件就绪,那么就会逆向执行上述操作,将进程从等待队列搬到运行队列,并将状态由S->R。
举例:
- 假设你叫李四,是个大混子,天天不学习,马上就要考数据结构了,为了能够及格,你打电话给了你班的一位学霸朋友张三来让他教我,整个过程我就是一个进程,打电话的过程就是在调用接口,张三就是所谓的OS操作系统,当电话接通了,但是张三说他在忙,于是我让张三别挂电话,我在电话这头一直等待你,此时这个等待过程中我什么也没干,只是等待张三,也就是说父进程在等待期间不做任何事情,这个过程就是阻塞等待。
非阻塞等待:
举例:
- 此时重复上述场景,当电话接通后,张三表示还在忙,那么我直接挂电话,此时我做些自己的事情,忙了一会后又给张三打了个电话,张三还在忙,那么我又挂电话继续做自己的事情,while(1)重复循环直至张三说自己ok了。
详细说明:
- 整个过程我依旧是用户,张三是OS操作系统,打电话就等价于调用waitpid函数,相当于是用户问操作系统子进程是否退出,当OS回应没有时,此时waitpid直接返回,此时用户不会调用wait而将自己阻塞住,此时用户在空闲时间段内做自己的事情,做一会之后再去问OS操作系统好了没,这个过程就叫做非阻塞等待。这种多次调用非阻塞接口,就是轮询检测。
总结:
1、阻塞等待:死等,就是上述的情况,父进程一直等待子进程,父进程不做任何事情。
2、非阻塞等待:我们可以不要让父进程死等,而是在等待期间,父进程去做自己的事情,等子进程退出时再来检测子进程的运行状态
代码示例:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h>int main() {pid_t id = fork();assert(id != -1);if (id == 0){//子进程while (1){printf("我是子进程,我的pid:%d, 我的ppid:%d\n", getpid(), getppid());sleep(3);}exit(10);}else if (id > 0){//父进程//基于非阻塞的轮询等待方案int status = 0;while (1){pid_t ret = waitpid(id, &status, WNOHANG);// WNOHANG:非阻塞->子进程没有退出,父进程检测时候,立即返回if (ret > 0){//waitpid调用成功,子进程退出了printf("等待成功,%d,退出信号是:%d,退出码是:%d\n", ret, status & 0x7F, (status >> 8) & 0xFF);break;}else if (ret == 0){//(waitpid调用成功了)等待成功了,但是子进程没有退出//子进程没有退出,我的waitpid没有等待失败,仅仅是检测到了子进程没有退出printf("子进程完成没?还没有,那么我父进程就做其他事情啦...\n");sleep(1);}else{//waitpid调用失败printf("waitpid调用失败了\n");}}}else{//do nothing }return 0; }
运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程推出后读取子进程的退出信息。

现在增加点设计,模拟让父进程在等待的过程中做些自己的事情,如下的代码:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> #define NUM 10 typedef void (*func_t)(); //函数指针 func_t handlerTask[NUM]; //样例任务 void task1() {printf("handler task1\n"); } void task2() {printf("handler task2\n"); } void task3() {printf("handler task3\n"); } void loadTask() {memset(handlerTask, 0, sizeof(handlerTask));handlerTask[0] = task1;handlerTask[1] = task2;handlerTask[2] = task3; } int main() {pid_t id = fork();assert(id != -1);if (id == 0){//子进程while (1){printf("我是子进程,我的PID:%d, 我的PPID:%d\n", getpid(), getppid());sleep(3);}exit(10);}else if (id > 0){//父进程//基于非阻塞的轮询等待方案loadTask();int status = 0;while (1){pid_t ret = waitpid(id, &status, WNOHANG);// WNOHANG:非阻塞->子进程没有退出,父进程检测时候,立即返回if (ret > 0){// waitpid调用成功,子进程退出了printf("等待成功,%d,退出信号是:%d,退出码是:%d\n", ret, status & 0x7F, (status >> 8) & 0xFF);break;}else if (ret == 0){//(waitpid调用成功了)等待成功了,但是子进程没有退出//子进程没有退出,我的waitpid没有等待失败,仅仅是检测到了子进程没有退出printf("子进程完成没?还没有,那么我父进程就做其他事情啦...\n");for(int i = 0; handlerTask[i] != NULL; i++){handlerTask[i](); // 采用回调的方式,执行我们想让父进程在空闲的时候做的事情}sleep(1);}else{//waitpid调用失败printf("waitpid调用失败了\n");}}}else{//do nothing }return 0; }

过程描述:父进程每隔一秒就去看看自己进程退出没有,若没有就继续等待,若退出了,就获取退出码。
四、进程替换
4.1 替换原理
程序替换的概念
- 用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,这就需要用到程序替换,而完成程序替换需要用到exec函数。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动李成开始执行。
为什么要完成程序替换
我们一般在服务器设计(linux变成)的时候,往往需要子进程干两件种类的事情:
- 让子进程执行父进程的代码片段(服务器代码)
- 让子进程执行磁盘中一个全新的程序(shell,想让客户端执行对应的程序,通过我们的进程。执行其他人写的进程代码等待),如C/C++(自己写的) -> C/C++/Python/Shell/Php/java……(别人写的)
上述的第二点需求就是用我自己写的C/C++代码调用别人不同语言的代码程序,完成这一项需求就需要用到程序替换。
程序替换的原理
如下图所示:
- 前面我们学习到,当fork创建子进程的时候,子进程的PCB、虚拟地址空间都以父进程为模板,页表中的代码段指向的是父进程中的代码段,数据也以写时拷贝的方式来和父进程进行共享,如果现在有一个全新的程序b.exe,并且我现在不想让子进程执行任何父进程相关的代码以及访问父进程的数据,并执行的是b.exe程序,此时把b.exe的程序加载到物理内存上,让子进程重新调整自己的页表映射,使其指向新的b程序的代码和数据,这种过程就叫做程序替换。

总结程序替换的原理:
- 将磁盘中的程序,加载入内存结构
- 重新建立页表映射,谁执行程序替换,就重新建立谁的映射,最终达到的效果就是让父进程和子进程彻底分离,并让子进程执行一个全新的程序!!!
问1:当进行程序替换时,有没有创建新的进程?
- 进程程序替换后,该进程对应的PCB、进程地址空间以及页表等数据结构均没有发生改变,只是重新建立了一下物理内存中的映射关系罢了,它的内核对应的数据结构没有发生变化,他的pid也没有发生变化,也就没有创建新的进程,只不过是让进程执行不同的程序罢了!!!
问2:子进程进行进程程序替换后,会影响父进程的代码和数据吗?
- 子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
问3:为什么要加载到内存中呢?
因为数据是存储在外设磁盘上的,CPU距离内存最近,读取是最方便的!
代码演示:
#include<stdio.h>
#include<unistd.h>
int main()
{printf("I am a process! pid:%d\n", getpid());execl("/usr/bin/ls","ls","-a","-l",NULL);//本该执行下面的hello程序的,但是调用execl替换了代码和数据,将会执行ls等命令printf("process done, pid:%d\n", getpid());return 0;
}
结果展示:

程序替换的本质:就是把程序的代码和数据,加载到特定的进程的上下文中!
我们平时写的C/C++程序要运行,必须先加载到内存中!如何加载呢?——加载器!(封装的底层就是exec系列的程序替换函数)
现在我们想让子进程也进行程序替换:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{if(fork()==0){//childprintf("command begin...\n");execl("/usr/bin/ls","ls","-a","-l","-i",NULL);printf("command end...\n");exit(1);}//fatherwaitpid(-1,NULL,0);//等待任意一个子进程printf("father wait success!\n");return 0;
}
结果展示:

当我们在执行子进程的替换时,我们的父进程照常执行自己的任务,两者之间互不影响呢?那是因为进程之间具有独立性!
4.2 替换函数
其实有6种以exec开头的函数,统称为exec函数
六种替换函数(实际上七种,换汤不换药):
- 上述过程中的程序替换是我们通过调用接口(函数)来让OS操作系统帮助我们完成的,此接口就是替换函数,替换函数有六种以exec开头的函数,他们统称为exec函数:
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
只要程序替换成功,就不会执行后续的代码,意味着exec* 系列的函数,执行成功的时候就不需要返回值检测!只要exec* 返回了,那就意味着替换失败,调用函数也失败了!(返回-1)所以exec函数只有出错的返回值而没有成功的返回值!
4.4 exec*接口介绍
| 函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
|---|---|---|---|
| execl | 列表 | 不带 | 是 |
| execlp | 列表 | 带 | 是 |
| execle | 列表 | 不带 | 不是,需要自己组装环境变量 |
| execv | 数组 | 不带 | 是 |
| execvp | 数组 | 带 | 是 |
| execve | 数组 | 不带 | 不是,需要自己组装环境变量 |
函数解释:
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值
命名理解:
这六个exec系列函数的函数名都以exec开头,其后缀的含义如下:
- l(list):表示参数可用列表的形式,一一列出
- v(vector):表示参数采用数组的形式
- p(path):表示能自动搜索环境变量PATH,进行程序查找
- e(env):表示可以传入自己设置的环境变量
事实上,只有execve是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,其它五个函数在man手册的第3节,也就是说其它五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的需求。

下图为exec系列函数族之间的关系:

4.5.替换函数使用实例
我们如果要执行一个全新的程序,需要做2件事情:
-
先找到这个程序在哪里
-
程序可能携带选项进行执行(也可以不携带),要明确告诉OS操作系统,我想怎么执行这个程序?要不要带选项?
下面几个替换函数均是按照上述逻辑执行的。
1、execl
int execl(const char *path, const char *arg, ...);
- 第一个参数是要执行程序的路径
- 第二个参数是可变参数列表(我们可以按照用户的意愿传入数量大小不等的参数),表示你要如何执行这个此程序,命令行怎么写(ls -l -a),这个参数就怎么填 “ls”,“-l”,“-a”,并以NULL结尾。
示例:
假设我执行的是ls -l -a程序:

当我make之后,会生成myexec可执行程序,现在运行此程序:

当我执行top、pwd命令也亦是同样的操作:

- 此时一个现象就产生了,当我调用execl替换程序函数成功后,后面的printf语句并没有执行,原因就在于一旦execl替换成功,是将当前进程的代码和数据全部替换了,后面的printf自然就被替换了,即该代码就不存在了。
问:程序替换用不用判断返回值?
- 程序替换不用判断返回值,这就是我们前面提到的,因为一旦程序替换成功了,就不会有返回值,而失败的时候,必然会向后执行,最多通过返回值得到什么原因导致的替换失败。
下面演示进程替换失败的场景(只需随便执行一个不存在的程序即可)

我们的上述操作是一个单进程程序,我没有创建子进程,相当于是把父进程的代码给替换了,现在想让子进程完成替换操作,如下:

上述操作就完成了让子进程执行全新的程序,以前是执行父进程的代码片段,运行结果如下:

问:子进程执行程序替换,会不会影响父进程呢?
- 答案是不会的,这个我上面详细讲解过,总结就是进程具有独立性,当程序替换的时候,代码和数据都发生了写时拷贝完成父子的分离。
#include <stdio.h>
#include <unistd.h>int main()
{printf("我是一个进程,我的pid:%d\n", getpid());// ls -l -a; execl("usr/bin/ls", "ls", "-l", "-a", NULL);printf("我执行完了,我的pid:%d\n", getpid()); return 0;
}
2、execv
int execv(const char *path, char *const argv[]);
- 第一个参数是要执行程序的路径
- 第二个参数是一个字符指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
例如,要执行的是ls -l -a -i
char *const argv_[] = { (char*)"ls", (char*)"-a", (char*)"-l", (char*)"-i", NULL };
execv("/usr/bin/ls", argv_);
前面的execl是一个一个参数传过去的,这里execv是直接传一个指针数组,示例如下:


补充:vim批量化注释、取消注释小技巧
- 注释:小写,ctrl+v,hjkl选中区域,切换大写,输入I,//,esc
- 取消注释:小写,ctrl+v,hjkl选中区域(注释区域),输入d
3、execlp
int execlp(const char *file, const char *arg, ...);
- 第一个参数是要执行程序的名字
- 第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾
例如,要执行的是ls -a -l
execlp("ls", "ls", "-a", "-l", NULL );
它和execl唯一的区别就是多了一个p,在我们执行指令的时候,默认的搜索路径是在环境变量PATH搜索的,而execl中的p就是此PATH环境变量,前面我们的execl和execv都是要指明路径的,而这里execlp命名带p了,因此就可以不带路径,只说出你要执行哪一个程序即可。示例:


- 这里出现了两个ls,含义是不一样的,第一个参数是供系统去找你要执行谁的,第二个是你想怎么执行它。
4、execvp
int execvp(const char *file, char *const argv[]);
- 第一个参数是要执行程序的名字
- 第二个参数是一个字符指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
例如,要执行的是ls -a -l -i
char *const argv_[] = { (char*)"ls", (char*)"-a", (char*)"-l", (char*)"-i", NULL };
execvp("ls", argv_);
此替换函数同样是带了p,所以它是从PATH路径里头找,我们只需要程序名即可,还带了v,我们就可以将命令行参数字符串统一放入数组中即可完成调用。示例:


5、execle
在正式讲解此替换函数execle前,先来看这样一个问题:
- 目前我们执行的程序,全部都是系统命令,如果我们要执行自己写的C/C++程序呢?如果我们要执行其它语言写的程序呢?
首先,一次makefile默认只能生产一个可执行程序,但是按如下修改一次可生产多个可执行程序:

现在有两个可执行程序,我现在的目标是让myexec把mycmd调起来,代码如下:
execl("/home/xzy/dir/date16/mycmd", "mycmd", NULL);//绝对路径 //execl("./mycmd", "mycmd", NULL);相对路径
详情如下:



上述实现了C可执行程序调C++可执行程序,现在来实现C可执行程序调用python可执行程序,如下一个python小脚本:

现在想让C可执行程序myexec来调用python,只需如下操作:
execl("/usr/bin/python3", "python3", "test.py", NULL);

解决了刚刚那个问题,现在再回过头来看execle替换函数:
int execle(const char *path, const char *arg, ..., char * const envp[]);
- 第一个参数是要执行程序的路径
- 第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾
- 第三个参数是你自己设置的环境变量
以我设置的"MYPATH"环境变量为例:
char* const env_[] = { (char*)"1314520", NULL };
execle("./mycmd", "mycmd", NULL, env_);
execle的第三个参数表示的是如果你想给你的程序传入对应的环境变量信息,也是一个字符指针数组,那就可以传入对应的环境变量参数,示例:
如下我们的mycmd.cpp文件:

如上我输出的是系统PATH中的环境变量,现在我想自定义环境变量MYPATH并交给子进程,我的myexec.c程序如下:

这里有个错误:
execle("./mycmd", "mycmd", NULL, env_);
当我make后并运行myexec可执行程序会发现有个问题:

当我自定义一个环境变量时,运行myexec,我PATH环境变量就输出不了了,当我把mycmd.cpp中的PATH语句那块给注释掉,再运行myexec,此时就能看到我自定义的环境变量MYPATH了:

为什么我把PATH的注释取消了,却输出不了我的PATH环境变量?相反的,把PATH注释掉才能打印我的MYPATH?
- 根本原因:自己添加环境变量给目标进程,是覆盖式的!!!
解决办法如下:
使用environ(系统环境变量的指针声明),利用export将MYPATH添加到系统环境变量中:

6、execve
int execve(const char *filename, char *const argv[], char *const envp[]);
- 第一个参数是要执行程序的路径
- 第二个参数是一个指针数组,表示你要如何执行这个程序,数组以NULL结尾
- 第三个参数是你自己设置的环境变量
例如,你设置了MYPATH环境变量,在myexec程序内部就可以使用该环境变量:
char* my_argv[] = { "myexec", NULL };
char* my_envp[] = { "MYPATH=helloWorld", NULL };
execve("./myexec", my_argv, my_envp);
7、execvpe
int execvpe(const char *file, char *const argv[], char *const envp[]);
- 第一个参数是要执行程序的路径
- 第二个参数是一个指针数组,表示你要如何执行这个程序,数组以NULL结尾
- 第三个参数是你自己设置的环境变量
此替换函数在我们前面讲解的基础上,已经不难理解了,就不给出测试用例了。
问:为什么会有这么多接口?execve为什么是单独的?
- 其实替换函数一共有7个,只不过execve是单独的:
有多个接口的原因在于调用替换函数的场景各不相同,所以有不同的接口,execve是单独的原因其实我在命名理解那也说明了,这里再强调下:
- 事实上,只有execve是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,其它五个函数在man手册的第3节,也就是说其它五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的需求。
相关文章:
<Linux>进程控制
进程控制 文章目录进程控制一、进程创建1.fork函数认识2.写时拷贝3.fork常规用法4.fork调用失败的原因二、进程终止1.进程退出场景2.进程退出码3.进程退出的方式三、进程等待1.进程等待是什么?2.进程等待的必要性3.进程等待的方法3.1.wait函数3.2.waitpid函数4.如何…...
有手就行 -- 搭建图床(PicGo+腾讯云)
🍳作者:贤蛋大眼萌,一名很普通但不想普通的程序媛\color{#FF0000}{贤蛋 大眼萌 ,一名很普通但不想普通的程序媛}贤蛋大眼萌,一名很普通但不想普通的程序媛🤳 🙊语录:多一些不为什么的…...
“蓝桥杯”递推和递归(一)——取数位
1. 算法简介 递推和递归虽然叫法不同,但它们的基本思想是一致的,在很多程序中,这两种算法可以通用,不同的是递推法效率更高,递归法更方便阅读。 (1)递推法 递推法是一种重要的数学方法&#…...
蓝桥杯·3月份刷题集训Day02
本篇博客旨在记录自已打卡蓝桥杯3月份刷题集训,同时会有自己的思路及代码解答希望可以给小伙伴一些帮助。本人也是算法小白,水平有限,如果文章中有什么错误之处,希望小伙伴们可以在评论区指出来,共勉💪。 文…...
python --获取内网IP地址
方法一 import socketdef get_local_ip_address():ip_address try:# 获取本机主机名hostname socket.gethostname()# 获取本机IPip_address socket.gethostbyname(hostname)except:passreturn ip_address方法二 import subprocessdef get_local_ip_address():ip_address …...
如何衡量你的Facebook广告活动的成功
投入大量资金和资源在Facebook广告上并不总能带来预期的回报,这很可能是由于缺乏恰当的衡量广告活动成功的方法。在这篇文章中,我们将介绍一些关键的指标,帮助你更好地了解如何衡量你的Facebook广告活动的成功。1.费用每次点击(CP…...
Linux对一个目录及其子目录所有文件添加权限
1、chmod指令 chmod是一个改变用户拥有指定文件的权限的命令.r:只读,w:写,x执行.也可以用数字 -rw------- (600) -- 只有属主有读写权限。 -rw-r--r-- (644) -- 只有属主有读写权限;而属组用户和其他用户只有读权限。 -rwx------ (700) -- 只有属主有读、写、执…...
宝刀未老?低代码何德何能受大厂们的推崇
风口之下,低代码蓬勃发展,本文从国内低代码的走红现象引入,浅析低代码发展中的变化趋势,重点探讨如此趋势之下,国内大厂如何通过低代码实现了良性发展。 一、国内爆火的低代码 据Gartner最新报告显示,到2…...
智能扑克牌识别软件(Python+YOLOv5深度学习模型+清新界面)
摘要:智能扑克牌识别软件利用视觉方法检测和识别日常扑克牌具体花色与数字,快速识别牌型并标注结果,帮助计算机完成扑克牌对战的前期识别步骤。本文详细介绍基于深度学习的智能扑克牌识别软件,在介绍算法原理的同时,给…...
SQL优化13连问,收藏好!
1.日常工作中,你是怎么优化SQL的? 大家可以从这几个维度回答这个问题: 分析慢查询日志 使用explain查看执行计划 索引优化 深分页优化 避免全表扫描 避免返回不必要的数据(如select具体字段而不是select*) 使用…...
【小技巧】公式从docx文件复制到doc文件变成了图片怎么办?
文章目录0、word文件后缀命名1、docx和doc默认的公式编辑方式2、MathTpye公式编辑器3、MathType 运行时错误‘53’:文件未找到:MathPage.WLL4、结束语0、word文件后缀命名 1997-2003的旧版本文件名后缀是.doc 从2007版以后,后缀名是.docx…...
Python3入门与进阶笔记(六):初识类
目录 一些解释 属性 类名建议首字母大写,通常用驼峰规则命名。变量名建议小写,下划线隔开。类最基本的作用是封装。 写在类内非方法中的语句在类加载的时候会执行,且只会执行一次,例如下面的print语句,类加载时就会…...
Prometheus监控实战系列九:主机监控
Prometheus使用各种Exporter来监控资源。Exporter可以看成是监控的agent端,它负责收集对应资源的指标,并提供接口给到Prometheus读取。不同资源的监控对应不同的Exporter,如node-exporeter、mysql-exporter、kafka-exporter等,在这…...
JVM知识整理
JVM知识整理 JVM的主要组成部分 JVM包含两个两个子系统(类加载子系统和执行引擎)和两个组件(运行时数据区与和本地库接口) 类加载子系统:根据给定的全限定类名来加载class文件到运行时数据区域中的方法区。执行引擎&a…...
【C++】二叉搜索树
A:你长大后想要做什么? B:写下“快乐”…… A:不,你理解错我的意思了,我是说 B:不,是你理解错了人生…… 文章目录一、二叉搜索树的实现1.struct TreeNode{}2.迭代版本2.1 Insert()插入结点(解决链接的问题)…...
leetcode -- 21. 合并两个有序链表
🐨目录📑1. 题目🛶2. 解法- 头插到新链表🐬2.1 思路🐬2.1 代码实现⛵3. 解法优化 - 带哨兵位🐋3.1 思路🐋3.2 代码实现🚤4. 题目链接📑1. 题目 将两个升序链表合并为一个…...
计算机组成原理|第四章(笔记)
目录第四章 存储器4.1 概述4.1.1 存储器分类4.1.2 存储器的层次结构4.2 主存储器4.2.1 概述4.2.2 半导体存储芯片简介4.2.3 随机存取存储器(RAM)4.2.4 只读存储器(ROM)4.2.5 存储器与CPU的连接4.2.6 存储器的校验4.2.7 提高访存速…...
【Unity3D-BUG记录】Unity3D中出现“动画片段必须标记为Legacy的警告”消除方法
推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 在开发中可能会遇到下面的警告: The AnimationClip…...
Spring Bean的定义(含创建Bean的三种方式)
🏆 文章目标:复习和理解下Spring Bean的定义 🍀 Spring Bean的定义(含创建Bean的三种方式) ✅ 创作者:Jay… 🎉 个人主页:Jay的个人主页 🍁 展望:若本篇讲解内…...
vue的路由-vue router(一)
vue的路由-vue router一、路由的基本使用HTMLrouter-linkrouter-viewJavaScript二、带参数的动态路由匹配三、嵌套路由四. 编程式导航导航到不同的位置替换当前位置横跨历史篡改历史五. 命名路由六. 命名视图嵌套命名视图七. 重定向和别名重定向别名八. 将 props 传递给路由组件…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...
Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
Mac软件卸载指南,简单易懂!
刚和Adobe分手,它却总在Library里给你写"回忆录"?卸载的Final Cut Pro像电子幽灵般阴魂不散?总是会有残留文件,别慌!这份Mac软件卸载指南,将用最硬核的方式教你"数字分手术"࿰…...
PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...
