一.操作系统学科上的状态

运行态

1.一个CPU对应一个运行队列,这个运行队列的结构体中有两个进程控制块PCB的指针,指向PCB队列的头和尾,
2.这个运行队列其实是大量的进程在排队,排队其实是进程的PCB去排队,PCB中会使用指针指向其对应进程的代码和数据,虽然名称叫做运行队列但并不是所有的进程都正在运行,运行指的是处于运行队列中的进程已经准备好可以被调度器随时进行调度去运行
3.处于队列头上的PCB会被优先调度,当头部的进程PCB被调度到CPU上运行片刻后会重新被调度器拿下来放到队列的尾上继续排队 ,这个把大量进程从CPU拿上去,放下来的动作叫做进程切换
4.我们知道CPU的运行速度是非常快的,运行速度是纳秒级别,不能以人的常规思维去看待CPU的运行速度,以我们的电脑为例,上面可以同时运行许多应用软件,例如网易云,浏览器,画图等,这些软件的运行其实就是进程的运行,我们人的肉眼看到这些进程在同时运行,给我们一种错觉是认为这些进程是在CPU上同时跑的
5.事实其实不是这样的 ,由于CPU的运行速度非常快,进程是一个一个从进程队列的头上被调度器调度到CPU上去运行的,运行片刻后就被拿下来放到运行队列的尾继续排队,即CPU同一时刻之只能执行一个进程 ,CPU的运行速度非常快,同时在进程PCB中还有时间片信息,这个时间片限定了进程在CPU上执行的时间,例如时间片的时间对应的是10ms,所以不可能出现一个进程一直在占用CPU的资源去执行直到进程的结束,在这一个时间段内,由于CPU的运行速度非常快,运行队列经过调度器调度会被CPU反复循环跑上N次,即出现了在一个时间段内,所有进程的代码都被执行,这个过程叫做并发执行
处于运行队列的进程是运行态,简称R(run)状态
凡是在运行队列中的数据,他的运行状态为运行态
一个进程只要把自己放到cpu上开始运行了,是不是一直要执行完毕,才把自己放下来?每个进程都会有一个时间片(每个进程最多再cpu中运行的时间),再一个时间段内,所有的进程代码都会被执行!这就是并发执行。
阻塞态

1.我们知道计算机的组成中有各种设备,即外设,操作系统是一款进行软硬件资源管理的软件,那么其对于设备的管理是采用的先描述再组织,即使用DCB设备控制块strcut dev对每一个设备进行描述为DCB设备控制块的对象,然后再使用一定的数据结构将这些对象链接起来,例如使用单链表将这些对象进行链接便于进行管理
同时在这些DCB设备控制块中有PCB结构体指针,当进程需要访问外设例如键盘时,键盘并没有准备好,这里指的是用户没有敲键盘输入数据,即此时键盘的状态是未准备好,即关闭状态,那么此时进程由于要从键盘中读取数据就只能等待,那么进程控制块PCB就会被链接到DCB中的进程控制块PCB指针上,形成队列,并且在操作系统学科上给这个队列起名阻塞队列,当这个进程在等待时,如果接下来还有进程也需要访问键盘,读取键盘的数据,就只能等待,即链接到阻塞进程最后一个进程的后面继续进行等待,那么此时阻塞队列中的进程的状态就为阻塞状态。即等待同一个设备的pcb在同一个阻塞队列中等待。
挂起状态

一个进程在阻塞队列中等待硬件资源,此时如果操作系统内部资源严重不足了,就会把进程的pcb保留,然后将代码和数据从内存交换到外设(磁盘)当中,
在某些情况下,当操作系统内部的内存资源严重不足的时候,因为操作系统是款进行软硬件资源管理的软件,它要对上提供良好的环境,这个良好指的是安全,稳定,高效,所以当内存严重不足的时候,操作系统必须节省出内存,保证程序的正常运行
那么如何节省呢?由于计算机存在各种设备,即外设,在计算机上的操作大多数都要转化为对某种外设的访问,外设不一定准备好,即外设的状态不一定打开,那么也就会出现大量进程排队等待外设的情况,即有相当多的进程处于阻塞状态 ,操作系统就会从处于阻塞状态的进程下手
通俗来讲,一个已经加载到内存中的程序叫做进程,细致来讲,那么进程是由内核数据结构PCB和对应的代码和数据组成,那么一个进程存在,其对应的代码和数据已经被加载到了内存,这个代码和数据要占用一定的内存,当存在大量的进程,那么就会有大量的内存和数据被加载到内存中,同时还会有大量的进程处于阻塞状态,阻塞状态不同于运行状态,阻塞状态是进程正在等待,这个等待的过程其实仅仅让进程的PCB进行等待即可,因为进程当前处于阻塞状态,没有办法从外设中读取数据,即无法运行,所以即使代码和数据不在内存中也没有关系 ,磁盘的空间相比较于内存非常大,那么操作系统会将处于阻塞状态进程的代码和数据放到磁盘的交换区,这个过程叫做换出,也就是进程的挂起,那么进程不会一直处于阻塞状态,外设也有可以进行读取数据的状态,即打开状态,那么进程等待结束,可以从外设中进行读取数据,那么此时操作系统会将处于磁盘中进程的对应的代码和数据拿回来,即换入,进程那么就可以从外设中读取数据,将进程链入CPU的运行队列中,此时进程处于运行状态, 就可以正常运行了
这种操作系统的内部资源严重不足的情况一般比较少,所以进程的挂起并不是都会有的,只有当操作系统内部的内存资源严重不足的时候,操作系统才会将进程挂起,并且挂起还会有其它形式其它触发条件,例如开发人员调试需求,系统管理操作,进程优先级策略等
Linux中对应的状态
下面状态在kernel源代码里定义:

R运行状态,S睡眠状态
R运行状态(running):并不意味着进程一定在运行中,它表明进程要么在CPU运行中,要么在CUP对应的运行队列里。S睡眠状态(sleeping):意味着进程在等待事件完成,同时也叫做可中断睡眠,浅度睡眠,即睡眠期间可以被唤醒,对应操作系统学科中的阻塞状态

此时,打印进程正在进行,但状态却是S,我们修改一下代码,将打印语句注释掉,再来看一下,
此时,我们继续运行程序查看进程状态,

此时状态就变成了R即运行态。那为什么第一个进程在运行打印时状态不是R呢? 你以为一直在向显示屏写入,但实际上由于cpu运行速度很快,大概率进程是在等待io设备就绪,即等待显示屏设备就绪,所以进程一直处于S状态。这种处于后台的进程通过ctrl c无法终止,需要执行kill指令。
cpu是纳秒级别的,外设是毫秒级别的,所以很进程大概率是在等待外设,绝大部分处于S状态。
我们再来看一个例子
写一个scanf读取程序
cpp
#include <stdio.h>
int main()
{
int a = 0;
scanf("%d", &a);
return 0;
}

1.当将程序运行起来后,我们的程序在屏幕上不断等待用户敲击键盘去写入数据
2.当我们使用ps工具查看进程状态,查看到STAT即state状态那一栏我们的进程对应的为S+,此时我们的进程就是S睡眠状态
3.那么思考为什么我们的进程会处于睡眠状态?我们的程序需要从键盘上读取数据,那么进程就是不断等待外设即键盘准备好,此时我们不去敲击键盘输入数据,那么键盘就会一直不准备好,即键盘状态是关闭,那么此时进程就会在键盘设备的阻塞队列中等待,此时就对应操作系统学科的阻塞状态,所以此时我们的进程处于休眠状态
D磁盘休眠状态
D磁盘休眠状态(disk sleep):也叫做深度睡眠,处于这个状态的进程通常会等待IO的结束,不响应任何请求,同时其也对应操作系统学科上的阻塞状态的一种特殊情况,处于深度睡眠的进程,不响应操作系统的任何请求,也无法被 kill -9杀死
即进程等待磁盘写入是的状态,此时操作系统不能杀死该状态的进程
深度和浅度睡眠都属于阻塞状态。
我们举个例子来理解一下
(该点的讲解为假设情况下,并不是真实的)当我们的进程要向磁盘中写入数据(假设磁盘的内存空间不足以写入该数据),那么磁盘拿到数据进行写入,此时进程等待磁盘数据写入数据是否成功,即等待磁盘的反馈,恰好此时操作系统的内存资源严重不足,需要节约出内存空间,看到了这个进程正在等待,那操作系统看到,心想:"我的内存资源都要严重不足了,系统都要卡死了,你还在这里悠哉游哉的等待",此时操作系统就将这个进程给杀死了,那么当磁盘尝试写入后,发现磁盘空间不足以写入该进程的数据,那么磁盘心想,这个数据太大了,反正我没有办法写入成功,但是我剩余的空间还可以写入其它较小的数据,那么我就将该进程对应的数据全部丢弃去完成其它进程对应的较小的数据的写入,那么丢弃了,磁盘回头一看向给该进程一个回应,该进程呢?进程没有了
那么此时进程被操作系统杀死了,那么进程中对应的代码和数据也一并被释放了,同时磁盘也写入数据失败,将数据直接丢弃了,那么此时就造成了数据丢失问题,那么小编分析一波**,操作系统是为了节约内存,维持良好的环境而杀死的进程,这没毛病,进程在等待磁盘的回应是被杀的,进程是受害者,磁盘放不下数据,将数据直接丢失了去完成其它进程对应的较小的数据的写入了,这也没毛病** ,这是不是很难评判是谁的责任,所以对于这种数据丢失情况无法评判是谁的责任,那在用户使用系统的时候数据肯定不能够频繁的丢失,如果数据频繁的丢失,那造成的后果是不可想象的
所以针对这种情况,那么会将该进程设定为深度睡眠,即对于操作系统不响应任何请求,那么进程不能被操作系统杀死,那么进程对应的代码和数据也不会丢失,只有接收到磁盘对应的回应才会响应苏醒 ,那么此时再遇到上面这种情况数据就不会丢失了
T停止状态
T停止状态(stopped/tracing stop):可以通过发送信号停止进程,这个被停止的进程也可以通过发送信号来继续运行,在当前阶段的学习中,stopped/tracing stop一般不做区分
tracing stop(即追踪暂停)指调试过程中的进程暂停,例如使用gdb调试器的过程中就对应追踪暂停状态,使用gdb对程序进行调试,那么到断点处,进程会暂停下来,那么gdb就可以控制进程,此时gdb就可以对当前程序的变量等信息进行查看,此时进程所处的状态就是tracing stop追踪暂停状态
T暂停状态有助于我们对进程进行一定的控制,追踪暂停调试

使用kill -19 pid再去查看进程状态,便是T状态
- kill -19 进程标识符 将进程暂停
- kill -18 进程标识符 让进程继续运行
- kill -9 进程标识符 杀死进程,终止进程
当进行gdb调试时候,gdb进程控制proc进程,断点处变成t状态
我们思考一下T(暂停状态)和S(休眠状态)有什么区别
T状态是被外部信号强制暂停退出,不消耗CPU,必须由另一个进程发送信号才能恢复。主要用于进程控制。而S状态是进程主动放弃CPU,等待某个事件(如I/O完成),当等待的实践发生后,内核自动唤醒进程。
僵尸进程(Z状态)重点
z状态是什么?为什么要维护这个Z状态呢?
进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态,进程的相关资源尤其是他task_struct结构体不能被释放!会被一直占用。造成内存泄漏。
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if (id ==0)
{
//child
int cnt = 5;
while (cnt)
{
printf("i am a child,my PID:%d,my PPID:%d\n,cnt:%d\n", getpid(), getppid());
cnt--;
sleep(1);
}
exit(0);
}
else
{
//father
while (1)
{
printf("i am a proc,my PID:%d,my PPID:%d\n", getpid(), getppid(),cnt);
sleep(1);
}
//父进程目前并没有针对子进程干任何事情
}
return 0;
}
上面代码是子进程运行五秒后结束,父进程一直处于死循环

前五秒父子进程都在运行处于s状态,当子进程结束,父进程还没有回收他的资源,此时便处于僵尸状态。
僵尸进程的危害
1.子进程的退出状态必须被维持下去,因为他要告诉关心他的父进程,父进程交给自己的任务自己完成的怎么样了,可是如果父进程一直不进行读取,那么此时子进程就要一直维护这个Z僵尸状态。
2.那么退出状态维护的其实是子进程的进程信息,即进程的PCB即task_struct结构体,**那么子进程对应的未进行释放的task_struct结构体会一直得不到释放,就会一直占用内存,**那么就会造成内存泄漏
孤儿进程
那如果父进程先结束会发生什么呢?
父子进程,父进程先退出,子进程的父进程会被改成1号进程(操作系统),父进程是1号进程--孤儿进程
孤儿进程:父子进程中,父进程先退出,子进程的父进程会被改为1号进程(操作系统),我们称该子进程被操作系统领养了,那么像这种父进程是1号进程的子进程我们称为孤儿进程
此时这个子进程就变成了后台进程,普通的ctrl+c无法进行退出,只能使用kill -9进程表示符,将子进程杀死
为什么被领养??因为孤儿进程未来也会退出,也要被释放。
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("i am a proc,my PID:%d,my PPID:%d\n",getpid(),getppid());
sleep(3);
pid_t id = fork();
if(id > 0)
{
int cnt=2;
while(cnt--)
{
printf("i am a father,my PID:%d,my PPID:%d\n",getpid(),getppid());
sleep(2);
}
exit(0);
}
else
{
while(1)
{
printf("i am a child,my PID:%d,my PPID:%d\n",getpid(),getppid());
sleep(2);
}
}
return 0;
}
当执行程序后,初始时只有一个当前进程,使用ps查看当前进程处于S+睡眠状态(前台进程)
当3秒后,当前进程调用了fork会创建子进程,当前进程成为父进程,子进程和父进程通过if分流,进入不同的执行流去执行不同的代码块,使用ps查看此时子进程和父进程都是处于S+睡眠状态(前台进程)
子进程会一直死循环间隔睡眠2s打印进程的PID和PPID
父进程会打印两次进程的PID和PPID后被exit系统调用终止进程,那么此时子进程的父进程的进程信息被bash回收释放了,那么此时子进程也要退出呀,子进程也要被父进程回收进程信息呀,但是却没有了父进程,这里的bash进程算的上是当前子进程的爷爷进程,可是任何一个父进程只对子进程负责,即bash使用fork创建出来的当前子进程的父进程 , bash只对当前子进程的父进程负责,bash的代码逻辑中没有要为当前子进程负责的逻辑,所以bash不能回收当前的子进程
操作系统肯定不能看下去这样的事情发生,因为任何一个进程都要退出,也要被释放,子进程没人回收这可不行,那么此时操作系统会成为子进程的父进程 ,即操作系统收养了当前的子进程
注意此时的子进程被操作系统收养,在后台运行,并且小编写的是死循环打印,那么子进程不会主动退出,那么只能使用kill -9 进程标识符,杀死子进 程进行退出

进程优先级
基本概念:
优先级:对于资源的访问,谁先访问,谁后访问
进程的优先级:CPU资源分配的先后顺序,优先级高的进程有优先执行访问权限的权利
为什么要有优先级:因为资源是有限的,进程是多个的,所以进程之间是竞争关系。
如果我们进程长时间得不到CPU资源,改进昵称的代码长时间无法得到推进--该进程的饥饿问题
如何查看我们进程的优先级呢,使用ps -l
- 运行程序后,进程不断在屏幕上打印,那么我们复制SSH渠道,在复制的SSH渠道中,使用ps -l查看当前运行进程,发现没有我们的程序proc,这是因为ps -l只会在当前SSH渠道中显示当前运行的进程,对于其它SSH渠道会默认不显示,需要使用ps -al显示全部当前运行的程序
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("i am process,my PID:%d,my PPID:%d\n",getpid(),getppid());
sleep(2);
}
return 0;
}
运行代码后输入指令查看优先级

PRI(priority):优先级
NI(nice):进程优先级的修正数据
PRI(new)=PRI+nice 数值越低优先级越高
Linux不想过多的让用户参与优先级的调整,再我们对应的范围内进行优先级调整nice[-20,19]
80->[60,99]
调整优先级:
nice和renice
nice调整当前bash的nice值,那么bash创建出来的子进程的nice值都会遵循bash的nice值
使用方法nice -n 你要调整的nice值 bash


注意:我们加入NI(nice)值后,会变成PRI(新)=PRI(旧)+NI,**注意这个旧的PRI为初始值一直不变。**运算一次后,再下一次运算时PRI的值仍为旧的PRI值
那么此时它会提示要你输入要调整进程对应的PID,将我们进程的PID输入18888,按下回车
此时会出先permisson denied,普通用户不能调整优先级
转为root用户,pri起始为80,当NI设为-30时,发现优先级值减去20,结果是60,如果范围是到-20,但当我们此时再输入100时,变成100,加了二十,他是在80最原始的优先级进行运算而不是60修改后的。