操作系统学习——何为虚拟

参考链接:

《现代操作系统(第三版)》
《计算机硬件基础》
《开机启动过程》
《Motherboard Chipsets and the Memory Map》
《什么是操作系统?》
《分段 分页 虚拟内存空间 逻辑地址 物理地址》

什么是操作系统

我们都知道电脑是由硬件和软件组成的,我们可以使用软件来操作和控制硬件资源,但是硬件是一直在更新迭代并且变化的,怎么来保证应用软件可以不随着硬件的变化而被淘汰呢?

计算机抽象结构

操作系统是一种运行在内核态的软件,可以直接操作硬件并管理内存和进程。而应用程序属于用户态,不直接操作硬件,操作系统暴露给用户态的是一种抽象的硬件接口。

如何做到这种抽象呢?一个进程可以让一个程序感觉自己独立运行在一个环境中,一个程序运行大致需要做:

  1. CPU要执行程序指定的CPU指令
  2. 执行的过程中必然要访问内存
  3. 执行的过程可能要与人交互

所以一个进程也围绕着虚拟出一个CPU, 一份内存展开。

怎么虚拟出一个CPU呢? 好办, CPU运行速度比人能察觉的快得多. 它可以在两个进程中间切换, 这一个瞬间给你显示网页, 下一个瞬间给你播放音乐. 对,事实就是这样的, 你的CPU在负责给你唱歌的时候,只是在数十个任务中时不时唱一句, 但是你会觉得音乐是连续的。

关于如何去虚拟内存下面介绍。

计算机硬件

一台计算机由下列五部分组成:运算器、控制器、存储器、输入设备和输出设备。

计算机组成

其中运算器和控制器组成了CPU;存储器包括内存、硬盘等等。而输入设备是人向计算机输送命令的设备,主要有鼠标和键盘。输出设备主要有显示屏。而通常我们的软件是放在硬盘里的,因为断电后不会丢失,当点击某个软件后,此时系统将软件运行数据放入内存中,而CPU会从内存中取得数据,之后软件启动。

计算机主板

CPU

CPU 是电脑的大脑,任何命令的执行都需要经过CPU。由于CPU访问内存的时间比CPU执行指令的时间慢的多,所以CPU内部有寄存器来保存一些常用到的数据,如变量和临时数据等。

寄存器分为以下几类:

  1. 通用寄存器:用来用来保存变量和临时结果。
  2. 程序计数器:由于CPU在一个时刻只能执行一个命令。且CPU是分时去执行任务的,在用户看来,CPU可以在同一个时间并行处理多个任务。当CPU处理完一个任务时,需要在切换到下一个任务,程序计数器就记录着下一个任务的内存地址。
  3. 堆栈指针:堆栈(先进后出),队列(先进先出),由于CPU在同一时间只能处理一个任务,所以当多个任务到来时,将这些任务放在堆栈中,CPU每次运行完任务时,都会从堆栈中取出下一个任务继续运行。
  4. 程序状态寄存器PSW:这个寄存器包含了条码位(由比较指令设置)、CPU优先级、模式(用户态或内核态),以及各种其他控制位。用户通常读入整个PSW,但是只对其中少量的字段写入。用户在运行程序时只能调用系统层面的接口,应用程序不会操作硬件,这个模式为用户态,操作系统操作硬件为内核态。所以软件在运行时要在用户态和内核态之间切换。用户程序必须使用系统调用(system call),系统调用陷入内核并调用操作系统,TRAP指令把用户态切换成内核态,并启用操作系统从而获得服务。

CPU实物图

CPU的针脚连接到主板的前端总线(Front-end Bus)上,进而与北桥芯片组相连。不同的针脚用途不同:有传送物理内存地址的,有传送数据和指令的,也有用来传送中断信号的。

CPU运行有三个模式,实模式,32位保护模式以及64位保护模式。实模式下CPU支持最大1MB内存寻址,32位CPU支持最大4GB内存寻址。

CPU在保护模式下执行程序的时候访问的内存地址实际上是逻辑地址(logical address),必须由MMU进行翻译成物理地址之后才能发往总线。

主板结构图

北桥会收到来自CPU的一系列物理内存地址请求,北桥需要根据内存映射(Memory Map)决定应该这些地址是应该发往RAM,Video Card,还是南桥芯片组。Linux系统下可以查看/proc/iomem文件来列出内存映射图。

内存映射图

具体物理内存地址和各区域范围根据不同芯片组和主板型号而定。棕色区域是从内存中划出的部分,注意到外设的地址也需要占用内存地址空间,这就是为什么32位操作系统安装了4GB内存后可用空间不到4GB的原因(除非使用Physical Address Extension技术)。
不过,CPU在64位保护模式下运行时,这些被硬件占用的内存地址空间可以通过映射到比实际RAM更大的内存地址这种方式来重新获得访问。这就是回收内存(reclaiming memory)技术。它需要主板芯片组的支持。

存储器

存储器包括:寄存器 缓存 内存 闪存(固态硬盘) 磁盘(机械硬盘) CMOS 磁带 虚拟内存。从左到又,速度变慢,容量变大。其中寄存器直接和CPU打交道,存储着一些CPU需要用到的数据,速度最快。

存储器分类

当一台机器有多个CPU时,每个CPU都需要知道对方的运行状态。此时缓存用来传输状态信息。内存通常称为随机访问存储RAM,就是我们通常所说的内存,容量一直在不断攀升,所有不能再高速缓存中找到的,都会到主存中找,主存是易失性存储,断电后数据全部消失。闪存的存储的速度比机械硬盘要快,且断电后数据不会消失,常常用在固态硬盘和数码相机的胶卷中。

还有一类存储器就是CMOS,它是易失性的,许多计算机利用CMOS存储器来保持当前时间和日期。CMOS存储器和递增时间的电路由一小块电池驱动,所以,即使计算机没有加电,时间也仍然可以正确地更新,除此之外CMOS还可以保存配置的参数,比如,哪一个是启动磁盘等,之所以采用CMOS是因为它耗电非常少,一块工厂原装电池往往能使用若干年,但是当电池失效时,相关的配置和时间等都将丢失。

虚拟内存

假设我们写了一段程序

1
2
3
4
5
6
7
int main() {
int a = 1;
int b = 2;
int c = a + b;
printf(%d, c);
return 0;
}

上面的程序经过编译大概变成了这样的CPU指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化a和b , mov指令把后一个操作数的值复制给前一个操作数
Mov a, 1
Mov b, 2

// %开头的是CPU寄存器, 你可能还记得它是最快最小的存储设备, 我们要把a和b从内存里拿出来, 放进寄存器里
// 接下来Add指令才能工作
Mov %eax, a
Mov %ecx, b

// 加两个寄存器的值, 和写入前一个寄存器中, 在这里是写入eax寄存器
Add %eax, %ecx

// 把这个值写回内存
Mov c, %eax

//打印, 即把内存里的值送到显示器

这个时候我们试着想一下,你和小明两个人写了一个同样的程序,都需要访问内存的同一个地址,事实上这样是没问题的,这是为什么呢?

程序所要使用的内存会被映射为虚拟内存: 物理内存地址 = F (虚拟内存地址)

A和B都在访问自己的0x00, 但是由于他们的映射关系F不同, 这两个访问会访问不同的物理地址(内存的真正的地址)。

虚拟内存空间是系统的一种技术,当程序被载入内存时,运用虚拟内存空间技术让程序误认为自己目前独占电脑内存,能够占用电脑所有的内存,访问所有内存地址。

32位系统程序的指针为32位(4字节),2^32 = 4GB,也就是说指针可以取值的方法有2^32种,可以访问2^32地址。这也就为什么有种说法:32位系统支持装最高4g内存。当程序载入内存后,系统为程序赋予4GB虚拟空间。而程序理解在虚拟空间中的地址就是逻辑地址。但是逻辑地址只是一种假象,并不是指系统真的为程序分配了4GB内存。如下图,程序载入虚拟内存空间:

程序载入虚拟内存

上图中程序的.text段基址是0x08048000,0x08048000就是虚拟地址,每个被载入内存的程序都有可能基址是0x08048000。这是运用了虚拟内存空间技术,实际中是不可能存在的,如果每个程序都使用真实地址0x08048000内存,那两个程序就互相干预,导致数据的混乱。

那是不是这 4GB 的虚拟地址空间应用程序可以随意使用呢? Windows 系统中,这个虚拟地址空间被分成了 4 部分: NULL 指针区、用户区、64KB 禁入区、内核区等。

  1. NULL指针分区是NULL指针的地址范围。对该区域的读写将引发访问违规。
  2. 用户分区是应用程序能使用的,大约 2GB 左右。 用户分区又进行了详细的分区。PE文件结构对程序进行了分区
  3. 禁止访问分区只有在win2000中有。这个分区是用户分区和内核分区之间的一个隔离带,目的是为了防止用户程序违规访问内核分区。
  4. 内核方式分区对用户的程序来说是禁止访问的,操作系统的代码在此。内核对象也驻留在此。

实际上程序载入内存时并不是一次性全部载入内存。程序运行时存在局部性现象,就是说程序在短时间内只会运行某一局部代码。因此仅需将那些当前需要的少数页面或段载入内存即可运行,但系统继续往下运行,发现缺页或缺段就会触发中断请求,由操作系统将程序请求的页或段载入内存,继续运行。

段式管理

段式管理是比较容易想到的方案. 在这个方案基于一个简单的映射关系:物理地址 = 虚拟地址 + 基地址偏移

每个进程访问的虚拟地址只要加上基地址偏移就能得到数据在内存中物理地址。

为了高效实现, 我们需要借助两个特殊硬件的帮助。

  • 段基址寄存器: 寄存器里保存一个进程的基地址偏移量, 每次CPU执行到访问内存的CPU指令的时候, CPU自动加上基地址偏移, 这样就实现了虚拟地址到物理地址的转换。
  • 段长度寄存器:这个寄存器是保证进程独立运行的, 每个进程都要申请好自己要使用的内存最大值, 保存在这个寄存器里, 接下来如果CPU在执行一条访问内存的CPU指令的时候发现该指令在访问的地址超过最大值, CPU拒绝执行这条指令。

段式管理

页式管理

段式管理是建立在运行前申请和分配内存机制之上的.如果你想在进程运行的过程中申请内存, 段式管理就会变得很困难。

页式管理把每4KB的内存空间划分为一个页, 从内存0x00处开始给页编号. 即0x00000000–0x00000FFF是第0页, 0x00001000-0x00001FFF是第1页。

操作系统维护着一个链表, 表上的是还空闲的物理页(每一个节点代表一个物理页), 每一次进程申请内存(无论是运行前还是运行中) , 操作系统计算进程需要几个页, 从空闲链表上取下相应数目的物理页, 把映射关系保存到对应进程里。

当进程访问某个逻辑地址中数据,分页系统地址变换机构,用页号检索页表,如果页号大于或等于页表长度,则产生地址越界中断。否则将页表初始地址与页号和页表项长度乘积相加得到页表物理号的地址,获取到物理块号。再将物理块得到的地址与页内地址组合得到物理地址。

页式管理

如果选择的页面太小,虽然可以提高内存利用率,但是每个进程使用过多页面,导致页表过长。降低页面换入换出效率。

进程和线程

一个进程是一个应用程序独立运行需要的虚拟环境。

比较术语的说法是: 进程是操作系统资源分配的最小单位. (什么是资源呢? CPU时间, 内存空间, 一个程序运行需要的所有硬件都叫做资源, 你会发现操作系统就是做资源管理的) 在一段时间内, 可能有多个应用程序要求运行, 这个时候, 操作系统决定什么时候谁应该运行这个过程叫做进程调度。

线程就是共享虚拟内存映射的进程, 实际上, 多个线程从属于一个进程, 这些线程都共享进程的虚拟内存映射。 由于线程共享内存,所以线程之间存在线程安全问题。

进程和线程关系

每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。

中断

计算机处理器的处理速度要远远大于硬盘数据的读写速度,所以就会造成处理器空闲状态,如果去利用这些空闲状态就是引入中断的原因。

中断的分类:

名称说明
程序中断某些条件下指令执行结果产生 如 除数为0,算术溢出
时钟中断由处理器内部的计时器产生,允许操作系统以一定的规律执行函数
I/O中断由I/O控制器产生,用于发信号通知一个操作的正常完成或者各种错误条件
硬件失效中断由诸如掉电或存储器校验错误等故障产生

中断过程

如上图由于完成I/O操作需要花费较长的时间,I/O程序需要挂起等待操作完成,因此用户程序会在WRITE调用处停留相当长的一段时间。
从应用程序的角度来看,并不需要添加额外的特殊代码来处理中断,处理器和操作系统来负责挂起用户程序,然后在同一个地方恢复中断。

中断执行流程

在中断处理过程中需要执行额外的指令来确定中断的性质,必然会造成一些额外的开销。但是相比较I/O的耗时,这些开销微不足道。
中断激活了很多事件,包括处理器硬件中的事件和软件中的事件。