冯诺依曼体系结构:
目前我们使用的计算机,包括笔记本,台式电脑,或者后端服务器,都是一堆硬件的集合,他们不是无序的组合在一起,首先它们要有协同能力,要求它们有协同能力,这就意味着它们之间一定要组织好,构成一个系统,他们才能对外输出,提供计算服务.
当代计算机都是由冯诺依曼体系结构构成计算的基本单元,冯诺依曼的体系结构非常简单,包括输入设备,输出设备,存储器,中央处理器.
输入设备将数据输入到存储器,中央处理器从存储器中读取数据进行计算,将计算结果输送到输出设备.
这套架构是冯诺依曼提出的一种硬件架构,也就是说,计算机如果想要对外提供计算服务,那么它的硬件必须遵循上述结构.
存储器.他指的就是内存.其他的像硬盘,u盘这样的存储设备,我们把它叫做外存,也就是外部存储,二者在体系结构上差别特别大,硬盘或者u盘归类到上述的结构中,其实是输入输出设备.
输入输出设备.我们知道计算机只能处理二进制数据,并且是以电子信号控制的,如果想让计算机处理我们的数据,首先需要将我们的数据处理成二进制数据,这就势必意味着这些工作需要有特定的设备来处理,让这些设备去访问计算机上,例如硬盘上的二进制数据.就像我们平常输入的字符串,本质上也是01编码,我们需要输入设备将输入的字符串转换为二进制数据,计算机处理完成后,将这些数据送给输出设备,我们也希望输出设备输出的是字符串而不是一串01数字.
所以输入输出设备通常是我们人与计算机,或者计算机与计算机之间进行数据传输的一些外部设备,常见的输入设备包括鼠标,键盘, 摄像头,话筒,磁盘,网卡. 输出设备包括显示器,播放器,磁盘,网卡. 以上的这些被叫做外设,有些是纯粹的输入设备或者输出设备,有的既是输出设备,又是输出设备.
最后是中央处理器,包括运算器和控制器.
对数据的运算主要体现在运算器上,我们日常进行的计算任务总结出来其实就两类,广义上的算术运算以及逻辑运算,算术运算实际上就是加减乘除,逻辑运算包括事件之间的与运算,或运算等,最终得到一个逻辑结果,真或者假.
另一个问题是,什么时候我们将数据从输入设备调取到寄存器,什么时候将计算结果输出到输出设备,这就需要有人来控制有人来协调,这就是控制器的作用,对我们的计算硬件流程进行一定的控制.运算器和控制器构成了中央处理器.
冯诺依曼体系结构中的五大单元都是独立的个体.每个单元之间都可以独立的操作而不需要依赖其他的单元,例如存储器可以独立的读取或者写入数据而不需要其他单元的干预. 但是,我们要的是他们之间能够相互协作,要相互协作,就一定要交换信息,所以各个硬件的单元需要用"线"连接起来,我们把这种线叫做总线,总线分为两类,包括系统总线和IO总线.内存与处理器连接的线叫做系统总线,内存与输入输出设备连接的线叫做IO总线.
存储与效率:
CPU,磁盘,内存本身有着数据的存储能力,CPU内部的存储器叫做寄存器,它的存储效率非常高,内存也还可以.但是像磁盘以及外设的存储效率就比较低下,用一张存储金字塔来表示它们的存储效率:
其他的像光盘或者磁带也是存储介质,但是它的存储效率很低,造价也很低,优点就是容量大.CPU内部的寄存器容量低,但是造价很高,它们在组成结构上不同,寄存器为了实现与CPU的快速通信以及数据读写,其内部的电子元件通常由晶体管或者电容器组成,而外村则是有一些磁性材料或者光学材料组成.
一般距离CPU越远的存储设备,容量越来越大,访问速度越来越低.CPU寄存器的存储效率是纳秒级别,内存是微秒级别,外部设备是毫秒级别, 所以说存储是分级的.那有人说,为什么我不直接把存储器去掉,而是直接让输入输出设备直接与CPU交互?从上面给出的存储效率可以知道,二者之间的存储代差太大了,具体的说,输入输出设备从外界收到一条数据并传给CPU需要1秒,CPU处理这条数据只需要0.00001秒.CPU处理完成1秒之后才能处理下一条输入的数据,所以浪费掉了太多的CPU资源.为了解决上述问题就有了中间的过渡设备,存储器,它访问速度适中,容量适中.这样就可以适配CPU与外设的速度差,缓解了上述问题.
其次是,存储器不会仅仅只加载CPU马上就要处理的数据,而是会预读取外设的输入或者输出数据,形象点说,当CPU读取需要的数据的时候,存储器不会从外设中现找,而是提前已经把数据加载进来,让他们排好队.这也就是为什么我们平常说的,要加载一个程序,就需要先将程序加载到内存中,当存储区从外设读取数据的时候,CPU可能在执行其他任务,当存储器加载完成后,CPU也刚好开始处理你的程序,这就协调了读写效率与处理效率的代差问题.
由于五大元件都是独立的单元,就像上面的例子,存储器从外设预读取文件,CPU处理数据完成后从存储器再读取数据,从哪里读取,什么时候读取,读取多少数据等等,这些工作统一由操作系统安排组织. 内存实际上就是一个硬件级别的缓存,它处于一个核心的地位.
如果说我单纯的追求效率而不考虑成本,那你可以把存储器都更换为寄存器,但是一台计算机的价格也会非常高,普通的人承担不起单价,不利于计算机的普及与推广,所以正是冯诺依曼体系,就好比内存,保证计算机的效率,同时也让计算机成本不算太高,这才让计算机得以普及和推广,所以说,冯诺依曼体系结构是让计算机技术成熟落地的关键技术之一.
虽然输入输出设备没办法直接与中央处理器进行数据交互,但是存在着控制信息的交互.
硬件说完我们说软件:
OS:
计算机种的硬件就是各自具有独立功能的单元,就像键盘,它的功能是接收外部的输入并且将输入转换为二进制写入存储设备,但是它的这种能力什么时候用?怎么用?
就像古代战争,皇帝手下有擅长外交的,有擅长军事的,还有擅长贸易的,当有外敌发动战争的时候,派谁去?什么时候去? 还需要其他人做什么?皇帝的任务就是去控制去驱使这些人,对应到我们的冯诺依曼体系结构中,也需要有人统筹组织这些硬件,担任这个皇帝角色的,就叫做操作系统.
操作系统就是一款进行管理硬件的的软件,他同时也需要对软件进行管理.那么就引出来三个问题,为什么要进行管理?操作系统进行么的管理是什?怎么管理?
为什么管理?
操作系统其实有着两套的存在意义.首先,计算机的层状结构,最底层是由冯诺依曼体系结构组织起来的硬件结构,所有的硬件能够被操作系统对应起来就需要有对应的驱动程序
驱动程序,也称驱动(英文称作driver),是一种计算机程序,用于控制计算机硬件设备的操作。它是操作系统和硬件设备之间的接口,使得操作系统可以与硬件设备进行交互和通信。驱动程序通常由硬件设备的生产厂商编写,它们提供了对硬件设备的控制和管理功能,以便操作系统可以正确地识别和使用设备。例如,如果您想使用打印机,您需要安装打印机驱动程序,这样操作系统才能正确识别打印机并与其进行通信。驱动程序通常包括一些特定的指令和命令,用于控制硬件设备的操作并将设备的状态反馈给操作系统。它们可以控制硬件设备的输入和输出操作,以及管理设备的资源和内存使用。
所以这里,操作系统帮助用户管理好底层硬件资源.如果软硬件资源被管理的不好,A程序修改了B程序的数据.B程序删除了A程序的部分代码,所以操作系统通过管理好硬件资源(手段),为用户提供了一个良好(稳定,高效,安全)的运行环境(目的).
其次,一些软件都是通过操作系统访问的硬件资源,也就是说,任何软件想要访问硬件资源都绕不开操作系统,为什么不让软件直接访问硬件呢?因为群众里面有坏人,如果任何一个未知程序都可以访问硬件,就像一个恶意程序,一直占用CPU资源,可以随便删除你磁盘上的信息,这就出现了问题.
既要保证一些软件的正常运行,又要保证自身安全,所以,操作系统同样将自己的代码进行了封装,只为软件提供一些函数接口,被叫做系统调用接口.所有访问操作系的行为,都只能通过系统调用访问.
有些人觉得直接使用系统调用不方便,对各种接口进行了上层封装,形成了各种各样的语言库文件,就有了各种各样的高级语言,语言都在变,但是底层始终不变.基于系统调用的的开发,我们叫做系统开发.
什么是管理以及怎么管理?
我们用一个具体的例子来解释.我们都做过学生,学校中的管理能够很好的解释我们是怎么被管理的.我们把模型简化一下,校长,辅导员,学生.校长就是一个典型的管理者,但是我们作为被管理者,校长很少直接面向我们进行管理,但是我们被管理的很好,并且一切工作也都有序,这就说明,管理者与被管理者之间不需要面对面才能实现管理.
那么面都不见,怎么把我管理好的?其实作为一个学生的管理者,我只需要知道这个学生修了那些门课,需要考多少门课,成绩是多少,有没违反校规,个人基本信息,拿到这些被管理者的状态信息就可以实现某种意义上的管理.再者说了,见面也不就是为了了解这个学生的状态信息吗? 只要得到管理信息,就可以在未来进行管理决策.所以说,管理的本质就是对数据的管理达到对人的管理.
管理者与被管理者面都不见,我怎么拿到你的信息的呢?事实是,我们在学校管理者很容易拿到我们的数据,但是不一定是管理者亲自去拿,我们可以通过其他途径来获取,就像我们的成绩直接被老师录入系统,一些状态数据,像心理状况,参加活动的活跃度等都被可以被辅导员很容易的拿到.
总体概括我们生活中的行为,无非就分为两类,一类是决策,一类是执行,就像你每天下课以后都在想,午饭吃啥?吃烤肉吧.这个行为就叫做决策,而去吃的行为就是执行这个决策.我们有很直观的感觉,知道一个管理者的任务就是做决策,所以从这种意义上说,辅导员实际上不是一个管理者,而是一个执行者.现实生活中的执行与决策在一些行为上可以说粘滞在一起.就像上面,你既是决策者,又是执行者,而在管理上,决策与执行实际上是分开的.
再回来这个问题,管理者如何拿到我们的数据?答案就是通过执行者.说到这里,我们把上面的一些概念跟操作系统对应起来,校长就是操作系统,辅导员就是驱动程序,学生就是被管理的软硬件资源.
操作系统可以不直接交互硬件,而是通过驱动程序获取硬件的状态数据来进行决策.
接着再说回学校的例子,学校的被管理者实际上是很多的,我们把他们的状态数据都获取并且放在管理者们面前,这个数据量是特别巨大的, 比方说我们需要从学校中选出身高最高的五个人参加篮球比赛,校长需要对这些个数据人为的进行遍历,校长给出一份excel信息模板,要统计的信息包括姓名,班级,身高,体重,学院,电话等信息.把这些被管理者描述起来.
校长为了以后管理方便,在计算机中用C语言定义了一个名为student的struct结构体,每一个结构体变量都代表一个学生:
这个过程其实就是描述的过程,或者说建模的过程.接下来我把每个学生都用该结构体定义出来,并且初始化它们的信息,更重要的是我还会通过next指针将学生之间连接起来,就得到了一个管理信息的链式结构,所以从此以后,我这个校长以后只需要把这个链表结构管理好就可以了.校长不仅写了链表增删查改的一些方法,还有一些诸如排序等的一些方法,此时的校长就成功的将学生的管理工作变成了对链表的增删查改.
所以在操作系统中,所有的被管理者包括硬件和软件有着它们共同的属性,我们把硬件通过描述它们的属性来构建出来它们的对象,然后再用数据结构组织起来,操作系统中的管理工作都变成了先描述再组织.
再说回来,当你作为一个校长将所有的学生描述起来以后,你发现你管理的一名学生十门课挂了九门,你觉得要开除这个学生,你把辅导员叫过来,告诉辅导员你要开除他,并且在链表中删除了这个学生的信息,对应到操作系统中,当你要求对某个硬件做什么的时候,你需要把指令下发给驱动程序,驱动程序给你反馈以后,你再对结构体进行相关的修改.
这就是操作系统如何对软硬件进行管理,先描述,再组织.
进程:
概念:
先浅略的介绍.
定义:已经加载到内存中的程序叫做进程:
在liunx中的进程,通过 ps ajx指令查看:
我们自己写一个简单的代码让他运行,看他是否在进程列表中 这里代码就是一个死循环 while(1)循环打印内容,用 ps ajx | head -1 ; ps ajx | grep process;查看进程列表:
现代的操作系统是一个多道操作系统,也就是可以同时运行多个程序,加载到内存中的程序以及CPU正在运行的程序不止一个,多个程序意味这每个程序有着自己的状态,有的正在运行,有的运行完成正在退出,有的等待执行,对于不同状态的程序做出不同的处理,这个任务就需要操作系统来完成.
操作系统要管理进程,那么如何管理?这个问题上面已经说过了,先描述,再组织.
PCB:
任何一个进程加载到内存,形成真正的进程,首要的工作就是将这个进程描述起来,也就是创建一个结构体对象,这个结构体被叫做PCB(process ctrl block),进程控制块.Linux操作系统下的PCB是: task_struct.
操作系统被称为计算机学科中的哲学,所以很多概念遵循的逻辑都是直接从现实世界中抽象出来的,PCB也是一样,它是计算机识别一个具体事物而抽象出来的属性集合,就像我们在生活中认识一个事物也是通过描述它的属性,自己思考一下,你是怎么区别猴子和人的,怎么区分两个不同的人.当你对一个事物认识不清楚的时候,其实也就意味这你对他属性提取的不够多.
那么计算机描述一个对象的方式也是通过属性集合,C语言中叫做结构体,C++中叫做对象,现代语言都被叫做面向对象的语言.通过new出一个对象,然后可以将该对象插入到一个数据结构中去,然后就各种操作,它就好像是把现实中的事物抽象到计算机中,然后通过加入影响因子不断地对现实世界进行推演,就像机器学习.
再回来说PCB,PCB描述一个进程,其中的进程编号唯一的标识一个进程,还有其他的成员来描述进程状态,优先级.
当我们把进程加载到内存中的时候,我们需要先创建并初始化该进程的PCB,然后将该进程对应的代码数据加载到内存中.也就是说,操作系统的PCB队列中有该进程的存在,并且该进程的代码和数据也都被加载到内存中,这个时候才能说该进程被创建.
到这里,我们自己给出liunx中进程的概念:
操作系统对进程的管理实际上就是对PCB结构体序列进行管理.
当一个进程需要被运行时,操作系统需要通过PCB,找到该进程的代码和数据所在的地址,所以,PCB中还需要有一个指向对应程序代码和数据的指针,找到后,加载带CPU开始执行,这就完成了对一个进程的管理.
对于多个进程,将描述它们的结构体进行连接,形成单链表,就可以完成对多进程的管理,也就是说,对进程的管理变成了对单链表的增删查改,对进程的管理变成了对PCB的管理.我们平常说的优先级队列,运行队列,等待队列等等,说的是PCB结构体在排队,而不是进程对应的程序和数据在排队.
以上的进程管理是所有的操作系统都要遵循的一套方法论,具体到特定的操作系统,实现的细节上可能有略微的差异.
Liunx中的进程组织:
就像前文提到的,Liunx中的PCB就是task_struct,他是PCB的一种
tack_struct结构非常大,下面是他结构体的成员变量:
其中的标识符,就相当于我们在学校的学号,来唯一的表示一个进程.
状态代表着当前的程序是正在运行,还是运行完毕,以及其他的一些状态.
多个进程同时运行意味这它们肯定要抢占CPU资源,谁先运行谁后运行就有了优先级的概念.
在程序运行时,当运行到某一行代码,而这个时候程序的时间片用完了,那么程序就会停止,当下一次运行的时候会从上次停止的代码出继续运行,而上次运行在哪里停止的,就需要我们记录 ,这个任务就由CPU内部的一个寄存器来完成,该寄存器被叫做程序寄存器.
操作系统要公平的调度每个进程,这就意味着,每个进程在CPU上运行的时间都是均衡的,记账信息就用来衡量调度器的调度是否公平
如果你想让进程等待运行,你就让把进程PCB连接到等待队列中,如果你想让进程运行,你就把进程PCB连接到运行队列中,所以你对进程的管理取决于你将PCB链入到哪个数据结构中,每个结构对应着相应的算法,算法背后是具体的应用场景.
这里要说的是,在学习编程语言的时候,我们学的很单纯,链表就是链表,二叉树就是二叉树,而我们在对进程做管理的时候,不仅仅将PCB链入一个单一的结构中,一个节点,他可能既是某个链表的节点,有可能是某个树或者队列的节点.上面的做法并不难实现,只需要在PCB结构体中多加几个成员,就像链表中的next指针,或者树结构中的left和right指针.所以Liunx中的数据结构关系非常的错综复杂.
以上的这些都是Liunx中PCB的一些局部重要属性,除了这些还有很多,关于信号的,关于网络的,关于文件的,这些内容在后面会慢慢介绍,在这里我们有一个大概的了解.
查看进程:
前面的内容介绍只是希望大家能对进程有一个宏观的了解,之后的内容就是对进程的属性慢慢介绍.
ps ajx 指令 :
先来查看进程的属性,查看进程属性有很多方法,像上面的ps指令:
在同一台机器上运行同一个程序,左窗口ps指令查看当前运行的进程:
上面的PID就是我们说的标识符属性,它用来唯一的标识一个进程,虽然运行的是一个程序,但是操作系统为了管理进程创建了两个PCB结构体,也就是创建了两个进程.
操作系统目录查看进程:
在Liunx中存在一个目录用来记录当前系统中正在运行的进程,家目录下的proc目录:
它是Liunx中比较奇怪也比较重要的一个目录,电脑关机以后,其中的内容会全部消失,开机的时候会自动创建.它是Liunx通过文件系统将我们的进程可视化的一个目录,上面的内容都是内存级别的.
上面的每一个以数字命名的目录都代表一个进程,其他的是普通文件,数字代表的其实就是进程的PID.
当你创建一个进程的时候,该目录下也会自动刷新你创建进程的PID,cd到该进程对应的目录,其中的文件都是PCB对应的一些属性,当进程停止的时候,相应的目录也会被删除.
这里还要说的说的一个属性:CWD ,在相应的进程目录下就可以找到:
全拼是current work direct,当前工作空间.像我们在C中用fread()或者fwrite()进行文件操作,新生成的文件会默认存放在当前工作空间中,这里说所的工作空间,指的是一个进程开始运行时所在的文件目录,也就是该进程运行的当前目录,cwd属性就负责将这个目录记录下来,之后的一些工作都默认会在该目录下进行.
通过系统调用查看进程标示符:
开始之前我们先说一下我们经常用的指令:
ps ajx | head -1; ps ajx | grep ...,第一个分号之前表示只显示进程表中的第一行,分号表示一条指令的结束,grep后的...表示你要查看的进程名,分号后的指令就是显示进程表中你要查找的进程.
kill -9 ...,杀掉某个进程.
ps指令其实是在遍历单链表,从单链表中获取每个进程的属性信息,我们还可以从进程内部通过系统调用getpid()来获取我的PID.因为PCB是操作系统内部的数据类型,所有的用户想要获取操作系统内部的数据只能通过系统调用.
需要包含<sys/types.h>头文件,不需要传递参数.谁调用这个函数,该函数就会返回对应程序的PID. 返回值是一个无符号整型.
写程序测试一下:
ps查看进程并且让程序运行:
一个进程是被其他的进程创建的,A进程创建了B进程,A进程就被叫做B进程的父进程,PPID用来标识某个进程的父进程.
同样可以通过getppid()来获取父进程的id.
这里查看一下父进程,看是谁调用的我们的程序:
这里的bash是我们的终端窗口,也就是说,我们键入的指令都是通过终端来创建我们的进程.换句话说,如果我们是远程机,每次我们登录Xshell时,系统都会自动为我们创建一个bash进程,也就是命令行解释的进程,系统会帮我们打印出命令行终端,我们所有通过bash创建的进程都是它的子进程.
通过系统调用创建进程:
又一个系统调用接口fork();
我们先简单的看一下现象,把fork()注释掉,程序正常打印:
当我们执行fork()函数后,fork()函数之后的内容被执行了两次:
当我们调用了fork(),fork()执行之前只有一个执行流,fork()之后就有两个执行流,man手册中有说明,fork()函数的返回值是pid_t类型,他会以当前进程的代码为模板创建一个子进程,如果成功,他会返回给父进程子进程的PID,给子进程返回0.如果失败,返回-1,没有子进程创建.
下面慢慢说为什么,还是先给出示例代码:
按照man手册的内容,fork()后的代码执行两次,那么if else中的两个代码块都会被执行,并且子进程与父进程的id也会不同:
运行结果也确实是这样.id==0和id>0同时成立了,并且正常逻辑是只有执行完一个循环才会执行下一个循环,但是这里却是两个循环同时执行.但是从这里看出来父子关系确实成立.
还记得我们说的,由于当前程序是在bash中运行的,这里父进程的PPID肯定是bash的PID.上面的程序大致执行过程是:
我们的程序process首先由bash像创建普通进程一样被创建出来,bash就给process进程分配一个PID叫做3750,后来程序执行到fork之后,就由fork将后面的代码一分为二,变成了两个执行分支,一个是父进程,一个是子进程.所以目前我们创建程序有两种方式,一是./process,普通的执行程序,系统会在指令层面为我们创建进程,二是在代码层面,通过fork()自己创建进程.
这里有几个问题.
1.为什么fork()要给子进程返回0,给父进程返回子进程的id?
返回不同的返回值是为了区别不同的执行流,执行不同的代码块.一般而言,fork()之后的代码父子共享.只不过我们需要通过一定程度的区分来识别父子进程.
一个父进程可能不止有一个子进程,而子进程只有一个父进程,所以将来父进程可能要对子进程做控制,它一定要从若干个子进程中寻找自己要控制的对象,如何寻找就是通过子进程的PID,所以父进程一定要知道子进程的PID,用来标识子进程的唯一性,而子进程要寻找父进程只需要通过getppid()来标识自己的父进程.
2.一个函数是怎么返回两次的? fork()函数在干什么?
fork()的功能就是创建子进程,那么在fork()函数执行return之前,它的核心工作已经完成了,内存中一定创建好了子进程的PCB,有了PCB,操作系统就可以直接修改子进程的属性.前面说过,一个进程等于PCB+对应的代码和数据,操作系统只是单纯的为子进程创建了一个结构体,肯定不会自动为该进程编写对应的代码块,所以,子进程与父进程只能共享代码.
也就是说,fork()函数,给子进程分配了PID,初始化了PPID,并且填充了一些其他的内容,同时前面说的PCB中的代码指针与父进程指向的是同一块代码,fork()函数创建完PCB,将该PCB连接到cpu执行队列,cpu根据父进程中PCB的代码指针找到相应代码执行,并且以同样的方式找到子进程的代码进行执行,也就是说,一份代码被执行了两次,所以return了两次.
说到这我们就可以知道,fork()创建子进程的PCB,初始化了子进程,并且实现二者代码的共享.
3.一个变量为什么有不同的内容,怎么理解?
在数据拷贝的时候,将变量A中存放的内容逐个字节的拷贝给B,我们把它叫做浅拷贝,但是如果A中存放的是一个地址,真正的内容是A指向的地址,这个时候再逐个字节的拷贝就会出现问题,另外再给B开辟一块空间,并且将A指向的内容拷贝到B空间中,我们叫做深拷贝,但是大多数时候,我们拷贝数据只是为了读取一下,并不修改其中的内容,所以Liunx中如果出现拷贝的情况,先会进行浅拷贝,如果B需要修改拷贝的内容,这个时候才为B新开辟一块空间,用来存放修改后的内容,这种操作我门把它叫做写时拷贝.
再回来说进程,子进程不仅共享父进程的代码,同时共享父进程的数据,如果子进程只是简单的读取父进程的数据,那么子进程访问的就是父进程的数据,而如果要对数据进行修改,则另外开辟一块空间,这也就是为什么,子进程中的id既大于0,又等于0,因为id对于两个进程来说代表是着不同的地址空间.
调度器:
上面的子进程被创建后,两个进程的运行顺序,我们作为用户是干预不了的,它的运行顺序交给调度器来完成. 这里又有了一个新的概念---调度器.
在Liunx中,当进程被创建好后,如果要运行,就将他链入到运行队列中,那么多进程,我挑选哪一个来加载到cpu中运行呢?这里,挑选一个进程放到cpu中去运行的工作是由调度器来做的.所谓的调度器,它的本质就是对于特定的数据结构进行查找的一个相关算法. 一个进程只能在cpu中运行,而电脑cpu只有一个,但是进程却成千上百个,所以进程只要在操作系统中存在,那么进程之间的关系就是竞争关系,一般的调度器原则都要求平均或者公平,也就是要求每个进程消耗的时间片相对平衡,他要保证不能有进程一直执行,而有的进程没被执行过.
下面还会有调度器算法的详细说明.
进程状态:
操作系统学科中将进程状态概括为三种,运行,阻塞,挂起.理论落实到实践还稍有区别,具体细节依不同的操作系统而各有差异,但都遵循上述的原则.就像PCB与task_struct,一个抽象,一个具体.
运行态:
对于操作系统来讲,任何一个程序想要运行都要持有cpu资源,当运行多个进程时,就是这种状态:
上面这张图,操作系统将需要运行的进程PCB组织起来,交给调度器运行,我们把这个调度器维护的队列叫做运行队列.这个运行队列同样是个结构体,其中就包含上面链表的首尾指针,当然还有其他诸多属性:
在Liunx中,凡是处于上述队列的进程我们把它叫做R状态,也就是运行态.不是我们想象的,只有正在cpu中执行的进程才是运行态.处在运行队列的含义就是告诉系统,我已经准备好了,我可以随时被调度.并且进程也不是在cpu中彻底执行完毕,执行时间取决于时间片.时间片同样是PCB中的一个成员变量.一个进程占用cpu的时间是被分配好的,也就是说,在一个时间段内,所有的程序都会被执行,这种情况我们把它叫做并发执行.
所以程序运行中,肯定会存在大量的将程序放入到cpu或者将程序从cpu中调下来的动作,我们把上述动作叫做进程切换.
阻塞态:
操作系统不仅对进程管理,而且对硬件管理,并且是以同样的方式进行管理,每个硬件对应一个相关属性的结构体,对硬件的管理从而转变为对结构体,也就是数据的管理.
当一个进程需要从外设读取数据,比如程序执行到scanf(),此时我还没有键入数据,程序肯定不能被放入到运行队列,缺少必要的数据,这个时候,我们只要将该进程链入到键盘所对应的队列中,等待读取数据:
需要访问外设的进程肯定有很多,肯定也要像访问cpu资源一样排队,也就是被组织成一个队列,waitqueue.这种等待特定设备的进程,我们称它正处于阻塞状态.我们的每个设备都有相应的等待队列,如果此时的设备是不可读的,那么我们就把进程相应的PCB链入到等待队列.
挂起状态:
阻塞队列有n多个,当一个进程处于阻塞状态的时候,说明此时它的PCB和相应的代码都被加载到内存中,PCB和代码同时在排队,现实生活中,会有内存严重不足的情况,这个时候,为了节省空间,并且保证运行正常的情况下,我们会将处理阻塞状态的进程的代码,从内存转移到硬盘中,这个时候,只留下PCB结构体排队,这种处于阻塞状态,并且只有PCB在排队的进程,我们常称该进程被挂起,将代码调出内存的过程我们叫做换出,将代码调取进来的过程我们叫做换入.
liunx中具体的进程状态:
R状态也就是我们说的运行态,liunx中r,s,t状态都可以被观察,下面也会进行演示:
死循环运行的时候,我们查看进程,就能看到进程的相关属性:
这里的状态是sleep状态,也就是阻塞状态,不是running状态,我们让程序只while循环,不打印内容:
这个时候就是运行状态,这是因为第一次的程序需要打印,需要去等待队列等待显示器设备,只有进程在运行队列中,这个时候我们查看进程列表才会观察到R状态,CPU运行很快,相对于进程在运行队列的时间和在等待队列中的等待时间,等待时间是相对较长的,这个时候我们要命中R状态非常困难.
第二个程序没有打印内容,也就是说不需要去等待队列,它要么在CPU中正在被执行,要么在等待队列,所以只会命中出R状态.
S状态还被叫做浅度睡眠,这里的D状态就叫做深度睡眠,这里直接说结论,处于浅度睡眠的进程可以对外部响应做出反应,而处于深度睡眠的进程不对外部做出响应.
关于深度睡眠.举个例子说,有一个进程的任务是向磁盘里写入数据,并且数据较大,2GB,该进程唤醒磁盘并交代任务后磁盘开始写入,这个时候该进程要做的任务就是等待磁盘写入完成,并且接收磁盘的反馈因为他需要向上层反应自己所执行的任务的状态,当他进行等待的时候,此时计算机的负荷又刚好特别大,操作系统此时需要杀掉一些不重要的进程来释放资源,也就是说,像刚才我们举例的进程一样,他什么也不做,那么操作系统就要杀掉类似的进程,假设此时操作系统还没有深度睡眠状态,该进程可以对外部指令进行响应,也就是可以响应kill指令,进程被杀掉,如果此时写入的数据是非常重要的数据,就像2GB的银行转账数据,总值两个亿,该数据丢失,只是为了释放内存资源,显而要避免这种情况发生,所以就有了深度睡眠状态,也就是Disk sleep.当进程处于该状态的时候,他对操作系统的指令不做任何响应,包括kill.
同时,由于像磁盘进行IO读写时速度较慢,当进程将自己设置为disk sleep状态的时候,它会将自己所占用的cpu资源释放给其他程序,这样,操作系统可以继续执行其他进程或任务,从而提高系统的整体性能和响应能力。
用一句话概括,进程在等待向磁盘进行写入时,此时所处的状态叫做深度睡眠状态.
进程的 "T" 状态通常表示进程正在停止或被挂起。它是进程状态的一种,用于表示该进程当前无法执行,并且正在等待某些条件满足或被其他进程重新唤醒。
当一个进程处于 "T" 状态时,它通常是由于接收到一个停止信号(如SIGSTOP、SIGTSTP)或被其他进程发送的挂起命令(如Ctrl+Z)而进入此状态。在这种情况下,进程会被暂停执行,并且不会占用CPU资源。
进程进入 "T" 状态后,它可能会等待某些条件满足,例如等待被其他进程发送的继续命令(如SIGCONT)或等待特定资源的可用性。一旦满足了这些条件,进程就可以从 "T" 状态中恢复,并继续执行。
需要注意的是,进程的 "T" 状态可能只是暂时的,一旦满足了恢复条件,进程就可以继续执行。因此,"T" 状态不同于进程的终止状态,进程可以从 "T" 状态中恢复而不是被终止。
具体的场景就像我们使用的gdb调试工具.
"Z" 状态表示进程处于僵尸状态。僵尸进程是指已经终止执行的子进程,但其父进程尚未对其进行善后处理(释放资源、获取退出状态等),导致僵尸进程在系统进程表中仍然存在。
当一个进程终止时,内核会向其父进程发送一个信号,以通知其子进程已经终止。父进程负责回收子进程的资源,并获取其退出状态。如果父进程没有及时处理子进程的终止消息,子进程就会变成僵尸进程.当一个进程被终止的时候,他不会立马变成终止状态,而是短暂的先变成Z状态,当父进程完成对其资源的回收,该进程才会变成x状态.
僵尸进程在系统进程表中仍然存在,但它们不再执行任何操作,并且不占用任何系统资源(如CPU或内存)。僵尸进程的存在通常是短暂的,因为一旦父进程对其进行善后处理,它们就会被完全清除。
僵尸进程的存在可能是父进程编程错误或意外的结果,因为父进程没有及时回收子进程。这可能是由于父进程忙于其他任务、未正确处理子进程终止的信号,或者父进程意外终止而无法回收子进程.
该进程是僵尸进程,意味这操作系统此时同样对该进程进行维护 如果僵尸进程一直存在,那么她所占用的资源也不会被释放最直接的就是PCB,程序终止而资源不被释放,导致的最大问题就是内存泄漏