Linux下学习进程控制

进程控制

进程退出引入

在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上没有修改好,见不得人,自己看就行,如果真的有人看,我会随缘更新。现阶段纯作纪念

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值