一、进程创建
1.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 id=fork();
if(id==-1)
{
printf("perror fork()!\n");
}
printf("after:pid is: %d,return is %d\n",getpid(),id);
sleep(1);
return 0;
}
结果展示:
我们可以看到,第一行输出是fork之前,只有父进程在执行,打印了before信息,fork创建子进程后,打印了两行after信息,分别由父子进程打印,注意到,进程29404打印了before的pid,而另外一个after却没有打印,这是为啥呢?
所以,fork之前父进程独立执行,fork之后,两个父子进程执行流分别执行。
注意:fork之后谁先执行,完全由调度器决定。(父子都有可能先执行)
写时拷贝
通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式各自一份副本(在物理内存中)。
1.2 fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.3 fork调用失败的原因
- 系统中太多的进程
- 实际用户的进程数超过了限制
二、进程终止
2.1 进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止(vs下叫做程序崩溃)
2.2 退出码
main函数的return值就是进程的退出码:
- 0:success
- 非0:failed
查看退出码的指令
echo $?
输出最近一次进程退出时的退出码(说的简单点就是上一条指令执行完毕后的退出码)
我们一般写程序时,main函数都是return 0,表示结果正确,但是如果我们不return 0呢?
#include<stdio.h>
int main()
{
printf("hello!\n");
return 123;//非0
}
我们可以运行此程序,观察退出码。
这里就可以提出我们退出码的意义?
它能够表示结果的正确与否,正确用0表示,因为那么多个数字,0只有一个,但是错误却有多个,用非0数字表示,错误的原因也是有多种可能的
退出码也是不能够随意乱写的,每一个退出码对应的数字,代表不同的错误,我们可以利用函数接口strerror
观察有哪些错误码
#include<stdio.h>
#include<string.h>
int main()
{
for(int i=0;i<100;i++)
{
printf("%d:%s\n",i,strerror(i));
}
return 0;
}
结果展示部分错误码对应的错误原因:
2.3 进程退出的方式
main
函数中return返回,代表退出进程!那么非main函数呢?它表示的其实就是普通的函数返回- 调用
exit
函数,它在程序的任意地方调用都是代表终止进程,参数是退出码,exit函数会完成一些收尾工作,例如资源的清理和释放,刷新缓冲区等。 - 调用
_exit
函数,它的作用是强制终止进程,不要进行后续收尾工作,比如刷新缓冲区(用户级别的缓冲区!)
介绍return退出、exit函数和_exit函数
1️⃣ return退出
return退出是一种最为常见的一种退出进程的方法,执行return n等于执行exit(n),因为调用main函数运行时,会将main的返回值当做exit的参数。
执行并查看退出码:
2️⃣ exit函数是标准C库中的一个库函数。
函数名称 | exit |
---|---|
函数功能 | 正常终止一个进程 |
头文件 | #inlcude<stdlib.h> |
函数原型 | void exit(int status) |
参数 | status:程序退出的状态 |
返回值 | 无 |
执行并查看退出码:
在调用exit之前,还会做一些其他的工作,
- 执行用户通过atexit或者on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓冲数据均被写入。
- 调用_exit函数。
结果展示:
3️⃣ _exit函数
_exit也是标准C库中的一个库函数,它和_Exit函数调用同义。
函数名称 | _exit |
---|---|
函数功能 | 正常终止一个进程 |
头文件 | #include<unistd.h> |
函数原型 | void _exit(int status) |
参数 | status:程序退出时的状态 |
返回值 | 无 |
_exit是强制退出进程,并不进行后续的收尾工作!
结果演示:
注意:status
定义了进程的终止状态,父进程通过wait来获取该值,虽然status是int,但是仅有低八位可以被父进程所用。所以_exit(-1)
时,在终端执行echo $?
时,发现返回值是255。
exit
和_exit
函数的区别:
exit函数退出进程前,exit函数会执行用户定义的清理函数、刷新缓冲区,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。
三、进程等待
3.1 进程等待是什么?
让父进程fork之后,需要通过wait
或者waitpid
等待子进程退出,父进程想要知道子进程完成的任务情况如何了。
3.2 为什么要让父进程等待?
- 通过获取子进程的退出信息(status),能够得知子进程的执行结果
- 可以保证“时序问题”,那就是保证子进程先退出,父进程后退出(避免孤儿进程,这样会导致资源泄露和进程管理混乱)
- 子进程退出的时候,会先进入僵尸状态,会造成内存泄漏的问题,需要通过父进程
wait
,释放子进程占用的资源
3.3 如何等待?
利用系统级别的函数wait
和waitpid
3.3.1 wait函数
函数名称 | wait |
---|---|
函数功能 | 暂停当前进程,直至子进程结束,并取回子进程结束时的状态 |
头文件 | #include<sys/wait.h> |
函数原型 | pid_t wait(int *status) |
参数 | status:子进程终止状态的地址 |
返回值 | >0:成功 <0:失败 |
说明:输出型参数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)
{
//child
int cnt = 5;
while (cnt)//5秒后子进程退出
{
printf("child[%d] is running!,cnt is %d\n", getpid(), cnt);
cnt--;
sleep(1);
}
exit(0);//退出子进程
}
printf("father wait begin!\n");
sleep(10);//休眠10秒
pid_t ret = wait(NULL);
if (ret > 0)
{
printf("father wait:%d,sucess\n", ret);
}
else
{
printf("father wait failed!\n");
}
sleep(10);//子进程被回收之后,让父进程再活上10秒钟
return 0;
}
3.3.2 waitpid函数
函数名称 | waitpid |
---|---|
函数功能 | 获取子进程结束时的状态 |
头文件 | #include<sys/wait.h> |
函数原型 | pid_t waitpid(pid_t pid,int *status,int options) |
参数 | pid:指定的子进程PID status:子进程终止状态的地址 options:控制操作方式的选项 |
返回值 | >0:成功 <0:失败 |
3.3.3 对于三个参数进一步说明:
1️⃣ pid:
1、pid<-1等待进程组识别码为pid绝对值的任何子进程.
2、pid=-1 等待任何子进程,相当于wait().
3、pid=0 等待进程组识别码与目前进程相同的任何子进程.
4、pid>0 等待任何子进程识别码为pid的子进程.
2️⃣ status:
用下面的常用的两个宏:
WIFEXITED(status)
: 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status)
: 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
3️⃣ options:
- 0:默认行为,阻塞等待(父进程什么都不做,就是等待子进程退出)
WNOHANG
:设置等待方式为非阻塞等待
注意:当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了
3.4 如何获取子进程status
3.4.1 如何理解status这个参数呢?
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
其中进程若是正常终止,高8位表示退出状态,即退出码。若是异常终止,则会被信号所杀,低7位表示终止信号,第8位表示core dump位。
3.4.2 获取退出码和退出信号
我们通过位运算可以,根据status得到退出码和退出信号:
status wait code=(status >> 8) & 0xFF; //退出码
status exit signal=status & 0x7F; //退出信号
#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)
{
//child
int 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)
{
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;
}
结果展示:
我们对于位操作是不是有点太过复杂和麻烦了,我们可以用上文提到的宏来代替位操作:
#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)
{
//child
int 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.5 阻塞等待和非阻塞等待
3.5.1 概念
都是等待的一种方式!
阻塞等待:死等,就是上述的情况,父进程一直等待子进程,父进程不做任何事情。
非阻塞等待:我们可以不要让父进程死等,而是在等待期间,父进程去做自己的事情,等子进程退出时再来检测子进程的运行状态
等待中可能需要多次检测,这里就提出来了一个方案:
3.5.2 基于非阻塞等待的轮询检测方案
问题:阻塞了是不是意味着父进程不被调度执行了?
阻塞的本质:其实就是进程的PCB被放入了等待队列中,并将进程的状态从R切换到S状态(不再分配CPU资源,从上层看就是卡住了!)
返回的本质:进程的PCB从等待队列切换到了运行队列,从而被CPU调度。
我们平时看到某些程序或者OS本身卡住了,长时间不动,我们就叫它hang住了。
wait/waitpid函数中有个参数是WNOHANG
(wait no hang):非阻塞等待
返回值结果:
- 子进程根本没有退出
- 子进程退出来(调用waitpid失败或者成功)
基于阻塞的等待轮询方案代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//child
int count = 3;
while (count--){
printf("child do something PID:%d, PPID:%d\n", getpid(), getppid());
sleep(3);
}
exit(0);
}
//father
while (1)
{
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret == 0)
{
//子进程没有退出,但是waitpid是成功的,需要父进程重复等待!
printf("father do nothing!\n");
}
else if (ret>0)
{
//子进程退出来,waitpid也成功了,获取到了退出结果!
printf("exit code:%d\n", WEXITSTATUS(status));
break;
}
else//ret<0
{
//等待失败!
perror("waitpid");
break;
}
sleep(1);//让父进程每隔一秒据监测一次
}
return 0;
}
过程描述:父进程每隔一秒就去看看自己进程退出没有,若没有就继续等待,若退出了,就获取退出码。
运行结果:
我们再来看看,就只有这一行简单的代码:
我们运行起来:
我们再查看一下ppid:13215
这里就有一个我们之前提到过的概念,我们再次明确一下:我们发现这里的13215就是我们的bash。
bash是我们的命令行启动的所有进程的父进程,那么我们的所有程序都是要子进程来执行的。
那么bash是怎么获得这些进程的退出结果的呢?
一定是通过wait等待的方式得到子进程的退出结果,所以我们能够通过echo $?
查到子进程的退出码!
目前我们创建子进程的目的:通过if else分流来让子进程执行父进程代码的一部分,现在我想让子进程执行一个“全新的程序”呢?——那就是进程的程序替换!
四、进程替换
4.1 替换原理
进程的程序替换:进程不变,仅仅替换当前进程的代码和数据的技术。用老的进程的外壳,去执行新进程的代码和数据,其中并没有创建新的进程。
代码演示:
#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("hello!\n");
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
return 0;
}
结果展示:
程序替换的本质:就是把程序的代码和数据,加载到特定的进程的上下文中!
我们平时写的C/C++程序要运行,必须先加载到内存中!如何加载呢?——加载器!(封装的底层就是exec系列的程序替换函数)
为什么要加载到内存中呢?
因为数据是存储在外设磁盘上的,CPU举例内存最近,读取是最方便的!
现在我们想让子进程也进行程序替换:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
if(fork()==0){//child
printf("command begin...\n");
execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
printf("command end...\n");
exit(1);
}
//father
waitpid(-1,NULL,0);//等待任意一个子进程
printf("father wait success!\n");
return 0;
}
结果展示:
当我们在执行子进程的替换时,我们的父进程照常执行自己的任务,两者之间互不影响呢?那是因为进程之间具有独立性!
可是父子代码不是共享的吗?
(我们之前提到的只是代码不修改的情况,现在是代码会被修改)进程替换会更改代码区的代码,同时会发生写时拷贝,所以父进程继续执行它本身的代码,而子进程转而去执行替换后的代码。
4.2 替换函数
其实有6种以exec
开头的函数,统称为exec函数
#include<unistd>
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[]);
4.3 函数解释
只要程序替换成功,就不会执行后续的代码,意味着exec*
系列的函数,执行成功的时候就不需要返回值检测!只要exec*
返回了,那就意味着替换失败,调用函数也失败了!(返回-1)
所以exec函数只有出错的返回值而没有成功的返回值!
4.4 exec*接口介绍
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不带 | 是 |
execlp | 列表 | 带 | 是 |
execle | 列表 | 不带 | 不是,需要自己组装环境变量 |
execv | 数组 | 不带 | 是 |
execvp | 数组带 | 是 | |
execve | 数组 | 不带 | 不是,需要自己组装环境变量 |
4.5 命名解释
这些函数看似容易混淆,其实隐藏着见名知意的规律
- l(list):表示采用参数列表,比如"ls",“-a”,“-l”,"-i"这样传参
- v(vector):表示参数采用数组
- p(path):有p自动搜索环境变量的PATH
- e(env):表示自己维护的环境变量
所有接口没有本质差别,只是参数不同
为什么有那么多接口?
是为了满足不同的应用场景!
execl函数的使用
见前面的引出小例子
execv函数的使用
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
if(fork()==0){//child
printf("command begin...\n");
// execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
char* argv[]={
"ls",
"-a",
"-l",
"-i",
NULL//必须以NULL结尾
};
execv("/usr/bin/ls",argv);
printf("command end...\n");
exit(1);
}
//father
waitpid(-1,NULL,0);//等待任意一个子进程
printf("father wait success!\n");
return 0;
}
结果展示:
相当于我们之前在命令行上传入的参数,我们现在将它打包成一个数组,我们将该数组的地址传入execv函数之中。本质上和execl函数是一样的!
execlp函数的使用
直接传入文件的名字即可,不用再带上路径了,因为有环境变量PATH的存在,它会去环境变量里面自动搜索文件路径。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
if(fork()==0){//child
printf("command begin...\n");
// execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
//
// char* argv[]={
// "ls",
// "-a",
// "-l",
// "-i",
// NULL//必须以NULL结尾
// };
// execv("/usr/bin/ls",argv);
//
//
execlp("ls","ls","-a","-l","-d",NULL);
//两个 ls 含义完全不同
printf("command end...\n");
exit(1);
}
//father
waitpid(-1,NULL,0);//等待任意一个子进程
printf("father wait success!\n");
return 0;
}
结果展示:
execvp函数的使用
传入参数是文件名和命令选项的指针数组的地址
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
if(fork()==0){//child
printf("command begin...\n");
// execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
// char* argv[]={
// "ls",
// "-a",
// "-l",
// "-i",
// NULL//必须以NULL结尾
// };
// execv("/usr/bin/ls",argv);
//
//
// execlp("ls","ls","-a","-l","-d",NULL);
char* argv[]={
"ls",
"-a",
"-l",
"-i",
NULL//必须以NULL结尾
};
execvp("ls",argv);
printf("command end...\n");
exit(1);
}
//father
waitpid(-1,NULL,0);//等待任意一个子进程
printf("father wait success!\n");
return 0;
}
结果展示:
execle函数的使用
相较之前的函数,不过多了一个e参数,表示自己维护的环境变量,意思就是你可以把指定的环境变量传给被替换的程序。
我们接下来验证的是,不再替换系统的指令,而是替换我我们自己写的程序了。
手动写的程序:
//myexe.c
#include<stdio.h>
int main()
{
printf("hello! I am your exe!\n");
extern char** environ;
for(int i=0;environ[i];i++)
{
printf("%s\n",environ[i]);
}
printf("my exe running....done!\n");
return 0;
}
我们修改名字:将myexec.c修改为myload.c(我们要在myload中替换myexe程序,意思就是利用myload运行myexe)
现在要求利用Makefile一次形成两个可执行程序
我们需要修改一下:
//Makefile
.PHONY:all
all:myexe myload
//设定一个伪目标all
myload:myload.c
gcc -o $@ $^ -std=c99
myexe:myexe.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f myload myexe
因为想要形成all,但是没用依赖方法,所以Makefile向下扫描,找寻生成myexe和myload的依赖方法!如果要生成多个,只需依葫芦画瓢,在后面添加即可!
结果展示:
改进myload.c:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
if(fork()==0){//child
printf("command begin...\n");
char* env[]={//这里就是自己维护的环境变量
"MYENV=hello",
"MYENV=hello",
"MYENV=hello",
"MYENV=hello",
NULL
};
execle("./myexe","myexe",NULL,env);
printf("command end...\n");
exit(1);
}
//father
waitpid(-1,NULL,0);//等待任意一个子进程
printf("father wait success!\n");
return 0;
}
现在我们来实验一下:
将myload.c里面的环境变量导入myexe.c,这就叫做带e的替换自定义的环境变量。
execve函数的使用
有了前面几个demo,这个函数用起来就更加简单了,我们只需将要运行的命令打包成一个数组即可。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
if(fork()==0){//child
printf("command begin...\n");
char* env[]={
"MYENV=hello",
"MYENV=hello",
"MYENV=hello",
"MYENV=hello",
NULL
};
char* argv[]={
"myexe",
NULL
};
execve("./myexe",argv,env);
printf("command end...\n");
exit(1);
}
//father
waitpid(-1,NULL,0);//等待任意一个子进程
printf("father wait success!\n");
return 0;
}
运行起来:
总结:
事实上,只有execve才是真正的系统调用,其他五个函数底层都是封装的execve函数。
五、做一个简单的shell
用下图的时间轴来表示事件发生的次序。shell由标识方块代表,它随时间的流逝,从左向右读取用户输入的指令,shell从用户读取ls,shell建立一个新的进程,然后在那个进程中运行ls进程,并等待子进程退出,然后读取新的命令。
具体步骤
1️⃣不断打印提示符
2️⃣获取我们输入的字符
3️⃣解析字符串
4️⃣检测是否需要shell本身执行的内建命令
5️⃣执行第三方命令(就是子进程的程序替换)
然后循环上述步骤,就可以实现一个简易版本的shell了。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<stdlib.h>
#define NUM 128
#define CMD_NUM 64
int main()
{
char command[NUM];//输入的命令行参数
for(;;)
{
//1.死循环打印提示符
char*argv[CMD_NUM]={NULL};//将我们的命令行参数看做一个一个字符串,拆开放入数组中
command[0]=0;//c语言中,字符串是以\0结尾的,这样做以O(1)的复杂度,将字符串清空
printf("[sjj@my_mini_shell]# ");
//2.获取命令行上的字符串
fgets(command,NUM,stdin);//从标准输入上获取后,放入command数组中
//因为我们输入的时候,会以回车作为结尾,回显出来的时候,会多出一个空行,这里处理一下
command[strlen(command)-1]='\0';
//printf("echo: %s\n",command);//回显刚刚获取到的字符串
fflush(stdout);//将缓冲区刷新,不然的话就不会在屏幕上直接打印
//3.解析字符串 以空格分隔解析
const char* separate=" ";
argv[0]=strtok(command,separate);//截取字符串
int i=1;
while(argv[i]=strtok(NULL,separate))//继续截取下一个字符时,传入NULL即可
{
i++;
}
//for(int i=0;argv[i];i++)
//{
// printf("argv[%d]:%s\n",i,argv[i]);
//}
//4.检测命令是否需要shell本身执行?内建命令
if(strcmp(argv[0],"cd")==0)
{
if(argv[1]!=NULL)
{
chdir(argv[1]);
}
}
//5.执行第三方命令
//让子进程替换程序
//其实这里替换的就是的bash,用我们写的程序去
if(fork()==0)
{
execvp(argv[0],argv);
exit(1);
}
//父进程等待子进程结束
waitpid(-1,NULL,0);
}
return 0;
}
运行效果展示:
谢谢观看!