现代计算机模型是基于-「冯诺依曼计算机模型」
计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存储器中取出数据进行指定的运算和逻辑操作等加工,然后再按地址把结果送到内存中去,接下来,再取出第二条指令,在控制器的指挥下完成规定操作,依此进行下去。直至遇到停止指令
程序与数据一样存贮,按程序编排的顺序,一步一步地取出指令,自动地完成指令规定的操作是计算机最基本的工作模型
「计算机五大核心组成部分」
控制器:是整个计算机的中枢神经,其功能是对程序规定的控制信息进行解释,根据其要求进行控制,调度程序、数据、地址,协调计算机各部分工作及内存与外设的访问等。
运算器:运算器的功能是对数据进行各种算术运算和逻辑运算,即对数据进行加工处理。
存储器:存储器的功能是存储程序、数据和各种信号、命令等信息,并在需要时提供这些信息。
输入:输入设备是计算机的重要组成部分,输入设备与输出设备合你为外部设备,简称外设,输入设备的作用是将程序、原始数据、文字、字符、控制命令或现场采集的数据等信息输入到计算机。
常见的输入设备有键盘、鼠标器、光电输入机、磁带机、磁盘机、光盘机等。
输出:输出设备与输入设备同样是计算机的重要组成部分,它把外算机的中间结果或最后结果、机内的各种数据符号及文字或各种控制信号等信息输出出来,微机常用的输出设备有显示终端CRT、打印机、激光印字机、绘图仪及磁带、光盘机等。
「计算机结构分成以下 5 个部分:」
输入设备;输出设备;内存;中央处理器;总线。
在冯诺依曼模型中,程序和数据被存储在一个被称作内存的线性排列存储区域。
存储的数据单位是一个二进制位,英文是 bit,最小的存储单位叫作字节,也就是 8 位,英文是 byte,每一个字节都对应一个内存地址。
内存地址由 0 开始编号,比如第 1 个地址是 0,第 2 个地址是 1, 然后自增排列,最后一个地址是内存中的字节数减 1。
我们通常说的内存都是随机存取器,也就是读取任何一个地址数据的速度是一样的,写入任何一个地址数据的速度也是一样的。
冯诺依曼模型中 CPU 负责控制和计算,为了方便计算较大的数值,CPU 每次可以计算多个字节的数据。
这里的 32 和 64,称作 CPU 的位宽。
「为什么 CPU 要这样设计呢?」
因为一个 byte 最大的表示范围就是 0~255。
比如要计算 20000*50,就超出了byte 最大的表示范围了。
因此,CPU 需要支持多个 byte 一起计算,当然,CPU 位数越大,可以计算的数值就越大,但是在现实生活中不一定需要计算这么大的数值,比如说 32 位 CPU 能计算的最大整数是 4294967295,这已经非常大了。
「控制单元和逻辑运算单元」
CPU 中有一个控制单元专门负责控制 CPU 工作;还有逻辑运算单元专门负责计算。
「寄存器」
CPU 要进行计算,比如最简单的加和两个数字时,因为 CPU 离内存太远,所以需要一种离自己近的存储来存储将要被计算的数字。
这种存储就是寄存器,寄存器就在 CPU 里,控制单元和逻辑运算单元非常近,因此速度很快。
常见的寄存器种类:
现代CPU为了提升执行效率,减少CPU与内存的交互(交互影响CPU效率),一般在CPU上集成了多级缓存架构
「CPU缓存」即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器
由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用,减少CPU的等待时间,提高了系统的效率,具体包括以下几种:
「L1-Cache」
L1- 缓存在 CPU 中,相比寄存器,虽然它的位置距离 CPU 核心更远,但造价更低,通常 L1-Cache 大小在几十 Kb 到几百 Kb 不等,读写速度在 2~4 个 CPU 时钟周期。
「L2-Cache」
L2- 缓存也在 CPU 中,位置比 L1- 缓存距离 CPU 核心更远,它的大小比 L1-Cache 更大,具体大小要看 CPU 型号,有 2M 的,也有更小或者更大的,速度在 10~20 个 CPU 周期。
「L3-Cache」
L3- 缓存同样在 CPU 中,位置比 L2- 缓存距离 CPU 核心更远,大小通常比 L2-Cache 更大,读写速度在 20~60 个 CPU 周期。
L3 缓存大小也是看型号的,比如 i9 CPU 有 512KB L1 Cache;有 2MB L2 Cache; 有16MB L3 Cache。
当 CPU 需要内存中某个数据的时候,如果寄存器中有这个数据,我们可以直接使用;如果寄存器中没有这个数据,我们就要先查询 L1 缓存;L1 中没有,再查询 L2 缓存;L2 中没有再查询 L3 缓存;L3 中没有,再去内存中拿。
「总结:」
存储器存储空间大小:内存>L3>L2>L1>寄存器;
存储器速度快慢排序:寄存器>L1>L2>L3>内存;
「CPU运行安全等级」
CPU有4个运行级别,分别为:
ring0只给操作系统用,ring3谁都能用。
ring0是指CPU的运行级别,是最高级别,ring1次之,ring2更次之……
系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。
应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。
如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。
这个过程也称作用户态和内核态的切换。
在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理
「时间局部性(Temporal Locality):」
如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。
比如循环、递归、方法的反复调用等。
「空间局部性(Spatial Locality):」
如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。
比如顺序执行的代码、连续创建的两个对象、数组等。
程序实际上是一条一条指令,所以程序的运行过程就是把每一条指令一步一步的执行起来,负责执行指令的就是 CPU 了。
「那 CPU 执行程序的过程如下:」
简单总结一下就是,一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。
CPU 从程序计数器读取指令、到执行、再到下一条指令,这个过程会不断循环,直到程序执行结束,这个不断循环的过程被称为 「CPU 的指令周期」。
CPU 和内存以及其他设备之间,也需要通信,因此我们用一种特殊的设备进行控制,就是总线。
当 CPU 要读写内存数据的时候,一般需要通过两个总线:
输入设备向计算机输入数据,计算机经过计算,将结果通过输出设备向外界传达。
如果输入设备、输出设备想要和 CPU 进行交互,比如说用户按键需要 CPU 响应,这时候就需要用到控制总线。
「中断的类型」
中断可以由 CPU 指令直接触发,这种主动触发的中断,叫作同步中断。
❝
同步中断有几种情况。
❞
另一部分中断不是由 CPU 直接触发,是因为需要响应外部的通知,比如响应键盘、鼠标等设备而触发的中断,这种中断我们称为异步中断。
CPU 通常都支持设置一个中断屏蔽位(一个寄存器),设置为 1 之后 CPU 暂时就不再响应中断。
对于键盘鼠标输入,比如陷阱、错误、异常等情况,会被临时屏蔽。
但是对于一些特别重要的中断,比如 CPU 故障导致的掉电中断,还是会正常触发。
「可以被屏蔽的中断我们称为可屏蔽中断,多数中断都是可屏蔽中断。」
「什么是用户态和内核态」
Kernel 运行在超级权限模式下,所以拥有很高的权限。
按照权限管理的原则,多数应用程序应该运行在最小权限下。
因此,很多操作系统,将内存分成了两个区域:
用户空间中的代码被限制了只能使用一个局部的内存空间,我们说这些程序在用户态 执行。
内核空间中的代码可以访问所有内存,我们称这些程序在内核态 执行。
❝
按照级别分:
❞
当程序运行在0级特权级上时,就可以称之为运行在内核态
当程序运行在3级特权级上时,就可以称之为运行在用户态
运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。
当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态(比如操作硬件)
「这两种状态的主要差别」
处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的
处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。
「为什么要有用户态和内核态」
由于需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者获取外围设备的数据,并发送到网络
「用户态与内核态的切换」
所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据,或者从键盘获取输入等,而唯一可以做这些事情的就是操作系统,所以此时程序就需要先操作系统请求以程序的名义来执行这些操作
「用户态和内核态的转换」
❝
系统调用
❞
用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如fork()实际上就是执行了一个创建新进程的系统调用
而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断
「举例:」
如上图所示:内核程序执行在内核态(Kernal Mode),用户程序执行在用户态(User Mode)。
当发生系统调用时,用户态的程序发起系统调用,因为系统调用中牵扯特权指令,用户态程序权限不足,因此会中断执行,也就是 Trap(Trap 是一种中断)。
发生中断后,当前 CPU 执行的程序会中断,跳转到中断处理程序,内核程序开始执行,也就是开始处理系统调用。
内核处理完成后,主动触发 Trap,这样会再次发生中断,切换回用户态工作。
❝
异常
❞
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常
❝
外围设备的中断
❞
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换
比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等
线程:系统分配处理器时间资源的基本单元,是程序执行的最小单位
线程可以看做轻量级的进程,共享内存空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
进程可以通过 API 创建用户态的线程,也可以通过系统调用创建内核态的线程。
用户态线程也称作用户级线程,操作系统内核并不知道它的存在,它完全是在用户空间中创建。
用户级线程有很多优势,比如:
但是这种线程也有很多的缺点:
操作系统无法针对线程调度进行优化:当一个进程的一个用户态线程阻塞(Block)了,操作系统无法及时发现和处理阻塞问题,它不会更换执行其他线程,从而造成资源浪费。
内核态线程也称作内核级线程(Kernel Level Thread),这种线程执行在内核态,可以通过系统调用创造一个内核级线程。
内核级线程有很多优势:
当然内核线程也有一些缺点:
「用户态线程和内核态线程之间的映射关系」
如果有一个用户态的进程,它下面有多个线程,如果这个进程想要执行下面的某一个线程,应该如何做呢?
❝
这时,比较常见的一种方式,就是将需要执行的程序,让一个内核线程去执行。
❞
毕竟,内核线程是真正的线程,因为它会分配到 CPU 的执行资源。
如果一个进程所有的线程都要自己调度,相当于在进程的主线程中实现分时算法调度每一个线程,也就是所有线程都用操作系统分配给主线程的时间片段执行。
❝
这种做法,相当于操作系统调度进程的主线程;进程的主线程进行二级调度,调度自己内部的线程。
❞
这样操作劣势非常明显,比如无法利用多核优势,每个线程调度分配到的时间较少,而且这种线程在阻塞场景下会直接交出整个进程的执行权限。
由此可见,用户态线程创建成本低,问题明显,不可以利用多核。
内核态线程,创建成本高,可以利用多核,切换速度慢。
因此通常我们会在内核中预先创建一些线程,并反复利用这些线程。
协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
「子程序」
或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。
「协程的特点在于是一个线程执行,那和多线程比,协程有何优势?」
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。
如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
在系统中正在运行的一个应用程序;程序一旦运行就是进程;是资源分配的最小单位。
在操作系统中能同时运行多个进程;
开机的时候,磁盘的内核镜像被导入内存作为一个执行副本,成为内核进程。
进程可以分成「用户态进程和内核态进程」两类,用户态进程通常是应用程序的副本,内核态进程就是内核本身的进程。
如果用户态进程需要申请资源,比如内存,可以通过系统调用向内核申请。
每个进程都有独立的内存空间,存放代码和数据段等,程序之间的切换会有较大的开销;
「分时和调度」
每个进程在执行时都会获得操作系统分配的一个时间片段,如果超出这个时间,就会轮到下一个进程(线程)执行。
❝
注意,现代操作系统都是直接调度线程,不会调度进程。
❞
「分配时间片段」
如下图所示,进程 1 需要 2 个时间片段,进程 2 只有 1 个时间片段,进程 3 需要 3 个时间片段。
因此当进程 1 执行到一半时,会先挂起,然后进程 2 开始执行;进程 2 一次可以执行完,然后进程 3 开始执行,不过进程 3 一次执行不完,在执行了 1 个时间片段后,进程 1 开始执行;就这样如此周而复始,这个就是分时技术。
用户想要创建一个进程,最直接的方法就是从命令行执行一个程序,或者双击打开一个应用,但对于程序员而言,显然需要更好的设计。
首先,应该有 API 打开应用,比如可以通过函数打开某个应用;
另一方面,如果程序员希望执行完一段代价昂贵的初始化过程后,将当前程序的状态复制好几份,变成一个个单独执行的进程,那么操作系统提供了 fork 指令。
也就是说,每次 fork 会多创造一个克隆的进程,这个克隆的进程,所有状态都和原来的进程一样,但是会有自己的地址空间。
如果要创造 2 个克隆进程,就要 fork 两次。
❝
那如果我就是想启动一个新的程序呢?
❞
操作系统提供了启动新程序的 API。
如果我就是想用一个新进程执行一小段程序,比如说每次服务端收到客户端的请求时,我都想用一个进程去处理这个请求。
如果是这种情况,建议你不要单独启动进程,而是使用线程。
因为进程的创建成本实在太高了,因此不建议用来做这样的事情:要创建条目、要分配内存,特别是还要在内存中形成一个个段,分成不同的区域。所以通常,我们更倾向于多创建线程。
不同程序语言会自己提供创建线程的 API,比如 Java 有 Thread 类;go 有 go-routine(注意不是协程,是线程)。