冯诺依曼体系结构
我们常见的计算机如:笔记本。不常见的计算机如:服务器。大部分都遵守冯诺依曼体系。
截⾄⽬前,我们所认识的计算机,都是由⼀个个的硬件组件组成:
- 输入(input)设备:键盘,鼠标、网卡、磁盘、摄像头、话筒等
- 输出(Output)设备:显示器、打印机、网卡、磁盘等
- 中央处理器(CPU):模型简化后就是运算器和控制器
Cache:
高速缓存存储器(不是冯诺依曼体系结构必要的组成部分)
RAM:
随机存取存储器。冯诺依曼体系结构中的存储器部分的一种。用于临时存储正在使用的程序和数据,是CPU能够直接访问的存储设备
ROM:
只读存储器。冯诺依曼体系结构中的存储器部分的一种。用于存储启动计算机时所需的固件程序等不易更改的信息。
CPU:
中央处理器。
程序要运行,必须先加载到内存,程序运行之前在磁盘中(因为程序就是编译好的在特定路径下的二进制文件)。所有程序是从”外设“加载到“内存”的。
在计算机中,数据流动的本质:实际是将数据从一个设备拷贝到另一个设备。所以,冯诺依曼体系结构的效率由:设备的“拷贝”效率决定
关于冯诺依曼,必须强调几点:
- 存储器指的是”内存“。而常说的磁盘(或者叫硬盘)则是”外存“
- 不考虑缓存情况,这⾥的CPU能且只能对内存进⾏读写,不能访问外设(输⼊或输出设备)
- 外设(输⼊或输出设备)要输⼊或者输出数据,也只能写⼊内存或者从内存中读取。
- ⼀句话,所有设备都只能直接和内存打交道。
下面是存储分级图:
为什么计算机的体系结构不是如下这样?
这样的体系结构会存在当CPU处理完数据后,输出设备还没把数据输出完,当CPU要从输入设备获取数据时,又需要等上一段时间。这样的体系结构的计算机的效率就会由外设决定。根据存储分级,离CPU越远的存储设备,效率就越低(木桶原理)
为什么不将所有的设备存储都换成寄存器
造价太高,不是土豪用不起。有了存储器,就能让CPU与外设之间速度不匹配做一定的适配,这样就能以少量的钱获得一台效率不错的计算机。现如今的计算机是性价比的产物
理解数据流动
我在北京登录qq给在南京的小王发消息,我从键盘输入消息到QQ中,因为QQ就是在内存中的,所有CPU可以直接读取QQ的消息,然后经过加密等一系列操作,在通过网卡发送到网络,小王那边通过网卡接收到我的消息,消息进入内存经过CPU解密等一系列操作得到我发送的内容最后输出到显示屏上,这一系列操作就是“数据的流动”。
操作系统(操作系统是什么)
是一个基本的进程管理,称为“操作系统”
操作系统:是一款进行软硬件管理的软件
操作系统包括:
内核(进程管理、内存管理,文件管理、驱动管理)
其他程序(例如函数库、shell程序等等)
狭义上来说的操作系统就是“内核”如:windows内核、Linux内核
广义上来说的操作系统如下:
安卓底层是Linux,也就说它最核心的部分用的是Linux内核
如何理解管理
管理例子:校长、辅导员、学生
日常生活人们做的事情就无非就两种:
决策
执行
在上述三个身份中,校长是管理者具有决策权,老师是负责执行校长的决策,学生是被管理者。从计算机的角度来看就是:学生是底层硬件,老师是驱动程序,操作系统是校长
身为管理者的校长与身为被管理者的学生,可以不需要见面。
那怎么进行管理?根据被管理者的数据进行管理。
那如何拿到数据?校长通过辅导员(中间层)来获取学生的数据
在计算机中,操作系统靠驱动程序获取硬件的数据
管理层让中间层去获取底层的数据时,要告诉中间层获取什么数据。比如校长让老师去获取学生的姓名,性别,年龄等等,然后根据这些属性定义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分时系统被编译内核裁掉或者关掉了
实时操作系统(如汽车的车载系统)
工业领域
制造领域
活跃队列
时间片还没有结束的所有进程都按照优先级放在该队列
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!
过期队列
在红色框中的三项属性与蓝色框中的三项属性完全相同,也就是另外一个队列,被称为——过期队列