总言
进程地址空间和进程控制相关介绍。
文章目录
- 总言
- 2、进程控制·续
- 2.3、进程等待
- 2.3.1、为什么需要进程等待
- 2.3.2、阻塞式等待
- 2.3.2.1、使用wait
- 2.3.2.2、使用waitpid
- 2.3.2.3、参数status基本介绍
- 2.3.3、一些细节与问题</font>
- 2.3.3.1、进程独立性说明
- 2.3.3.1、父进程凭什么拿到子进程的数据
- 2.3.3.3、堆栈内存泄漏和操作系统层面的内存泄漏差异性
- 2.3.4、如何等待2.0(进一步细节展示+非阻塞式等待)
- 2.3.4.1、参数说明2.0:pid、status相关宏设置
- 2.3.4.2、waitpid第三参数options:非阻塞式等待设置处
- 2.3.4.3、实操演示
- 2.4、进程替换
- 2.4.1、是什么
- 2.4.2、怎么办1.0:execl函数演示
- 2.4.2.1、execl程序替换:不创建子进程
- 2.4.2.2、execl程序替换:创建子进程
- 2.4.3、怎么办2.0:其它exec函数演示
- 2.4.3.1、execv函数演示
- 2.4.3.2、execlp\ececvp函数演示
- 2.4.3.3、execle\execvpe函数演示
- 2.4.3.4、ececve 系统调用接口
2、进程控制·续
2.3、进程等待
2.3.1、为什么需要进程等待
1、子进程退出,父进程不管子进程,子进程就会进入僵尸状态。处于僵尸状态的子进程会导致内存泄露问题,那么,如何回收僵尸进程呢?此处就需要用到进程等待相关知识。
2、我们学习父进程创建子进程,是为了让子进程办事。那么子进程把任务完成得怎样,父进程需要关系吗?如果要,它是如何得知的?如果不要,又该如何处理?此处也需要用到进程等待的知识。
2.3.2、阻塞式等待
2.3.2.1、使用wait
1)、wait :基本验证,回收僵尸进程
先对僵尸进程简单回顾(详细可以看进程概念那一篇博客)
这是用于观察的脚本代码:
while :; do ps axj | head -1 && ps axj | grep test.out | grep -v grep; sleep 1 ; echo "------------------------------------------------------------"; done
下述为僵尸状态的演示代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);//表示进程运行完毕,结果不正确
}
else if(id==0)
{
int cnt=5;
while(cnt)
{
printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
cnt--;
sleep(1);
}
exit(0);//终止子进程
}
else{
while(1)
{
printf("I am parent, pid:%d ,ppid:%d \n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
使用
wait
回收僵尸进程演示
该函数基本介绍:
该函数的返回值:
使用演示:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(void)
{
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);//表示进程运行完毕,结果不正确
}
else if(id==0)
{
int cnt=5;
while(cnt)
{
printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
cnt--;
sleep(1);
}
exit(0);//终止子进程
}
else{
printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
sleep(7);
pid_t ret=wait(NULL);//阻塞式等待
if(ret>0)
{
printf("wait child process successfully!,ret:%d\n",ret);
}
//使用wait:可以不用让父进程做死循环等待了
while(1)
{
printf("I am parent, pid:%d ,ppid:%d \n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
往后我们编写多进程时,以便都是采用fork+wait/waitpid的方法。
2.3.2.2、使用waitpid
1)、waitpid :获取子进程退出结果
waitpid
简单介绍
pid_ t waitpid(pid_t pid, int *status, int options);
2)、waitpid 演示一:阻塞式等待
waitpid(pid, NULL , 0)
,等价于使用wait(NULL)
先简单演示一下waitpid的基本使用方法:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(void)
{
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);//表示进程运行完毕,结果不正确
}
else if(id==0)
{
int cnt=5;
while(cnt)
{
printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
cnt--;
sleep(1);
}
exit(0);//终止子进程
}
else{
printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
sleep(7);
pid_t ret=waitpid(id,NULL,0);//注意此处父进程获取得的id即子进程id,详细请了解fork返回值。
//pid_t ret=wait(NULL);//阻塞式等待
if(ret>0)
{
printf("wait child process successfully!,ret:%d\n",ret);
}
//使用wait:可以不用让父进程做死循环等待了
while(1)
{
printf("I am parent, pid:%d ,ppid:%d \n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
2.3.2.3、参数status基本介绍
1)、问题引入
waitpid(pid, &status , 0) //status为输出型参数,可用于获取子进程退出时的状态信息
演示代码如下:我们将子进程退出码设置为自己意属的值,而后通过status获取该值来观测结果。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(void)
{
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);//表示进程运行完毕,结果不正确
}
else if(id==0)
{
int cnt=5;
while(cnt)
{
printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
cnt--;
sleep(1);
}
exit(111);//终止子进程,设置子进程退出码
}
else{
printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
//sleep(7);
int status=0;
pid_t ret=waitpid(id,&status,0);//注意此处父进程获取得的id即子进程id,详细请了解fork返回值。
//pid_t ret=wait(NULL);//阻塞式等待
if(ret>0)
{
printf("wait child process successfully!,ret:%d,status:%d\n",ret,status);
}
}
return 0;
}
待程序执行完成,我们获得status
的值,却发现该值与我们与其所设想的不一致。
[wj@VM-4-3-centos t1113]$ ./test.out
I am parent, pid:22927, ppid:4925
cnt:5 , I am child, pid:22928, ppid:22927
cnt:4 , I am child, pid:22928, ppid:22927
cnt:3 , I am child, pid:22928, ppid:22927
cnt:2 , I am child, pid:22928, ppid:22927
cnt:1 , I am child, pid:22928, ppid:22927
wait child process successfully!,ret:22928,status:28416//可看到此处status的返回值是一个很大的数值
这是什么原因呢? 我们下面将提及。
关于阻塞式等待父子进程顺序说明: 文中提到的代码有一个特点,即只有子进程退出的时候,父进程才会使用waitpid/wait
函数进行返回,即父进程在子进程之后仍旧活着。
这说明 waitpid/wait
目前情况下可以让进程具有一定的顺序性 ,将来我们也可以让父进程进行更多的收尾工作。
2)、status详细介绍
0、综述:
1、进程正常终止,status的次低8位表示进程退出码
为了获取该码,我们可以做以下变化:
(status>>8)&0XFFF
先将status得到的值右移8位,这样次低8位的值就到了最低8位上,然后我们让按位与上0XFF(0000 0000 1111 1111),这样status更高位的数值就都为0了。
现在我们来看一看结果:
[wj@VM-4-3-centos t1113]$ ./test.out
I am parent, pid:557, ppid:31111
cnt:5 , I am child, pid:558, ppid:557
cnt:4 , I am child, pid:558, ppid:557
cnt:3 , I am child, pid:558, ppid:557
cnt:2 , I am child, pid:558, ppid:557
cnt:1 , I am child, pid:558, ppid:557
wait child process successfully!,ret:558,status:111 //status打印的值确实如我们设置一般
[wj@VM-4-3-centos t1113]$
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(void)
{
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);//表示进程运行完毕,结果不正确
}
else if(id==0)
{
int cnt=5;
while(cnt)
{
printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
cnt--;
sleep(1);
}
exit(111);//终止子进程
}
else{
printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
//sleep(7);
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
printf("wait child process successfully!,ret:%d,status:%d\n",ret,(status>>8)&0XFF);
}
return 0;
}
2、进程异常退出,status最低7位表示进程收到的型号
一个前提认知:进程异常退出或者崩溃,本质上是操作系统杀掉了该进程。
操作系统如何操作这个过程呢?本质是通过发送信号的方式。
为了获取相关信号,我们对原先实验的代码做如下改动并验证:
(status&0X7F)
OX7F: 0000 0000 0111 1111
发送的信号为0,说明进程正常终止。
此时,根据我们之前学的进程终止三情形,我们可以通过后面的status的返回值来判断它是正常跑完终止后的哪种状态。
[wj@VM-4-3-centos t1113]$ ./test.out
I am parent, pid:5662, ppid:31111
cnt:5 , I am child, pid:5663, ppid:5662
cnt:4 , I am child, pid:5663, ppid:5662
cnt:3 , I am child, pid:5663, ppid:5662
cnt:2 , I am child, pid:5663, ppid:5662
cnt:1 , I am child, pid:5663, ppid:5662
wait child process successfully!,ret:5663,signal:0, status:111
[wj@VM-4-3-centos t1113]$
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(void)
{
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);//表示进程运行完毕,结果不正确
}
else if(id==0)
{
int cnt=5;
while(cnt)
{
printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
cnt--;
sleep(1);
}
exit(111);//终止子进程
}
else{
printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
//sleep(7);
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
printf("wait child process successfully!,ret:%d,signal:%d, status:%d\n",ret,(status&0X7F),(status>>8)&0XFF);
}
return 0;
}
接下来,我们使用一个子进程异常情况来实验:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(void)
{
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);//表示进程运行完毕,结果不正确
}
else if(id==0)
{
int cnt=5;
while(cnt)
{
printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
cnt--;
sleep(1);
//以下为使子进程异常的演示代码:
int a=10;
a /=0;
}
exit(111);//终止子进程
}
else{
printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
//sleep(7);
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
printf("wait child process successfully!,ret:%d,signal:%d, status:%d\n",ret,(status&0X7F),(status>>8)&0XFF);
}
}
return 0;
}
需要注意的是,进程异常不仅仅只体现在内部代码有问题上,当我们外力人为直接杀掉进程, 也算作进程异常。比如我们之前执行kill -9 +子进程pid时:
2.3.3、一些细节与问题
2.3.3.1、进程独立性说明
问题一:
已知父进程通过wait/waitpid
可以拿到子进程的退出结果。既然如此,我们为什么要用wait/waitpid
函数呢?直接定义一个全局变量来完成它不是更加简洁吗?
相关代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int code=0;//定义一个全局变量
int main(void)
{
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);
}
else if(id==0)
{
int cnt=5;
while(cnt)
{
printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
cnt--;
sleep(1);
}
code=111; //在子进程处直接设置全局变量
exit(111);
}
else{
printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
printf("code:%d\n",code);//在父进程处读取该值
printf("wait child process successfully!,ret:%d,signal:%d, status:%d\n",ret,(status&0X7F),(status>>8)&0XFF);
}
return 0;
}
结果如下:
[wj@VM-4-3-centos t1113]$ ./test.out
I am parent, pid:11689, ppid:10835
cnt:5 , I am child, pid:11690, ppid:11689
cnt:4 , I am child, pid:11690, ppid:11689
cnt:3 , I am child, pid:11690, ppid:11689
cnt:2 , I am child, pid:11690, ppid:11689
cnt:1 , I am child, pid:11690, ppid:11689
code:0
wait child process successfully!,ret:11690,signal:0, status:111
[wj@VM-4-3-centos t1113]$
可以看到code读取失败直接为0,这是因为父子进程代码具有独立性,在子进程中修改code值时发生了写时拷贝。故不能用此法来获取子进程退出状态。
2.3.3.1、父进程凭什么拿到子进程的数据
问题二:
既然进程具有独立性,进程退出码不也是子进程独有的数据吗。那么父进程凭什么拿到子进程的数据?wait/waitpid
在这期间扮演什么角色做了什么事?
回答:
1、首先,从僵尸进程谈起。僵尸进程退出后,至少会保留该进程的PCB信息。即task_struct里面保留了任何进程退出时的退出结果信息。
2、wait、waitpid本质上是在读取子进程tast_struct结构体内的数据信息。
3、wait、waitpid属于系统调用函数,本质上是操作系统本身进行处理,因此其有权限拿到task_struct该内核结构对象的数据。
2.3.3.3、堆栈内存泄漏和操作系统层面的内存泄漏差异性
问题三:
假如我们创建了一个进程,在该进程中使用了malloc或new
一块空间,之后进程退出,请问在没有free或delete
的情况下,该内存是否会泄露?和子进程的僵尸状态带来的内存泄露有什么区别?
回答:对于前者,属于用户在堆区申请的空间,进程退出后不会造成内存泄漏,因为操作系统会自动回收该空间。对于后者,这属于操作系统层面的内存泄漏。
2.3.4、如何等待2.0(进一步细节展示+非阻塞式等待)
2.3.4.1、参数说明2.0:pid、status相关宏设置
1)、waitpid参数介绍一:pid
在之前我们只是简单的介绍了waitpid的基本使用方法,对于其参数pid我们直接使用了fork返回的id值,此处需要注意:
id > 0
时,我们等待的是指定进程
id== -1
时,我们等待的是任意一个子进程,等价于wait( )
接口。
2)、直接获取stauts:使用系统提供的宏
WIFEXITED(status): (查看进程是否是正常退出),若为真,则表示正常终止子进程返回的状态。
WEXITSTATUS(status): (查看进程的退出码),若WIFEXITED非零,提取子进程退出码。
两个宏的记忆方法:W
/IF
/EXITED
、W
/EXIT
/STATUS
。
演示代码一:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);
}
else if(id==0)//子进程
{
int cen=5;
while(cen)
{
printf("I am child:%d\n",cen--);
sleep(1);
}
exit(111);
}
else //父进程
{
int status=0;
pid_t result=waitpid(id,&status,0);//阻塞等待子进程状态变化
if(result>0)
{
if(WIFEXITED(status))
{
//父进程等待成功
printf("子进程执行完毕,子进程退出码:%d\n",WEXITSTATUS(status));
}
else
{
printf("子进程异常退出:%d\n",WIFEXITED(status));//此处是在子进程异常退出时打印看看状态
}
}
}
return 0;
}
执行结果:可看到WIFEXITED(status)
执行为真,后续将退出码直接打印出来。
这样子的一个好处是,我们可以不用了解具体的操作内核去做位移转换运算,直接通过使用系统提供的宏来获取子进程退出信息。
[wj@VM-4-3-centos t1113]$ ./test.out
I am child:5
I am child:4
I am child:3
I am child:2
I am child:1
子进程执行完毕,子进程退出码:111
2.3.4.2、waitpid第三参数options:非阻塞式等待设置处
1)、options参数简单介绍
根据上述几个演示代码,可以看到阻塞式等待中,父进程只在做一件事,即处于阻塞状态中,等待子进程状态发生改变。这样使用父进程有些大材小用,因此延伸出了让父进程既能等待子进程,同时也能处理其它任务的操作方法:
这就需要学习waitpid的第三参数:options
1、options== 0
,默认为阻塞等待
2、options== WNOHANG
,代表父进程为非阻塞等待
PS:WNOHANG
是系统提供的一个宏(定义成宏的原因:魔鬼数字)
相关记忆方式:W
/NO
/HANG
2)、关于阻塞式等待、非阻塞式等待相关理解说明
如上述,简单举例了系统调用函数waitpid相关实现框架,根据第三参数options来判断进程等待方式。
对于阻塞式等待,父进程不会执行后续代码操作,这是由于父进程在系统内部被挂起,即父进程的pcb(进程控制块)被放入等待队列中,其结果为进程阻塞在系统函数内部,只有当条件满足的时候,父进程被唤醒(相关进程控制块重新加载到CPU中被执行),而后接着执行。注意:这里waitpid重新调用时,是通过EIP寄存器来判断上此切换出的相关代码位置,并非重新执行所有代码。
对于非阻塞等待,使用waitpid后,waitpid判断子进程没有退出后会直接返回,换句话说,非阻塞式等待是通过轮询检测的方案来实现一次次调用的。
3)、相关意义说明
比如scanf、cin等,虽然上层看是通过语言实现的,但其内部使用了相关的系统调用接口,实际上,网络代码中大部分为IO类别,它们会不断面临阻塞与非阻塞接口。
2.3.4.3、实操演示
1)、基础演示一:
以下把如何用非阻塞式等待进行轮询检测的相关框架列举出来,代码如下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
if(id<0)
{
return -1;
}
else if(id==0)//子进程
{
int count=5;
while(count)
{
printf("I am child:%d\n",count--);
sleep(1);
}
exit(9);//测试:此处数字无具体含义
}
else //父进程:演示非阻塞式等待,轮循检测
{
int quit=0;//类似flag,用于标记何时退出
while(!quit)
{
int status=0;
pid_t ret=waitpid(-1,&status,WNOHANG);
if(ret>0)//等待成功+子进程退出
{
printf("等待子进程退出成功,退出码为:%d\n",WEXITSTATUS(status));
quit=1;
}
else if(ret==0)//等待成功+子进程尚未退出
{
printf("子进程尚在运行中暂时未退出,此时父进程可处理其它事件\n");
//……
//这里可以写父进程要处理的内容
}
else//waitpid等待失败
{
printf("wait失败。\n");
quit=1;
}
sleep(1);
}
}
return 0;
}
2)、基础演示二:
我们将上述演示一中,父进程待处理事件完善一下,做一个简单示范:
#include<iostream>
#include<vector>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
typedef void (*Handler_t)();//函数指针类型
std::vector<Handler_t> handlers;//函数指针数组
//待处理的临时任务:这是在非阻塞等待时,交给父进程的任务
void fun1()
{
printf("待处理临时任务1\n");
}
void fun2()
{
printf("待处理临时任务2\n");
}
//Load函数:可用于注入待处理任务,将其设置为Load可做到切块处理,便于更改
void Load()
{
handlers.push_back(fun1);
handlers.push_back(fun2);
}
int main()
{
pid_t id=fork();
if(id<0)
{
return -1;
}
else if(id==0)//子进程
{
int count=5;
while(count)
{
printf("I am child:%d\n",count--);
sleep(1);
}
exit(9);//测试:此处数字无具体含义
}
else //父进程:演示非阻塞式等待,轮循检测
{
int quit=0;//类似flag,用于标记何时退出
while(!quit)
{
int status=0;
pid_t ret=waitpid(-1,&status,WNOHANG);
if(ret>0)//等待成功+子进程退出
{
printf("等待子进程退出成功,退出码为:%d\n",WEXITSTATUS(status));
quit=1;
}
else if(ret==0)//等待成功+子进程尚未退出
{
printf("子进程尚在运行中暂时未退出,此时父进程可处理其它事件\n");
//…
if(handlers.empty())
Load();
for(auto iter:handlers)
{
//执行相关任务
iter();
}
}
else//waitpid等待失败
{
printf("wait失败。\n");
quit=1;
}
sleep(1);
}
}
return 0;
}
2.4、进程替换
2.4.1、是什么
1)、问题引入:fork的两种用法说明
在前文,我们介绍了fork的一种用法,实际上fork通常有两种常见用法:
①一个父进程希望复制自己,使父子进程同时执行不同的代码段。此时,父子进程代码相同,数据写时拷贝各自一份。
②一个进程要执行一个不同的程序。此时父子进程代码不同,数据也不同。
那么有没有相关实现方法?这里就要用到进程替换。
2)、进程替换介绍
进程替换: 通过特定接口,加载磁盘上的一个权限的程序(包括代码和数据),加载到调用进程的地址空间中,让子进程执行相关程序。
说明:事实上,操作系统为我们提供了这类函数,当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
3)、细节理解
问题一:进程替换,有没有创建新的进程?
回答:调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
问题二:如何理解将程序放入内存中?
回答:实际指将程序加载到内存中,和当前进程页表建立映射关系,可通过操作系统相关接口调用完成,即接下来要介绍的exec系列函数。
2.4.2、怎么办1.0:execl函数演示
2.4.2.1、execl程序替换:不创建子进程
1)、函数介绍
man execl可查询进程替换相关函数。
path
为对应程序所在路径,arg, ...
是可变参数列表,可以传入多个不定个数参数,但其最后一个参数必须传递NULL
,表示参数传递完毕。
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 execvpe(const char *file, char *const argv[],char *const envp[]);
2)、基础演示
这里以execl函数为例,演示不创建子进程时,如何进行程序替换:需要注意,这里path
的参数为const char *
,我们传入的是字符串。
相关代码:
演示一:
[wj@VM-4-3-centos T0714]$ ll
total 40
-rw-rw-r-- 1 wj wj 155 Jul 14 19:25 makefile
-rw-rw-r-- 1 wj wj 1236 Jul 14 12:27 proc01.c
-rwxrwxr-x 1 wj wj 8616 Jul 14 12:28 proc01.out
-rw-rw-r-- 1 wj wj 166 Jul 14 19:23 proc02.c
-rw-rw-r-- 1 wj wj 1962 Jul 14 13:02 proc02.cpp
-rwxrwxr-x 1 wj wj 8360 Jul 14 19:25 proc02.out
[wj@VM-4-3-centos T0714]$ ./proc02.out
当前进程开始:
......
当前进程结束
[wj@VM-4-3-centos T0714]$ cat proc02.c
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("当前进程开始:\n");
printf("......\n");
printf("当前进程结束\n");
return 0;
}
演示二:
[wj@VM-4-3-centos T0714]$ ll
total 40
-rw-rw-r-- 1 wj wj 155 Jul 14 19:25 makefile
-rw-rw-r-- 1 wj wj 1236 Jul 14 12:27 proc01.c
-rwxrwxr-x 1 wj wj 8616 Jul 14 12:28 proc01.out
-rw-rw-r-- 1 wj wj 201 Jul 14 19:31 proc02.c
-rw-rw-r-- 1 wj wj 1962 Jul 14 13:02 proc02.cpp
-rwxrwxr-x 1 wj wj 8408 Jul 14 19:31 proc02.out
[wj@VM-4-3-centos T0714]$ ./proc02.out
当前进程开始:
makefile proc01.c proc01.out proc02.c proc02.cpp proc02.out
[wj@VM-4-3-centos T0714]$ cat proc02.c
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("当前进程开始:\n");
execl("/usr/bin/ls","ls",NULL);
printf(".....\n");
printf("当前进程结束\n");
return 0;
}
[wj@VM-4-3-centos T0714]$ which ls
alias ls='ls --color=auto'
/usr/bin/ls
演示三:
[wj@VM-4-3-centos T0714]$ make
g++ -o proc02.out proc02.c
[wj@VM-4-3-centos T0714]$ ./proc02.out
当前进程开始:
total 48
drwxrwxr-x 2 wj wj 4096 Jul 14 19:37 .
drwxrwxr-x 4 wj wj 4096 Jul 14 12:06 ..
-rw-rw-r-- 1 wj wj 155 Jul 14 19:25 makefile
-rw-rw-r-- 1 wj wj 1236 Jul 14 12:27 proc01.c
-rwxrwxr-x 1 wj wj 8616 Jul 14 12:28 proc01.out
-rw-rw-r-- 1 wj wj 245 Jul 14 19:37 proc02.c
-rw-rw-r-- 1 wj wj 1962 Jul 14 13:02 proc02.cpp
-rwxrwxr-x 1 wj wj 8408 Jul 14 19:37 proc02.out
[wj@VM-4-3-centos T0714]$ cat proc02.c
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("当前进程开始:\n");
//execl("/usr/bin/ls","ls",NULL);
execl("/usr/bin/ls","ls","-al",NULL);
printf(".....\n");
printf("当前进程结束\n");
return 0;
}
3)、相关说明
问题一:如何理解使用execl后不再执行后续代码?
回答: exec系列函数如果调用成功,则会将当前进程中的所有代码和数据统统替换,包括已执行的和未执行的。
以下述代码为例做解释: 使用execl后,进程内部代码数据在这里替换为ls
,后续printf(".....\n");
和printf("当前进程结束\n");
将不会被执行,而execl前的printf("当前进程开始:\n");
也会被一并替换,只是它在替换前已经被执行。
int main()
{
printf("当前进程开始:\n");
execl("/usr/bin/ls","ls",NULL);
printf(".....\n");
printf("当前进程结束\n");
return 0;
}
问题二:关于execl返回值。为什么调用成功没有返回值?
回答: 原因同上。该系列函数,若调用成功,则加载新的程序并从启动代码开始执行,不再返回。如果调用出错则返回-1。所以exec函数只有出错的返回值,而没有成功的返回值。
举例: 实际使用时,可根据需要在execl后加上exit,一旦程序在此处退出,意味着进程替换执行失败。
int main()
{
printf("当前进程开始:\n");
execl("/usr/bin/ls","ls",NULL);
exit(-1);//表示进程替换失败。
printf(".....\n");
printf("当前进程结束\n");
return 0;
}
2.4.2.2、execl程序替换:创建子进程
1)、为什么要有创建子进程的替换方式?
问题说明: 在上述中,我们直接使用了execl系列,可实现将当前进程进行替换,那么为什么还需要额外创建子进程来完成此项任务?
从需求角度考虑: 若原先父进程原先代码为刚需,要求不能更动(例如后续还需要执行、用到),那么创建子进程用于进程替换可以保证不影响父进程的同时,替换上新的代码数据。(PS:进程具有独立性,因此只会替换掉子进程的代码数据段,不会把父进程的一并替换)
从分工角度考虑: 让父进程聚焦在读取数据、解析数据上,指派进程执行代码功能。
2)、如何操作?
以下为一个演示案例: 这里我们以ls -a -l
为例。
[wj@VM-4-3-centos T0715]$ ls -a -l
total 32
drwxrwxr-x 2 wj wj 4096 Jul 15 14:57 .
drwxrwxr-x 5 wj wj 4096 Jul 15 11:22 ..
-rw-rw-r-- 1 wj wj 73 Jul 15 12:20 makefile
-rwxrwxr-x 1 wj wj 8616 Jul 15 14:57 test01
-rw-rw-r-- 1 wj wj 982 Jul 15 14:57 test01.c
-rw-rw-r-- 1 wj wj 1130 Jul 15 14:54 test02.c
[wj@VM-4-3-centos T0715]$ which ls
alias ls='ls --color=auto'
/usr/bin/ls
演示结果如下:
execl("/usr/bin/ls","ls","-a","-l",NULL);
总览:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#define NUM 16
int main(int argc, char *argv[], char *env[])
{
pid_t id=fork();
if(id<0)
{
return -1;
}
else if(id==0)//子进程
{
printf("子进程开始运行,pid:%d\n",getpid());
//在子进程中使用进程替换
//ls -a -l
execl("/usr/bin/ls","ls","-a","-l",NULL);
exit(-2);//进程替换失败
}
else//父进程
{
printf("父进程开始运行,pid:%d\n",getpid());
//让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
int status=0;
pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
if(ret)
printf("wait success, exit code:%d\n",WEXITSTATUS(status));
}
return 0;
}
关于为什么需要进程等待说明: 实际上这里的顺序性涉及到后续实际运用。我们可以结合上述1)中内容来理解,假父进程为总指挥官,旗下有各子进程作为执行者,则父进程需要不断下达子任务,委派子进程执行并将结果反馈。那么,单独使用fork时,由于CPU调度,无法确保谁先执行完成退出,加入进程等待,可以保证父进程在子进程之后退出。
故而上述演示案例,我们也可以嵌套一层while(1)
循环,保证父进程时刻运行。
int main(int argc, char*argv[], char *env[])
{
while(1)
{
pid_t id = fork();
if(id == 0)
{
//子进程
//……进程替换
}
else
{
//父进程
//……
}
} //end while(1)
return 0;
}
3)、进程替换中,父子进程代码和数据的关系解释
根据先前所学,fork之后,父子进程代码共享,数据写时拷贝;
这里引入进程替换,当新程序加载之前,fork后父子进程仍旧是代码共享,数据写时拷贝。但当子进程加载新的程序时,相当于一种写入,此时子进程的代码也要进行写时拷贝。
由此说明,进程替换后,父子进程代码、数据都进行写时拷贝。
2.4.3、怎么办2.0:其它exec函数演示
1)、exec系列函数再展示
#include <unistd.h>
extern char **environ;
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 execvpe(const char *file, char *const argv[],
char *const envp[]);
关于上述几个函数的记忆与理解:
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
2.4.3.1、execv函数演示
函数声明如下:
int execv(const char *path, char *const argv[]);
基本说明:
这些系列函数的使用大体无区别,只是在细节上做一定修改。比如这里的execv,根据第二参数char *const argv[]
,需要传递的是指针数组,因此这里我们建立一个数组用于传递相关指令:
#define NUM 16
char* const _argv[NUM]={(char*)"ls",(char*)"-a",(char*)"-l",NULL};
execv("/usr/bin/ls",_argv);//注意路径写法
演示结果如下:
相关代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#define NUM 16
//int main(int argc, char *argv[], char *env[])
int main(void)
{
pid_t id=fork();
if(id<0)
{
return -1;
}
else if(id==0)//子进程
{
printf("子进程开始运行,pid:%d\n",getpid());
//在子进程中使用进程替换
//ls -a -l
char *const _argv[NUM] = {
(char*)"ls",(char*)"-a",(char*)"-l",(char*)"-i", NULL};
execv("/usr/bin/ls", _argv);
//char *const argv[] = {"ps", "-ef", NULL};
//execv("/bin/ps",argv);
//execl("/usr/bin/ls","ls","-a","-l",NULL);
exit(-2);//进程替换失败
}
else//父进程
{
printf("父进程开始运行,pid:%d\n",getpid());
//让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
int status=0;
pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
if(ret)
printf("wait success, exit code:%d\n",WEXITSTATUS(status));
}
return 0;
}
补充: 也可以加上颜色编辑:
2.4.3.2、execlp\ececvp函数演示
函数声明如下:
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
p(path)
: 有p自动搜索环境变量PATH
。因此这里的const char *file
若是环境变量PATH中的程序,可不用写绝对路径,能够直接搜索到。
相关演示:
execlp("ls","ls","-a","-l",NULL);
char *const _argv[NUM] = {
(char*)"ls",(char*)"--color=auto",(char*)"-a",(char*)"-l",(char*)"-i", NULL};
execvp("ls",_argv);
演示结果如下:
相关代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#define NUM 16
//int main(int argc, char *argv[], char *env[])
int main(void)
{
pid_t id=fork();
if(id<0)
{
return -1;
}
else if(id==0)//子进程
{
printf("子进程开始运行,pid:%d\n",getpid());
//在子进程中使用进程替换
//ls -a -l
//execlp("ls","ls","-a","-l",NULL);
char *const _argv[NUM] = {
(char*)"ls",(char*)"--color=auto",(char*)"-a",(char*)"-l",(char*)"-i", NULL};
execvp("ls",_argv);
//execv("/usr/bin/ls", _argv);
//char *const argv[] = {"ps", "-ef", NULL};
//execv("/bin/ps",argv);
//execl("/usr/bin/ls","ls","-a","-l",NULL);
exit(-2);//进程替换失败
}
else//父进程
{
printf("父进程开始运行,pid:%d\n",getpid());
//让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
int status=0;
pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
if(ret)
printf("wait success, exit code:%d\n",WEXITSTATUS(status));
}
return 0;
}
2.4.3.3、execle\execvpe函数演示
1)、如何执行我们自己写的C、C++二进制程序、其它语言的程序
对我们自己写的C、C++程序
说明:需要写明程序所在路径。
演示结果如下:
相关代码:
test02.c
中:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#define NUM 16
//const char *filepath="/home/wj/one.-studybylinux/study2023/T2307/T0715/proc02.out";
const char *filepath="./proc02.out";
int main(int argc, char *argv[], char *env[])
{
pid_t id=fork();
if(id<0)
{
return -1;
}
else if(id==0)//子进程
{
printf("子进程开始运行,pid:%d\n",getpid());
execlp(filepath,"proc02.out","-b",NULL);
exit(-2);//进程替换失败
}
else//父进程
{
printf("父进程开始运行,pid:%d\n",getpid());
//让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
int status=0;
pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
if(ret)
printf("wait success, exit code:%d\n",WEXITSTATUS(status));
}
return 0;
}
proc02.c
中:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(int argc,char*argv[])
{
if(argc !=2)
{
printf("can not exectue!\n");
exit(1);
}
if(strcmp(argv[1],"-a")==0)
{
printf("hello a!\n");
}
else if(strcmp(argv[1],"-b")==0)
{
printf("hello b!\n");
}
else
{
printf("default!\n");
}
return 0;
}
makefile
中:
.PHONY:all
all:test02.out proc02.out
test02.out:test02.c
gcc -o $@ $^
proc02.out:proc02.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -rf *.out
对其它语言的程序
方法同上,只要有相关运行脚本即可。实际上exec系列函数功能类似于加载器。
execlp("./test.py", "test.py", NULL);
execlp("bash", "bash", "test.sh", NULL);
execlp("python", "python", "test.py", NULL);
2)、带e的exec系列函数,需要自己组装环境变量
相关演示如下:
相关代码:
在test02.c
中设置一个环境变量,char *const _env[NUM]
,使用execle
进程替换。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#define NUM 16
//const char *filepath="/home/wj/one.-studybylinux/study2023/T2307/T0715/proc02.out";
const char *filepath="./proc02.out";
int main(int argc, char *argv[], char *env[])
{
//设置一个环境变量:fork后子进程能继承父进程的环境变量
char *const _env[NUM]={(char*)"MY_ENV=2233445566",NULL};
pid_t id=fork();
if(id<0)
{
return -1;
}
else if(id==0)//子进程
{
printf("子进程开始运行,pid:%d\n",getpid());
execle(filepath,"proc02.out","-a",NULL,env);
//execle(filepath,"proc02.out","-a",NULL,_env);
//execlp(filepath,"proc02.out","-b",NULL);
exit(-2);//进程替换失败
}
else//父进程
{
printf("父进程开始运行,pid:%d\n",getpid());
//让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
int status=0;
pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
if(ret)
printf("wait success, exit code:%d\n",WEXITSTATUS(status));
}
return 0;
}
proc02.c
,形成proc02.out
文件,让子进程替换上:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(int argc,char*argv[])
{
if(argc !=2)
{
printf("can not exectue!\n");
exit(1);
}
printf("获取环境变量为:MY_ENV:%s\n",getenv("MY_ENV"));
if(strcmp(argv[1],"-a")==0)
{
printf("hello a!\n");
}
else if(strcmp(argv[1],"-b")==0)
{
printf("hello b!\n");
}
else
{
printf("default!\n");
}
return 0;
}
意义说明:
execle(myfile, "mycmd", "-a", NULL, env);
我们知道main函数有环境变量参数char *env[]
。假设该main函数是父进程的,使用上述进程替换函数,就可以直接通过父进程的env
,在子进程中获取到相应的环境变量。
int main(int argc, char *argv[], char *env[])
{
//子进程
//父进程
}
2.4.3.4、ececve 系统调用接口
int execve(const char *path, char *const argv[], char *const envp[]);
事实上,只有execve
是真正的系统调用,其它五个函数最终都调用 execve
,所以execve
在man手册 第2节,其它函数在man手册第3节。