logo头像

Hacked By Swing

非专业计算机基础 (2)

接上篇

上一篇中我们将计算机内部的原理大概搞了清楚了,现在我们知道计算机内部是怎么工作的了,知道了 CPU 和内存都是什么东西了,但是和我们现在熟悉的计算机使用方式还有一定的距离,我们还有许多的问题没有解决,这一篇当中,我们会继续解决一些问题,进一步让大家对现在我们看到的用到的东西有更深入的理解。

CPU 中断和外设

在上一篇当中我们已经解释了 CPU 是如何工作的,那么我们的计算机就是按我们上一篇这么工作的了吗?当然不是,比如有一个重要的问题我们就没有解决:CPU 是按照指令不停的执行的,指令在内存里,那么按理说,我们没有办法干涉 CPU 的执行过程,那么如果计算机是我们现在学到的这样,一旦开机,它就只能够完成一个任务,然后默默地结束。

似乎有哪里不对?是的,我们在使用计算机的时候,是会 ”干涉” 计算机的运行过程的,这个干涉就包括:鼠标、键盘,通过双击的操作,计算机才知道该执行哪个程序对应的指令,这个过程目前我们还不知道是怎么进行的。

事实上这些设备我们都可以叫做外设,外部设备,因为它们不是计算机运行起来所 “必需“ 的,比如你在没有插键盘和鼠标的时候,也可以打开一台计算机,只是后面会无法使用罢了,那么既然可以开机就说明是可以运行指令的,也就是并不是 ”必需“ 的。

这些外设会干涉计算机的运行过程,将一些信息发送给计算机,计算机就会处理这些信息完成相应的任务。这个过程是如何进行的呢?

在 CPU 中有一个概念叫做中断,顾名思义,其实就是中断 CPU 的运行过程,中断之后难道 CPU 就不工作了?当然不是,CPU 会去运行提前设置好的 ”中断处理程序” ,中断处理程序就是一段指令,只不过放在了 CPU 规定的特定位置(内存里的特定位置),这个中断处理程序的任务就是去提取外设传入的信息了。外设传入的信息在 CPU 接收到的时候就会放到特定的位置(比如特定的寄存器,或是特定内存地址位置),然后中断处理程序就可以去这个由 CPU 提前规定好的位置完成操作了。

硬盘和内存

在之前的内容中,我们一直没有提到硬盘的问题,但是我们提到了内存,其实硬盘和内存是十分相似的,它们都是用于存储的设备,只不过硬盘使用起来更加复杂,不像内存,就是一个很多行一列的表格。既然硬盘很麻烦,内存很简单,为什么我们还需要使用硬盘?

这个问题有几个原因:

  1. 内存内容是会丢失的。由于内存硬件的特点,一旦断电,内存里保存的内容就都不见了。在我们台式电脑运行的时候如果突然断电了,然后马上又来电了,你重新开机之后,屏幕上还是你之前完全一样的内容吗?这就是因为内存里的内容都丢失了。
  2. 内存速度快,但是,贵。如果有自己攒电脑经验的同学,或是自己买过电脑、了解过电脑的配置的同学应该知道,内存一般也就 8 G ,16 G ,大的不过也就 32 G (以个人的常用电脑为例),但是硬盘多大?硬盘现在最少也是 256 G 起步,1 T (也就是 1024 G)也是家常便饭,为什么?因为它们的价格差距大,一个 8 G 的内存可能和 1 T 的硬盘(机械硬盘)差不多价格,所以如果全部用内存,哪怕不用考虑丢失的问题,这个成本也是不可以接受的。

所以,我们的数据一般都放在硬盘里。但是由于硬盘的速度慢,比 CPU 慢了太多(思考一下你从 U 盘里拷文件到你自己电脑上的时候),所以如果 CPU 直接从硬盘里读取数据就会太慢了,以至于你的电脑随便一操作就卡住了,因为在读取硬盘数据。这样当然是不可以的,所以我们采用了一个很经济的方案:文件还是放在硬盘里,但是如果想要使用,就把它整个放到内存里,然后 CPU 就负责和内存交互。

将硬盘里的数据(也就是我们说的文件)放到内存里的操作,就叫做加载(或者装载)。

操作系统

好了,现在我们的计算机可以接收到输入(也就是我们按下了键盘的哪个键,我们如何移动或是点击了鼠标)了,我们的目标是尽量贴近我们目前熟悉的计算机使用方式。还差哪里呢?我们现在如何使用计算机?我们可以同时运行多个程序,只需要双击之后就可以运行一个程序,那么现在我们学到的知识可以完成什么任务了呢?其实基本上都可以完成了,但是还没有办法运行多个程序,因为,还记得我们的 CPU 是一条指令一条指令地执行程序的,一个程序就是一段指令,不执行完成是没有办法执行别的程序的。

其实这种情况在很久以前是确实存在的,很久以前的计算机就是只能一个程序一个程序的执行,那个时候这种方式叫做批处理(现在我们在 Windows 上还能见到 “批处理文件” 的格式,这里的批处理其实就是指的一个程序一个程序的执行,然后连续地执行许多程序)。但是这和我们现在的使用方式是不一样的,最大的区别就是因为我们的操作系统。

我们目前大多数人使用的是 Windows 操作系统,也有使用 Mac 操作系统,甚至 Linux 操作系统的(当然我估计大多数使用 Linux 操作系统的同学不需要看这个系列),这些操作系统都有一个共同的特点,就是可以同时运行多个程序,这其实不是由于硬件上的区别(当然硬件的速度越快当然越好,否则就会很卡),主要是因为操作系统和很久以前的操作系统不一样了。

说了很多题外话,那么具体是怎么做到同时执行多个程序的呢?其实是因为操作系统也是一个程序(一个软件),也就是那个最早执行起来的程序,然后它来负责执行别的程序,想想,我们开机的时候是不是先会进入到操作系统当中,然后才能执行别的程序的呢?所以,为了执行多个程序,操作系统首先自己得先运行起来。

在上一节里,我们提到了硬盘的问题,所以一个程序是放在硬盘里的。但是硬盘里除了程序还可以有别的文件,比如你的笔记本文件 txt 格式,或是你自己用 word 写的文档,是 doc 格式的,它们的格式不一样,那么操作系统怎么知道你想要运行的程序到底是不是一个程序呢?所以操作系统自己也规定了 ”程序“ 是什么格式,这个程序在硬盘上,用更专业的说法,叫做可执行文件,在 Windows 上,就是 exe ,所以 exe 类型的我们就可以双击运行,这个格式是由不同的操作系统自己规定的,不同的操作系统之间是互相不认识别人的程序的格式的,所以在 Windows 上你没有办法运行 Mac 系统的程序。

之后,为了运行多个程序,操作系统就会用一个巧妙的方法:一个程序运行一会,不用等到你的程序运行完,而是等到给你分配的运行时间到了,就换别的运行。由于计算机的速度相对人的感知速度来说是很快的,所以它们不停的交换,就让我们觉得程序 “好像是在同时运行” ,但其实在 CPU 里依然是一条指令一条指令的执行的(有一种情况是真正的在同时运行,那就是多核,这个时候相当于有多个 CPU ,所以 CPU 依然是一条指令一条指令运行,只不过有了更多个,就可以真正同时运行了)。

到现在我们的大致概念应该已经建立起来了,我们知道了操作系统是什么,知道了操作系统是怎么运行程序的,知道了程序是什么,也知道了程序在硬盘和内存中是不一样的。现在我们来解决几个细节问题:

  • 一台计算机中的内存只有一个(按照我们之前的说法,内存可以认为是一个 N 行一列的大表格,那么这个表格只有一个,否则内存的地址就无法唯一确定一个位置了),那么几个程序同时运行,它们所使用的内存也都是这个内存,它们为什么不会互相干扰?
  • 有了上一个问题,进一步的,操作系统也只是一个程序啊,操作系统如果被其他程序干扰怎么办?
  • 要做到同时运行,需要一个程序运行一会,那么一个程序怎么知道自己运行结束了?难道自己谦让?那要是我这个程序不讲道理,就不谦让,怎么办?

这些内容都是操作系统需要解决的,我们通过几个机制来讲解这几个问题。

CPU 环 (ring)

这个机制可以解释为什么操作系统不会被其他程序干扰。CPU 环是 CPU 里的一个机制,这个机制很简单,就是把 CPU 的所有指令分为了两类,分别是 “特权指令” 和 “非特权指令“,然后将 CPU 的运行状态分为几个环,比如分为 0 、 1 、 2 、 3 四个环,听起来很麻烦,是,现在的操作系统也嫌它麻烦,于是全部把环 1 和环 2 视而不见,只用环 0 和环 3 ,环数越少的,能力越强,比如环 0 是最强的,可以运行所有的指令,包括特权指令和非特权指令,环 3 就不太行了,只能运行非特权指令。

环 0 切换到环 3 是可以随便切换的(反正所有指令我都可以运行了),但是环 3 切换到环 0 就只能通过特定的指令,然后切换之后也只能到特定的位置。

总的来说,就是操作系统自己运行的时候是环 0 的状态(我们也叫做操作系统运行在环 0 ),然后如果要运行别的程序,就先切换到环 3 ,然后在切换到其他程序的入口位置。那么如果别的程序需要回到操作系统,比如使用操作系统的一些功能怎么办呢? CPU 里设计了特殊的指令叫做 ”系统调用“ 指令,这个指令就可以让你回到环 0 ,但是回去之后,你不能任意设置你想要运行的指令的地址,而是只能走到特定的位置,于是操作系统就在这个地方等你,操作系统自己占据了这个特殊位置,于是你也就只能回到操作系统。

通过这个机制,普通程序就无法干涉到操作系统了,反过来,操作系统却可以随便干涉普通程序。

虚拟内存

可能在很久以前还在使用 Windows XP 的时候,一些同学曾经在使用的时候碰见过系统提示 “虚拟内存空间不足” ,现在已经没见过了。这个虚拟内存是什么呢?

其实就是为了解决程序之间互相干扰的问题,操作系统采用了一种叫做虚拟内存的机制来处理。这个虚拟内存,就是指让每一个程序都 “以为” 自己能够使用整个内存,而实际上,它们只用了内存当中的一个部分。能这么做有个假设的前提,这个前提几乎所有程序都满足,就是一个程序不会真正把整个内存完完全全给用掉(如果真有,操作系统就会选择不让你再执行了,否则你就影响了别的程序,或者更严重————直接死机)。

那么用什么方法呢?这个方法需要操作系统和 CPU 的共同支持,这个方法叫做分页。这种方法首先将内存分成一页一页的连续的部分,每一页就是一小段固定大小的,比如原本内存一共有 20 字节这么大,然后我分成 5 字节一页,那么内存就被分为了 4 页。然后,通过设置 CPU ,让 CPU 在翻译地址的时候不直接翻译,而是进行一次映射,这样就实现了让程序以为自己用的所有的内存空间。

我们通过一个图来理解这个过程:

1
2
3
4
5
6
7
8
9
10
         真正的内存          CPU 翻译       程序以为的内存
地址 0 +---------------+ 0 +---------------+
| 一页 | +------ | 被使用 |
5 +---------------+ | 5 +---------------+
| | <----+ +---- | 被使用 |
10 +---------------+ | 10 +---------------+
| | |
15 +---------------+ |
| | <------+
20 +---------------+

在一个程序运行的时候,当它使用地址 0 的时候,它以为自己使用的是地址 0 ,其实使用的是真正的内存的地址 5,我们把真正的内存的地址叫做 ”物理地址“ ,程序以为的内存的地址就叫做 ”虚拟地址“ 。为什么需要这样映射呢?因为这样映射之后,就像图里的一样,明明使用的是连续的内存,但是其实在真正的内存(也就是 “物理内存”)里并不是连续的。这样有什么好处?好处就在于,现在哪怕你程序用的是连续的,我也可以给你分割了使用,那么你没有使用的那些位置,比如图里这个程序的虚拟内存的地址 10 和 地址 15 这两个页,我就可以不分配给你真正的物理内存,那么就省了一些内存出来,省出来的就可以拿给其他程序使用了。

这么操作之后,就可以让程序员在写程序的时候完全不需要考虑其他程序是不是存在了,反正你怎么操作也不会影响到别的程序。

时钟中断

这个机制是最简单的一个机制,在之前我们已经讲到了什么是中断,其实中断不仅外部设备可以触发,自己也可以触发(区别仅仅也就是触发的来源而已,处理上反正也不会有什么区别)。自己触发的一种情况就是时钟中断,在 CPU 中有一个一直不停的时钟,这个时钟每隔一小段时间就会触发一个中断,这个时间非常短,叫做 CPU 的时钟周期,差不多在纳秒级别(不到 10 纳秒),其实 CPU 的性能指标里有个时钟频率(一般以赫兹为单位,不过因为太大了,实际上用的是兆赫,要是对买电脑有研究的同学可能知道,一般频率越高越好,这个频率其实就是和时钟周期对应的,频率决定了 1 秒多少次,时钟周期就是 1 次多久。所以这个中断是非常频繁的,那么这个中断可以用来干什么呢?

其实中断在运行的时候也是由操作系统处理的(运行在环 0 的状态),操作系统会记录下运行的时钟周期有多少,也就是说,一个普通程序在操作系统里运行的时候其实过程是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
程序1运行指令1
程序1运行指令2
-------------- <-- 触发时钟中断
操作系统将运行的周期数加 1
操作系统查看现在程序运行了多久了,是不是该换别的程序了,如果不该换
操作系统切换回原来正在运行的程序
++++++++++++++ <-- 切换回环 3
程序1运行指令3
程序1运行指令4
-------------- <-- 触发时钟中断
操作系统将运行的周期数加 1
操作系统查看现在程序运行了多久了,发现该换程序2了
操作系统把程序 1 现在的运行情况(寄存器里的值,用到的内存映射关系是怎么样的)记录下来
操作系统把程序 2 原来记录下来的运行情况取出来并且恢复
操作系统切换回程序 2
++++++++++++++ <-- 切换回环 3
程序2运行指令1
程序2运行指令2
...

通过这种方法,操作系统就可以同时运行多个程序了。

小结

这一篇主要是对操作系统的一些内容做了一些解释,结合上一篇,我们基本上现在可以想象出我们现在所使用的计算机的一个运行情况了。

更新

  • 2020-2-11 2:19 完成第一版