Linux---进程状态

在上篇中知道了

1.一个程序被运行必须先加载到内存当中

  1. 一个进程包括PCB+自己代码和数据(整体操作系统而言) task_struct+代码和数据(具体的操作系统linux而言)

3.通过先描述后组织 操作系统对进程的管理转换为了对一个链表的增删查改。

在一个task_struct中包含了各种信息 而进程状态其实就是里面的一个信息, 进程状态在task_struct中其实就是一个数字,通过数字来代表它的状态。

运行 阻塞 挂起状态

进程需要通过CPU来调度,但是系统里面会有很多的进程,而CPU却只有几个或者一个,所以进程的task_struct会通过runqueue来管理起来,由其在requeue中的顺序来决定执行的顺序。

运行状态

只要task_struct属于调度队列当中,该进程就属于运行状态。

阻塞状态

例如: 一个c程序的进程, 它要运行会进入调度队列中, 当排到它的时候开始执行它的代码 在代码中遇到scanf的时候, 此时需要等待我们硬件的输入(键盘)**,**此时它就处于阻塞状态。

那我们一直不输入呢?

现象是它就一直在那里等,直到我们输入了内容按下回车键, 此时它才会继续运行。

那么其在等待我们键盘输入的过程中, 这个进程还在原来的调度队列中吗?

不在原来的调度队列中了! 它需要一直等待键盘输入,如果它一直在调度队列中不就影响到其他进程的运行效率了吗?所以它会被放到wait_queue中, 而wait_queue是管理硬件的结构体的一个成员变量中, 等待键盘输入的过程中它就一直在这个wait_queue中, 此时该进程的状态就是阻塞状态,在调度队列中的进程会继续的运行, 这样就不会影响到其他的进程了。

而这个进程直到键盘输入成功了,此时这个硬件的结构体对象通过里面的成员变量接收到这个信息了,就会把wait_queue里面的这个进程重新放到调度队列中 就重新变回了运行状态。

挂起状态

在上面的阻塞提到了管理硬件的结构体中会有wait_queue,wait_queue里面的进程处于阻塞的状态下.

如果此时内存的空间严重的不足了操作系统 需要进行一些处理,就会把处于阻塞状态的这些进程的代码和数据放到磁盘中的swap交换分区当中而task_struct保留 ,此时这些代码数据被放到交换分区中的进程的状态就叫做 阻塞挂起

当此时键盘输入成功后,操作系统 一定是优先知道 的 就会把交换分区中的代码和数据重新加载到内存当中,然后处于阻塞状态的进程重新回到调度队列中。

而如果此时内存空间依然不足的时候, 操作系统就会把调度队列中靠后的一些进程的代码和数据放到磁盘swap交换分区中 task_struct保留, 此时代码和数据被放到交换分区中的进程的状态就叫做就绪挂起

由上面我知道

进程状态的变化, 表现之一 就是要在不同的队列中进行流动 ,本质都是数据结构的增删查改

上面这些是对具体的linux中而言的, 而下图是对所有操作系统而言的,下面是对所有的操作系统都符合的一个版本,但是具体的操作系统可能会有一些差别。

理解一个进程同时输入多种数据结构

在上面中一个进程既输入调度队列,又能属于双向链表,还能输入等待队列, 他们之间是怎么同时存在并且能够转换的?

在之前我们实现双向链表的时候都是用下面这样的方式-------一个结构体里面存着变量及该结构体的两个指针, 一个指向前一个节点, 一个指向后一个节点。通过里面存的指针就能找到下一个节点进而访问里面的数据

而在linux中用下面这种方式

先用一个结构体list_head来包含了prev及next指针 而在task_struct中保存着各种数据及刚刚的list_head 此时通过list_head里面的next及prev就能找到下一个或者前一个tast_struct的list_head

但是此时有一个问题----通过 next及prev我们只能访问到前后tast_struct的list_head, 并不能访问task_struct里面的其他信息呀?

首先我们知道一个结构体对象的地址和第一个成员变量的地址是相同的

例如 task_truct a &a==&a.x

结论

通过(task_struct)(next-(((task_struct)0)->links))就可以由某一个list_head对象来找到当前所属的task_struct的第一个位置了

过程分析

(task_struct)0 以0为task_struct类型的第一个地址

((task_struct)0)->links list_head在task_struct中的偏移量

(task_struct)(next-(((task_struct)0)->links)) 由某个list_head对象next 来找到当前所属的task_struct的第一个位置

也可以由c中offsetof这个宏来计算结构体的偏移量在<stddef.h>头文件中

所以我们只要知道一个task_struct的head_struct对象就可以找到该task_struct结构体对象的所有信息!这个问题是不用担心的。

基于此就可以在task_struc写类似于head_struct的结构体 ,这样一个task_struc就可以同时属于多种数据结构 , 也就可以理解了上面一个PCB为什么既能输入调度队列,又能属于双向链表,还可以在等待队列当中。

linux进程状态 实验

如下,linux中各种状态在里面存的是数字,打印时候转换为相应字符串就可以了。

R(运行状态)

如下 我们写了一个简单的程序后把它运行

然后通过while :; do ps ajx |head -1 && ps ajx | grep code; sleep 1; done(每一秒查一次code的进程) 查看它的进程发现一直处于**S+(阻塞状态)**的状态下这是为什么呢?

因为代码里面有printf,实际上运行时只有一小段时间是在执行代码, 大部分时间处于等待IO的过程中,所以我们观察到的大部分都处于S+的状态下。

而在把printf给去掉后 就可以发现这个进程处于**R+**状态(运行状态)了

R+后面的+是什么意思呢?

如下,在让这个程序运行的时候再后面加一个&,再次观察该进程状态就会发现它处于R的状态,加号消失了。

所以 在后面带一个&可以让进程在后台进行, 进程的状态后面带+就说明当前的进程在前台运行---- 会阻塞我们继续在命令行进行输入,而没有+就说明该进程在后台运行。

杀掉进程

如果我们要杀掉这个进程可以用 kill -9 pid 的方式

如下在执行 ./code &后一行显示出了该进程的pid,或者我们可以通过查看进程pid, 然后通过这个pid来把这个进程给杀掉。

s(阻塞状态--浅睡眠)

如下在代码写一个scnaf函数,程序运行时候由上面理论知识我们知道进程就会处于阻塞状态 ,查看的时候发现果然处于S状态

T(阻塞状态---暂停状态深度睡眠)

如下 在编译时候再后面加 -g让该程序处于debug下,然后通过gdb调试, 打了断点后通过r运行到断点处后发现----当前进程就处于t状态(追踪状态)。

程序运行之后通过ctrl+z后把该程序暂停后再次查看就会发现此时处于T状态

为什么要暂停?不直接结束进程?

当处于该状态通常是发生了意外的问题系统给终止了,然后交给用户,由用户自己决定接下来的处理。

S和T都属于阻塞状态,为什么要有T呢?

s(浅度睡眠)是可中断休眠,此时可以通过kill来杀掉他,而D(深度睡眠)是不可中断休眠不能被kill杀掉

例如下面这样的场景

此时有一个进程,这个进程任务是向磁盘当中写10mb的数据,而这个进程在通知磁盘之后不能直接结束,因为它要接收磁盘是否把这10mb的数据写入成功了,但是此时内存里面空间已经严重不足了,操作系统看到这个进程一直在这里等着没什么事干,就把他给杀掉了!那此时磁盘如果写入数据失败了,就不能把写入失败的信息告诉进程进而传达给用户。

所以 此时这10mb的资源泄漏了 并且用户自己还不知道!

但是这也不是操作系统、磁盘、进程任何人的问题, 操作系统不这样处理可能造成更严重后果,它并不知道这个进程还很重要,所以此时有了D的状态用来标识类似于这个进程这样的情况, 处于D(深度睡眠)的情况下, 不能通过kill把它杀掉 只能在它完成后自己结束。

一点信号拓展

ctrl+z其实是通过信号把进程暂停了 通过kill -l可以查看所有信号

之前的kill -9杀死进程就是用的9号信号

通过kill -19 可以达到和ctrl+z同样的作用 kill -18 可以让暂停的进程继续运行

x(死亡状态) z(僵尸状态)

一个进程在结束之后就进入了x状态, 所以我们也观察不到。重点在于僵尸状态。

z僵尸状态存在的意义?

fork可以创建子进程,而父进程创建子进程一定是为了让子进程去完成什么任务 ,而这个任务完成的结果也要通过这个子进程来获取(结束之后把信息存在PCB当中),而子进程在结束之后不会再被调度了,其代码和数据没有意义了就会把它的代码和数据释放掉,而PCB内容不会释放掉,此时这个子进程的状态就是僵尸z状态。

模拟验证z(僵尸状态)

如下,代码子进程在五秒之后就会结束, 而父进程会一直进行下去。

通过观察进程状态可以发现,在子进程结束之前其是S状态,而在子进程结束之后其就变成了Z状态 ,并且在该行最后还有<default>(失效的无用的)

父进程是通过子进程的这个PCB来获取信息并且回收子进程输出信息的僵尸状态的,那么如果父进程一直不管,不回收,不获取子进程的输出信息,那么这个Z状态的PCB就会一直存在,就会造成了内存泄漏的问题 ,解决方式是通过wait waitpid的方式在之后学习中会了解到

补充两点

1.内存泄漏

内存泄漏问题如:malloc或者new向堆区申请了空间但是一直没有被释放

但是内存泄漏问题在程序结束后会自动解决,会自动释放堆区空间,所以内存泄漏真正的危害是对那些一直需要运行不退出的进程(常驻内存的进程)而言的,例如 我们平常使用的各种软件大部分都是这样的

2.内核结构申请

在之前的数据结构中我们知道创建一个对象需要先动态开空间然后对对其进行初始化, 而动态开空间也是有消耗的 ,所以对于那些已经开好空间不再需要使用的对象可以不释放,而是把它存储在某数据结构的缓存中 ,在之后需要创建新的对象的时候 可以不再动态开空间了,而是直接把刚刚数据结构缓存中的一个拿过来进行初始化替换 这样就节省了开空间的消耗

孤儿进程

僵尸进程是子进程先结束了父进程没结束,子进程的PCB还会保留就处于僵尸状态了,而孤儿进程是父进程先结束了,而子进程没有结束。

如下 可以看到父进程结束之后,子进程的父进程变成了一个pid为1的进程,被该进程领养了

该一号进程其实可以看做操作系统

为什么要领养?

假设如果不领养的话,子进程结束后还没有父进程把它回收它就会进入僵尸状态 ,PCB里面的资源不会被释放 直到有一个进程给它处理了 而这个处理的人就只能是父进程和系统 而父进程已经结束了 所以只能由系统来管理 。 所以如果不被领养的话就会造成僵尸进程的危害。

并且我们可以发现刚刚的程序不能在命令行通过ctrl+c来终止,因为变成孤儿进程后该进程就变成了后台进程ctrl+c不能直接取终止后台进程, 此时我们可以用kill -9来把它杀掉。

相关推荐
go_bai1 小时前
Linux-线程2
linux·c++·经验分享·笔记·学习方法
shizhan_cloud2 小时前
DNS 服务器
linux·运维
优质&青年2 小时前
【Operator pormetheus监控系列四----.alertmanager和Rules服务配置】
运维·云原生·kubernetes·prometheus
q***13342 小时前
Linux系统离线部署MySQL详细教程(带每步骤图文教程)
linux·mysql·adb
闲聊MoonL2 小时前
【AMBA】Caches协议分析
笔记
小雪_Snow3 小时前
Ubuntu 安装教程
linux·ubuntu
BUG_MeDe3 小时前
openssl 生成自签名证书步骤
服务器·https·ssl·数字证书
IT逆夜3 小时前
linux系统安全及应用
linux·运维
运维行者_3 小时前
网站出现 525 错误(SSL 握手失败)修复指南
服务器·网络·数据库·redis·网络协议·bootstrap·ssl