文章目录
前言
调度:调度就是会不会被cpu执行
普遍操作系统的知识是凌驾于所有操作系统之上的,因为这些知识放在任何一个操作系统都是正确的。这方面优点是:便于我们学习;缺点是:学习的内容不多,不会涉及linux等操作系统深入知识点,只是一个大致的模板,与伪代码一样
进程状态
我们可能在书上面见过各种各样的进程状态。但是,我们前面知道许多书上的代码是伪代码,伪代码一般只提供一种思路,然后把这种思路在各种平台上面进行执行
那么,同理,我们书上所接触的进程状态是一种通用的进程状态,拿到哪一个操作系统中都不会错
所以,书上的进程状态各个操作系统基本上都有,只不过每个操作系统或多或少有些不一样
一、普遍的操作系统
这些进程状态书上一般都会有(后面会具体讲),而且每一个操作系统也基本上都有上面的进程状态,只不过表现方式不同
进程状态概念:
上一节知道当一个程序加载到内存中,操作系统会通过创建好的PCB(进程控制块:task_struct,描述进程的结构体),来实例化对象,该实例化对象的PCB内部信息就是讲该程序的属性/信息。而每一个对象的PCB内部都有一个位置存放整型成员变量,该成员变量的值不同,就表示该进程处于什么进程状态
也就是说,状态的本质是:
进程的PCB对象中内部的一个整型成员变量,也就是一个整数
普遍的操作系统运行状态一般有如下几种:
运行、新建、就绪、挂起、阻塞、等待、死亡、停止、挂机
我们下面讲比较重点的三个:运行、阻塞、挂起
1、运行状态
概念: 并不是该进程被cpu执行才叫做运行状态,而是进程的PCB对象在运行队列的内部等待cpu的执行,这就叫做运行状态。在linux中,运行状态通过R表示,我们下面会讲到
我们前面知道了操作系统内部会创建一个PCB,每当加载进来一个程序,PCB就实例化一个对象,来存放加载进来的程序,然后对象通过链表等数据结构就行链接
而操作系统中还要一个重要的数据——运行队列
,运行队列由cpu来维护。一个cpu只有一个运行队列(多核就有多个运行队列,但是运行队列数量终究是比不过进程数量的)。这个队列里面有一个head头指针,这个指针就是指向PCB对象的,也就是内核数据结构中的节点(一般是指向第一个进程的PCB对象)。那么,如果我们要执行某个程序,就将该进程的PCB对象放入运行队列中,进行排队,等待cpu的执行,这样该进程就处于运行状态。
简易的运行队列:
这个head指针就指向要被执行的进程PCB对象,后面通过链表的方式链接,这些runqueue(运行队列)内部的PCB对象所对应的进程就处于运行状态
所以,网络上有一个小问题:让进程入队列是什么意思?
意思就是:>将进程的PCB(进程控制块)/task_struct结构体对象放入到运行队列中
通过上一节的冯诺依曼结构我们知道:
对于cpu来说,外设(硬件等)访问速度很慢。
但是我们在执行进程的时候,或多或少都会访问外设(printf/cout要显示器显示结果、scanf/cin从键盘输入数据,或者通过某种方法向网络(访问网卡)中写数据…)。
所以,外设或多或少都会被访问到
2、阻塞状态
概念:进程的PCB对象在外设的等待队列中,该进程的状态就称之为阻塞状态
但是,除了cpu数量少以外,外设的数量也是有限的!我们的电脑中/以后公司内部,也是只有少数的硬件设备
那么,举个例子:
现在有几十个进程,9个访问网卡、8个访问显卡、7个访问磁盘…
拿磁盘来说,因为磁盘数量少,所以访问磁盘的进程也需要排队等待(将进程的PCB对象放到等待队列中进行等待),这个时候就占用了外设资源。
每一个外设都是通过操作系统进行管理的,既然要管理,那么就必须遵循先描述后组织的方法。所以,每一个外设都有一个结构体(class)/结构体(struct)来描述(描述外设的结构体)。所以,上面的举例中,因为cpu只会执行运行队列的进程,要等待访问外设的进程不会执行,那么多个进程要访问磁盘,这个时候进程的PCB对象就都进入了外设的等待队列
(顾名思义,等待队列就是进程的PCB对象等待访问磁盘的机会),等待访问磁盘的机会。而进程的PCB对象在等待队列中,就称之为进程的阻塞状态
。而外设的结构体中也有一个指针用来维护这些PCB对象。所以得知:外设不仅仅有自己的描述结构体,还能够通过指针维护自己的等待队列。这个时候,进程的PCB内部的进程状态对应整型变量就会发生改变,就不再是运行状态了。同理,其他外设也是如此。等到进程访问完外设之后,进程又重新进入到运行队列中,这个时候又恢复到运行状态了,然后就可以被cpu执行了
小结(重要知识点)
1、一个cpu只有一个运行队列(多核有多个)
2、让进程进入队列的本质:将进程的PCB对象放入到运行队列中
3、进程在运行队列中,该进程就是运行状态(R),并不是被cpu执行才是运行状态
4、进程不仅仅会等待(占用)cpu资源,也可能随时随地要等待(占用)外设资源
5、所谓的进程的不同状态,本质就是进程的PCB对象在不同的队列中,等待某种资源
6、这些拖拽、排队等行为,并不是对进程本身进行操作,而是进程的PCB实例化对象进行操作
3、新建/就绪状态
这个通过名字我们就很容易理解,就是一个进程刚刚被创建好,也就是make操作,所以我们不需要过多描述
4、挂起状态
概念:将内存中的数据和代码转移到磁盘的状态称之为挂起状态
假如我们有许多进程,而这每个进程都要访问磁盘,那么这些进程的PCB对象都会进入外设等待队列,所以每个进程都是阻塞状态,这些进程都不会被cpu调度(不会被cpu执行)。那么,我们要花费大量的时间等待访问外设(因为对于cpu来说,访问外设速度太慢了),而这段时间内,我们大量的进程对应的代码和数据,不仅仅短期内不会被执行,而且还会占用内存的大量空间。如果,此时我们的内存空间不够了怎么办?这个时候又有其他要访问磁盘的进程要想加载到内存怎么办???
这个时候,为了解决阻塞状态的进程占用内存导致内存不足,操作系统就给出了解决方法:
操作系统会将阻塞状态进程的数据和代码转移到磁盘上存储,反正阻塞状态的进程的代码和数据短期内不会被执行。那么这样一来,每一个阻塞状态的进程就变成了挂起状态。这个时候,内存中还是有每一个挂起进程的PCB实例化对象,只是进程的代码和数据在磁盘中,减少了内存占用,并且PCB对象内部的整型变量发生变化,变成了挂起状态
将内存的相关数据加载/保存到磁盘,就叫做数据的换入换出
小结
阻塞状态和挂起状态的区别:阻塞不一定挂起,挂起一定阻塞
二、linux操作系统
Linux内核源代码
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
这段代码中的各个状态就对应上面的普遍操作系统中的状态:
"R (running)", /* 0 */ ------------------------> 运行状态
"S (sleeping)", /* 1 */ ------------------------> (浅度)睡眠状态
"D (disk sleep)", /* 2 */ ------------------------> 深度睡眠状态(linux操作系统特有的)
"T (stopped)", /* 4 */ ------------------------> 停止/暂停状态
"t (tracing stop)", /* 8 */ ------------------------> 追踪暂停状态
"X (dead)", /* 16 */ ------------------------> 死亡状态
"Z (zombie)", /* 32 */ ------------------------> 僵尸状态
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的
进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
1、运行状态(R)
2、(浅度)睡眠状态(S)
是不是感觉很奇怪,明明程序在运行啊,怎么进程是睡眠状态呢?
那是因为,我们看着程序运行很快,循环不断打印,其实打印的速度,对于cpu运算的速度来说太慢了,因为访问外设速度十分慢,所以我们这个程序从开始加载带内存(也就是./myprocess)到kill -9
杀掉程序,99%的时间都用来访问显示器了。所以,cpu运算完之后,进程的PCB对象都在等待队列中,花费了大量时间来等显示器就绪,这个时候的进程都是阻塞状态的,也就是linux中的S睡眠状态
3、停止状态(T)
kill -19 +pid使进程暂停
那么,有暂停就有继续:
知识点1
linux进程中,STAT下面的进程状态如果后面有+,那么该进程就是前台进程,不能够执行命令行指令,可以被ctrl+c结束掉;
但是如何进程状态后面没有+,那么该进程就是后台进程,能够执行命令行指令不能被ctrl+c结束掉,只能kill +9+pid杀掉
4、深度睡眠状态(D)
我们前面使用的printf,scanf等都算是浅度睡眠,所以S也可以叫做浅度睡眠状态。而浅度睡眠状态可以被ctrl+c直接终止的。而深度睡眠状态不可以被终止,操作系统无法杀死,只能断电或者进程自己醒来来解决
举例:
假如进程A有着几万条非常重要的代码数据,要存放到磁盘中。磁盘就开始存储代码数据,而进程A就在内存中什么都不用做了。如果这个时候进程过多了,内存不够了,甚至是将许多进程进行挂起状态内存还是不够,操作系统就会出来杀掉一些不干活的进程,因为再不清理出来一些内存,操作系统就要崩了。然而,内存还是不够,那么操作系统就会继续杀掉一些进程,要么一直杀进程直到操作系统稳定下了,要么就杀完所有的进程,要是杀掉所有进程都不行,那么操作系统只能崩了,等自动恢复。当操作系统把进程A杀掉之后,磁盘存储A中的代码数据也失败了,这个时候磁盘找不到A,因为A被杀掉了,所以磁盘也不会保留A中的代码数据,直接丢弃掉。这个时候,用户需要进程A中的重要数据,但是找不到了,通过调查之后就找到操作系统、进程A和磁盘。磁盘先站出来说:这不关我的事,我本来就有可能成功有可能失败,当我失败的时候去找进程发现他不见了,和我没有关系;这时候进程也出来说话了:这和我也没有关系,我是被操作系统杀掉了,怎么能回应你磁盘呢?操作系统发现他们两个把矛头指向了自己,就气愤的说:我有我的职责,内存不够了,我必须杀掉进程防止内存不够,这就是我的任务,并不是只针对你这一个进程,所有进程在我眼里都是一样的。用户一听,觉得这三个没什么问题,于是就对操作系统说:下次对于这样的情况,不要杀掉指定的进程,让该程序进入深度睡眠状态,操作系统、进程和磁盘都表示没问题。这个时候,这个深度睡眠状态的进程就处于所谓的深度睡眠状态,不可被操作系统杀死,只能断电处理或者等进程自己醒过来
知识点2
深度睡眠状态(D)只有在高IO的状态下才能看见,到了这个程度再严重一些的话操作系统就会出一些问题
dd命令:将临时文件数据拿到磁盘做IO
linux操作系统不会把挂起状态暴露出来,我们看不到进程是不是处于挂起状态,linux系统认为不需要用户知道进程是否被挂起
三、两种特殊进程
僵尸进程(Z)和孤儿进程
1、僵尸进程
1、僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
2、僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
注意:僵尸进程已经是一个死了的进程,我们ctrl+c/d 或者kill -9都不能杀死;一个进程变成僵尸进程只是该进程对应的数据和代码被释放了,而进程的PCB/task_struct对象还存在,如果父进程/操作系统从进程PCB对象中拿到了退出信息,那么就释放掉进程PCB对象,如果不拿退出信息,该进程就一直处于僵尸状态
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 int main()
5 {
6 pid_t id = fork();
7 if(id == 0)
8 {
9 printf("子进程pid:%d ppid:%d\n", getpid(),getppid());
10 sleep(5);
11 exit(1);
12 }
13 else if(id > 0)
14 {
15 while(1)
16 {
17 printf("父进程pid:%d ppid:%d\n", getpid(),getppid());
18 sleep(1);
19 }
20 }
21 return 0;
22 }
可以看到父子进程前面都是s+状态,当子进程休眠5秒之后,调用exit()函数,子进程直接退出了,父进程还在while循环,此时父进程不会拿到子进程退出信息,所以子进程状态就是Z+
僵尸进程的危害
1、进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?
是的!
2、维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?
是的!
3、那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?
是的!
因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
4、会引发内存泄漏?
是的!
如何避免我们后面讲
2、孤儿进程
前面我们知道了子进程先退出,父进程后退出叫做僵尸进程(Z),那如果父进程先退出,子进程后退出呢?
我们来看看样例:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 int main()
5 {
6 pid_t id = fork();
7 if(id == 0)
8 {
9 while(1)
10 {
11 printf("子进程pid:%d ppid:%d\n", getpid(),getppid());
12 sleep(5);
13 }
14 }
15 else if(id > 0)
16 {
17 while(1)
18 {
19 printf("父进程pid:%d ppid:%d\n", getpid(),getppid());
20 sleep(1);
21 }
22 }
23 return 0;
24 }
在上面代码跑的过程中,我们kill -9杀掉父进程
来看看结果:
我们看到,父进程杀死之后,子进程状态由S+变成了S,也就是由前台程序,变成了后台程序。
并且子进程的PPID变成了1,这个1对应的就是操作系统
,也就是父进程被杀掉,子进程被操作系统领养,操作系统成为了该子进程的父进程(冯诺依曼体系中,进程被操作系统管理)。如果操作系统不领养该子进程,该进程就变成了僵尸进程,万一一个父进程下面有好多子进程,那么就可能引发一系列问题,比如说内存泄漏
那么,我们杀掉父进程,父进程不会变成僵尸进程吗?
答案是:不会的,因为父进程也有他的父进程,而父进程的父进程就是bash,bash对于普通父进程而言会对子进程进行及时回收,更加负责任
四、进程优先级
基本概念
1、cpu资源分配的先后顺序,就是指进程的优先权(priority)。
2、优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
3、还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
1、什么叫做优先级?
进程优先级就是我们的进程都能够被cpu执行,只不过是哪一个进程先执行,哪一个进程后执行
所以,权限是进程能不能做的问题;而优先级是先做还是后做的问题。二者有区别
2、为什么会存在优先级?
这是因为我们所使用的硬件等资源是有限的,比如:电脑只有1个或者几个cpu(1核/多核)。这个时候多个进程都想要cpu进行调度,那么必定会出现资源的争夺。所以,优先级就决定了进程的先后执行顺序,合理分配(利用有限)资源
3、linux中优先级特点
linux中,我们的ps axj
指令结果中的PRI下面的数字就是我们的进程优先级,这个数字和我们学生的成绩排名一样,数字越低优先级越高。
PRI计算公式:
PRI=基础优先级值(80)+nice值(nice值就是我们用来修改优先级的)
nice值的修改区间为[-20,19],所以PRI(优先级)的范围为[60,99]
linux中查看nice的时候,ni就是nice
样例1
1、先运行程序
2、通过ps -l PID
来看看默认PRI值
3、进行修改操作
总过程:进入top -> 输入“r” ->输入修改进程的PID -> 输入nice值
第一步:sudo top(要提权执行)
第二步:输入“r”,然后输入要修改PRI的进程的PID,然后回车
第三步:输入修改的nice(也就是ni)值,区间在[-20,19],回车
最后看结果:
可以看到进程26775的PRRI减少了10
样例2
可以看到,如果我们输入的niec值过大或者过小,进程的PRI只会在[60,99]之间
这是因为,如果我们可以随意改变进程PRI的值,那么就会出现进程一直被优先执行,这对其他进程很不友好,打破了进程调度的平衡而且某一些恶性进程如果优先级过高,在我们电脑直接执行的时候会产生严重的影响
五、进程的其他概念
1、 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
2、独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
3、并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
4、并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发(这里的一段时间内采用:时间片轮转的方式)
六、进程切换
我们的电脑上面只有一个cpu,一次只能执行一个进程。但是我们可以同时将多个进程进行执行,也就是我们可以在vs上面跑多个程序。一个cpu居然在某一段时间内,调度多个进程,这是为什么呢?
其实这就是进程切换的效果。我们到目前为止的认识都是,cpu将一个进程被调度完之后再调度另一个进程,其实不是这样的,而是每一个进程在cpu中都只有固定的执行时间,时间到了cpu就调度另外一个进程,直到cpu调度完全部的进程。这个过程就叫做进程切换
进程切换的深入理解:
linux的cpu(我的是1核的)只能执行一个进程,但是在一段时间内我们可以运行多个进程,这是因为cpu速度十分快,cpu的速度是以纳秒为单位了,我们人的反应是跟不上了的。所以我们人看到的一瞬间其实就是cpu的一个时间段内,将执行的多个进程按照一定的周期运行,如果正在被调度的基础没有被执行完,那么操作系统会将该进程执行到哪里来了,有什么临时变量都保存起来(也就是保存寄存器内部该进程的数据),直接调度下一个进程,如果下一个进程也没有被执行完,操作系统也会将该进程在cpu内部的寄存器数据保存起来。等到再次访问这些被保存数据的进程时,就将这些保存的数据都放到寄存器上面,继续执行上次进程执行到的地方
而操作系统将cpu中寄存器内部的数据进行保存与再次执行进程将之前拿到进行保护的数据加载到寄存器内部,开始继续上次的执行。这个过程就叫做进程的上下文保护
进程的上下文保护:
我们cpu内部有一套寄存器,这些寄存器是被所有进程共享的。cpu调度PCB对象,就会拿到PCB对象对应的数据和代码。当进程被调度时,会产生很多临时变量,这些临时变量就存放在寄存器中。所以进程的切换就是把进程的上下文保护与进程的上下文恢复,进程停止运行,寄存器内部的临时变量等数据就被保存,进程重新被cpu调度时,将临时变量等数据加载到寄存器中。
所以进程上下文就是一个进程完成它的时间片后在cpu中所保存的临时数据