在之前的Linux《进程概念》当中我们已经了解了进程基本的概念,那么接下来在本篇当中我们将开始进程控制的学习;在本篇当中我们先会对之前的学习的创建子进程的系统调用fork再进行补充了解,并且再之后会重点的学习进程的终止、进程等待以及进程的替换。学习完这些知识之后再下一篇章当中就可以试着自己实现Shell,通过本篇的学习将会让你对进程有更深的理解,一起加油吧!!!
1.进程创建
在之前初识进程的时候我们就了解了要创建子进程需要使用到系统调用fork,那么接下来我们再复习一下fork的使用并且再补充一些相关的知识。
1.1 fork函数
在linux中fork函数是非常重要的函数,它从已存在进程中创建⼀个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调用fork时内核就会进行以下的操作:
• 分配新的内存块和内核数据结构给子进程
• 将父进程部分数据结构内容拷贝至子进程
• 添加子进程到系统进程列表当中
• fork返回,开始调度器调度
1.2 fork的返回值
• 子进程的返回为0
•父进程的返回值是对应子进程的pid
1.3 写实拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的⽅式各自一份副本。具体见下图:
有了写实拷贝的存在就可以保持父进程和子进程的独立性。并且使用写实拷贝可以提高进程在创建时子进程的效率以及减少内存的浪费。
1.4 fork的常规用法
1• ⼀个父进程希望复制自己,使父子进程同时执⾏不同的代码段。例如,父进程等待客⼾端请求,生成子进程来处理请求。
2• ⼀个进程要执行⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数。
注:以上的第二点将在以下的进程替换当中进行详细的讲解。
1.5 fork调用失败的原因
• 系统中有太多的进程
• 实际用户的进程数超过了限制
2. 进程终止
在了解进程终止时首先要了解到进程的退出会有以下的三种情况:
• 代码运行完毕,结果正确
• 代码运行完毕,结果不正确
• 代码异常终止
通过之前的学习我们知道了程序当中main函数的返回值其实时有意义的,其实在Linux当中main函数并不是程序起始的位置,而是在通过一个名为start的函数调用main的,这时因为我们编译生成的可执行程序其实是被操作系统进行处理过的。main函数的返回值会通过相应的寄存器返回给start函数。
除了以上的知识之外接下来还需要了解的是在程序当中main函数的返回值通常表明的是对应程序的执行情况
1.1 退出码
在了解了进程终止的相关概念之后接下来来继续了解进程的退出码:
退出码(退出状态)可以告诉我们最后⼀次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。代码 1 或 0 以外的任何代码都被视为不成功。
注:当进程出现异常终止时其的退出码就是没有意义的了。
在Linux当中要打印最近一个进程(程序)的退出码可以使用以下的指令:
echo $?
那么接下来就来看以下的代码:
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
以上的代码就是一个很简单的C程序就是向显示器当中输出hello world,此时我们在main函数当中的返回值时0,那么接下来就将以上的代码编译成为可执行程序之后运行之后再使用echo $?查看对应的退出码。
查看以上的进程就可以看到进程的退出码是0
当我们将以上的代码修改为以下的形式时:
#include <stdio.h>
int main()
{
printf("hello world\n");
return 3;
}
此时再运行对应的程序再使用echo $?查看对应的退出码。
其实在程序当中不同的返回值是代表不同的出错原因,那么在Linux当中一共有多少的错误码呢?
接下来我们就通过一个程序来验证看看,代码如下所示:
#include <stdio.h>
#include<string.h>
int main()
{
for(int i=0;i<200;i++)
printf("strerror[%d]->%s\n",i,strerror(i));
return 0;
}
以上我们使用到了函数strerror,在此该函数能将对应的错误码对应的错误信息向我们展示出来。
在此我们不知道退出码的个数具体是多少,那么在此就猜一个数假设是200。
以上代码输出的结果如下所示:
通过以上的输出就可以看出Linux当中对应的退出码其实是有133个的。
以上我们就了解了退出码的基本概念,那么接下来我们就要继续的思考,在main函数当中main函数的返回值就是进程的退出码,那么除了在main函数当中使用return能设置进程的退出码,那么是否还有其他的方法能设置退出码呢?
其实除了在main函数当中使用return还可以使用exit来设置对应进程的退出码,并且在如何地方调用exit都会让对应的进程退出,并且将进程的退出码的返回值返回给父进程。
#include <stdio.h>
#include<string.h>
#include<stdlib.h>
int func()
{
printf("hello\n");
exit(1);
return 1;
}
int main()
{
func();
printf("hello world\n");
exit(1);
return 0;
}
以上的代码编译为可执行程序之后输出的结果如下所示:
使用echo查看进程对应的退出码:
这时就会发现以上的代码在调用了func函数之后就退出了,这是因为在func函数当中使用了exit,那么执行到该语句之后就进程会退出,之后main函数当中接下来的语句就不会执行了。
1.2 exit和_exit
接下来我们来看看exit和_exit有什么区别
首先要了解的是exit是C语言提供的函数,而_exit是系统调用
接下来来看以下的代码:
#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello world\n");
sleep(1);
exit(1);
return 0;
}
通过之前的学习我们知道由于缓冲区的存在,那么以上的代码在执行的时候首先会在使用printf的时候将hello world 从缓冲区当中刷新。之后使用sleep使得进程休眠一秒之后再使用exit使得进程退出。
接下来运行以上代码编译生成的可执行程序,看看是否和我们的预期一样
通过执行的结果是发现和我们的预期是一样的。
如果将以上的代码修改为以下的形式呢?
#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello world");
sleep(1);
exit(1);
return 0;
}
以上将原来代码当中的printf函数的\n去了,那么此时在调用完printf函数之后就不会将缓冲区进行刷新,而是要等到程序结束的时候才会刷新,因此以上代码的输出结果就是先暂停1秒;之后输出hello world之后瞬间程序就结束。
接下来运行以上代码编译生成的可执行程序,看看是否和我们的预期一样
通过执行的结果是发现和我们的预期是一样的。
如果将以上函数当中的exit替换为_exit还会像以上输出的结果一样吗?
#include <stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello world");
sleep(1);
_exit(1);
return 0;
}
接下来运行以上代码编译生成的可执行程序,看看是否和我们的预期一样
通过以上的输出结果这时就有一个奇怪的点了,那就是以上的程序最终在程序结束的时候没有将hello world输出到显示器上,那是不是说明在调用_exit的时候是不会不会将缓冲区的内容进行刷新呢?
确实是这样的, _exit系统调用在使得进程退出的时候是不会在退出的时候将缓冲区内的数据进行刷新的,这也是_exit和exit最主要的区别。
通过以上我们就可以得出以下的结论:
进程如果使用exit退出,那么在退出的时候会进行缓冲区的刷新
进程如果使用_exit退出,那么在退出的时候不会进行缓冲区的刷新
其实通过以上exit和_exit的学习就可以得出我们谈到的缓冲区一定不是在操作系统内部的,因为_exit就是系统调用但是却没有进行刷新。而使用了exit缓冲区却进行了刷新,这就说明缓冲区一定是C语言提供的。
3. 进程等待
之前在学习进程的状态的时候就了解到当父进程不回收子进程的退出信息此时子进程就会一直处于僵尸状态,此时如果父进程进行处理就可能会造成内存泄漏。因此在要进行进程等待的最重要的原因是回收子进程大的进程资源,除此之外还可以选择性的获取子进程的退出信息。那么父进程要如何接收子进程的退出信息呢?
在此就需要使用到wait和waitpid系统调用,接下来就来详细了解这两个系统调用的使用方法。
1. wait
在此首先使用man手册来查询wait系统调用的相关信息
在此就可以看出该系统调用的参数有一个,其实这个参数是一个输出型参数在此我们先不用了解,等到之后了解waitpid的时候再一起了解。
以上就可以看出该系统调用的返回值当为-1时表示进程等待失败,反而得到的就是对应的等待子进程的pid。
接下来来看以下的代码:
#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t pid=fork();
if(pid==0)
{
int cnt=5;
while(cnt)
{
printf("我是子进程,pid:%d\n",getpid());
sleep(1);
cnt--;
}
exit(1);
}
sleep(5);
pid_t wat=wait(NULL);
if(wat>0)
{
printf("进程等待成功,子进程的pid:%d\n",wat);
}
else{
printf("进程等待失败!\n");
}
sleep(10);
return 0;
}
在以上的代码当中使用fork创建的一个子进程之后再子进程当中每隔一秒打印一条信息,打印5条信息之后子进程就退出。接下来子进程就进入了僵尸状态,之后再使用sleep使得父进程休眠5秒以便于我们观察到子进程变为僵尸进程,之后使用wait将子进程的进程退出信息的回收,之后观察子进程是否成功的被回收。
接下来就将以上的进程编译成为可执行程序并且使用ps进行监视。
通过以上的输出结果就可以看出当子进程进程退出之后就进入了僵尸状态,之后使用wait回收子进程的退出信息之后子进程就结束了Z状态。
在wait当中我们还要了解到的是当子进程还未退出的时候父进程就会一直阻塞在wait处,直到子进程退出;这就和之前在C当中的scanf类似当用户未在键盘当中输入对应的数据时就会一直阻塞。
2.waitpid
以上我们了解了wait的时候那么接下来再来了解waitpid的使用。
一样的再使用man手册来查询waitpid的使用
在此就可以看到waitpid系统调用的参数有三个,接下来就来了解这三个参数分别表示什么。
在此第一个参数表示的是父进程要进行等待的子进程的pid,若等待任意一个子进程就将该参数设置为-1。
在此第二个参数为输出型参数,我们可以通过该参数来获取子进程的退出信息。
第三个参数表示是否进行阻塞等待,等我们将前面两个参数的使用了解透彻之后再进行理解。只需要知道默认为0,表示阻塞等待。
来看以下的代码:
#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t pid=fork();
if(pid==0)
{
int cnt=5;
while(cnt)
{
printf("我是子进程,pid:%d\n",getpid());
sleep(1);
cnt--;
}
exit(1);
}
sleep(5);
pid_t wat=waitpid(pid,NULL,0);
if(wat>0)
{
printf("进程等待成功,子进程的pid:%d\n",wat);
}
else{
printf("进程等待失败!\n");
}
sleep(10);
return 0;
}
将以上的代码编译生成可执行程序之后接下来再使用ps进行监视,此时就发现使用waitpid和wait实现的效果是一样的。
那么接下来就将以上的代码再进行修改创建一个输出型参数status来获取子进程的退出码。
此时运行可执行程序就会发现输出的结果好像和我们的预期不一样啊?
在此就发现子进程的退出码怎么256呢,以上在代码当中我们子进程当中退出码不是设置的是1吗?
其实是我们当前对输出型参数的认知还是不全面的,输出型参数最终经过waitpid系统调用之后的值其实不只是对应子进程的退出码。
正确的格式如下所示具体:
在status当为int一共有32比特位,在此高16位我们不进行讲究,只关心低16位。子进程的退出码其实被设置到8到15位的,而第7位表示的是core dump标志当前我们还不需要了解这是什么;在之后信号的学习将进行讲解。在此0到6位存储的是子进程的终止信号。
通过以上输出型参数的了解现在我们就知道要获取status当中的子进程的退出码就需要使用到位运算。
进行的操作如下所示:
接下来重新编译以上的代码,生成的结果如下所示:
接下来在修改代码将子进程的信号也输出
运行结果如下所示:
在之前学习进程的终止时我们就了解了当代码异常终止的时候进程的退出码就没意义了,在输出型参数当中如果进程异常退出,那么对应的status的后七位比特位就不为0,这时就说明代码是异常退出的,该status当中的退出码就是无意义的。
注:其实在使用waitpid时Linux当中提供了对应的宏来实现从输出型参数当中获取退出码。
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
那么是对于以上代码当中使用位运算来获取退出码的操作就可以使用以上提供的宏来实现,实现的代码如下所示:
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t pid=fork();
if(pid==0)
{
int cnt=5;
while(cnt)
{
printf("我是子进程,pid:%d\n",getpid());
sleep(1);
cnt--;
}
exit(1);
}
//sleep(10);
int status=0;
pid_t wat=waitpid(pid,&status,0);
if(wat>0)
{
if( WIFEXITED(status))
{
printf("进程等待成功,子进程的pid:%d,退出码:%d\n",wat,WEXITSTATUS(status));
}
}
else{
printf("进程等待失败!\n");
}
sleep(10);
return 0;
}
那么接下来我们就压思考wait和waitpid是如何实现获得子进程的退出信息的呢?并且我们可不可以直接创建一一个全局的变量之后在子进程当中修改该变量的方式来获取子进程的退出码呢?
对于以上的问题在使用全局变量的方式肯定是行不通的,因为当在子进程当中修改对应的全局变量时是会进行写实拷贝的,在此在父进程当中是无法获取到修改之后的值的。
其实子进程的在进入到僵尸状态的时候对应的PCB是不会销毁的,这时子进程的退出信息就存储在task_struct当中,父进程就可以通过子进程的task_struct来获取对应的退出信息。
在Linux源代码当中就可以看到task_struct当中有以下的成员变量。
以上我们就了解了waitpid当中前两个参数是该如何使用的,那么接下来就可以来了解第三个参数又是有什么作用的。
在此要了解第三个参数的使用就需要先来了解阻塞调用和非阻塞调用。接下来我们来通过一个故事来了解这两个概念的区别。
假设你是张三最近要考试学校里的期末考了,你平时有努力的学习这考试对你来说问题不大,但是对你的同班的李四来说就很困难了,因为他平时不学要在期末考之前突击一下靠自己通过考试还是比较困难的,所以李四就想到了你。在考C++的前一天就打电话问你现在有空吗想这时候来找你复习,但是这时候你还在做PHP的课设就和李四说等你做完课设再帮他复习,听完之后李四就把电话给挂了。过了一段时间之后李四又打电话给你,可是你还是没有做完,重复多次之后你终于将课设做完了,之后李四就来找你复习C++了。有了你的帮助李四成功的通过了C++的考试,过了几天之后李四又问你数据结构复习的怎么样了,那么这时候李四就再找到了你问你数据结构复习的怎么样了想再来找你复习,此时你就对他说数据结构还每复习完等我复习完再帮你复习,此时你就先回到了宿舍了,还是打电话给李四但是和之前不同的是这次你打完电话之后没有将电话给挂了,而是就这样等到你复习完之后就直接来找你,在此时李四也没有空闲着他先把老师给的数据结构的PPT看了看,这样至少会对知识先有点印象便于之后的复习。等到你复习完之后李四就来找你了,通过你仔细的讲解最后李四成功的通过了数据结构的考试。
在以上的故事当中小李在第一次找你的时候是通过不断的打电话来了解当前你是否将你的工作搞完,这时这种方式其实就和我们要了解的阻塞调用类似,父进程就像小李在等待你也就是子进程的时候是无法同时进行其他的工作的;而小李第二次来想找你复习数据结构的时候就是同时在等你完成复习的工作的同时还在看PPT,那么这时就和我们要了解的非阻塞调用类似,父进程就像小李在等待你也就是子进程的时候是可以同时进行其他的工作的。
以上在了解了阻塞调用和非阻塞调用的概念之后接下来我们就可以来试着使用waitpid的第三个模板参数了。
在此使用的方式如下所示:
pid_ t waitpid(pid_t pid, int *status, int options);
options:默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
当使用waitpid进行非阻塞调用的时候当子进程没有结束时该系统调用的返回值为0,这就和之前阻塞调用不同。
接下来来看以下的代码:
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t pid=fork();
if(pid==0)
{
while(1)
{
printf("我是子进程,pid:%d\n",getpid());
sleep(1);
}
}
//sleep(10);
while(1)
{
int status=0;
pid_t wat=waitpid(pid,&status,WNOHANG);
if(wat>0)
{
printf("进程等待成功,子进程的pid:%d,退出码:%d,信号:%d\n",wat,WEXITSTATUS(status),status&0x7F);
break;
}
else if(wat==0)
{
printf("本次调用结束,子进程没有退出!\n");
sleep(1);
}
else{
printf("进程等待失败!\n");
break;
}
}
return 0;
}
以上的代码当中我们就让子进程是进行死循环的,之后父进程使用waitpid来获取子进程退出信息此时使用的是非阻塞的调用,那么此时在不断的使用waitpid来获取子进程的退出信息都是显示子进程没有退出。只有当我们使用kill指令将子进程杀掉之后才能看到进程等待成功。
将以上的代码编译为可执行程序程序之后运行的如下所示:
那么接下来就来思考非阻塞调用的作用是什么呢?
其实在此就是让等待方在等待的时候能做自己的事。
#include <stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#define NUM 5
//函数指针
typedef void(*func_t)();
//函数指针数组
func_t handlers[5];
void DownLoad()
{
printf("我是一个下载的任务...\n");
}
void Flush()
{
printf("我是一个刷新的任务...\n");
}
void Log()
{
printf("我是一个日志记录的任务...\n");
}
void DownLoad()
{
printf("我是一个下载的任务...\n");
}
void Flush()
{
printf("我是一个刷新的任务...\n");
}
void Log()
{
printf("我是一个日志记录的任务...\n");
}
//将函数插入到函数指针数组当中
void registerHandler(func_t h[],func_t f)
{
int i=0;
for(;i<NUM;i++)
{
if(h[i]==NULL)break;
}
if(i==NUM)return;
h[i]=f;
h[i+1]=NULL;
}
int main()
{
registerHandler(handlers,DownLoad);
registerHandler(handlers,Flush);
registerHandler(handlers,Log);
pid_t pid=fork();
if(pid==0)
{
while(1)
{
printf("我是子进程,pid:%d\n",getpid());
sleep(1);
}
}
while(1)
{
int status=0;
pid_t wat=waitpid(pid,&status,WNOHANG);
if(wat>0)
{
printf("进程等待成功,子进程的pid:%d,退出码:%d,信号:%d\n",wat,WEXITSTATUS(status),status&0x7F);
break;
}
else if(wat==0)
{
int i=0;
for(;handlers[i];i++)
{
handlers[i]();
}
printf("本次调用结束,子进程没有退出!\n");
sleep(1);
}
else{
printf("进程等待失败!\n");
break;
}
}
// sleep(10);
return 0;
}
以上的代码就是就是先创建了一个函数指针数组,之后在registerHandler函数当中将对应函数都插入到数组当中。之后在父进程进行进程等待的时候当子进程还未结束的时候就可以执行对应的任务。
4.进程替换
fork() 之后,父子自执⾏⽗进程代码的⼀部分如果子进程就想执行⼀个全新的程序呢?
进程的程序替换来完成这个功能!
程序替换是通过特定的接口,加载磁盘上的⼀个全新的程序(代码和数据),加载到调用进程的地址空间中!
那么接下来我们就来看看进程替换的效果是什么样的?来看以下的代码
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("我的程序要运行了!\n");
execl("/usr/bin/ls","ls","-l",NULL);
printf("我的程序运行结束了!\n");
return 0;
}
你觉得以上的代码编译成可执行程序输出的结果是什么呢?
通过以上的输出结果就可以看出以上代码当中execl之后的printf是没有执行的,其实这时就是发生了程序的替换,要了解以上执行的的结果为什么是这样的就需要来了解程序替换的原理。
4.1 程序替换原理
我们知道进程是由内核数据结构对象和代码和数据构成的,那么进行进程替换时就会将当前要替换的进程的代码和数据替换,但是原先的进程的内核数据结构是没有被替换的。在此在进程替换前后当前的进程的pid是不变的。
注:在此进程替换的过程当中并没有创建新的进程,只是用当前进程的代码和数据用新的进程的代码和数据进行覆盖式的写入。
一般情况下都是在指定进程的子进程当中进行进程的替换,这样就可以让子进程执行父进程给的任务。
在此在进程替换当中我们需要使用的是exec*系列的函数,以下是exec*系列函数的特点:
1.当我们使用exec*系列的函数之后就不会再执行之后的代码了,这时后面的代码已经不存在了。2.exec*系列函数只有失败的返回值,没有成功的返回值;因此我们不需要对函数的返回值进行判断,只要有返回值就是进程替换失败,返回值为-1
4.1 exec*系列
以上我们了解了程序替换是什么之后接下来就来学习系统当中提供的exec*系列的函数,以下是我们需要了解的函数
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[]);
int execve(const char *filename, char *const argv[],char *const envp[]);
1. execl
int execl(const char *path, const char *arg, ...);
在此就可以看到execl是一个可变参数函数,在此该函数的第一个参数是需要进行替换的参数的路径,而之后的参数就描述的对应的替换之后进程是如何执行的。在调用该函数最后的参数需要以NULL结尾。
接下来来看以下的代码:
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我的程序要运行了!\n");
if(fork()==0)
{
execl("/usr/bin/ls","ls","-l",NULL);
exit(1);
}
waitpid(-1,NULL,0);
printf("我的程序运行结束了!\n");
return 0;
}
在以上的代码当中就是使用fork来创建子进程之后再子进程当中使用execl来进行程序的替换将原来的子进程替换为ls,以上的代码编译为可执行程序之后的输出的结果如下所示:
那么除了替换系统当中的指令还能替换我们自己创建的程序吗?
其实是可以的,接下来看以下的代码,在此我们创建一个C++的源文件
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
cout<<"I am C++,my pid is:"<<getpid()<<endl;
return 0;
}
将test.c内修改为以下的形式:
以上的代码输出的结果如下所示:
在此就可以看出替换前后的进程的pid是相同的。
2. execlp
int execlp(const char *file, const char *arg, ...);
和execl类似也能实现程序的替换,但是和execl不同的是execlp函数的第一个参数只需要传对应的文件名即可,之后execlp会自动的在PATH环境变量当中查找指定的命令。之后的参数和之前的execl相同,需要传对应指令的执行方式,最后以NULL结尾。
例如以下的代码:
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我的程序要运行了!\n");
if(fork()==0)
{
printf("我是子进程,我的pid:%d\n",getpid());
execlp("ls","ls","-l",NULL);
exit(1);
}
waitpid(-1,NULL,0);
printf("我的程序运行结束了!\n");
return 0;
}
3. execv
int execv(const char *path, char *const argv[]);
在此execv的的第一个参数和execl的相同,第二个参数是一个指针数组,这时和之前的函数不同的是就需要先将要替换的指令使用存储到对应的数组当中,再由该函数接收数组。并且该函数的参数不需要以NULL结尾。
例如以下的代码:
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我的程序要运行了!\n");
char *const argv[]={(char* const)"ls",(char* const)"-l",NULL};
if(fork()==0)
{
printf("我是子进程,我的pid:%d\n",getpid());
execv("/usr/bin/ls",argv);
exit(1);
}
waitpid(-1,NULL,0);
printf("我的程序运行结束了!\n");
return 0;
}
以上我们要让进程使用ls指令替换此时使用ececv就需要先创建一个命令行参数的数组argv,之后将该数组成为execv的函数参数。
其实通过以上的函数我们即可以理解了main函数当中的命令行参数是如何从获取的,其实就是通过bash先获取命令行数据,之后再创建子进程再使用execv进行进程的替换。
4.execvp
int execvp(const char *file, char *const argv[]);
在此execvp的参数第一个是要进行替换的指令的名称,第二个参数是命令行的指针数组。
例如以下的代码:
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我的程序要运行了!\n");
char *const argv[]={(char* const)"ls",(char* const)"-l",NULL};
if(fork()==0)
{
printf("我是子进程,我的pid:%d\n",getpid());
execvp("ls",argv);
exit(1);
}
waitpid(-1,NULL,0);
printf("我的程序运行结束了!\n");
return 0;
}
5. execvpe
int execvpe(const char *file, char *const argv[],char *const envp[]);
在此再execvpe函数当中有三个参数前两个参数和之前的函数一样就不再进行讲解,而在此的最后一个参数是含环境变量的数组。此时就可以将要传的环境变量通过该函数传给替换之后的子进程。
例如以下的代码:
pro.cc
#include<iostream>
#include<unistd.h>
#include<cstdio>
int main(int argc,char* argv[],char* env[])
{
std::cout<<"I am C++,my pid is:"<<getpid()<<std::endl;
for(int i=0;i<argc;i++)
{
printf("argv[%d]:%s\n",i,argv[i]);
}
printf("\n");
for(int i=0;env[i];i++)
{
printf("env[%d]:%s\n",i,env[i]);
}
printf("\n");
return 0;
}
test.c
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我的程序要运行了!\n");
char *const argv[]={(char* const)"mypro",(char* const)"-l",NULL};
char* const env[]={(char* const)"AAA=12345",(char* const)"BBB=54321",NULL};
if(fork()==0)
{
printf("我是子进程,我的pid:%d\n",getpid());
execvpe("./mypro",argv,env);
exit(1);
}
waitpid(-1,NULL,0);
printf("我的程序运行结束了!\n");
return 0;
}
将以上的两个文件的代码分别编译为mypro和mytest两个可执行程序之后,先运行mypro输出结果如下:
运行mytest结果如下:
以上我们就可以看出使用ececvpe时是会将父进程当中环境变量表的env环境变量传给子进程的,但是此时是env是会将原本的环境变量给覆盖的,那么如果是要将原本的父进程的环境变量追加新的变量再传给子进程那么要怎么做呢?
在此有两个方式
1.使用execvp+putenv
在之前我们就了解了获取环境变量可以使用getenv,在此设置新环境变量使用的是putennv
以上的代码修改之后如下所示:
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我的程序要运行了!\n");
char *const argv[]={(char* const)"mypro",(char* const)"-l",NULL};
//char* const env[]={(char* const)"AAA=12345",(char* const)"BBB=54321",NULL};
if(fork()==0)
{
printf("我是子进程,我的pid:%d\n",getpid());
putenv((char* const)"AAA=12345");
putenv((char* const)"BBB=54321");
execvp("./mypro",argv);
exit(1);
}
waitpid(-1,NULL,0);
printf("我的程序运行结束了!\n");
return 0;
}
执行结果如下所示:
2.使用putenv+environ
以上的方法1当中我们使用的是execvp,但是如果我们就要使用execvpe呢?
其实是可以的,但是这时就需要使用到全局的指针environ
以上的代码修改之后如下所示:
#include <stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我的程序要运行了!\n");
char *const argv[]={(char* const)"mypro",(char* const)"-l",NULL};
char* const env[]={(char* const)"AAA=12345",(char* const)"BBB=54321",NULL};
if(fork()==0)
{
printf("我是子进程,我的pid:%d\n",getpid());
//putenv((char* const)"AAA=12345");
// putenv((char* const)"BBB=54321");
for(int i=0;env[i];i++)
{
putenv(env[i]);
}
extern char** environ;
execvpe("./mypro",argv,environ);
exit(1);
}
waitpid(-1,NULL,0);
printf("我的程序运行结束了!\n");
return 0;
}
执行结果如下所示:
6. execle
int execle(const char *path, const char *arg,..., char * const envp[]);
在此execle函数的参数在以上函数的讲解当中都了解过了,在此就不进行讲解了
7. execve
int execve(const char *filename, char *const argv[],char *const envp[]);
我们使用man手册查询exec*就会发现execve不在里面,那么这是为什么呢?
其实是因为execve是系统调用,而之前的函数是C库当中提供的。
实际上以上我们了解的exec*都是封装了系统调用execve
这也就可以解释为什么我们在使用execvp的时候没有将父进程的环境变量表显示的传给子进程,但是之后子进程当中却能获得,这其实是因为在使用execvp函数的时候本质上是调用了execve系统调用,此时就会将父进程的环境变量表给传给子进程。
8.总结
以下是exec*系列函数的总结
以上就是本篇的全部内容了,接下来在下一篇当中我们将使用我们现有的知识试着实现自主的Shell命令行解释器 ,未完待续……