目录
[1 冯诺依曼体系结构:](#1 冯诺依曼体系结构:)
[2 操作系统 (Operator System)](#2 操作系统 (Operator System))
[2.1 管理的本质:](#2.1 管理的本质:)
[2.2 管理的方法:](#2.2 管理的方法:)
[2.3 系统调用:](#2.3 系统调用:)
[1 进程概念:](#1 进程概念:)
[2 简单见识进程:](#2 简单见识进程:)
[2.1 进程属性 :](#2.1 进程属性 :)
[3 简单见识系统调用:](#3 简单见识系统调用:)
[4 进程状态:](#4 进程状态:)
[4.1 普遍的操作系统层面:](#4.1 普遍的操作系统层面:)
[4.1.1 运行状态:](#4.1.1 运行状态:)
[4.1.2 阻塞状态:](#4.1.2 阻塞状态:)
[4.1.3 挂起状态:](#4.1.3 挂起状态:)
[4.2 具体的Linux操作系统层面:](#4.2 具体的Linux操作系统层面:)
[4.2.1 进程状态查看:](#4.2.1 进程状态查看:)
[4.2.2 僵尸进程中的僵尸状态:](#4.2.2 僵尸进程中的僵尸状态:)
[5 孤儿进程:](#5 孤儿进程:)
[6 进程优先级:](#6 进程优先级:)
[6.1 优先级的概念:](#6.1 优先级的概念:)
[6.2 优先级存在的原因:](#6.2 优先级存在的原因:)
[6.3 Linux优先级特点:](#6.3 Linux优先级特点:)
[7 进程切换:](#7 进程切换:)
一、计算机体系结构
1 冯诺依曼体系结构:
对于上图,我们首先要知道:
- 存储器是内存,它具有掉电易失的特性;
- 磁盘是外存 (外存指的是除了内存以外的其它具有永久性存储的设备),它属于外设的一种,外设主要包括输入设备(如键盘、话筒等)与输出设备(如显示器、音响等),磁盘既是输入设备又是输出设备 ,以后我们要讲的网卡也是。 同时,外设是相对于内存和CPU所说的;
- 运算器+控制器+其它=CPU(中央处理器);
- CPU相当的快,内存较快,外设较慢;
- CPU 是用来计算 的,内存 是用来临时存储 的,外设 是用来永久存储的;
- CPU其实很笨,它只能被动的接受别人的指令与数据,以达到计算别人数据的目的;
- CPU要执行别人的指令就必须先认识别人的指令。CPU有自己的指令集,它可以让CPU认识别人的指令。所以,我们写完代码并编译形成二进制可执行程序,本质就是让我们的程序变成CPU可以认识的指令。
- CPU的数据是从内存里来(原因在于内存的速度快于外设);
- CPU 在读取与写入的时候,在数据层面 ,只和内存打交道,目的是为了提高整机效率。
- 内存天然没有数据,需要提前从磁盘里拿。
- 开机的过程就相当于把操作系统从磁盘加载到内存。
- 操作系统帮我们实现谁来将数据加载到内存,什么时候加载,加载多少,加载完了不用了怎么办等操作。
- 把数据从外设加载到内存与把数据从内存搬到外设的过程叫做IO的过程
结论:
- 在数据层面上,CPU不和外设直接打交道,只和内存直接打交道;
- 所有的外设,如果有数据需要载入,只能载入到内存中。内存的写出也只能写到外设中。
所以,这就解释了:
- 为什么程序要运行必须加载到内存?
因为CPU要执行我的代码,访问我的数据,只能从内存中读取,而我的程序是放在磁盘上的,所以必须加载到内存中。(体系结构规定)
- 体系结构为什么这么规定?
为了提高整机效率。
2 操作系统 (Operator System)
- 操作系统的定义:操作系统是一个进行软硬件管理 的软件。
- 操作系统管理软硬件的原因:为的是:【对下】通过合理的管理软硬件资源(手段),【对上】为用户提供良好的(稳定的、高效的和安全的)执行环境(目的)。
- 操作系统是如何做好管理的?
2.1 管理的本质:
我们这里以一个学校的场景来解释 。
首先,在学校里,校长作为管理者角色,学生作为被管理者角色。我们学生作为被管理者,在学校里很少见到校长,但是校长却能够管理好我们,这是为什么呢?对于这个问题,这里首先有个结论:管理者不需要和被管理者直接交互,依旧能够把被管理者管理起来 。那他们是如何做到的呢?首先,我们要先知道:管理者是指对重大事宜有决策权利的人。但是决策是要有依据的。那依据是从哪里来的呢?是从被管理者的数据得来的。比如:学校要评选优秀学生,那校长就会依据学生的成绩来决定谁来得这个奖,这个成绩就是学生的数据。所以,这里可以得到如下结论:
- 管理的本质是对数据做管理!
2.2 管理的方法:
我们已经知道了管理的本质是对数据做管理,但是管理者和被管理者是不直接交互的,那管理者如如何拿到被管理者的数据并且一直拿到的呢?这里就需要一个执行者了。比如在学校里的辅导员。
这里,我们就得到了一些结论:
- 校长通过对数据做管理,来进行对被管理者进行管理;
- 数据的采集和决策的执行是由辅导员来做的。
但是,学生是很多的,因此,数据也会很多。这么多数据该如何管理就成了问题。但好在校长之前是一个资深的程序员,他发现他所要管理的学生的信息种类是一样的。 比如,每个学生都有姓名、性别和年龄等。因此,校长就用这些数据定义了一个结构体,然后通过数据结构来管理这些数据。
因此,我们得出了一个结论:
所有管理者在做管理时,都要分两个阶段:先描述 (把被管理对象抽象出来变成一个结构体),再组织(把所有根据结构体定义出来的对象,将它们设计成为特定的数据结构,将对数据的管理转换为对特定数据结构的管理)! 在C++中,先描述的过程就是面向对象的过程(class)。
因此,管理的方法是先描述,再组织。
将这些知识作用于计算机上,操作系统就可以对硬件或软件做管理了。
先描述是语言的话题,再组织是数据结构的话题。
2.3 系统调用:
我们这里再来一个场景:
在一个银行里,会有行长、安保和各种设备等
我们知道,银行行长不仅可以管理桌椅板凳这些设备,还能管理安保、部门经理这些人。同样的, 操作系统作为一款管理软件,它不仅可以管理硬件,同时也可以管理软件。
那么,另外一个问题来了:为什么银行不让我们进入他的仓库,使用他们的电脑呢?而且,我们在办理业务的时候,他们还要在柜台设置一块块厚厚的玻璃,挡住我们,只给我们开一个小口,这是为什么呢?我又不是坏人。
原因是,银行不相信任何人,而且自己非常容易受到伤害,所以,银行要将自己保护起来。但是,你保护自己就保护自己嘛,为什么还要设块玻璃在那里,干脆直接关门得了。可是,银行存在的意义就是为人民服务,为人民提供存取等服务,满足用户需求。所以,银行就面临一个问题:如何做到既保护自己,又对外提供服务?因此,就产生了玻璃窗。
同样地,操作系统也不相信任何人以及程序,但是又必须给上层用户提供各种服务,所以就产生了操作系统接口 ,简称系统调用。由于操作系统是由C语言写的,所以这些接口就是C式的接口,也就是操作系统通过C语言给我们提供的函数调用。这些接口必须由操作系统调用,因为操作系统作为一个管理者,它必须对被管理者的信息了如指掌,对被管理者负责。所以,计算机里的硬件以及各种软件就必须通过系统接口来访问,不能被其他人直接访问。
回到银行,我们在银行办理业务会发现有很多复杂的流程。有一天,一个大妈来到银行说要存钱,但是柜员却告诉她要填表才能存钱,但是大妈不认识字,怎么办呢?于是,就出现了大厅经理,他告诉大妈说:"我帮你弄。",于是,在大厅经理的帮助下,大妈完成了存款。后来,大妈又来到了银行,她经过上次的事后,发现原来那个大厅经理既热心又靠谱,于是后面有什么需求都去找这个大厅经理帮助解决。
同样地,我们使用这些系统调用接口也会很麻烦,要使用它们还要懂操作系统,所以,在系统调用接口之上就产生了一种软件叫做shell,以及各种库(lib)和界面。
至此,我们就构建好了计算机软硬件体系结构。
总结:
计算机管理软硬件
- 描述起来,用struct结构体
- 组织起来,用链表或其他高效的数据结构
系统调用和库函数概念 :
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
下面我们就将讲述进程了,这里先问一下:操作系统是怎么管理进程的呢 ?很简单,先把进程描述起来,再把进程组织起来!
二、进程
1 进程概念:
在书本中,进程常被说成:一个运行起来(加载到内存)的程序,但是,其实这是不太准确的。
首先,我们知道程序的本质就是文件,而文件是放在磁盘上的 。根据冯诺依曼体系结构规定:程序要运行必须加载到内存。
当程序被加载到了内存,CPU是否就可以直接读取这个程序了呢?其实没这么简单。
当我们磁盘里有很多的程序需要被加载到内存,被CPU执行时,操作系统要不要管理这些加载进来的程序呢?答案肯定是要的。
那操作系统是如何管理这些进程的呢?答案是:先描述,再组织!
为了描述进程,就出现了一个PCB(进程控制块 Process Control Block)的概念。
- PCB是一种数据结构,它被用来存储进程信息,可以理解为进程属性的集合。
- Linux操作系统下的PCB是task_struct
进程的属性不存在于我们的可执行程序内,而是等程序被加载到内存之后,操作系统自动为我们加的。
所以,当一个进程被加载到内存时,操作系统会自动为该进程添加PCB(也就是由task_struck定义的结构体变量或对象),它们每一个都指向自己的代码,这样就完成了先描述。
由于每个PCB里都有一个next,操作系统就可以把每个PCB连接起来,然后为其链头命个名(比如process_head)。当CPU需要调度一个进程时,操作系统就遍历所有进程的PCB ,然后找到优先级最高的进程,把它的代码交给CPU,CPU就可以执行该进程的代码了。当一个进程需要退出时,我们只需要看哪个PCB属性中的状态是死亡的,然后遍历一下这个链表,找到那个状态是死亡的结点,然后把它对应的代码和数据释放掉,然后再释放掉相应PCB,这个进程也就被释放掉了。这就是再组织。
因此,我们可以得到:
所谓的对进程管理,变成了对进程对应的PCB进行相关的管理,转化成了对链表的增删查。
struct task_struct(内核结构体)---> 创建 struct task_struct 对象(内核对象)---> 将该结构和我们自己的代码与数据结合起来 ---> 完成先描述,再组织的工作。
结论:进程 = 内核数据结构(task_struct)+ 进程对应的磁盘代码
2 简单见识进程:
我们这里先简单见一见进程,后面再认真讲。
先创建一个源文件:
命令:ps ajx
作用:查看所有进程(ajx可以换成axj)。
这样,我们就可以在运行程序后,通过grep过滤找到进程了。
命令:ps ajx | head -1
作用:显示进程的属性 。
2.1 进程属性 :
PPID:父进程ID,代表这个进程是由哪个进程发展衍生而来的,是父进程的代号。
PID:进程ID,代表这个进程的代号。
PGID:进程组ID。
SID:会话ID。
TTY:终端号。
TPGID:终端进程组ID。
STAT:进程状态。
UID:用户ID,代表执行者的身份。
TIME:进程占用CPU的时间。
COMMAND:进程名。
很多属性都不能一下讲完,遇到了再说。
命令:kill -9 进程id
作用:结束进程。
3 简单见识系统调用:
同样地,这里也先见一见,后面都会讲的。
getpid()
作用:获得进程的id。
pid_t其实就是个整数。
调用getpid()。
运行程序后:
一个小问题:当我们将程序运行起来时,如果删除了该程序的二进制文件,这个程序还可以跑吗?
在这里,我们会发现,当我们把该程序的二进制文件删除时,该程序还在运行。从这里我们可以看出,一个程序被加载到内存变成进程以后,就与该进程所对应的可执行程序没有关系了。这是一种普遍情况,但是也存在特殊情况,后面遇到了再说。
getppid()
作用:获得父进程的id。
在这里,我们会发现当运行程序时,进程的PID会一直变化(这是正常的,因为程序被加载到内存,每次都要为该程序创建PCB) ,但是父进程的PID却没有发生变化。为什么呢?
我们通过grep找到了父进程,它的名字叫-bash,其实它就是一个shell,-bash是在我们登录linux之后,操作系统就会自动生成该程序,后面要执行什么任务,-bash就会生成子进程去执行这些任务(程序),目的是为了不影响shell(-bash)本身,当子进程出问题了,父进程不会受到影响。
shell命令以及原理:
Linux严格意义上说的是一个操作系统,我们称之为"核心(kernel)" ,但我们一般用户,不能直接使用kernel。 而是通过kernel的"外壳"程序,也就是所谓的shell,来与kernel沟通。如何理解?为什么不能直接使用kernel?
从技术角度,Shell的最简单定义:命令行解释器(command Interpreter)主要包含:
- 将使用者的命令翻译给核心(kernel)处理。
- 同时,将核心的处理结果翻译给使用者。
对比windows GUI,我们操作windows 不是直接操作windows内核,而是通过图形接口,点击,从而完成我们的操作(比如进入C盘的操作,我们通常是双击C盘盘符)。
shell 对于Linux,有相同的作用,主要是对我们的指令进行解析,解析指令给Linux内核。反馈结果再通过内核运行出结果,通过shell解析给用户。
fork()
作用:创建子进程。
我们这里用fork()创建一个子进程:
程序运行后:
我们会发现打印执行了两次,因为fork是一个函数,在没有执行函数前会有一个父进程在运行,执行fork()函数后,父进程与子进程同时运行下面剩下的代码,所以printf()会执行两次。
同时,我们也可以看到:
我们这里来看看fork()的返回值。
我们发现这个函数居然有两个返回值!
当我们用一个id变量接收fork()的返回值时,按照fork()的返回值要求,上述结果是没有问题的。但是我们的id居然在后续没有修改的情况下,有不同的内容!
由于fork()是给父子进程返回不同的值,那么它们就可以依据id的值进行不同的操作。
我们会发现父子进程在同时运行。 这就是我们所说的多进程。
我们会发现在一个程序里,两个死循环同时在运行,并且if与else if都同时成立了,这是以往我们没有见到的,这是又fork()这个函数引起的,这里我们先不讲为什么,先了解现象即可,后面再讲为什么。
因此,对于fork(),我们得出如下结论:
- fork()之后,会有父进程+子进程两个进程在执行后续代码,也就是说,fork()后续的代码,被父子进程共享。
- 通过返回值不同,让父子进程执行后续共享的代码的一部分(并发式编程)。
4 进程状态:
4.1 普遍的操作系统层面:
对于进程状态,我们以前听说过的状态可能包括:新建、运行、阻塞、挂起、等待、停止、死亡等,这些其实都是在普遍的操作系统层面上的, 我们这里主要讲其中的运行 、阻塞 、挂起,因为它们是最重要最难理解的,后面的其它状态,我们在具体的linux系统里面再介绍。
4.1.1 运行状态:
首先,我们知道操作系统需要对硬件做管理,为了做好管理,操作系统里面会存在与各个硬件相关的硬件描述结构体,里面主要包括硬件的属性以及相关的方法等。
假设磁盘里面有一个 test.exe 程序,当它被加载到内存时,操作系统为了对其进行管理,就会产生一个PCB,里面包括了进程的所有属性与方法,它可以找到我们写的代码。当CPU需要执行一个进程时,就会找到这个PCB,然后执行里面的方法。
当一个进程需要在CPU上被运行起来,CPU就必须在内核里面去维护一个运行队列 去完成对进程的运行的管理。(因为我们CPU的数量是很少的,而进程是很多的,不可能一个进程对应一个CPU。所以,就会存在多个进程在同一个CPU上面运行的情况,所以,就出现了运行队列。同时,一个CPU只有一个运行队列 ),当一个进程需要在CPU上被运行时,就会让该进程的PCB结构体对象入运行队列去排队。当轮到这个进程去被CPU运行时,CPU会依据该进程的PCB找到该进程的代码与数据。
我们知道CPU是很快的,当进程的PCB在运行队列里面排队时,很快就会将这些进程都执行一遍,所以这些PCB需要随时准备好以便CPU去通过它们去运行程序。
所以,我们在这里得出一个结论:
进程PCB在CPU的运行队列里面等待就叫做运行状态 。不是这个进程正在被运行才叫运行状态。
状态是进程内部的属性,是属性就被存放在该进程的PCB中,而在一些操作系统中,这个属性是一个整数类型的,所以,就可以用整数来表示进程的状态。
4.1.2 阻塞状态:
我们知道CPU很快,但是其它外设相对于CPU又很慢。
可是,很多的程序都或多或少的会访问外设,而外设的数量也是很少的。所以,又会存在多个进程同时访问外设的情况。
此时,外设的结构体里面也会存在自己的阻塞队列。当CPU正在执行一个进程时,如果遇到一段代码需要访问外设时,由于CPU很快,外设很慢,CPU不会等待该进程访问完外设之后再继续运行,而是把该进程的PCB从运行队列里面拿出来,操作系统先将该进程的状态改为阻塞状态,再把它放到需要访问的外设的阻塞队列里面。当该进程访问完外设时,就告诉操作系统,然后,操作系统又会将该进程的状态改为运行状态,放到CPU的运行队列里面,CPU后面就会继续来运行该进程。
在该进程访问外设期间,CPU会继续调度运行后续的在运行队列里面的进程。
因此,我们又得出一个结论:
进程PCB在外设的阻塞队列里面等待就叫做阻塞状态。
4.1.3 挂起状态:
当多个程序被加载到内存,并且它们都要去访问某一个外设,而等待外设又需要花费很长的时间。与此同时,程序被加载到内存后,这些进程又会占用内存空间,而短暂时间内,这些进程又不会被CPU调度运行,当其它进程不断被加载到内存,这时,内存空间就可能不够。
当内存不够时,操作系统就会将一部分进程的代码和数据暂时保存到磁盘,只把这些进程的PCB留在内存里,这样就节省了内存空间。我们于是就把一个进程的代码和数据被暂时换出到磁盘叫做挂起状态 。
当这个进程需要的外设资源准备好后,操作系统会重新加载该进程的代码和数据到内存,然后将该进程的状态改为运行状态,CPU就去调度运行该进程。我们将 把进程相关的代码和数据加载或保存到磁盘的操作 叫做 内存数据的换入换出。
4.2 具体的Linux操作系统层面:
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux内核里,进程有时候也叫做任务)。 下面的状态在kernel源代码里定义:
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列 里。
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T暂停状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
4.2.1 进程状态查看:
我们这里先看看运行状态:
- 对于R状态,我们编写一个纯计算的代码:
运行并查看该进程:
我们会发现当前运行的进程显示的状态为R+,+这里先不说,后面会讲到。
- 对于S状态,当我们为该程序添加打印代码时:
运行并查看该进程:
我们会发现该进程的状态由R+状态变成S+状态了。可能会有人会觉得应该是R+状态,但是,其实我们在执行该进程时绝大多数时间都是在等IO就绪(等显示器就绪),所以该进程会显示S+状态。 可以看出:S状态是阻塞状态的一种。
- 对于查看T状态,我们可以先使用如下命令显示操作系统里面的信号。
命令:kill -l
对于9号信号我们之前已经使用过,它表示杀掉进程。我们这里使用18和19号信号。
测试代码:
运行该程序并且执行19号信号与18号信号:
现象:当进程运行时,该进程最先是R+状态,通过19号信号暂停进程 后, 该进程的状态变为T状态,通过18号信号继续进程后,该进程的状态变为了R状态,+没有了。
为了更好的表现现象,我们这里修改一下代码。
同样地,执行上述操作:
当我们使用ctrl+c结束进程时,不会结束该进程,但我们却可以继续执行命令,这是因为该进程变成了后台进程,必须使用 kill -9 才能结束该进程。所以,我们可以得到:有+是前台进程,无+是后台进程 。T状态其实也是阻塞状态的一种。
- 对于D状态,我们在这里称其为深度睡眠状态(S为浅度睡眠状态)。
想象一个场景,假设在内存里有一个进程A拥有大量的数据需要写入到磁盘保存起来,磁盘就把数据拿过来保存在某一个区域,进程A就在内存里等磁盘IO的完成,由于磁盘很慢,它就需要等很久。在这期间,如果内存不够了,操作系统就会去看什么进程可以被挂起,为的就是释放内存。当挂起这些进程后,如果内存还是不够,操作系统就会主动的杀掉进程。它看见进程A占着内存资源却什么事都不干,在那里干等着,于是就把它杀掉了。当磁盘把数据写完,但是却写失败了,它返回给进程A时,发现进程A不见了,自然错误也没法报给上层。但是,刚才的数据怎么办呢?不可能让磁盘一直拿着吧,它还有事要做呐,于是磁盘就把这些数据给丢弃了。这时,上层用户来看结果,发现数据怎么不见了,就询问磁盘、进程A与操作系统到底是哪个的问题。
磁盘表示它没错,虽然它数据写成功的概率很高,但是也会有写失败的时候呀,万一磁盘空间不够了呢?我作为一个做事的,肯定会出现做失败的情况啊,凭什么怪我嘛。
进程A表示它也没错,它也是做事的。我做事做到一半被人杀死了,我还冤枉呐。跟我有什么关系。
操作系统表示它更没有错了,它说:"用户赋予我的权利不就是管理好软硬件资源吗,一个闲着没事干的进程为什么不能被杀掉?我不把它杀掉,对其它进程公平吗?我又不是搞双标,我对所有进程都一样。"用户听了之后,感觉它们好像是都没有错,但是怎么办呢?
于是,用户就给进程A了一个免死金牌,当进程A在等待的时候,不能被杀掉,要杀杀其它进程去,这样就解决了问题。
这样,就出现了一种新的状态,叫做D状态。在该状态下的进程无法被杀掉,只能通过断电或者进程自己醒来解决。只有在高IO的情况下才可以看见,一般都看不见。
D状态其实也是阻塞状态的一种。
- 对于t状态,它其实也是一种暂停状态。
测试代码:
当我们使用gdb调试该程序时,我们为代码当中的某一行打了断点, 当我们再次运行该程序,程序运行到断点处停下来,此时该进程的状态为t状态,表示当前进程正在被追踪 ,它也是阻塞状态的一种。
4.2.2 僵尸进程中的僵尸状态:
首先,我们先来理解为什么会有僵尸状态。
我们首先要知道,我们把进程创建出来就是让它帮我们完成某项任务,我们让它完成任务,肯定就要知道它完成的结果如何,这些结果其实都是由操作系统或者父进程去关心的。所以,当一个进程执行完以后,不能立刻释放该进程对应的资源,而应该保存一段时间,让操作系统或者父进程去读取(读取这里暂时先不讲)。
就比如说:假设一个人去网吧打游戏,结果旁边有个人打游戏太激动了,一不小心猝死了,他就去弄他,发现他已经没有了呼吸,于是,他就赶紧打电话给120和110,然后医院和警察都到了现场,医生发现这个人真的game over了,怎么办呢?此时肯定不是拉出去直接埋了。警察肯定需要在那里拉好警戒线,然后让法医去收集一些东西去化验,检查这个人的死亡原因,给受害者的家人以及社会一个交代。当这些都做完了后,再通知家属可以把人领回去了。在这里,警察就相当于OS或者父进程,猝死的那个人就相当于一个进程退出了,它退出后不是立即被回收的,而是让OS或者父进程来获取它的退出结果,退出之后,再把它的Z状态改为X状态,再由我们的系统去回收。所以,从这个人猝死到被警察检查完的这个时间段内所处的状态就是Z状态。僵尸状态是一个问题,我们后面再讲。
查看Z状态:
我们这里创建一个子进程,让父进程不退出,并且父进程什么都不做,让子进程正常退出。
这里编写一个监控脚本,用于每秒刷新并显示系统中名为myproc
的进程的详细信息,监控该进程的运行状态。
我们会发现,子进程最开始是S+状态,在运行了5s后变成了Z+状态,变成了僵尸状态(defunct:死的),当我们 ctrl+c 后,父子进程就都终止了,资源(可以理解为PCB)就被操作系统接手了,操作系统自动回收了这些资源。
剩下的僵尸进程需要到进程控制的时候再讲。
暂时总结一下僵尸进程:
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
僵尸进程危害的危害:
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!
- 因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 内存泄漏?是的!
- 如何避免?后面讲 。
5 孤儿进程:
对于僵尸进程,它是子进程先退出,而父进程不退出。对于孤儿进程则刚好相反,父进程提前退出了,而子进程还没有退出。我们现在都知道,一个子进程退出后,它的资源要被父进程回收,但是现在父进程都退出了,那如果子进程要退出了,又由谁来回收子进程的资源呢?
我们这里编写一段代码:
当我们把父进程退出,子进程不退出后:
我们会发现,子进程的父进程PPID变成了1,它其实就是操作系统。
同时,我们会发现父进程退出后,没有出现Z状态那是因为它的父进程-bash自动的就回收了该进程的资源。
所以,我们可以得到:
当父进程先退出,子进程还没退出,子进程会被操作系统领养(1号进程),这个被领养的进程就变成了孤儿进程。如果不领养,那么子进程退出的时候,对应的僵尸将没有人能回收了。
如果是前台进程创建的子进程,如果变成孤儿进程了,会自动变成后台进程。
6 进程优先级:
6.1 优先级的概念:
优先级就是指一个资源应该被谁先获取,应该被谁后获取,先获得叫做优先级高,后获得叫做优先级低。
6.2 优先级存在的原因:
因为资源太少,进程太多。
6.3 Linux优先级特点:
首先,优先级的本质其实就是在PCB里面的一个整数数字(也有可能是几个),比如说我们在银行里办理业务时存在的号数,它就规定了办理业务的优先级。
在Linux中,优先级是由两个整数确定的,它们分别是PRI(priority)和NI(nice)。
命令:ps -al
作用:显示进程优先级。
测试代码:
测试结果:
我们在这里就可以看到PRI和NI了。在Linux中,一个进程的最终优先级=老的优先级(80)+NI。NI是用于在进程运行中对优先级进行调整。NI的取值范围为 [-20,19) 。
我们可以使用 top 命令修改进程的优先级,首先在命令行输入top ,然后弹出一个界面,输入 r ,然后输入要修改进程的PID即可。
测试代码:
运行程序并使用top命令:
输入 r 和需要修改优先级的进程PID:
输入需要修改的NI值:
我们会发现NI最大只能是19。
同样地,我们测试NI的最小值:
我们在这里可能会遇到一个报错,因为调优先级会影响调度器的效率,所以我们设置时,需要设置一下权限。
命令:sudo top
最后,我们会发现NI最低为-20。
注意:PRI值越小,优先级越高。
简单总结一下:
- CPU资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
7 其它概念:
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级;
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰,比如打原神的时候,我也在b站看教程,b站崩溃了,并不影响我原神的运行,只是让我解谜速度变慢了。同时,独立性也适用于父子进程之间;
- 并行 : 多个进程在多个CPU下分别同时进行运行(不是运行状态),这称之为并行;
- 并发 : 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
我们这里先讲一下并发。
假设我们的电脑只有一个CPU,那么在任何时刻,CPU上就只能有一个进程正在被运行。但是当一个进程正在使用CPU时,不是要等CPU把它运行完之后,它才从CPU上退出来,而是按照时间片轮转的策略进行的。也就是说,当一个进程在使用CPU资源时,只能使用一段时间,过了时间之后,就会从CPU上被拿下来,放入运行队列(或其它队列)后面,重新等待CPU调用它。这样,就可以让多个进程在同一时间段内都运行一下。
7 进程切换:
我们已经知道,在一段时间内,计算机通过进程切换,让多个进程的代码都可以被运行,那么进程是如何切换的呢?
首先,我们要知道,在CPU里面有一套寄存器,这套寄存器里面包含了很多寄存器,它们是用来存数据的。当我们计算机要执行某个进程时,CPU会将该进程的PCB的地址加载进某个寄存器里,该寄存器可以直接找到该进程的PCB,自然而然就可以执行该进程了。
然后,我们要知道,CPU永远在做取指令 、分析指令 与执行指令 这三种事情(这些指令相当于我们自己写的代码)。在寄存器中,有一个叫做指令寄存器(pc/eip),它被用来存储当前正在执行指令的下一条指令的地址(相当于标识下一次应该从该进程的哪个位置去读取代码)。
再然后,我们还要知道,当我们的进程在运行的时候,一定会产生非常多的临时数据。(比如做加法时,每次的计算结果会存放在寄存器里。)这份数据属于当前进程。CPU内部虽然只有一套寄存器硬件,但是,寄存器里面保存的数据是属于当前进程的。这就告诉我们,寄存器硬件不等于寄存器内的数据。除此之外,我们上面也说了,进程在运行的时候,它会占有CPU,但是,进程不是一直要占有到进程结束。而是依靠时间片来占有CPU的。既然是依据时间片,就必然存在进程还没有在CPU上跑完就被拿下来的情况,那我们肯定就要考虑下次它回来的情况。
举个例子:
假设有个大学生特别喜欢当兵,他就在大二的时候去应征入伍了,但是他也没有和导员说,直接就去了。他当了两年兵回来之后,发现学校把他退学了,原因是他神秘失踪,上课也不去,考试也没考。这就叫做走的时候什么也没有带走。我们在读书期间一定会产生许多临时数据(比如:考试情况、学习情况等)。他就直接这么走了,学校又不知道他走了,学校就认为他是在旷课。这显然不太好。所以,当他应征入伍的时候,应该先让学校知道,然后让学校保存学籍,学校就会把他的档案归档,甚至有可能直接把档案给他(应该概率很小吧)。当他当了兵回来之后,不是直接就去上课,如果这样的话,可能考试的时候都没有他的位置(学校又不知道他回来了)。而是先去告诉学校恢复学籍。我们将离开学校保留学籍的过程叫做上下文保护 ,把回到学校恢复学籍的过程叫做上下文恢复。
我们这里的学校就相当于CPU,应征入伍的这个人就相当于一个进程,部队就相当于某种等待队列,学籍就相当于CPU中的寄存器内的数据。
所以,我们得到如下结论:
- 进程在切换的时候,要进行进程的上下文保护;【】;
- 进程在恢复运行的时候,要进行进程的上下文恢复;
- 上下文是指CPU中的寄存器内的数据,不是寄存器;
- 在任何时候,CPU里面的寄存器内的数据,看起来是在大家都能看到的寄存器上,但是,寄存器内的数据,只属于当前运行的进程。也就是说,寄存器被所有进程共享,寄存器内的数据是每个进程各自私有的,它被叫做当前进程的上下文数据。