万字讲解操作系统硬件,进程的部分概念
任何平台,进程都是具有独立性的,就是我结束了一个进程并不会影响其他的进程,所以父进程和子进程在共享代码的时候,由于代码不能修改,所以是没有问题的,你读你的我读我的,相互之间并不影响,但是如果是数据就不一样了,数据是可以修改的所以我改了可能你也会改,因此从理论上来说,我们的子进程必须想办法拷贝一份相同的数据且独立的数据出来!
一、认识冯诺依曼体系结构
1.1结构:

我们常⻅的计算机,如笔记本。我们不常⻅的计算机,如服务器,⼤部分都遵守冯诺依曼体系。这是冯诺依曼体系的结构图,我们一一来进行拆解:
1.输入设备:鼠标、键盘、摄像头、话筒、磁盘(文件读取)、网卡(网络接受)……
2.存储器:内存
3.中央处理器:CPU
4.运算器:对我们的数据进行计算任务(算数运算、逻辑运算)
5.控制器:对我们的计算机硬件流程进行一定的控制
6.输出设备:显示器、播放器硬件、磁盘(写入文件)、网卡(网络发送)……
冯诺依曼体系就是由这么多独立的硬件结合在一起的,那么这些独立的硬件是如何联系在一起的呢?这就需要总线了,总线分为系统总线,IO总线,内存总线。
其实还有一个结构叫做I/O桥,I/O桥像一个枢纽一样,通过系统总线和IO总线和内存总线将和内存和I/O设备联系在一起,再让CPU能够加载我们的内存。
1.2冯诺依曼体系这样设计的原因:
我们可以清楚的看到CPU不跟我们的输入和输出设备进行联系,而是只跟我们的内存打交道。
这里就利用了高速缓存原理:

举个例子,我们经过一系列处理,将hello.c的程序转化为了hello这个可执行程序,hello这个可执行程序在我们的磁盘上,也就是外设上,当程序加载时,他们被复制到了我们的内存中,当处理器开始运行程序时,指令又从内存复制到处理器中,而对于字符串也类似,开始在磁盘上,后来又被复制到内存,然后再从主存复制到显示器上,这些复制就是开销,减缓了程序运行的速度。
而 CPU的速度是纳秒级别的,而硬盘是毫秒级别的,所以他们的速度差距太大了!!因此需要内存(微秒级别)在中间做一个中间商。
CPU在运行时不仅仅会运行你现在的程序,其实它并不会一直闲着,他也会进行其他的运算。
比如说二进制可执行程序其实也是文件,文件是在磁盘上存储的,我们要运行这个程序就必然要将其先读取到内存中,而当外部设备在将数据加载到内存中时,其实cpu可能正在进行其他的运算,当你把数据预先都加载到内存中的时候,cpu恰好开始执行你的代码,所以我们会发现cpu从存储器中拿数据和外部设备导入到存储器这两个过程是可以同时进行的!!所以这是并型而不是串型!!大大提高了效率!!这也是内存存在的真正价值!!
二、OS体系:
我们需要一个像大脑的角色来对我们的计算机进行管理,让计算机在什么时候该干什么事情,让管理更具有逻辑性,OS是用来管理软件和硬件的角色。

我们创建操作系统的目的就是对下来管理软硬件,对上为用户提供一个良好的管理环境。
软件是通过驱动程序来与我们的硬件打交道的。
那OS是如何对软硬件进行管理的呢?
其实OS本身是不信任我们的用户的,它会将自己的数据全部保护起来,然后留一个小接口---->OS的设计者用C语言写了一些内部的,数方法开放给外部,我们把它称之为系统调用!
其实在我们的这些接口内部,设计者肯定对其做了保护,只要你的行为是违法的,那么他就不允许你访问这个接口和数据,因此我们只能通过系统调用和操作系统打交道,但是系统调用本身使用难度也很大,所以就需要程序员基于系统调用去设计一些上层的软件或者接口去供普通用户使用。
三、OS是如何进行管理的
直接不卖关子,其实就是通过数据结构,举个例子,我们校长想要管理成千上万的学生如何进行管理呢?通过结构体进行管理,一个结构体对应存储着一个学生的所有信息,然后如何串联起来呢?我们可以给每一个结构体里面加一个next指针,将每个结构体串联起来,这样就形成了一个链表,对学生的管理可以转换为对链表的增删查改,大大提高了我们办公的效率
3.1总的来说:
1.管理者和被管理者是不需要见面的
2、管理者只需要得到管理信息,就可以做出管理决策,所以管理的本质是通过对数据的管理从而实现对人的管理
3、管理者一方面可以通过执行者去拿到数据,另一方面可以通过将被管理者的一些信息用结构体描述出来,然后再用数据结构组织在一起,然后再通过该数据结构的特点实现相关需求的算法来拿到数据。
同理,在我们的计算机体系当中,OS就相当于管理者,而我们的软硬件资源相当于被管理者,而我们的驱动程序相当于执行者。
这里就引申到了先描述再组织的思想,管理者通过结构体来描述被管理者,然后再通过数据结构组织起来,用相关的算法去实现管理。
四、进程的概念
4.1进程的概念:
1.已经加载到内存中的程序 2.正在运行的程序 但是很明显,OS系统中不仅仅有一个进程,有时候很多进程会一起运行(比如说王者荣耀和QQ音乐),所以这就意味着我们得想办法把进程给管理起来,这里就用到了先描述再组织的思想,我们引入一个新的概念,PCB。
4.2如何理解PCB呢?
PCB(进程控制块),当我们的程序加载到内存时,操作系统会创建一个结构体对象(PCB)来对进程进行描述,PCB的本质就是所描述对象的属性的集合。
Linux的操作系统下的PCB是task_struct,他是是Linux内核的一种数据结构,它会被装载到内存里并且包含着进程的信息。
4.3task_struct的内容:

可以看出,一个可执行程序需要运行,就需要我们的OS系统创造一个PCB出来,但具体按照什么逻辑去运行是取决于你的代码和数据的,所以我们就可以定义进程:进程=内核PCB数据结构对象+自己的代码和数据。
五、如何组织进程?
我们知道进程=PCB+你自己的代码和数据,但是实际上OS并不会对你的代码和数据做管理,你的代码和数据以可执行程序的形式存储在磁盘上,等运行时,会将你的代码和数据从磁盘上拷贝到内存当中,所以OS的本质是对你的PCB做管理,在你的PCB内部有一个指针,会指向在内存中的代码和数据,然后再交给CPU运行。

但是PCB特别多,所以我们需要想办法管理起来,其实在我们的Linux中task_struct主要是以双链表的形式组织起来,你可能会疑惑,使用一个顺序表来存储不是更好吗?其实在OS内部对于进程的管理方式并没有像我们以前学的数据结构那么纯粹,他的场景会更加复杂,也就是说该进程可能会需要根据不同的需求被存储在队列中、双链表中、二叉树中、栈中……所以将进程按照节点的方式链接起来其实会更方便我们将这个进程放在不同的数据结构中,然后我们可以通过对应的指针信息来讲他们更好地管理起来。
所以不同的数据结构有不同的特点,用哪个数据结构进行管理取决于你的算法,而算法的不同取决于你的应用场景。
六、查看进程
我们电脑开机的时候,其实就是将OS从外设搬到内存中,因为只有在内存中才能对进程进行管理。
6.1进程的状态:
TASK_RUNNING:可运行(正在 CPU 运行 / 等待调度)
TASK_INTERRUPTIBLE:可中断睡眠(等待资源,信号可唤醒)
TASK_UNINTERRUPTIBLE:不可中断睡眠(IO 操作,信号无法唤醒)
TASK_ZOMBIE:僵尸状态(进程退出,父进程未回收)
TASK_STOPPED:停止状态(被调试 / 信号暂停)

其实我们状态的改变本质就是在不同的队列中进行流动,本质都是对数据结构的增删查改
6.2 ps ajx查看所有的进程

1.PID:进程 ID(唯一标识)
2.PPID:父进程 ID(谁创建了它)
3.PGID:进程组 ID
4.SID:会话 ID
5.TTY:所属终端,?= 后台无终端进程
6.TIME:占用 CPU 时长
7.CMD:进程执行命令
我们其实可以写一个死循环的代码方便我们查看进程
命令:ps axj | head -1 && ps axj | grep mycode
代码:

![]()
这里的PPID是父进程的PID,可以看出第二个其实是我们的grep进程,他的父进程其实是我们的bash终端,因为OS会给每一个登录用户分配一个bash终端。
然后第一个是我们写的进程,他正处于S的状态,也就是可中断睡眠状态,因为有sleep,进入睡眠。
我们写的这个进程因为是一个死循环,能够一直进行下去,用ctrl+c都杀不了,只能用kill -9才能杀死
6.3 /proc

/proc的里面存储的都是一些内存级的信息,开机时存在。关机时消失,蓝色的是目录文件,一个进程里面可能包含很多信息。
七、通过系统调用获取进程标识符
7.1理解PPID
在前面代码中我们能够发现我们的可执行程序的父进程是-bash命令行,为什么会这样去设计呢?
我们都知道其实bash命令行的作用一方面是解释命令,另一方面是为了阻止用户的非法操作,而我们每一条指令或者是可执行程序其实都是一个进程,因此我们的bash命令行其实是先创建了一个子进程去执行对应的指令,然后自己就可以继续去帮助别的指令创建进程,这样的好处就是一旦子进程崩了,并不会影响bash命令行进程处理其他的指令!
7.2理解fork函数
fork函数是内核提供的系统调用,作用是创建一个子进程。
特点:
调用一次,返回两次
子进程几乎是父进程的 “拷贝”
父子进程同时运行,谁先跑不一定
使用fork函数之后内核做了什么?
创建一个新的PCB,拷贝父进程的大部分代码,给子进程分配PID,子进程加入调度队列,准备运行,父子进程同时从fork之后运行,看情况进行写时拷贝
如何理解调用一次,返回两次呢?
fork函数调用,给父进程返回子进程的PID,给子进程返回0,失败返回-1。

上面是fork被调用的流程图。
7.2系统调用的接口getpid



可以看到,我们有两个进程,第一个是父进程,第二个是子进程,状态都是休眠状态,而且我们if和else都运行了,所以也就说明id返回了两次。
当我们重新运行程序的时候,只会改变子进程的id,父进程id并不会改变,而当我们把机器关了重新开了的似乎,父进程id却改变了,这说明父进程(bash命令行)是在打开机器的时候就创建好的一个进程!
7.3为什么需要子进程?
1.父进程专心干主业,把子任务交给子进程去做,实现并发干活、互不阻塞,让父子进程分别执行不同的代码块
2.实现并发执行,不阻塞主进程父进程等待 IO、等待用户输入、等待网络连接时,会卡着不动。创建子进程让子进程去等待、去耗时操作,父进程继续正常运行。
3.程序多任务运行一个程序同时做几件事:一边打印、一边读写文件、一边计时,靠多子进程实现。
4.权限与资源隔离子进程拥有独立进程空间、独立 PID子进程崩溃不会连累父进程,稳定性更强。
总结:
任何平台,进程都是具有独立性的,就是我结束了一个进程并不会影响其他的进程,所以父进程和子进程在共享代码的时候,由于代码不能修改,所以是没有问题的,你读你的我读我的,相互之间并不影响,但是如果是数据就不一样了,数据是可以修改的所以我改了可能你也会改,因此从理论上来说,我们的子进程必须想办法拷贝一份相同的数据且独立的数据出来!但是其实这样也不太可取,因为父进程的数据可能有非常多,但是我们的子进程可能只是共享了其中一部分的代码,并且也不一定会用到所有的数据,所以如果只是简单粗暴地把这些数据拷贝过来了,势必会造成大量的资源浪费。因此OS的设计者就不想让子进程直接地去拷贝数据,而是当程序运行的时候,当OS检测到子进程需要去修改父进程的数据的时候,他就会让子进程等一等,然后为他开辟一个新的空间把对应的数据拷贝过来再让他修改。因此我们可以总结出,无论是代码还是数据,父子进程在前期都是共享的,只不过当OS一旦检测到子进程需要去对数据进行修改的时候,需要多少才会开多少空间,这就是数据层面的写时拷贝。
7.4如何理解一个函数返回两次?

因为父子的代码是共享的,而return ret也是代码,所以父子各返回一次,一共返回两次。
7.5一个变量为什么会有两个不同的内容
原因是因为子进程要修改父进程的数据的时候,发生了写时拷贝,所以该数据其实有两份内容,然后因为进程的在运行的时候是具有独立性的,所以此时父进程和子进程通过if else 分流去执行共享的代码。 但是要注意的是,子进程被创建好之后,究竟是先运行子进程还是先运行父进程,其实是由调度器(因为CPU只有一个,所以他的作用就是在当前进程中选一个合适的放到CPU中,进程之间会竞争CPU资源,所以调度器会遵循着自己的一套原则来保证进程之间的公平性)去决定的!
八、进程状态细讲
1.操作系统:

我们的进程状态有以上几种,运行阻塞挂起。
1.1运行状态:

因为有一个调度器需要确保CPU的资源被合理使用,所以需要维护一个运行队列,他将进程的task_struct结构体连接起来,而被链接起来的进程就会按顺序被调度器调度,此时处于运行队列的这些进程就处于运行态,这说明运行态并不指的是正常运行的进程,而是处在运行队列中并且随时可以被调度的进程!
1.1.1并发执行和调度切换
调度器将进程放到CPU上去运行,并不代表必须要将进程全部运行完才会被放下来!因为
(1)进程当中可能会存在一些死循环的进程
(2)调度器要尽量保证公平性,不能让一个进程占用CPU太长时间。那么什么时候应该把这个进程放下来,就要取决于时间片,当一个进程在超过时间片还没有结束的时候,就要把他放下去然后重新放在新的运行队列的位置。
(3)CPU运行速度是很快的,所以其实我们人所能感受到的,所以在一个时间段内必然所有的进程都会被执行,称之为并发执行。 而大量地把进程从CPU上拿上来在放下去的这个过程,称之为进程切换!
1.2阻塞状态
操作系统管理硬件的过程也是需要先描述再组织,因此不同的硬件设备都需要维护一个阻塞队列,当该硬件没有准备好的时间,该进程只能在阻塞队列中等待。

1.3挂起状态
当操作系统的内部资源严重不足的时候,需要在保证正常运行的前提下想办法把一些内存里的资源置换到磁盘中来节省空间,我们将被置换的进程所处的状态叫做挂起状态。
一般来说,导致内存资源不足的原因是因为存在大量处在阻塞队列的进程 ,所以我们要办法将一些资源置换到磁盘中,但是为了不影响阻塞队列的管理,所以大多数情况下并不止直接将task_struct结构体置换过去,而是将该进程的数据和代码先置换过去,而当执行到该进程的时候,再通过某种方式将其数据和代码置换回来。
其实在我们的电脑中存在一个交换分区,该分区就是专门用来置换一些导致内存溢出的资源
总结:
1、挂起状态就是PCB在排队,但是他对应的代码和数据被暂时移到外设中,节省内存空间 。
2、运行状态就是 PCB 被 CPU 调度,进程的代码和数据在内存中,正在 CPU 上执行指令
3、阻塞状态就是 PCB 在等待某个事件(如 IO 完成、信号、定时器),进程主动放弃 CPU,代码和数据仍在内存中,不参与调度,直到事件发生才被唤醒。
2.linux中内核管理进程状态方法
2.1进程状态查看的指令
ps axj命令:
2.1.1 R状态: 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列中。
2.1.2 S状态:S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 )。 其实就相当于是阻塞状态(因为需要跟硬件保持联系)
2.1.3 D状态:D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
2.1.4 X死亡状态:这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
2.2!!!!!!!僵尸状态
先来个例子:
比如你正在公园跑步,突然看见一个老人走了两步就倒地上了,这时候你叫了120,120发现人已经没救了,于是走了。然后你又叫了110,但是110并不会立马清理现场,因为本质查明真相的原则,可能会需要先带着法医对尸体进行检测然后再确认结果,比如说异常死亡或者是正常死亡(因为家人需要了解情况),然后才会去清理现场。 其实这段已经死亡一直到清理现场之前的这段时间,就相当于是僵尸状态。
回到进程的角度,一个进程在退出的时候并不是立即把所有的资源全部释放,而是要把当前进程的退出信息维持一段时间,因为父进程需要关心子进程!!
为了观察这个现象,我们需要想办法让子进程在中途退出,所以需要exit函数

情况1:子进程先退出,父进程还在:



我们可以看出,子进程退出,在父进程没有对子进程进行回收的情况下,子进程保持Z状态,相关的资源尤其是task_struct结构体不能被释放!资源会一直被占用!
为什么要存在僵尸状态?
因为他要告诉关心他的进程(父进程),你交代给我的事情我办得怎么样了,所以他一定要等到父进程读取之后才能完全死亡
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护!!需要占用内存。
情况2:父进程先退出,子进程还在



说明如果父进程比子进程先一步消亡,那么子进程会变成孤儿进程,他的PPID会变成1 也就是被系统进程给收养。
为什么要被领养呢?
因为孤儿进程未来也会消亡,也会被释放!
ctrl+c为什么无法中止异常进程,他的底层原理是什么?
本质上是在一瞬间父进程会被bash进程回收掉!所以子进程也在父进程退出的一瞬间被收回掉了! 所以由于子进程的PPID不是bash进程而是系统进程,所以无法中止。
子进程是bash进程的孙子进程,为什么父进程消亡后不由bash进程回收而是交由系统进程回收?
因为bash做不到,因为孙子进程不是他去创建的!他没有这个权限,而系统进程可以做到,因为要将孤儿进程托孤给系统进程,当然不同的操作系统具体的实现方法可能也不同!
九、Linux具体是怎么维护进程的?
Linux内部维护进程主要是采用双链表的形式管理,但是由于其可能有不同的应用场景需求,所以有些时候我们也要把它放到队列、二叉树等各种数据结构中管理,所以为了方便这样的操作,我们的task_struct结构体里面必然需要维护各种各样类型的指针!

首先并不是整个task_struct结构体链接在一起,而是通过单独创建一个head结构体来进行链接,所以其实节点都是指向该结构体中间的位置而不是头部。
既然链表链接的并不是头部,那么我们通过节点的链接找到了下一个节点的某个位置,要如何去找到头部呢??
1、将0强转成task_struct结构体的类型,其实就是假设在0位置都有一个task_struct结构体大小的内存,然后找到他的head节点并取他的地址,由于低地址是0,所以找到head节点的地址就其实就相当于知道了head在task_struct的偏移量 ——> &(task_struct*)0—>node
2、当前找到的位置然后用当前指向的位置(比如start) 减去这个偏移量,就可以找到该结构体的头部。——>start
3、最后将这个头部的地址强转成task_struct* 就可以拿到整个PCB结构体了 ,就能访问里面的其他数据
总结: ( task_struct*)(start-&(task_struct*)0—>node)——>other。
十、进程优先级
10.1优先级的概念
优先级指在多任务、资源分配或决策过程中,对不同事项或任务进行重要性排序的机制。它帮助确定哪些任务应优先处理,哪些可以延后,以确保资源(如时间、人力、计算能力)的高效利用。
10.2优先级的应用场景
任务管理:在项目管理或日常工作中,优先级用于区分紧急且重要、紧急但不重要、重要但不紧急等任务类型。
计算机系统:操作系统通过优先级调度算法(如抢占式调度)决定进程或线程的执行顺序。
网络通信:数据包根据优先级标记(如QoS)决定传输顺序,确保关键数据低延迟。
10.3优先级的确定方法
紧急-重要矩阵(艾森豪威尔矩阵):将任务分为四类(紧急/重要、紧急/不重要等),优先处理高紧急性和高重要性任务。
数值权重法:为每个任务分配权重分数,综合截止时间、依赖关系等因素计算优先级。
动态调整:优先级可能随环境变化(如新任务插入、资源变动)实时更新。
10.4优先级的影响因素
截止时间:临近截止的任务通常优先级更高。
依赖关系:前置未完成的任务会阻塞后续任务,需优先处理。
资源约束:优先级可能受限于可用人力、预算或设备。
10.5优先级的潜在问题
优先级反转:低优先级任务持有高优先级任务所需资源,导致系统阻塞(常见于实时系统)。
过度优化:频繁调整优先级可能导致效率下降或决策疲劳。
我们可以用几个问题来更好的理解:
问题1:优先级vs权限
权限的意义是这件事我能不能做,而优先级的意义就是对于资源的访问谁先谁后(cpu资源分配的先后顺序,就是指进程的优先权)
问题2:为什么需要有优先级
因为资源是有限的,而进程是多个的,所以进程之间存在竞争关系,优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可能会改善系统性能。
问题3:操作系统是如何做的
操作系统会根据自己的一套规则来尽可能地保证进程的良性竞争,如果进程长时间得不到CPU资源,那么该进程的代码就长时间无法得到推进(进程的饥饿问题),具体需要去给进程维护运行队列,让进程按顺序去执行,但为了防止某些进程运行时间过长,还会有时间片的限制(调度器在工作)
问题4:人为可以调整优先级吗?
优先级是可以被人为调整的,我或许可以通过调整优先级让自己的某一个进程可以在同一时间内一直被调度,但是其实Linux并不希望我们有过高的权限,所以他的调整也不是无规则地调整,是带有一定限制的!
10.6查看和调整优先级的方法?
指令是ps -l

UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI和NI
PRI(priotity)即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
NI(nice)其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行 所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别。


总结:
![]()
linux中访问任何资源,都是进程访问。
十一、Linux内核的调度算法

整体架构:O (1) 调度器的核心数据结构
O (1) 调度器的核心设计目标,是让 “选择下一个运行进程” 的操作时间复杂度固定为 O (1),不受系统进程数量影响。它的核心结构是每个 CPU 独立维护的两个进程优先级队列:
运行
struct prio_array {
unsigned int nr_active; // 队列中的进程总数
unsigned long bitmap[5]; // 优先级位图(标记哪些队列有进程)
struct list_head queue[140]; // 140个优先级队列
};
- 每个 CPU 都有两个 prio_array:
active:活跃队列,存放等待被调度的进程expired:过期队列,存放用完时间片的进程
- 调度时,只从
active队列选进程;当active为空时,原子交换两个队列的指针,实现 O (1) 级别的队列切换,避免遍历所有进程。
关键机制拆解(对应图中细节)
1. 140 个优先级队列 + 位图快速定位
- 140 个优先级:
- 0~99:实时进程优先级(图中标注 “不考虑”,默认只分析普通进程)
- 100~139:普通进程优先级,对应
nice值 - 20~19,PRI = 80 + nice(默认 nice=0,PRI=80)
- 位图 bitmap [5]:
- 5 个
unsigned long(每个 32 位),共32×5=160位,仅使用前 140 位 - 每一位对应一个优先级队列,为 1 表示该队列有进程,为 0 表示队列为空
- 调度时通过位运算快速找到第一个非 0 位,定位最高优先级的进程队列,时间复杂度 O (1)
- 例如
bitmap[0]的第 0 位对应queue[0],第 1 位对应queue[1],以此类推
- 5 个
2. 时间片轮转与 active/expired 队列切换
- 进程被调度后,运行自己的时间片;时间片用完后,进程被移出 CPU,放入
expired队列,重新分配时间片 - 调度器始终从
active队列选进程运行,expired队列的进程暂时不会被调度 - 当
active队列为空时,调度器原子交换active和expired的指针,原expired队列变成新的active队列,进程重新获得调度机会,实现公平轮转
3. 进程优先级计算
- 普通进程的动态优先级公式:
动态优先级 = 静态优先级 + 交互修正静态优先级由nice值决定,交互修正会根据进程的 IO 等待行为调整优先级,提升交互体验 - 调度器始终优先选择 ** 动态优先级最高(数值最小)** 的进程运行,通过位图直接定位最高优先级队列,无需遍历所有进程
核心设计思想与解决的问题
1. 解决 O (n) 调度器的性能瓶颈
早期调度器需要遍历所有进程才能找到最高优先级进程,时间复杂度 O (n),进程数量多时性能极差;O (1) 调度器通过位图 + 双队列设计,将调度时间固定为 O (1),大幅提升了系统响应速度。
2. 兼顾公平性与交互性
- 公平性:每个进程都能分配到时间片,用完后进入过期队列,不会出现进程 “饿死” 的情况
- 交互性:通过调整动态优先级,让频繁 IO 的交互进程(如终端、浏览器)获得更高优先级,提升用户体验
3. 多 CPU 亲和性
每个 CPU 都有独立的运行队列,进程默认在创建它的 CPU 上运行,减少跨 CPU 调度带来的缓存失效开销;同时通过负载均衡机制,将进程迁移到空闲 CPU,提升多核利用率。
补充:O (1) 调度器的局限与后续发展
O (1) 调度器解决了早期调度器的性能问题,但也存在明显缺陷:
- 交互性优化的判断逻辑复杂,容易出现 “进程饿死” 或 “优先级反转” 问题
- 多核负载均衡的实现不够精细,高并发场景下调度开销仍较高
因此 Linux 2.6.23 版本引入了CFS(完全公平调度器),彻底替代了 O (1) 调度器,成为至今仍在使用的默认调度器。
十二、Linux内核进程切换的方法
1.并行和并发的概念
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行(少见,大多数配置的电脑都是只有一个cpu)
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为 并发(常见)
2.我们对于CPU更近一个的理解
2.1、我们的函数返回值是局部变量,但是为什么可以被外部拿到呢?
因为将数据存储在CPU寄存器中带了出去!比如return a会转化成move eax 10的指令
2.2、系统如何得知我们的进程执行到了哪句代码?
因为有程序计数器pc(在PCB内部)以及eip(栈帧寄存器),他们的作用就是记录当前进程正在执行的下一行指令的地址 (其实我们选择语句、循环……的跳转都跟pc有关)
2.3、寄存器有很多,具体有哪些种类呢?
通用寄存器:eax、ebx、ecx、edx……(需要什么就做什么的寄存器)
栈帧寄存器:ebp、esp、eip……(ebp和esp是维护栈顶和栈底的,而eip是存储程序计数器的值,表示着进程的下一条指令应该从哪里执行)
状态寄存器:status(记录当前CPU的一些状态)
2.4、寄存器扮演什么角色呢?
寄存器存储的空间并不大,但是速度快,所以他的作用就是 提高效率,将进程的高频数据放入寄存器中!
2.5、如果返回的是内置类型,寄存器可以做到,但如果返回的是一个特别大的对象呢?
因为内置类型很小,所以默认可以被翻译成mov eax ,但如果是一个对象的话,其实本质上来说就是进行了一个拷贝构造,如果是C++11的话可能还会涉及到移动语义,所以本质上来说也是由一个个语句组成的,所以就允许使用多个寄存器来工作。 相当于就是多次mov。
2.6、寄存器总结
(1)CPU寄存器里面保存的是进程的临时(高频)数据——>进程的上下文信息
(2)由于进程保存的是临时的数据,所以新的数据进入的时候可能会需要覆盖老的数据,因此进程的一些更重要的数据应该被放在离CPU更近的地方!!因为那样更安全更不易丢失!!(比如说重要数据肯定不适合放在通用寄存器上)
3.进程切换的本质
本质:进程从CPU离开的时候,也要将自己的上下文数据保存好然后带走,保存的目的是为了更好地回来。
问题1:进程在被切换的时候要做什么
(1) 保存上下文 (2)恢复上下文
问题2:pc帮助我们记录下一条指令要运行的位置,那么要保存的数据究竟是存放在哪里呢?
存储在PCB中,PCB内部应该有相关的结构体,在寄存器要去执行其他进程之前将相关的数据先存在内部,然后寄存器就可以离开了,当后面寄存器回来的时候就可以帮助进程恢复之前的数据。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)