本文前半部分和上篇文章相同,之所以加进来是因为这两篇文章均是进程控制部分内容。如果大家已经看过前两部分内容,可直接从2.1起看!
目录
0.进程创建
相比大家对下面这段代码已经不陌生了,我们在介绍fork()的时候就已经写过一遍了,fork()有两个返回值,同一个pid会有不同的值,这是上篇我们说到的伪内存问题。而本篇我们要看看fork()创建时,操作系统会干什么事情?
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("我是子进程,pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
}
else
{
while(1)
{
printf("我是父进程,pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
当一个进程调用fork之后,就有两个二进制代码相同的进程,而且他们都运行在相同的地方。但是每个进程都将可以开始他们自己的旅程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("我是一个进程:pid = %d\n",getpid());
fork();
printf("我依旧是一个进程:pid = %d\n",getpid());
return 0;
}
当我们运行完时发现,fork()之后代码共享。
fork()之前父进程独立执行,fork()之后,父子两个执行流分别执行。
那么fork()之后是否之后fork()之后的代码被父子进程共享的??
结论:一般情况下,fork()之后,父子共享所有的代码 ,因此fork()之后,父进程共享了全部的代码,只不过子进程只能从fork开始执行。子进程继承了父进程的eip(程序计数器),但是如果子进程想找到之前的代码也是可以的。
fork()之后,操作系统做了什么?
我们都知道进程=内核的进程数据结构+进程的代码和数据。当fork()创建的时候是创建子进程的内核数据结构(struct tast_struct + struct mm_struct... + 页表) + 代码继承父进程,数据以写实拷贝的形式来共享或者独立!因此,fork()之后,操作系统创建结构,代码以共享的形式,数据以是写实拷贝的形式来实现两个进程整体保持独立性!也就是说,父进程或者子进程如果有一方进程挂掉,不会影响另一方。
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
那么为什么要写时拷贝?在创建子进程的时候就把数据分开不行吗?
答案是不行的,具体原因有一下三点:
- 父进程的数据,子进程不一定全用,即使使用,也不一定全部写入 ,因此会有浪费空间的嫌疑
- 最理想的情况,只有会被父子修改的数据进行分离拷贝,不需要修改的共享即可--但是从技术角度实现很复杂
- 如果fork的时候,就无脑拷贝数据的子进程,会增加fork的成本(内存和时间)
所以最终采用写时拷贝。只会拷贝父子修改的数据,变相的就是拷贝数据的最小成本,但是拷贝的成本依然存在。之所以写时,是因为这是延迟拷贝的策略,只有真正使用的时候操作系统再给你分配资源。因此这种写时拷贝变相的提高内存的使用率。
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
1.进程终止
关于终止的认识
我们在写C/C++程序的时候,每个程序都有一个main函数,这个函数也叫做入口函数。我们经常会习惯的写上
return 0 ,那么这里将会产生两个问题:
- return 0 给谁返回?
- 必须返回0吗?返回别的数字可以吗?
此时我们首先要了解到进程退出的场景:
- 代码跑完,结果正确
- 代码跑完,结果不正确
- 代码没跑完,程序异常了
任何一个程序无外乎这三种退出场景,本篇文章主要介绍前两种场景。我们将举一个例子来加什么对这两种场景的认识和理解:假设张三要参加期末考试,他无非就3中情况:1.正常参加考试,考了100分。2.正常参加考试,考了20分。3.为正常参考,原因也多种多样。类比到这里,我们也能够理解,一个程序也无外乎这三种情况,代码跑完,结果正确;代码跑完,结果不正确;代码未跑完,程序发生异常。就好比我们写一个排序算法要将一组数据进行排序要么代码跑完,排序成功;要么程序跑完,排序失败;要么程序都压根没跑完。
我们用0表示sucess结果正确,非0表示结果失败。(非0标识不同的原因也不同)
因此retun X(X叫做进程退出码)进程退出码表征了进程退出的信息,这个进程退出信息将来要将父进程读取的。因此这个退出信息码非常的重要。因此我们这里回答了return是给父进程返回
我们写一段程序验证一下
#include <stdio.h>
int main()
{
return 123;
}
当我们运行这个代码的时候,该进程的父进程是bash,因此这个程序的退出码我们可以使用bash下的命令echo来查看退出码
echo $?
$?
这个$?表示在bash中,最近一次执行完毕时,对应进程的退出码!当我们再查看一次的时候发现是0,大家也不要觉得奇怪。这是因为在shell看来,echo $?这条命令也被当成是一个进程(虽然他不是),因此就会变成了0
进程退出码
在我们刚刚说正常退出 进程退出码是0 0表示success,那么异常退出的时候其他的退出码都表示什么含义呢?
比如这里看一个ls 跟上一串随机字符,我们查看退出码就为2(非0)
因此,一般而言,失败的零值该如何设置呢?以及默认表达的含义?这里我们大家也不需要刻意记忆每个进程退出码对应的含义,因为我们可以自定义来设置,或者用的时候查一查就行。那么我们现在看看系统的代码是什么含义,我们可以使用strerror函数进行查看(下图为man帮助手册查看的strerror的作用及其用法)
#include <stdio.h>
#include <string.h>
int main()
{
int i = 0;
for(;i<100;++i)
{
printf("%d:%s\n",i,strerror(i));
}
return 0;
}
我们在这里大概看几个,我们看到0表示success,1表示权限不允许(可执行程序),2表示找不到文件
因此我们可以得出结论:不同的进程退出码可以对应不同错误原因。
进程终止的常见做法
一般我们有两种做法最常见:
- 在main函数中return,代表进程结束,非main函数return表示函数调用结束,为什么其他函数不行呢?
- 在自己的代码中任意地点中,调用exit(),即使非main函数也可以退出
exit
我们来看看exit的用法
我们写一段简单的程序看看
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void fun()
{
printf("fun()\n");
exit(20);
}
int main()
{
fun();
return 123;
}
我们执行这段代码,我们通过查看进程退出码可以确定该程序是从exit推出的还是return出去的。通过结果我们可以看到,程序是从exit退出的。
因此如果以后我们想终止一个进程,可以在想终止的地方调用exit()。
_exit
这里我们之所以介绍_exit仅仅是因为他和我们刚刚介绍的exit长得很像,我们在这里也不需要特别记忆_exit的用法。在此处,我们就简单介绍一下_exit如何使用,以及_exit和exit的区别。
我们通过查看_exit发现,_exit是一个系统调用,其实exit调用了_exit。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
void fun()
{
printf("fun()\n");
_exit(123);
}
int main()
{
fun();
return 20;
}
此时我们发现_exit和exit好像没有什么区别,实际上他俩还是有区别的,我们来看看下面这段代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("hello world");
sleep(1);
exit(111);
return 20;
}
我们首先使用exit来提前终止进程,我们查看结果发现hello world能够被刷新出来
而当我们在调用_exit时,显示器什么也没输出。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("hello world");
sleep(1);
_exit(111);
return 20;
}
结论:exit终止进程会刷新缓冲区 _exit终止进程不会刷新缓冲区(_exit我们就了解这么多)
关于终止,内核做了什么?
进程 = 内核结构 + 进程代码和数据
当进程终止时,代码和数据一定会被释放掉。对于内核结构(tast_struct && mm_struct),操作系统可能并不会释放该进程的内核数据结构。
2.进程等待
2.1为什么要进程等待
- 解决僵尸进程问题 -- 解决内存泄漏的问题
- 解决获取子进程的退出状态问题。首先:一个进程理应该获得子进程的退出状态。在今天,我们只讨论父进程必须通过进程等待的方式获取子进程的退出状态。
2.2进程等待的必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。因此我们需要想办法让该进程由Z状态变为X状态。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
2.3如何等待--进程等待的方法
2.3.1wait方法
我们使用 man 手册查看wait方法
我们刚才说到了进程等待可以解决将进程由僵尸状态变成释放状态,我们写一段代码来验证一下
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(1);
}
}
else
{
//parent
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
sleep(40);
pid_t ret = wait(NULL);
if(ret<0)
{
printf("等待失败\n");
}
else
{
printf("等待成功: result: %d\n",ret);
}
sleep(20);
}
return 0;
}
这段代码,我们可以预期看到3个现象:
- 子进程由运行变成Z状态
- Z状态在等待成功时就没了
- result的返回值等于子进程的pid
我们写一段监控监本代码来实时查看这个过程
while :; do ps ajx | head -1
&& ps ajx | grep wait | grep -v grep;
echo"--------------------------------------------";
sleep 1;done
以上就是wait的基本调用。我们使用wait()的方案可以解决子进程Z状态,让子进程进入X状态。
2.3.2 waitpid
wait的作用是等待任意一个退出的进程,而接下来我们重点介绍一个waitpid(). wait()/waitpid() 是系统调用!!
返回值pid_t:
>0 : 等待子进程成功,返回值就是子进程的id
<0:等待失败
第一个参数pid:
>0:是几,就代表等待拿一个子进程,pid = 1234 ,等待1234进程,因此就是等待指定进程
-1: 等待任意进程
第二个参数status:
- status是什么:
这个参数,是一个输出型参数,通过调用这个函数,从函数内部拿出来特定的数据。因为status是一个整形指针,因此拿出来的一定是要一个整数。从内部拿出来的数据就是从子进程的进程控制块(tast_struct)中拿出子进程退出的退出码!
- status的构成
我们关于status只需要关心改整数的低16个比特位。这16个比特位会分为3个部分。次低8位(8-15)存放这子进程的退出码
关于低7位的作用,我们刚刚说到,代码跑完结果正确,代码跑完,结果不正确,那么代码异常呢?因此低7位的作用就是处理异常。一个进程如果异常退出,是因为这个信号收到了特定的信号!!
第三个参数options:
0 : 我们可以先设置为0,0表示阻塞等待。什么意思呢?可以理解为父进程在等待回收子进程,但是子进程就是不返回呢,那么父进程就等待受阻了,这时父进程就阻塞等待了。
2.3.3waitpid()验证
验证status次低8位是子进程退出码:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
//child
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(1);
cnt--;
if(!cnt)
{
break;
}
}
exit(20);
}
else
{
//parent
int status = 0;
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success,ret : %d,我所等待的子进程的退出码:%d\n",ret,(status>>8)&0xFF);
}
}
}
我们假设这里把退出码换成31-->exit(31);
此时我们已经成功的获得了子进程的退出码。
但是我们发现使用位操作的成本是比较高的,因此linux给我提供了一些宏来直接可以调用
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
因为我们可以改写一下等待代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(5);
break;
}
exit(0);
}
else
{
//parent
int status = 0;
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
//是否正产退出
if(WIFEXITED(status))
{
printf("子进程是正常退出的,退出码:%d\n",WEXITSTATUS(status));
}
}
}
return 0;
}
验证status最低7位的作用 -- 处理异常
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(1);
}
exit(0);
}
else
{
//parent
int status = 0;
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success,ret : %d,我所等待的子进程的退出码:%d,退出信号是:%d\n",
ret,(status>>8)&0xFF,status&0x7F);
}
}
}
这段代码子进程会一直死循环运行,在子进程运行期间,父进程在阻塞等待子进程。
我们可以使用kill -l 查看其他的进程信号
我们刚刚验证了 9号信号 我们再验证一个3号信号看看
通过这两个验证我们确实已经发现了status的低7位是存储的进程异常退出的信号。
其他信号的验证
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(5);
//除零异常
int a = 10/0;
}
exit(0);
}
else
{
//parent
int status = 0;
printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait success,ret : %d,我所等待的子进程的退出码:%d,退出信号是:%d\n",
ret,(status>>8)&0xFF,status&0x7F);
}
}
}
我们查看8号信号发现是浮点数异常
那父进程拿到了子进程的退出码和退出信号,父进程先看谁呢?
进程退出码对应的前两种进程退出:1.代码跑完,结果正确;2.代码跑完,结果不正确
而进程一旦出现异常,只需要关心退出信号,退出码没有任何意义
2.3.4 阻塞等待和非阻塞等待
阻塞等待:
当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待其实本质就是当前进程自己变成阻塞状态,等条件就绪的时候再被唤醒。因此所谓的阻塞就是进程阻塞。
进程的非阻塞等待:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
将options设置为WNOHANG就非阻塞等待,如果是非阻塞等,子进程没有退出,返回值为0,等待失败,返回-1,等待成功,返回子进程pid
那么我们将代码改成非阻塞等待
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
sleep(3);
}
exit(0);
}
else
{
//parent
int status = 0;
while(1)
{
pid_t ret = waitpid(-1,&status,WNOHANG);
if(ret>0)
{
printf("等待成功,%d,exit sig:%d,exit code:%d\n",
ret,status&0x7F,(status>>8)&0xFF);
}
else if(ret == 0)
{
//等待成功了 但是子进程没有退出
printf("子进程好了吗,还没,那么父进程做其他事情.....\n");
sleep(1);
}
else{
//出错了 暂时不处理
}
}
}
}
以上就是非阻塞轮询检测。
使用kill -9 pid 杀掉子进程,此时父进程等待成功,获取子进程pid以及退出信号
3.进程程序替换
3.1进程程序替换是什么?(概念,原理)
子进程执行的是父进程的代码片段,如果我们想让创建出来的子进程,执行全新的程序呢?想让父子进程彻底分开!
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变.
程序替换的原理:将磁盘中的程序加载入内存结构,重新建立页表映射,谁执行程序替换,就重新建立谁的映射(子进程),效果:让我们的父进程和子进程彻底分离,并让子进程执行一个全新的程序!!!
3.2为什么要让子进程执行一个新的程序呢?
我们一般在服务器设计(linux编程) 的时候,往往需要子进程干两件种类事情:
- 让子进程执行父进程的代码片段(服务器代码)
- 让子进程执行磁盘中一个全新的程序(shell,想让客户端执行对应的程序,通过我们的进程,执行别人写的进程代码等等),C/C++,python,shell,java......
3.3 怎么做(编码,如何进行程序替换)
3.3.1 见一下最基本的代码
进程替换函数
其实有六种以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[]);
如果想要执行一个全新的程序(本质就是磁盘上的文件),我们需要做几件事情
- 先找到这个程序在那里?
- 程序可能携带选项进行执行(也可以不携带),明确告诉OS,我想要怎么执行这个程序
1.execl
其中参数列表中:"..." 叫做可变参数,说白了就是可以按照用户的意愿传入参数的大小个数,如果还不理解,大家肯定都用过C语言中的printf函数吧,printf有没有规定你只能打印几个参数呢?没有的,这是根据用户自己来定义的!这就是可变参数
execl 第一个参数 path: 就是告诉OS,这个程序在哪里。
execl 第二个参数:就是告诉OS,我想怎么执行。
我们使用C语言来看看,我们发现调用了ls -l -a 的命令
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("我是一个进程,我的pid是:%d\n",getpid());
// ls -a -l
execl("/usr/bin/ls","ls","-l","-a",NULL);
printf("我执行完毕了,我的pid是:%d\n",getpid());
return 0;
}
我们再调用一个top命令看看
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("我是一个进程,我的pid是:%d\n",getpid());
// ls -a -l
//execl("/usr/bin/ls","ls","-l","-a",NULL);
//top
execl("/usr/bin/top","top",NULL);
printf("我执行完毕了,我的pid是:%d\n",getpid());
return 0;
}
这个就是程序替换!但是在程序替换的过程中,发生了什么问题呢?我们发现最后一行代码没有打印!
答案是因为一旦execl替换成功,是将当前进程的代码和数据全部替换了!!后面的printf是代码,是代码已经被替换了,该代码就不存在了!
所以这个程序替换函数用不用判断返回值? 为什么?
int ret = execl(....);
execl是一个程序替换,一旦替换成功了,还会执行返回语句吗?不会了,因为程序替换成功之后没机会了,那么这里的程序替换的返回值有意义吗?答案依然是有意义的,因为替换成功才不会执行,那么失败的时候,必然会继续向后执行,可以通过返回值得到什么原因导致的替换失败!
那我们依然用代码来看看替换失败的时候
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("我是一个进程,我的pid是:%d\n",getpid());
// ls -a -l
int ret = execl("/usr/bin/lsssss","ls","-l","-a",NULL);
printf("我执行完毕了,我的pid是:%d,ret = %d\n",getpid(),ret);
return 0;
}
3.3.2 引入进程创建
我们刚刚写的程序并没有创建子进程,都是自己替换自己,而我们有时候就想让子进程做这个事情,那么我们把进程创建进入进来,我们修改一下代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
execl("/usr/bin/ls","ls","-a","-l",NULL);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
// ls -a -l
//execl("/usr/bin/ls","ls","-l","-a",NULL);
//top
//int ret = execl("/usr/bin/lsssss","ls","-l","-a",NULL);
//printf("我执行完毕了,我的pid是:%d,ret = %d\n",getpid(),ret);
return 0;
}
通过这个例子我们得到新的结论:子进程执行程序替换,不会影响父进程的程序,因为进程具有独立性!因此子进程无论怎么替换都不会影响父进程!
那么请问如何做到的?子进程和父进程如何在代码和数据上做分离的呢?当时我们说数据层面上发生写实拷贝!那么么代码不是共享的吗,父进程不会发生变化吗?实际上,当程序替换的时候,我们可以理解为,代码和数据都发生了写实拷贝完成父子的分离!
3.3.3大量的测试各种不同的接口
我们刚刚测试了一下execl接口,还有其他的接口,我们挑几个来测试一下。
execv
execv第一个参数和execl一致,而第二个参数是一个指针数组,这个指针数组指向的是什么?
我们也很好理解,刚刚execl是将一个一个char* 传入函数内,而execv是把这些char*整成一个数组,最后把这个指针数组传入execv即可
我们通过代码测试一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
char* const argv_[] = {
(char*) "ls",
(char*) "-l",
(char*) "-a",
NULL
};
execv("/usr/bin/ls",argv_);
//execl("/usr/bin/ls","ls","-a","-l",NULL);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}
因此我们发现execl和execv这两个接口其实并没有很大的区别,只是传入参数的方式不同而已!
execlp
第一个参数:你想执行什么程序 -- 找到它 我们执行指令的时候,默认的搜索路径是path,这里的p就是表示的path,因此带p的可以不带路径,只说出你要执行哪一个程序即可
第二个参数:如何执行它。
我们通过代码来验证一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
execlp("ls","ls","-a","-l",NULL);
//这里出现的两个ls,可以省略吗,含义一样吗?
//不能省略 含义不一样
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}
execvp
有了上面几个的铺垫,我们再看这个接口就特别的好理解了,第一个参数在Path找,第二个接口传入一个指针数组。我们也来验证一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
char* const argv_[] = {
(char*) "ls",
(char*) "-l",
(char*) "-a",
NULL
};
execvp("ls",argv_);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}
execle
这里的前两个接口都非常熟悉了,这里最后一个接口叫做环境变量。那么为什么要有这个接口呢?
说到环境变量之前我们先来看一下这个问题,我们刚刚提到过,进程替换可以让我们执行其他语言写的程序,那么我们怎么来执行呢?(我们使用execl 函数来调用)
我们现在的目标是想用我们写的myproc.c把mycmd.cpp调用起来,那么怎么来用呢?
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
execl("/home/Lxy/code/linux-code/practice/10-28/mycmd",/*使用绝对路径的方式*/
"mycmd",NULL);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}
#include <iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
return 0;
}
我们发现,我们成功的用我们程序调用了cpp文件,我们刚刚使用的是绝对路径,我们也可以使用相对路径,只需要将路径修改为相对路径即可
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
execl("./mycmd","mycmd",NULL);//使用相对路径的方式
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}
我们再运行发现,仍然是可以成功的。当然也可以用我们的程序来调用python语言,shell脚本语言等等,在这里我就不写代码了,将我自己的实验截图放在此处。
测试这么多,我们知道了任何程序都可以用系统级接口调用其他语言的
谈完这个话题我们再来谈谈环境变量,execle这个函数多了一个e,这个e就是环境变量,如果你想给这个函数传入环境变量,我们就可以传入环境变量。
首先我们先来传入一个系统存在的环境变量,我们使用myproc.c程序调用这个cpp文件
#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
cout<<"PATH:"<<getenv("PATH")<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
return 0;
}
发现没有任何问题,那么如果我们想 传入一个自己手动写的环境变量呢?我们就可以使用execle函数了
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("我是父进程,我的pid是:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//child
//我们让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是%d\n",getpid());
char *const env[] = {
(char*)"MYPATH=youcanseeme!",NULL };
//e:添加环境变量给目标进程是覆盖式的
execle("./mycmd","mycmd",NULL,env);
exit(1);//只要执行了exit 意味着execl函数失败
}
//这里就是父进程
int status = 0;
int ret = waitpid(id,&status,0);
if(ret == id)
{
printf("父进程等待成功\n");
}
return 0;
}
#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
//cout<<"PATH:"<<getenv("PATH")<<endl;
cout<<"---------------------------------\n";
cout<<"MYPATH:"<<getenv("MYPATH")<<endl;
cout<<"---------------------------------\n";
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
return 0;
}
模拟实现shell
讲完这些接口我们发现还剩下的其他接口参数大致相同,我们掌握上面几个接口之后,我们就可以使用其他的接口!至此,进程控制结束,我们可以简易实现一个shell,我把实现的思路和代码整理成一篇新的博客,大家可以点击此处查看[ Linux ] 手动实现一个简易版的shell
(本篇完)