目录
一.进程终止
进程退出
exit(int status)和_exit(int status)
exit的方式退出
_exit的方式退出
退出码
二.进程等待
进程等待方法
wait
waitpid
非阻塞等待
三.进程替换
execl
execv
execle
四.shell模拟实现
一.进程终止
什么是进程终止,这应该很好理解,就是一个进程结束了,但是一个进程结束又分为三种情况:
1.代码跑完了,结果正确
2.代码跑完了,结果错误
3.代码没跑完,直接崩溃了
对于第三种情况来说,我们很好判断,但是我们怎么判断第一种和第二种情况呢,一段代码跑完了,我怎么知道结果正确呢还是错误呢?
在解释之前,我们来思考一个问题,当我们在写一个C/C++程序的时候,在main函数的末尾总要写一个return 0;
这个return 0到底是什么,为什么要写return 0,对于其他函数的调用来说,写一个return,很好理解,因为return可以帮我们把想要的结果带回来, 同样的,main函数中的return也有这样的用处,main函数的return可以把程序的结果正确性反馈给我们,具体怎么做呢,看下面的代码。
1 #include<stdio.h>2 3 int main()4 {5 int begin=1;6 int end=100;7 int count=0;8 int i=0;9 for(i=begin;i<end;i++) 10 {11 count+=i;12 }13 if(count==5050)14 {15 return 0;16 }17 else18 {19 return 1;20 }21 }
在这段代码中,我写了一个从1加到100的程序,但是当程序执行完后,我需要知道我写的程序是否正确,即最终结果是否是5050,所以这个时候我需要用到return来判断,如果结果正确,则返回0,错误返回1,通过这样的方式,可以判断我们所写的程序的运行结果是否正确。
那这个结果又如何判断呢查看呢?
当我们的程序运行完后,可以通过下面这命令来获取程序的运行结果
echo $?
全过程如下图所示。
echo $?
这个命令是将最近一次运行的程序的程序的退出码打印出来
进程退出
在上面我们提到了程序的退出码,那么程序的退出码又是什么呢?
其实程序的退出码就是我们上面所说的程序退出结果是否正确。
进程退出的方式又有三种:
1.通过main函数的return退出
2.在任意地方调用exit()退出
3.在任意地方调用_exit()退出
对于第一种方法,我们已经讲解过了,那些现在来看一下第二、三种方式,在任意地方调用exit(),_exit(),
exit(int status)和_exit(int status)
exit()和_exit(),它们的作用是立即终止当前进程,同时参数中的status就是所谓的退出码,当进程通过exit或者_exit来退出的时候,其程序的退出码就是参数status。
那么exit和_exit有什么区别呢?
1.exit是C语言库所提供的函数,_exit是系统所提供的接口。
2.exit会主动刷新缓冲区等操作,而_exit不会。
对于区别1,这个应该很好理解,那区别2呢?我们来看一下代码来解释。
exit的方式退出
1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 5 int main()6 {7 printf("hello world");8 exit(1); 9 }
对于exit的方式退出,我们可以看到,这是一个正常的结果,把hello world打印出来后再退出的。
_exit的方式退出
1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 5 int main()6 {7 printf("hello world");8 _exit(1); 9 }
对于_exit的退出方法,可以看到,并没有把hello world打印出来。
所以exit和_exit的区别是会不会刷新缓冲区,exit会把缓冲区的东西全部刷新出来打印到屏幕了,而_exit不会,但是远不止这样的区别,exit还会执行用户自定义的清理函数和关闭流等操作。
同时exit和_exit是一个包含与被包含的关系,其实在exit中,是通过调用_exit来实现的,只不过在调用之前会先进行缓冲区的刷新,执行用户自定义的清理函数和关闭流操作等等。
总的来说,一般都是使用exit来终止我们的进程的。
退出码
在上面,我们解释可以通过退出码来判断一个程序是否运行正确,同时也讲到了程序退出的三种办法,但是现在有一个问题,程序的退出码都是数字,那这些数字都代表什么呢?
退出码0代表结果正确
退出码非0代表结果错误
但是非0的数字有很多,且这些数字都代表什么错误呢?
别怕,我们来写一个程序将所有的退出码的退出信息打印出来。
在C语言库函数中,有这么一个函数:strerror(),这个函数可以将相应退出码转换为对应的退出信息。
1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 #include<string.h>5 int main()6 {7 int i=0;8 int j=150;9 for(i=0;i<j;i++) 10 {11 printf("退出码:%d,退出信息:%s\n",i,strerror(i));12 }13 return 0;14 }
运行这段代码,就可以将所有的退出码所对应的退出信息打印出来,如下图所示。
这里的退出码信息很多,有133条,这里就不全截图了。
二.进程等待
什么是进程等待?为什么要有进程等待?
进程等待是指父进程等待子进程的一种行为,那么为什么父进程要等待子进程呢?
首先我们要先知道,在一个父进程中,它是可以创建一个子进程的,一般来说,当这个子进程结束的时候,父进程要回收子进程相应的资源,但是如果子进程的运行时间比父进程长,导致父进程提前结束而被它的父进程回收了资源,那么此时的子进程是一种孤儿进程,当子进程运行结束的时候,由于没有父进程回收资源,则子进程会陷入一种僵尸状态。
一旦一个进程变成了僵尸状态,那么就没有办法可以将它杀死,用kill -9也没有办法,则这个僵尸状态的进程就会占用系统资源且导致内存泄漏的问题。
所以进程等待的必要性是避免这种情况发生,同时还有另外一个原因,在上面我们提到,当一个程序结束的时候,我们需要知道这个程序的退出信息,这个程序的退出信息会传递给它的父进程,如果它的父进程比它先走,那么这个退出信息又怎么办呢?
所以这个时候就要有进程的等待。
所以要进程等待的两个原因是:
1.父进程要回收子进程的资源
2.父进程要获取子进程的退出信息
进程等待方法
那么既然要有进程的等待,那么具体怎么做呢?
在linux系统中,提供了几个系统接口给我们用于进程的等待,如下图所示:
wait
wait是最简单的等待方法,其中这个函数的返回值是所等待进程的pid,status参数是一个int类型的指针,这个指针所指向的内容存储着所等待进程的退出信息。
具体用法,如下所示:
1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/wait.h>4 #include<unistd.h>5 #include<stdlib.h> 6 int main()7 {8 pid_t id=fork();//创建子进程9 10 if(id==0)//说明是子进程11 {12 int cnt=5;13 while(cnt--)14 {15 printf("我是子进程,我的pid是:%d,我的ppid是:%d\n",getpid(),getppid());16 sleep(1);17 }18 exit(10);//进程退出19 }20 else if(id>0)//说明是父进程21 {22 int status=0;23 pid_t ret=wait(&status);24 printf("父进程等待成功,等待进程的id是:%d\n",ret);25 }26 27 return 0;28 }
运行结果:
waitpid
通过查手册可以看到waitpid有三个参数
pid_t waitpid(pid_t id,int* status,int option)
其中,id为需要等的进程的id,option为以什么方式等待,status保存着所等进程的退出信息,option有阻塞等待和非阻塞等待两种方式,在这里我们先以阻塞等待方式来进行演示。
1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/wait.h>4 #include<unistd.h>5 #include<stdlib.h>6 int main()7 {8 pid_t id=fork();//创建子进程9 10 if(id==0)//说明是子进程11 {12 int cnt=5;13 while(cnt--)14 {15 printf("我是子进程,我的pid是:%d,我的ppid是:%d\n",getpid(),getppid());16 sleep(1);17 }18 exit(10);//进程退出19 }20 else if(id>0)//说明是父进程21 {22 int status=0;23 pid_t ret=waitpid(id,&status,0);//0就是阻塞等待的意思24 printf("父进程等待成功,等待进程的id是:%d,其退出信息是:%d\n",ret,status); 25 }26 27 return 0;28 }
运行结果:
通过运行结果我们可以看到,与wait的方式大差不差,但是输出的退出信息却是2560,而我们所写的子进程的退出码是10,就是exit(10)那一句。为什么会这样呢?
那是因为status不是我们所想的那样只是一个单独的整形,它是一个位图结构,具体我们看图
对于这个status,我们只关心它的前16位,其中在这16位中,它的后八位记录着这个进程的退出状态(退出码),前7位记录着这个进程所收到的终止信号。具体这么做呢,我们来看代码。
1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/wait.h>4 #include<unistd.h>5 #include<stdlib.h>6 int main()7 {8 pid_t id=fork();//创建子进程9 10 if(id==0)//说明是子进程11 {12 int cnt=5;13 while(cnt--)14 {15 printf("我是子进程,我的pid是:%d,我的ppid是:%d\n",getpid(),getppid());16 sleep(1);17 }18 exit(10);//进程退出19 }20 else if(id>0)//说明是父进程21 {22 int status=0;23 pid_t ret=waitpid(id,&status,0);//0就是阻塞等待的意思24 printf("父进程等待成功,等待进程的id是:%d,其退出状态是:%d,收到的信号是:%d\n",ret,(status>>8)&0xff,status&0x7f);//通过位运算,将其退出状态和信号取出来 25 }26 27 return 0;28 }
运行结果
非阻塞等待
在上面的waitpid中,我们默认用的是阻塞等待,但是这里还有一个等待方式:非阻塞等待,那么这两个等待方式又有什么区别呢?
阻塞等待:当子进程没有退出的时候,则父进程进行一个干等,直到子进程退出了,才等待成功,在这个等待的过程中,父进程是不执行其他任何操作,只是干巴巴的等待子进程的退出。
非阻塞等待: 不管子进程有没有退出,父进程只要等待了,就等待成功了,换句话说,就是阻塞等待期间,父进程不能执行其他任何操作,而非阻塞等待期间,父进程可以执行其他操作,这种操作也称为轮询。
这句话可能很难理解,我们来看一下代码。
1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/wait.h>4 #include<unistd.h>5 #include<stdlib.h>6 int main()7 {8 pid_t id = fork();//创建子进程9 if(id == 0)//说明是子进程10 {11 int cnt1=9;12 while(cnt1--)13 {14 printf("子进程正在运行\n");15 sleep(1);16 }17 }18 else if(id > 0)//说明是父进程 19 {20 int status = 0;21 while(1)//进行轮询操作22 {23 pid_t ret = waitpid(id,&status,WNOHANG);//WHOHANG代表非阻塞等待24 if(ret == 0) 25 {26 //说明等待成功,但是子进程没有退出27 printf("等待子进程成功,子进程没有退出,还在运行\n");28 }29 else if(ret > 0)30 {31 //说明等待成功,子进程退出了32 printf("等待子进程成功,子进程退出\n");33 break;34 }35 else36 {37 //说明等待失败38 printf("等待子进程失败\n");39 break;40 }41 sleep(3);42 }43 }44 return 0;45 }
运行结果:
从代码和运行结果我们可以看到,与阻塞等待不同的是,非阻塞等待是一个一瞬间的过程,只要等待成功了,不管子进程是否退出,等待都结束,但是我们通过一个while循环,就可以达到一直循环等待的操作,这种操作叫做轮询,在轮询的期间,父进程可以执行其他任务,这是阻塞等待做不到的。
下面来演示一下非阻塞等待期间,父进程执行其他的任务。
如下图代码所示,在父进程等待子进程的期间,父进程去执行别的任务。
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>typedef void (*pfunc)();//typedef函数指针pfunc arr[4];//定义函数指针数组void Task1()//任务1
{printf("正在执行任务1\n");return;
}void Task2()//任务2
{printf("正在执行任务2\n");return;
}void Task3()//任务3
{printf("正在执行任务3\n");return;
}void LoadTask()//加载任务
{arr[0]=Task1;arr[1]=Task2;arr[2]=Task3;arr[3]=NULL;
}int main()
{LoadTask();//加载任务pid_t id = fork();//创建子进程if(id == 0)//说明是子进程{int cnt1=9;while(cnt1--){printf("子进程正在运行\n");sleep(1);}}else if(id > 0)//说明是父进程 {int status = 0;while(1){pid_t ret = waitpid(id,&status,WNOHANG);//WHOHANG代表非阻塞等待if(ret == 0){//说明等待成功,但是子进程没有退出printf("等待子进程成功,子进程没有退出,还在运行\n");int i=0;for(i=0;i<3;i++)//遍历任务{arr[i]();}}else if(ret > 0){//说明等待成功,子进程退出了printf("等待子进程成功,子进程退出\n");break;}else {//说明等待失败printf("等待子进程失败\n");break;}sleep(3);}}return 0;
}
运行结果:
三.进程替换
在上面我们所讲到的父进程创建子进程中,都是子进程去执行父进程的一部分代码,那么能不能创建一个子进程去执行磁盘中其他的代码了,答案是可以的,这个过程叫做进程替换。
在C语言库函数中,提供下面一系列的函数来给我们使用去进行进程替换。
可以看到有很多函数,但是不要怕,其实你只会其中一个,其他的也就很容易了,我们拿第一函数来举一个例子。
execl
我们先写两份代码,其中在myexec.c中,我们通过进程的替换将子进程替换成./mybin这个程序。
mybin.c
#include<stdio.h>int main()
{printf("我是被替换的进程\n");return 0;
}
myexec.c
#include<stdio.h>
#include<unistd.h>int main()
{pid_t id = fork();//创建子进程if(id == 0)//说明是子进程{//在子进程中进行进程的替换execl("./mybin","./mybin",NULL);//将子进程替换成./mybin,并且也./mybin的方式执行 }else if(id > 0)//说明是父进程{}sleep(1);printf("进程替换结束\n");return 0;
}
在myexec.c中,我们通过execl来进行一个子进程的替换,其中,在这个函数中,要传递的参数如上,第一个参数是你要替换程序的路径,这个路径可以是绝对路径,也可以是相对路径,后面的参数是你要用什么样的方式去执行,最后一定要加一个NULL。
这就是进程的替换。
当我们的代码运行起来的时候,我们创建的子进程就会去执行./mybin的代码
那么在exec一类的函数中,有那么多个,这些又有什么区别呢?
我来挑一些演示一下。
execv
#include<stdio.h>
#include<unistd.h>int main()
{pid_t id = fork();//创建子进程if(id == 0)//说明是子进程{//在子进程中进行进程的替换char* const _argv[]={(char*)"./mybin",(char*)NULL};execv("./mybin",_argv);//将子进程替换成./mybin,并且也./mybin的方式执行 }else if(id > 0)//说明是父进程{}sleep(1);printf("进程替换结束\n");return 0;
}
execle
这个函数的进程替换的作用是,将我们的环境变量也传过去。如下面所示:
myexec.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>int main()
{printf("父进程running......\n");pid_t id = fork();if(id==0)//子进程{char* const env_[]={(char*)"MYPATH=111222333",NULL};execle("./mybin","./mybin",NULL,env_);exit(1);}return 0;
}
mybin.c
#include<stdio.h>
#include<stdlib.h>int main()
{printf("SHELL:%s\n",getenv("SHELL"));//获取环境变量printf("HOME:%s\n",getenv("HOME"));printf("MYPATH:%s\n",getenv("MYPATH"));printf("替换进程!!!\n");printf("替换进程!!!\n");printf("替换进程!!!\n");printf("替换进程!!!\n");printf("替换进程!!!\n"); return 1;
}
运行结果:
通过代码和结果,我们可以看到当使用execle的时候,可以将我们自定义的环境变量传递过去。
同时进程的替换不仅可以替换相同类型的语言程序,也能替换其他类型语言的程序,也就是说在我们的程序中,你可以去替换python的程序以及所有后端语言的程序。
四.shell模拟实现
当进程替换学会了,我们就可以来实践一下了,其实在我们的命令行解释器shell中就有用到进程的替换,我们可以来模拟实现一下。
代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>#define NUM 100int main()
{while(1){//输出printf("[用户名@主机名 当前路径]");//输出提示符fflush(stdout);//刷新缓冲区char linemanned[NUM];//定义数组存储我们输入的命令char* in = fgets(linemanned,sizeof(linemanned)-1,stdin);//将命令输入到linemanned数组中linemanned[strlen(linemanned)-1] = 0;//去掉末尾\n//ls -a -l 假设我们输入的名字是这样的,所以要进字符串分割char* arr[NUM];//定义一个指针数组存储分割后的字符串int i=0;arr[i]=strtok(linemanned," ");i++;while(arr[i-1]!=NULL){arr[i]=strtok(NULL," ");i++;}arr[i]=NULL;int id = fork();//创建子进程来运行我们的命令if(id == 0)//说明是子进程{execvp(arr[0],arr);//进行进程替换去运行我们的命令exit(1);//如果子进程创建失败,则用exit退出,且退出码为1}int ret = waitpid(id,NULL,0);//进行一个进程的等待}return 0;
}
当代码运行起来的时候,我们就可以用我们自己的命令行解释器了。如下图所示:
但是我们的这个命令行解释器还不完善,比如说,当我们使用cd命令来切换路径的时候,是做不到的,为什么呢?
在讲解之前,我们先来理解一下什么是当前路径。
当前路径可以理解为当前进程的工作目录,在我们没有输入命令之前,当前的进程是我们的命令行解释器shell,所以当前路径可以理解为当前shell的工作目录,当我们进行cd命令的时候,其实是在改变shell的工作目录,但是在我们上面的代码中,我们是通过创建一个子进程来运行我们的命令,所以当我们使用cd命令的时候,其实是在改变这个子进程的工作目录,不是shell的工作目录,所以固然达不到我们想要的效果。
所以要想实现cd命令,则需要shell亲自去执行(这种命令也叫内建命令/内置命令),而不是派子进程去执行,同时在这里要用到chdir这个函数。
这个函数的作用是改变当前工作目录,所以我们的代码要进行修改,修改完成后如下:
int main()
{while(1){//输出printf("[用户名@主机名 当前路径]");//输出提示符fflush(stdout);//刷新缓冲区char linemanned[NUM];//定义数组存储我们输入的命令char* in = fgets(linemanned,sizeof(linemanned)-1,stdin);//将命令输入到linemanned数组中linemanned[strlen(linemanned)-1] = 0;//去掉末尾\n//ls -a -l 假设我们输入的名字是这样的,所以要进字符串分割char* arr[NUM];//定义一个指针数组存储分割后的字符串int i=0;arr[i]=strtok(linemanned," ");i++;while(arr[i-1]!=NULL){arr[i]=strtok(NULL," ");i++;}arr[i]=NULL;//进行判断是否cd命令if(arr[0]!=NULL && strcmp(arr[0],"cd") == 0){if(arr[1]!=NULL){chdir(arr[1]);}continue;}int id = fork();if(id == 0)//说明是子进程{execvp(arr[0],arr);exit(1);//如果子进程创建失败,则用exit退出,且退出码为1}int ret = waitpid(id,NULL,0);//进行一个进程的等待}return 0;
}
此时我shell就能实现cd的功能了,但现在的shell功能还不是很完善,我在这里再改进一下,就不过多讲解了。
增加了一个颜色的实现以及获取子进程的退出信息,并稍作修改。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>#define NUM 100char linemanned[NUM];//定义数组存储我们输入的命令
char* arr[NUM];//定义一个指针数组存储分割后的字符串int lastcode = 0;//存储退出码
int lastsign = 0;//存储信号
int status = 0;//存储子进程的退出信息int main()
{while(1){//输出提示符printf("[用户名@主机名 当前路径]");fflush(stdout);//刷新缓冲区//将我们在控制台输入的命令 输入到 linemanned数组中char* in = fgets(linemanned,sizeof(linemanned)-1,stdin);linemanned[strlen(linemanned)-1] = 0;//去掉末尾\n//ls -a -l 假设我们输入的命令是这样的,所以要进行字符串分割int i=0;arr[i++]=strtok(linemanned," ");//使用strtok函数来完成分割if(strcmp(arr[0],"ls") == 0)//如果此时的命令是ls命令,则我们给它带上颜色显示{arr[i++]="--color=auto";}//继续进行字符串分割while(arr[i-1]!=NULL){arr[i++]=strtok(NULL," ");}arr[i]=NULL;//进行判断是否cd命令if(arr[0]!=NULL && strcmp(arr[0],"cd") == 0){if(arr[1]!=NULL){chdir(arr[1]);}continue;}//创建子进程执行我们的命令int id = fork();if(id == 0)//说明是子进程{execvp(arr[0],arr);exit(1);//如果子进程创建失败,则用exit退出,且退出码为1}//子进程运行完成后,要进行一个进程的等待来获取子进程的运行结果int ret = waitpid(id,&status,0);lastcode = (status>>8) & 0xff;lastsign = status & 0x7f;}return 0;
}