当前位置: 首页 > news >正文

【Linux篇】0基础之学习操作系统进程

冯诺依曼体系结构

我们常见的计算机如:笔记本。不常见的计算机如:服务器。大部分都遵守冯诺依曼体系

截⾄⽬前,我们所认识的计算机,都是由⼀个个的硬件组件组成:

  • 输入(input)设备:键盘,鼠标、网卡、磁盘、摄像头、话筒等
  • 输出(Output)设备:显示器、打印机、网卡、磁盘等
  • 中央处理器(CPU):模型简化后就是运算器和控制器

Cache:

高速缓存存储器(不是冯诺依曼体系结构必要的组成部分)

RAM:

随机存取存储器。冯诺依曼体系结构中的存储器部分的一种。用于临时存储正在使用的程序和数据,是CPU能够直接访问的存储设备

ROM:

只读存储器。冯诺依曼体系结构中的存储器部分的一种。用于存储启动计算机时所需的固件程序等不易更改的信息。

CPU:

中央处理器。

程序要运行,必须先加载到内存,程序运行之前在磁盘中(因为程序就是编译好的在特定路径下的二进制文件)。所有程序是从”外设“加载到“内存”的。

在计算机中,数据流动的本质:实际是将数据从一个设备拷贝到另一个设备。所以,冯诺依曼体系结构的效率由:设备的“拷贝”效率决定

关于冯诺依曼,必须强调几点:

  1. 存储器指的是”内存“。而常说的磁盘(或者叫硬盘)则是”外存“
  2. 不考虑缓存情况,这⾥的CPU能且只能对内存进⾏读写,不能访问外设(输⼊或输出设备)
  3. 外设(输⼊或输出设备)要输⼊或者输出数据,也只能写⼊内存或者从内存中读取。
  4. ⼀句话,所有设备都只能直接和内存打交道。

下面是存储分级图:

为什么计算机的体系结构不是如下这样?

这样的体系结构会存在当CPU处理完数据后,输出设备还没把数据输出完,当CPU要从输入设备获取数据时,又需要等上一段时间。这样的体系结构的计算机的效率就会由外设决定。根据存储分级,离CPU越远的存储设备,效率就越低(木桶原理

为什么不将所有的设备存储都换成寄存器

造价太高,不是土豪用不起。有了存储器,就能让CPU与外设之间速度不匹配做一定的适配,这样就能以少量的钱获得一台效率不错的计算机。现如今的计算机是性价比的产物

理解数据流动

我在北京登录qq给在南京的小王发消息,我从键盘输入消息到QQ中,因为QQ就是在内存中的,所有CPU可以直接读取QQ的消息,然后经过加密等一系列操作,在通过网卡发送到网络,小王那边通过网卡接收到我的消息,消息进入内存经过CPU解密等一系列操作得到我发送的内容最后输出到显示屏上,这一系列操作就是“数据的流动”。

操作系统(操作系统是什么)

是一个基本的进程管理,称为“操作系统

操作系统:是一款进行软硬件管理的软件

操作系统包括:

内核(进程管理、内存管理,文件管理、驱动管理)

其他程序(例如函数库、shell程序等等)

狭义上来说的操作系统就是“内核”如:windows内核、Linux内核

广义上来说的操作系统如下:

安卓底层是Linux,也就说它最核心的部分用的是Linux内核

如何理解管理

管理例子:校长、辅导员、学生

日常生活人们做的事情就无非就两种:

  1. 决策

  2. 执行

在上述三个身份中,校长是管理者具有决策权,老师是负责执行校长的决策,学生是被管理者。从计算机的角度来看就是:学生是底层硬件,老师是驱动程序,操作系统是校长

身为管理者的校长与身为被管理者的学生,可以不需要见面。

那怎么进行管理?根据被管理者的数据进行管理。

那如何拿到数据?校长通过辅导员(中间层)来获取学生的数据

在计算机中,操作系统靠驱动程序获取硬件的数据

管理层让中间层去获取底层的数据时,要告诉中间层获取什么数据。比如校长让老师去获取学生的姓名,性别,年龄等等,然后根据这些属性定义struct对象,建立一个链表结构,在对这个链表进行增删查改。

在计算机中就是根据硬件的属性定义成struct对象,再建立一个链表结构(只是用链表来举例),最后根据链表对硬件进行增删查改。

将学生先描述成一个结构体,然后组织成一个结构,这一过程叫做“先描述,再组织”,这也是操作系统管理硬件的方法

设计操作系统的目的(为什么设计操作系统)

目的是为了给用户程序提供一个良好的运行环境。

以管理软硬件的为手段

软硬件体系结构:层状结构。如上图

访问操作系统,必须使用系统调用--就是函数,只不过是系统提供的

我们的程序,只要你判断出它访问了硬件,那么它必须贯穿整个操作系统

例如:printf的本质是我把我的数据写到了硬件,也就是显示器。用户使用printf就会调用c语言的标准库,在printf中封装了操作系统的接口,根据接口调用驱动管理,再根据驱动管理,调用驱动程序,最后输出到底层硬件上

库可能在底层封装了系统调用

理解系统调用

操作系统要向上提供服务,但操作系统不相信任何用户或者人。

如何理解服务:

比如printf打印的时候,本质是将字符串显示到显示器这个“硬件”上,在scanf输入时,是将键盘上的硬件数据读入到软件程序里,这一过程就肯定会有操作系统参与,所有操作系统就需要给用户提供访问硬件的能力,这访问硬件的能力提供出来就叫做“服务”

现实生活中,银行就类似操作系统:不相信任何用户或者人,但又要给人提供服务。

去贷款时,不相信人,所以不让我们直接进仓库拿钱,而是通过一个窗口,为人提供服务。在操作系统中,因为不相信用户,所提提供了系统调用

系统调用的本质

用户与操作系统之间,进行某种数据的交互

系统调用

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层使用,这部分有操作系统提供的接口就叫做“系统调用”

因为系统调用在使用上功能比较基础,对用户要求较高,所以有些开发者会对系统调用进行封装,从而形成了库,有了库,就很有利与上层开发者进行二次开发

进程

基本概念

在程序被执行前,是一个被存放在磁盘的可执行程序文件,执行它时,会被加载到内存中,加载进内存的是可执行程序文件的代码和数据。在内存中除去被我们执行的程序,还有操作系统,操作系统会把加载进内存的程序描述成一个结构体对象,然后用链表结构将它组织起来进行管理。而进程指的就是:内核描述的数据结构对象+程序自己的代码和数据==PCB(task_struct+程序自己的代码和数据)

描述进程-PCB

在操作系统学科中,这个结构体被称为:PCB。中文来讲就是:进程控制块。

task_struct--PCB的一种

Linux是一款具体的操作系统,所以Linux中PCB具体的叫做“task_struct”。同时他在内核中是一个结构体,所以也被叫做“任务结构体”

在Linux中描述进程的结构体叫做task_struct。

task_struct是Linux内核的一种数据结构对象,它会被装载到RAM(内存)⾥并且包含着进程的信息。

通过上述所说,OS会把加载进内存的程序描述成一个结构体对象,然后用链表将它们组织起来。所以OS对进程的管理就变成了对链表的增删查改。

所以在CPU对进程进行调度的时候,是先拿到PCB,在通过PCB找到对应的代码和数据再进行调度。不是跳过PCB直接去找对应的代码和数据。

task_ struct的属性

内容分类:

标识符:描述本进程的唯⼀标⽰符,⽤来区别其他进程。

• 状态:任务状态,退出代码,退出信号等。

• 优先级:相对于其他进程的优先级。

• 程序计数器:程序中即将被执⾏的下⼀条指令的地址。

• 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针 • 上下⽂数据:进程执⾏时处理器的寄存器中的数据[休学例⼦,要加图CPU,寄存器]。 • I/O状态信息:包括显⽰的I/O请求,分配给进程的I/O设备和被进程使⽤的⽂件列表。

• 记账信息:可能包括处理器时间总和,使⽤的时钟数总和,时间限制,记账号等。

记账信息:可能包括处理器时间总和,使⽤的时钟数总和,时间限制,记账号等。

• 其他信息

• 具体详细信息后续会介绍

曾经在linux上使用过的指令、工具、自己的程序,运行起来全部都是进程

组织进程

所有运⾏在系统⾥的进程都以task_struct双链表链表的形式存在内核⾥。换句话说:所有的进程都会链入到全局的双链表当中。

查看进程

运行下面这段代码

getpid()函数包含在"sys/types.h"头文件中,它的作用是返回进程的"pid"。这个pid就是进程标识符。所以调用getpid本质是从当前进程的PCB中将pid拷贝出来。

这个函数是一个系统调用,它的返回值是"pid_t",其实也就是“int”

我们可以通过top/ps指令来查看进程

如果要查自己执行起来的进程:ps axj | grep 进程

如果要现实属性则使用:ps axj | head -1;ps axj | grep 进程(也可以ps axj | head -1&&ps axj | grep只是写法不同)

第一行就是刚刚执行的进程,第二行是grep进程。因为:grep自己就是一个指令,当他执行起来去过滤出包含myprocessd进程时,本身也就成为了一个进程,这个进程在执行过滤出包含myprocess的进程的指令。

若只想看到第一行,不想看到grep则使用:ps axj | head -1;ps axj | grep 进程 | grep -v grep

也可以通过查看目录来查看进程ls /proc”。Linux允许用户以文件的形式查看进程。它把内存的数据,以文件的形式呈现出来,让用户动态的查看相关数据。“/proc”就是一个内存级别的文件系统,与磁盘无关。/proc目录下的数字目录表示特定进程目录的pid。

进入一个进程的目录查看内容。查看我们执行的进程8250,发现一个“exe”文件记录了这个进程可执行程序文件的路径,说明一个进程被启动时,知道自己是从哪来的。如果我们把可执行程序文件删掉,会发现这个进程还在跑,因为删掉的只是磁盘中的文件。当程序运行时,已经把可执行程序文件拷贝一份到内存中了,所以至少在目前,删了可执行程序文件对进程不会有什么影响

查看上图,会发现还要一个文件叫“cwd”==current work dir,这个文件记录了进程是从哪个路径下启动的。所以在c语言学习阶段,为什么"fopen"创建文件不传入路径,会默认在当前路径下创建,因为进程会记录自己的“当前路径”。在进程启动时,会在PCB内部维护"cwd"

若要更改“当前路径”,可以使用系统调用“chdir()

# 每隔一秒就查看进程的信息
while :; do ps ajx | head -1 ; ps ajx | grep myprocess | grep -v grep; sleep 1; done

查看父进程pid

getppid();

很容易发现,每次进程启动时,它的pid都不一样,这就好比你考上了“清华”,第一年因为专业不好退学了,第二年又考上了清华。这两年时间,你的学号不可能会一样。第一年可能是“123”,第二年可能就是“432”了。

为什么父进程的pid每次都一样?

查看父进程的pid会发现父进程是"bash",得知命令行启动我们自己的程序时,父进程都是“bash”。在我们每次登录云服务器时,操作系统会给每个登录用户分配一个"bash"。

由此我们知道"bash"是一个进程。

那下面这张图这段字符串是啥?

学了c语言,我们知道当我们printf后接一个scanf程序就会停住等待用户输入,这里也是如此。我们输入的所以命令都是以字符的形式交给了bash。 

启动进程的两种方式:fork()启动,或者是执行命令行启动(执行一个可执行程序文件)

若想杀死一个进程

kill -9 +进程pid或者ctrl +c

用代码创建子进程
fork():
创建一个子进程。两个返回值

若创建成功:返回子进程的pid给父进程,返回0给子进程

若创建失败:返回-1给父进程

验证系统调用

运行后,如果真的创建了一个进程,那么第二句printf会被执行两次。其中一个进程是父进程,另一个是fork创建的子进程

一个进程包括:PCB+它自己的代码和数据。创建一个子进程一般是把父进程的PCB给子进程拷贝一份,所以父子进程里面很多属性值都是一样的,但也有些值是不一样的,如:pid、ppid。

子进程默认会指向父进程的数据与代码。所以子进程会执行父进程之后的代码。如上图第二句printf

为什么fork()给父子返回不同值?

因为在linux下“父:子=1:n”的,一个进程只能拿到自己和父进程的pid,拿不到子进程的pid,并且父进程具有这么多的子进程,需要标识符来标识子进程,所以需要返回子进程的Pid。

为什么一个函数会返回两次?

因为fork()作用是创建子进程的,当创建完子进程后,后续代码会父子进程都执行一遍,所以fork()会返回两次

为什么一个变量(下图的id)即==0又大于0?(进程之间具有独立性)

父子进程,底层是PCB各自独立,代码共享且都只读,数据以写时拷贝的方式各有一份,所以子进程的数据被修改不会影响父进程的数据。因此就达到了返回值即==0又大于0.下图的gval就是如此。(父进程的数据被修改也不会影响子进程,因为它们是以写时拷贝的方式各有一份数据)

进程状态

进程可以有多个状态,打个比方:我们在上课时,状态叫上课中,休息时,叫休息中,睡觉时,叫睡觉中。所以每个人在世界上都是有自己对应的状态。状态决定了我们正在做什么

对于进程状态实际上就是:定义在task_struct结构体中的一个整形变量。

进程状态:操作系统对进程的运行状态进行的描述,这些状态随着进程的执行和外界条件的变化而转换

  • 创建状态:当一个新进程被创建时,它首先处于新建态

  • 就绪状态:当进程已经准备好运行,但还没有被CPU调度执行时,它处于就绪态

  • 运行状态:当CPU调度器选择了一个就绪态的进程,并开始执行它时,该进程处于运行态

  • 阻塞状态:当进程由于某些原因无法继续执行时,如等待I/O操作完成、等待某个事件发生等,它会进入阻塞态。

  • 终止状态:当进程执行完成或者被终止时,它进入终止态。

运行&&阻塞&&挂起

进程多,CPU少,所以进程想在CPU上去运行,本质上是CPU在系统内部维护一个叫“调度队列”的东西。CPU若要调度一个进程,本质上是选择进程的PCB来调度,通过PCB的内存指针,找到对应的代码和数据。

一个CPU只有一个调度队列。在Linux内核里这个调度队列叫做:runqueue。这个调度队列的类型就是“task_struct*”类型。也就是说,runqueue就是一个指针,可以帮我们找到要运行的进程。每一个进程都会链入到一个全局的“双链表中”,runqueue就是指向这个双链表的指针,也就是说“这个双链表既是链表结构,也是队列结构”。

因为双链表遵循队列的从头出,从尾进的规则,所以也被叫做队列。

在操作系统学科中,有一种调度算法既不是Linux的调度算法也不是大部分操作系统的调度算法:FIFO(先进先出)。CPU按照顺序执行调度队列中PCB。越靠前的PCB,说明优先级越高,越靠后的,说明优先级越低。

进程的运行状态

运行状态:进程在调度队列中,进程状态就是running。

running(细分为下面两个种,只不过就绪和运行当作成一种状态,为running):

  • 进程正在被CPU调度运行(运行)

  • 准备好随时被CPU调度(就绪)

阻塞状态:等待某种设备或者资源就绪

如scanf、cin就是在等待键盘硬件就绪,当键盘没有按下时,处于键盘硬件不就绪状态,按下后处于就绪状态

操作系统为了对软硬件进行管理也要创建数据结构。用硬件来举例:

在这个结构体中,包含的就是目标设备的所有属性,也就是说,struct_device可以直间或间接获得我们对应的数据。(将结构体组织成一个设备队列

我们都知道,在内存当中,每一种设备都要对应一种struct_device结构体,当我们读磁盘读网卡的时候如果对应的设备没有就绪,那么我们的进程就要阻塞等待了。

在操作系统中我们如何理解阻塞等待?

我们可以在结构体里加上一个等待队列。

所以我们对应的每一个设备它都有一个等待队列

当CPU运行我们的代码时,需要读取键盘的数据,但此时操作系统发现键盘没有任何活跃状态,那么操作系统便会把这个进程从CPU中拿下来,然后将进程的PCB放到特定的设备等待队列当中(wait_queue)。此时进程不在运行队列当中,就无法被调度,那它的状态就被成为阻塞状态

所有从运行状态到阻塞状态的本质就是:将进从PCB链入的不同的队列当中

若此时按下键盘,操作系统会第一时间获得键盘的数据,并在等待队列中查找键盘的PCB,将它的状态设置为“活跃”,接着查看它的等待队列的指针,不为空,就将

键盘设置为“运行状态”,然后把PCB链入到”运行队列“当中,当被CPU调度时,便把数据读到进程文件当中。

所有从阻塞状态回到运行状态的本质:将设备的PCB链回到运行队列当中

状态的变化,变现之一,就是要在不同的队列中进行流动。本质都是数据结构的增删查改

挂起状态

当进程处于阻塞状态时,又来了几个进程,但此时操作系统的内存不足了,那这时操作系统便会把阻塞状态下的PCB的代码和数据“唤出”到磁盘中(会保留PCB),此时的状态就叫做”挂起状态“。当下次资源就绪时,进程便会被重新唤醒,然后将对应的数据与代码唤入到内存中,在链入到运行队列,等待CPU调度。

从将处于阻塞状态的进程挂起到磁盘中,这种挂起叫阻塞挂起

若操作系统的内存严重不足,则可能会把运行队列的进程挂起到磁盘中,这种挂起叫:运行挂起

linux内核当中链表的话题

linux进程状态(上面为理论部分)

linux进程的状态是被维护在一个叫“task_state_array[]”的数组里

  • R运⾏状态(running):并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏ 队列⾥。

  • S睡眠状态(sleeping):意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。

  • D磁盘休眠状态(Disksleep)有时候也叫不可中断睡眠状态(uninterruptiblesleep),在这个 状态的进程通常会等待IO的结束。

  • T停⽌状态(stopped):可以通过发送SIGSTOP信号给进程来停⽌(T)进程。这个被暂停的 进程可以通过发送SIGCONT信号让进程继续运⾏。

  • X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。

//进程状态用整数表示,这些整数存储在进程的task_struct结构体中
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 */};

查看进程

将下面这段代码执行起来,查看对应的进程

会发现,进程状态一直处于:S+状态。这是因为:代码中有printf语句,在代码执行期间,大部分时间是在等待IO设备就绪。如果进程监视的频繁会发现,进程的状态是在“R”与"S"之间来回切换的。如果要让进程一直处于“R”状态,注释掉printf语句就行。

这表示该进程是在前台执行的(也就是命令行上启动的)。当我们把进程放到后台执行,就没“+”了

“&”表示进程后台执行

S状态

阻塞状态在Linux内核下具体叫“S”,可中断休眠或者浅睡眠,即在等待资源就绪时可被杀掉。

scanf等待用户输入,进程进入阻塞状态。

暂停状态

“T“与”t“。

当用gdb调试代码,并让代码运行到断点处时,该进程就处于”t“状态。

若用户不是通过gdb调试让进程暂停,而是因为收到”SIGSTOP“信号让进程暂停,此时进程就处于”T“状态

D状态(深度睡眠或者不可中断休眠)

也是阻塞状态的一种,只不过该状态的进程不可被杀。

可用dd命令模拟D状态:

dd if=/dev/zero of=~/test.txt bs=4096 count=100000

X状态(结束状态)

死亡状态。

Z状态(僵尸状态-- >结束状态)

在子进程死亡之后,父进程获取子进程相关信息之前的状态叫做”僵尸状态“。

若父进程一直不回收子进程,则子进程的僵尸状态将一直保持,这样会导致内存泄漏的问题

信息存在哪:

task_struct中

模拟z状态:

  #include<stdio.h>#include<unistd.h>#include<sys/types.h>int main(){int count=5;pid_t id =fork();if(id==0){while(count){printf("我是一个子进程,我正在运行:%d,我的pid:%d\n",count,getpid());count--;sleep(1);}}else{while(1){                                                                                                   printf("我是一个父进程,我正在运行...\n");sleep(1);}}return 0;}

监视子进程状态,从S+到Z+状态

内存泄漏问题

例如僵尸进程,如果父进程不回收处于僵尸状态的子进程,那此时的子进程就存在内存泄漏的问题。

但只要进程退出,内存泄漏问题就不存在了。

那什么样的进程具有内存泄漏问题是比较麻烦的?

  • 常驻内存的进程

小知识点:

关于内核结构申请:

创建一个进程,就要创建一taskstruct结构体,销毁一个进程,就要将该结构体的资源释放掉。为了不频繁的创建taskstruct结构体,可以在系统里创建一个叫”废弃的taskstruct-- >unuse"的链表,将要释放的taskstruct保存在该链表中,当要创建其他进程时,就将链表中的task_struct拿出来初始化一下。

这样就形成了一个数据结构对象的缓存,不用一直申请内存,加速了操作系统创建进程和释放进程的速度。在Linux中,该方法叫做:slab

孤儿进程

当子进程的父进程退出后,子进程就会被1号systemd/老系统叫init进程领养,此时,这个被领养的子进程就叫:孤儿进程

 一号进程是谁?

  • 操作系统

  • 如每个用户登录时被服务器分配的bash,就是由一号进程或者一号进程相关的进程分配的。

为什么要领养?

如果不被领养,在子进程进入僵尸状态的时候,它就会因为没有父进程而成为僵尸进程,这样就会造成内存泄漏的问题。

优先级

是什么

是进程得到CPU资源的先后顺序

为什么要有优先级?

主要是因为:目标资源稀缺,导致要通过优先级确认谁先谁后的问题

优先级vs权限

好比在学校食堂吃饭,学生不能去教职工食堂吃饭,这是因为没有权限。当学生到食堂餐厅排队,知道自己能打到饭,只不过是时间问题,这是优先级

  • 优先级:能获得资源,先后的问题

  • 权限:能否得到某种资源的问题

优先级是如何实现的
  • 它就是进程PCB中的一种属性,也就是一种数字(int)

  • 值越低,优先级越高,反之优先级越低

我们现在所使用的操作系统是基于时间片的分时操作系统,考虑公平性,优先级可能发生变化,但变化幅度不会太大。

基于时间片的分时操作系统:cpu调度进程时,会给每个进程分配运行时间,比如10纳秒,让进程在10纳秒时间内运行完,运行完后再调度下一个进程。

进程UID

Linux里识别用户不是通过名字来识别的,而是通过每个名字的用户id来识别的,叫:UID

查看用户id:

  • ls -ln

进程也有UID。在用户创建文件的时候,会记录用户的UID。在启动进程的时候,进程也会把对应的UID保存起来,表明这个进程是谁启动的、

系统是如何知道我访问文件的时候,是拥有者、所属组还是other?

Linux系统中,访问文件实则是进程在访问。进程访问文件的时候,通过自己保存的UID与文件的UID一一对比,拥有者对上了,说明你就是拥有者,所属组对上了,说明你就是所属组,否则就是other。

在Linux系统中,访问任何资源都是进程访问,进程就代表用户。

进程PRI与NI

PRI:进程的优先级,默认:80 ---- >PRI取值范围[60,99]

NI:进程优先级的修正数据,nice值,默认为“0”---- >NI值的取值范围[-20,19]

  • 进程真实的优先级=PRI(默认值)+NI

修改优先级:top指令,进入top指令后,在输入r(renice),在输入要修改的进程的PID,再输入NI值。

  • 修改优先级时,每次都是以PRI的默认值为基础,+-NI值进行修改的(为了方便修改,减少操作步骤。不然不记得之前的优先级了,还得重新查看一下,麻烦)

优先级设立不合理,会导致优先级低的进程长时间得不到CPU资源,进而导致:进程饥饿

进程的竞争、独立、并行、并发

  • 竞争性:系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为 了⾼效完成任务,更合理竞争相关资源,便具有了优先级、权限

  • 独⽴性:多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰

  • 并⾏:多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏

  • 并发:多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称之为并发

进程切换

死循环进程如何运行

  • 一旦一个进程占有CPU,不会把自己的代码跑完。因为存在一个叫时间片的的东西。

  • 死循环进程,不会卡死系统,不会一直占有CPU

CPU与寄存器

当cpu去执行一个进程的时候,这个时候执行的过程就和PCB的关系不大了,因为它执行的时候主要是执行访问对应进程的代码和数据,也就是说我们的CPU它其实是直接访问我们当前进程的代码和数据

而我们的CPU并不是直接把代码数据一股脑全塞到cpu里,而是需要一个个的来处理,所以在我们CPU里就存在很多寄存器,而每一个寄存器都会有它对应的功能

1. 寄存器CPU内部的临时空间

2. 寄存器就是寄存器,寄存器不等于寄存器里面的代码和数据

关于寄存器,可以回顾函数栈帧的创建与销毁。后面课程也会讲到一些寄存器,现阶段了解一下就行

如何切换

例子:小王,在大学期间去当兵,要先通知学校,让学校保留学籍,在通知学校前,得先和导员讲。等小王当兵回来,在恢复学继。

  • 学校---CPU

  • 导员---调度器

  • 小王---进程

  • 学籍---进程运行时的临时数据,即保存在寄存器里的内容(当前进程的上下文数据)

  • 保留学籍---保存当前进程的上下文数据

  • 恢复学继---恢复进程的上下文数据,讲保存起来的数据,恢复到CPU内的寄存器中

  • 去当兵---从CPU上剥离下来

具体切换

这里的把数据带走其实就是把数据拷贝出来,然后再把当前进程(A)放到队列的结尾,然后再将新进程(B)放上去,进程B把进程A被拷贝的数据直接覆盖,当到A的时候,再根据我们之前拷贝的上下文数据内容,恢复上下文数据继续运行就可以了

进程切换最核心的部分:保存和恢复当前进程的硬件上下文的数据,即CPU内寄存器的内容

当前进程把自己的进程硬件上下文数据保存在哪里?

  • 保存在进程的taskstruct里面的一个tssstruct tss对象中

CPU如何知道一个进程是否被调度了呢?

  • 在task_struct里,有一个变量标记了进程是否为第一次调度,一次都没被调度,则指为0,否则就为1

CPU如何调度进程?

  • 在内核层面上有一个全局的指针(struct task_struct* current),这个指针永远都会指向当前进程,即,当选择好要调度的进程时,便会把当前进程的地址填入到这个指针里,CPU往后调度进程时,直接去找current指针中保存的进程就可以了
  • 架构不同,current的位置也不同。有的架构下会把该指针放到寄存器内部,为的就是加速CPU找对应的进程

看 ⼀下Linux内核0.11代码

时间⽚:当代计算机都是分时操作系统,没有进程都有它合适的时间⽚(其实就是⼀个计数 器)。时间⽚到达,进程就被操作系统从CPU中剥离下来

Linux2.6内核进程O(1)调度队列

调度和切换共同构成了调度器,即:调度器既要将进程保存切换下来,又要选择一个进程进行调度。

操作系统有:

  • 分时操作系统---linux其实是既支持分时又支持实时,只不过有些linux分时系统被编译内核裁掉或者关掉了

  • 实时操作系统(如汽车的车载系统)

    1. 工业领域

    2. 制造领域

活跃队列

时间片还没有结束的所有进程都按照优先级放在该队列

  • nr_active:总共有多少个运行状态的进程

  • queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!

  • 先看蓝色框内的内容,有个叫 queue[140] 的数组,这里的 queue 数组表示活动状态进程的进程队列

  • 其中在queue数组中,索引【0~99】号下标我们是不用的,这是因为0-99号下标对应的是 实时进程的优先级,实时进程是内核里更加重要的进程,放在前100位由操作系统控制,避免系统抢占的情况。

  • 所以我们只剩下 [100-139] 这个范围可操控,其实这也就和我们优先级的可控范围大小相同,正好对应队列的四十个空位,而OS通过某种映射关系,将可控优先级映射到数组 【100-139】的下标

比如:一个进程的优先级60,那么它将链入到下标为100的位置(在数组中有一个taskstruct的指针,当进程链入某位置时其实就是让这个指针指向这个进程),如果有相同优先级的进程,则链入到它的后面,优先级比它低的,则会往后面的位置存放。系统在调度进程时,则就是遍历这个数组,若taskstruct指针为空,则往后遍历,不为空,就执行该下标的进程。在执行某下标位置的进程时,则采用先进先出的调度方法。

进程如何知道自己该链入到哪个下标处?

当前的优先级-60+(140-40)。

> queue表的本质就是一个hash表

宏观遍历数组检查,局部先进先出

调度器如何快速挑选进程?

bitmap[5]位图,bitmap是unsigned int类型的,有32比特,同时在该位图中有5个元素,所有共160比特。在bit位上不是1就是0,所有在挑选进程的时候,只需先通过Bitmap来挑选队列,若该bit位上的值为1,则说明该队列不为空,CPU就调度该队列上的进程若为0则说明该队列为空,就继续判断其他bit位的值

那么为什么要用位图?

  • 遍历整个队列的时间开销要远大于查找位图。

  • 所以,bitmap是用来检测队列中是否有进程,检测对应的比特位是否为1!

过期队列

在红色框中的三项属性与蓝色框中的三项属性完全相同,也就是另外一个队列,被称为——过期队列

http://www.xdnf.cn/news/885853.html

相关文章:

  • 2021 RoboCom 世界机器人开发者大赛-高职组(初赛)解题报告 | 珂学家
  • Spring中@Primary注解的作用与使用
  • Dockerfile实践java项目
  • 哈希算法实战全景:安全加密到分布式系统的“核心引擎”
  • 25_06_05Ubuntu系统root密码破解
  • Vite模块联邦(vite-plugin-federation)实现去中心化微前端后台管理系统架构
  • ROS:pcd点云转为路径规划的pgm文件和yaml文件
  • PHP的namespace
  • 第十三节:第五部分:集合框架:集合嵌套
  • ubuntu24.04 使用apt指令只下载不安装软件
  • BENTLY模块特价型号3300/16-14-01-03-00-00-01找小游、主要应用领域
  • ArcGIS Pro 3.4 二次开发 - 公共设施网络
  • windows server2019 不成功的部署docker经历
  • python项目如何创建docker环境
  • 无 sudo 权限下 Conda 安装 GCC 全攻略:虚拟环境适配、版本冲突解决与实战指南
  • 负载均衡将https请求转发后端http服务报错:The plain HTTP request was sent to HTTPS port
  • RAG:大模型微调的革命性增强——检索增强生成技术深度解析
  • Android15 launcher3
  • proteus8安装教程
  • PLM软件:如何打通产品研发全生命周期的“任督二脉”?
  • N2语法 強調、限定
  • C获取unix操作系统的信息
  • Cursor 1.0 版本 GitHub MCP 全面指南:从安装到工作流增强
  • 代码训练LeetCode(24)数组乘积
  • 【JavaEE】Spring Boot项目创建
  • STM32手册上标称的18MHz GPIO翻转速度和你实际测量到的速度之间的差异是预期之内且合理的
  • 量子计算+AI:特征选择与神经网络优化创新应用
  • 【汇编逆向系列】六、函数调用包含多个参数之多个整型-参数压栈顺序,rcx,rdx,r8,r9寄存器
  • 三表查询SQL怎么写?----小白初学+案例引入
  • 【Linux网络篇】:从HTTP到HTTPS协议---加密原理升级与安全机制的全面解析