文章目录
进程控制
进程退出引入
在c程序中的main函数 return 123有什么意义呢?
退出码会被父进程读取,一般为shell进程(bash)
使用==$?==查看最近一次进程退出的退出码
echo $?
代码运行完毕,结果正确
0: success
结果不正确
!0: failed
我们更想知道为什么不正确?这些错误是有多种可能的,用数字去充当这些可能
使用strerror接口,将错误数字转换为错误描述(返回字符串)使用string.h头文件,linux下添加c99
for(i...)
printf("%d %s\n",i,strerror(i));
(除0操作)…
程序崩溃后退出码也没有意义了,但可以通过一些方式得到一些退出的原因
进程退出的方式
1.程序中main函数return,代表进程退出!(非main函数return,是函数返回)
2.使用exit终止进程,且在任意位置调用都会终止进程
void exit(int status);//status表示退出码
exit 或者 main的return本身会要求系统进行缓冲区刷新
3.使用*_exit*(from unistd.h),强制终止进程,不要进行进程后续的收尾工作,比如刷新缓冲区(用户级缓冲区)
void _exit(int status);
- exit
经过一秒的停顿后,"hello world"被显示到了屏幕上(这里我们不带‘\n’,这样就能保证字符串不被自动刷新到屏幕)
- _exit
我们看到,没有任何显示
进程退出在OS层面的动作
系统层面,进程退出就相当于少了一个进程,同时free掉其PCB、mm_struct、页表和各种映射关系,代码、数据申请的空间也要释放
进程等待
是什么?
fork()后子进程父进程都可能在运行,又因为子进程出现的目的是为了帮助父进程完成某种任务
父进程要知道子进程任务完成的怎么样,一般在父进程fork之后,需要通过wait/waitpid等待子进程退出,这就是进程等待。
为什么要进行进程等待?
-
1 通过获取子进程退出的信息,能够得知子进程执行结果
-
2 可以保证时序问题:子进程先退出,父进程后退出,这样获得的信息才有意义
-
3 进程退出的时候会先进入僵尸状态,会造成内存泄漏的问题,需要通过父进程wait,释放该子进程占用的资源(僵尸状态无法用利器kill-9杀掉)
…
如何进行进程等待操作?
- 使用wait或者waitpid接口!
在man手册里查看其用法
为了验证我们所说的,使用如下一段简单的代码进行测试
#include <stdio.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(0);
}
//parent
sleep(10);
printf("father wait begin\n");
pid_t ret=wait(NULL);
if(ret>0)
{
printf("father wait:%d, success\n",ret);
}
else
{
printf("father wait failed\n");
}
sleep(10);
}
以上代码证明了wait可以用于回收僵尸进程,使用xshell观察:
复制一个SSH渠道然后写一个简单的脚本监控进程状态:
while :; do ps ajx | head -1 && ps ajx| grep myproc| grep -v grep;sleep 1;echo “##########################”; done
这是运行后监控脚本的一部分截图,从监控脚本的显示中我们可以看到起初父进程和子进程都处于S+状态,5s后子进程结束,但父进程仍然在sleep,子进程变为Z+僵尸状态,再过5s后,wait语句执行,父进程开始等待,等待成功,子进程被回收。只有父进程运行。
waitpid()
man手册:
如果我们只关注第一个参数其用法类似wait,如下:
#include <stdio.h>
#include <>
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(0);
}
//parent:
sleep(10);
printf("father wait begin\n");
//pid_t ret=wait(NULL);
pid_t ret=waitpid(id,NULL,0);//等待指定一个进程
//pid_t ret=waitpid(-1,NULL,0);//等待任意一个子进程
if(ret>0)
{
printf("father wait:%d, success\n",ret);
}
else
{
printf("father wait failed\n");
}
sleep(10);
}
waitpid的status参数
waitpid的第二个参数(int* status)是一个输出型参数,如果传递NULL则表示不关心子进程的退出状态信息
pid_t waitpid(pid_t,int* status,int options);
父进程拿到什么status结果,一定和子进程如何退出强相关!!
最终一定要让父进程通过status得到子进程执行的结果(经典三种情况)
如果进程异常终止,本质是这个进程因为异常问题收到某种信号!可据此判断代码是否正常跑完
status在这里不能简单的当作整型来看待,可以当作位图来看待(只研究status低16比特位)
signal为0,说明当前程序没有收到任何信号,不是异常中止的
若进行除0操作,(忽略报错直接运行)会有:
我们看到退出信号是8号,这说明程序异常终止了,8号信号是SIGFPE,意为浮点溢出、浮点错误
(statis>>8)&0xFF //打印退出码 (status)&0x7F //打印退出信号
为了便于我们操作,系统直接提供了关于查看返回信息的接口:
WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(产看进程是否正常退出)
WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
第三个参数options
现实举例:
阻塞等待:牛爷爷在图图楼下等图图,牛爷爷给图图打电话,图图说要等等他,双方都不挂断,等着回应
非阻塞:牛爷爷在楼下等图图,每过一会打一个电话问图图好了没(检测状态)
可能需要多次检测,这就是基于非阻塞等待的轮询方案
概念:
阻塞和非阻塞都是等待的一种方式:
谁等? --父进程
等谁?等什么?–子进程,子进程退出
阻塞本质:
父进程使用waitpid()等待,将其pcb链入等待队列中,R状态变为S状态,什么都不做,等待子进程
子进程返回,父进程pcb从等待队列拿到运行队列,从而被cpu调度
如何进行非阻塞等待?
int status=0;
while(1)//轮询
{
pid_t ret=waitpid(id,&status,WNOHANG);//传递第三个参数,进行非阻塞等待
if(ret==0){
//子进程没有退出,但是等待成功,需要父进程重复进行等待
printf("Do father things\n");
}
else if(ret>0){
//子进程退出了,waitpid成功了,获取到了对应的结果
//打印信息
break;
}
else{//ret<0
//等待失败
perror("waitpid");
break;
}
}
看到某些应用或者os本身,卡住了长时间不动,称之为应用程序hang住了
即WNOHANG:非阻塞
进程程序替换
以前的子进程都是使用if/else语句让子进程执行父进程代码的一部分
那如何让子进程执行一个全新的程序呢?
答案:采用程序替换!
-
进程不变,仅仅替换当前进程的代码和数据的技术叫做进程的程序替换
-
程序替换的本质就是将指定的程序的代码和数据,加载到特定进程的上下文中!!
进程具有独立性!!
子进程程序替换不影响父进程,虽然父子共用一套代码,但是更改代码会发生写时拷贝(是的,程序替换会发生代码的写时拷贝)
exec返回值,只要返回,一定出错
exec*承担加载器的角色
各个程序替换函数的基本使用
man手册:
命名理解:
execl
int main()
{
printf("i am a process! pid:%d\n",getpid());
execl("/usr/bin/ls","ls","-a","-l",NULL); //execl执行程序替换 //你要执行谁? //命令行上如何执行 //必须传空结束
printf("hahahahahahaha\n");
printf("hahahahahahaha\n");
printf("hahahahahahaha\n");
printf("hahahahahahaha\n");
printf("hahahahahahaha\n");
return 0;
}
执行结果:
execv
int main()
{
if(fork()==0){
printf("begin\n");
char* argv[]={
"ls",
"-a",
"-l",
"-i";
NULL
};
execv("/usr/bin/ls",argv);
printf("end\n");
exit(-1);
}
waitpid(-1,NULL,0);
printf("wait success!\n");
return 0;
}
至此,大致得出接口名中l的作用:用list列表传参,为v则使用数组传参
execlp
int main()
{
if(fork()==0){
printf("begin\n");
execlp("ls","ls","-a","-l",,"-d");
printf("end\n");
exit(-1);
}
waitpid(-1,NULL,0);
printf("wait success!\n");
return 0;
}
此接口自动在环境变量中搜索程序位置,只需输入文件名(操作名)即可定位程序
综上,那么剩下的接口都是排列组合:
execvp
int main()
{
if(fork()==0){
printf("begin\n");
char* argv[]={
"ls",
"-a",
"-l",
"-i";
NULL
};
execvp("ls",argv);
printf("end\n");
exit(-1);
}
waitpid(-1,NULL,0);
printf("wait success!\n");
return 0;
}
还有一个较为特殊的接口:execle,e表示自己维护环境变量(自定义)
execle
首先引入:
(tips)Makefile一个生成多个可执行程序
makefile只识别第一个依赖,那就把要生成的都集合到第一个依赖中,注意clean时也要多加
.PHONY:all
all:myexe myload
myexe:myexe.c
gcc -o $@ $^
myload:myload.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f myexe myload
回到execle
int main()
{
if(fork()==0){
printf("begin\n");
printf("end\n");
exit(-1);
}
waitpid(-1,NULL,0);
printf("wait success!\n");
return 0;
}
进程程序替换总结补充
exec系列接口族没太大区别,但还有些许差别:
-
其中execve是操作系统提供的,是系统调用,在2手册
-
剩下的接口都是库函数,是对execve的简易封装,在3手册
实现自己的Shell(模拟原理)
通过上面进程程序替换的学习,我我们已经具备了写一个简单的shell的能力
- 首先我们要模仿bash写一行字符串,就像我们平时在终端输入命令时,都可以看到的那句话,这里不能用\n刷新,因为那样会让命令出现在下一行,故这里使用fflush接口,这是C语言的东西,不懂自行查阅
如果你想观察这一刻,可以在这里让它sleep久一点,make后运行,就能看到以下效果:
其中前缀为lljh的就是我们自己的shell的c程序
- 接着我们要设法获取标准输入
定义一个数组来存放命令,然后初始化这个数组,在C语言里此办法可以以O(1)的时间复杂度清空字符串,使用fgets接口接收标准输入(可以读取空格),它是文件操作里的接口,但是在一些情况下非常好用!
- 接着我们解析输入的字符串
输入的命令是以空格分隔,所以我们据此进行分割字符串
(既然用了c,那就一c到底)
使用strok来截取分割字符串,用法很简单,自行查阅,将分割出的字符串依次放进命令数组。然后使用循环依次放入后续字符,strok有个特性,就是如果你要分割的字符串是前一个调用的字符串,传NULL它就会自动接着分割。这里报错不用管,因为最后一个\0自然会退出
- 终于到执行阶段了!调用子进程执行第三方命令!
经典的fork和waitpid,如果exec返回说明出问题了,使用1退出码退出,父进程wait一下子进程并保存打印其退出码,这些都是老生常谈了
- 运行!
错误输入返回演示:
一切都如所想,实现成功,但是真的成功了么?
当我反复使用常用命令时发现,pwd和cd…这里出了问题,我们始终没法发挥cd的功能,当前目录始终是这样,这是为什么?如何解决?
原因是我们现在的命令都是在fork()出的子进程中执行的,子进程执行cd…回退路径并非shell的,pwd显示当前进程所处路径,所以使用cd…不会发生改变。所以我们想要shell去执行这个命令才能达到我们想要的效果,引入内建命令!
内建命令
刚才fork()使用的“ls -a -l ”等命令都是第三方命令(相当于独立的程序),而这里我们使用cd时,使用的应该是内建方式的命令,即内建命令,所谓内建命令,就是不创建子进程,让父进程shell(bash)自己执行,实际上相当于函数调用
内建命令使用:
使用系统当中的接口chdir,可以直接切换到指定路径,写一个if语句特判检测输入的命令是否需要shell本身执行,即内建命令
实现效果:
至此,我们的简单shell模拟程序就已经完全实现,当然还有很多不足,比如目录的显示等等,日后学习了更多知识后我会完善
写在最后
学业繁忙,这是久违的博客,还有一些在typora上没有修改好,见不得人,自己看就行,如果真的有人看,我会随缘更新。现阶段纯作纪念