不会开机的男孩

胡言乱语计算机一

| Comments

操作系统是连接硬件和应用软件之间的纽带。至少目前是这样的。而操作系统这门课也是计算机专业的必修课之一。无奈当时混沌。并没有真正的上好这一门课,之所以叫胡言乱语。是因为这里面的水对我来说实在是太深了。任何一个小的问题背后都是一个深渊。所以第一篇,从最初的(大学课程最初)开始讲起。

8086,应该是学计算机最开始的地方。可以说是我们现在x86系列的最简单,最基础的实现。里面的设计都或多或少的影响到了后面系列的实现。所以,学校从这里开始,的确是非常明智的,虽然当时我不这么认为。但是想要了解或是明白8086的设计,那么也就要带出另外的那些更为底层的,计算机指令集,机器语言,引脚,门,电压等等。当然我不是说这些不重要,但是如果有这些基础,的确可以加快,加深8086以及其他系列的一些知识的理解。这里就略过这些东西。一是自己能力不足,二是我觉得现在谈这个真的不是很重要。想想,这2个原因其实也就一个 :)。

存储器是计算机的核心部件。现在的计算机围绕存储器来构建,所以必须从存储器开始。

在CPU眼中,存储器保存的东东。只有2种:指令和数据。当然退而求其次,存储器中没有指令和数据之分,只有0和1。这个世界的确是非常的和谐简单。那么CPU是如何分别这2种东东呢。这完全取决于CPU自己。当遇到二进制信息如1000100111011000时,CPU可以把他看成大小为89D8H的数据处理,也可以看做是指令mov ax bx来执行。

存储器被划分为若干个存储单元,一般来说,一个存储单元大小为一个Byte。一个拥有128个存储单元的存储器。容量为128个字节。

那么存储器被划分为了多个存储单元,从0开始排序。CPU从内存中读取数据,首先需要的就是存储单元的地址,也就是CPU需要知道读取哪一个存储单元中数据。当然CPU不仅仅需要知道地址,还需要告诉存储器要做什么操作,是读还是写?而且在计算机中,也不仅仅只有一个存储器,也不仅仅只有一种设备需要去操作。还需要指明对哪一个设备操作。所以。CPU对数据的读写,需要以下的基本信息。

存储单元地址(地址) 设备选择,读写命令(控制) 读写的数据(数据) 那么CPU通过什么来将这些信息传递给设备呢?CPU计算机中的这些设备处理传输信息都是电信号,连接这些设备的导线为总线。总线根据传送信息不同,分为地址总线,控制总线,数据总线。

CPU从3号单元中读取数据过程:

alt text

CPU通过地址线将地址信息3发出 CPU通过控制线发出读命令,选中存储器,通知到读数据 将3号单元中的数据8通过数据线交给CPU 既然知道了CPU读取数据的流程,那么CPU能够找到多少个这样的地址是我们遇到的下面的问题。显然,地址总线上能传递多少种不同的地址,那么CPU就可以找到多少个存储单元的地址。

如果CPU有10根地址总线,1根能够提供2种信号:1、0。那么10根就能提供210个,也就是1024种。那么我们就说CPU的寻址大小为1K。或这个CPU的地址空间为1K。

CPU与各个设备之间传递数据是通过数据总线进行。所以,数据总线的宽度决定了CPU和外界数据传递的速度。我们很容易想到16根数据线可以一次传递2个字节。

同样类似的。控制总线的宽度,也决定了CPU对外部的控制种类。所以,决定了CPU对外部的控制能力。

当我们买电脑的时候。除了考虑CPU以外,还需要搞定一个好的主板。当我们打开电脑之后,看到的首先也是这个大家伙。而且如果你的电脑主板被烧坏的话,那么基本上这个电脑主机也就完蛋了。可见主板在现在计算机中的地位。

主板最基本的作用就是通过它把计算机的核心部分通过总线(地址、控制、数据总线)相连,并且还需要为扩展预留接口。当我们买到一个主板时,会看到有非常多的接口卡槽,而事实上CPU控制这些设备就是通过总线去控制这些接口卡来进行的。

alt text

上面的那些东东,不管是显卡,声卡,网卡。都有两点相同。

都和CPU总线相连 CPU进行读写操作是,都是通过控制线发出内存读写命令。 也就是说,CPU操作他们的时候,都把他们当做内存来对待。把这些不同的设备组成一个大的逻辑存储器。这个逻辑存储器就是我们的地址空间。

alt text

在上面这个图中,所有的物理存储器都被看作一个由许多存储单元构成的逻辑存储器,每一个都有一个地址段,也就是一段地址空间。CPU往这段空间中读写数据,其实就是读写了物理存储器。

那么我们可以看出。CPU的地址总线宽度是在是太重要了。在我们的8086中,地址总线宽度为20,也就是可以搞定220个不同的地址。也就是说8086的地址空间大小为1MB。

不同的计算机系统,的地址空间分配是不同的。如下是8086的

alt text

看到这幅图,那么我们就可以从容的写一个HelloWorld程序输出到我们的屏幕上。

因为我们可以直接在A0000~BFFFF中写数据,而这些数据会跑到显卡中最后跑到屏幕上。

那么我们现在明白了。CPU访问内存单元时,需要给出这个内存单元的地址,所有的内存单元构成存储空间是一个一维线性空间。每一个内存单元在这个空间中都有唯一地址,这个唯一的地址,就是物理地址。

CPU通过地址总线送入存储器的。必须是一个内存单元的物理地址。同样,这个地址在CPU内部中必须搞定这个地址,再发送到地址总线之前。不同的CPU形成物理地址的方式也不同。而我们现在所考虑的就是8086是如何搞定这个物理地址的。

那么我们又必须要了解些其他知识。8086是16位结构的CPU。那么他的意思是

运算器一次最多处理16为数据 寄存器最大宽度16位 寄存器和运算器之间的内部线为16位。 也就是说,8086一次只能处理,传输,存储(寄存器)16位的地址。从我们大多数人的思维,一个地址,也就是一个指针,最好和一个整数的长度一致。但是,我们知道8086的地址总线为20位。达到了1MB的寻址。为什么会是这样的呢?在很久很久以前,当CPU的技术从8位发展到16位的时候,地址总线本来也应该是16位,也就是64K。但是大家发现这个太小了。然后intel决定采用1M。这个在当时,的确是非常的大,而盖茨甚至还有“无论对谁来说,640K内存都足够了”的言论。当然。这里并没有不敬在里面。只能说,计算机的发展实在是太迅速了。所以,地址总线的宽度为20位。但是这个带来了一个问题。面对16位的ALU,如何来填补这个呢?

Intel设计了一种在当时看来一个非常巧妙的方法。也就有了我们现在看到的8086地址翻译。16位段地址+16位偏移来形成这个20位的地址。

随着计算机的发展,我们越来越的希望计算机能够处理更多的事情,伴随着CPU运算能力的提升。整个计算机的性能主要是卡在了CPU利用率上。面对“优秀”的CPU,我们并没有充分的利用它实在是暴殄天物。所以我们希望我们的CPU能够给我们做更多的事情,最好不要停。就像老早的资本家总是希望工人天天干活一样。

不幸的是,在当时的DOS操作系统下面。是单任务的。并不支持多任务。我们不能在听音乐的时候打开文本文档编辑。那么,构造计算机的那些老前辈们,想到的一个招数是时间片。每个程序都有机会获得这些时间片,通过不断的轮询,只要这个时间足够短,那么人类是无法觉察出来。我们会有这个错觉,好多的程序再一起执行。

虽然我们在DOS可以利用内存驻留的技术实现类似的体验,但是这个却并不是安全的。因为我们往往是通过修改中断向量表来做。我们无法保证其他程序是否正确修改中断向量表。而且如果我们的程序通过这里修改,并成为我们程序的一部分时,也就意味着,其他的程序也能这么做。那么我们很难保证计算机中的各个程序能够互不影响。同样,包括操作系统。这也就意味着我们无法构建一个安全的环境,让我们的操作系统,以及各个程序不互相影响,制约。

同样,当我们将CPU时间片分给那些程序的时候。在一开始的初期,并不是我们这样的多任务。而是一种叫做协作式多任务。操作系统控制CPU的时间片,而每个程序形成一个队列。每个程序在获得CPU时间后必须归还CPU。注意,这里的归还是程序本身的事情,而不是操作系统的事情。也就是说,如果有一个程序不想归还时间片,或是他不小心陷入一个死循环,那么别的程序也就无法执行,甚至包括操作系统自己本身。因为他自己也在那个队列里面傻等。那么这整个世界也就变得混沌不堪了。因为操作系统,并不能识别哪一个程序是不良的程序。

造成这些问题的根本原因在于,我们并没有等级的概念。也就是说,整个硬件资源对我们的每一个程序都是平等的。事实上8086下我们看到了任何一个程序,都可以通过段+偏移来实现访问整个地址空间。甚至是中断向量表还有硬件。所以,在这个原始社会下。我们达到了真正意义上的公平,但是也验证了低下生产力的现实。

所以,为了实现这些功能。必须有硬件的支持。那么80386也就跳入了我们的视野。事实上,他就是为了支持我们的想法(实现多任务,实现各个任务互不影响)而诞生的。

在开始介绍80386之前。我们好好思考一下我们需要实现的功能。

实现等级观念,有些程序需要有特权。执行一些系统的核心部分,而一些程序必须在一些限制上运行。具体的讲则是有些地址空间不能访问,有些寄存器不能读取或是修改。 需要提供一个复杂的内存管理,来帮助我们实现各个任务的独立的地址空间。这样可以保证一个任务不会随意修改另一个任务的数据。 其实,让我们说到根上。其实我们需要实现针对地址空间的保护。只有实现了这种保护机制,我们才能保护操作系统的代码,维护操作系统的特权。而有了操作系统的支持下,我们才能继续去谈内存管理,和保护操作系统之上的各种程序之间不互相影响。搞定了这些之后,我们就不难理解8086的缺陷以及80386为什么要实现这些功能了。当然。这个过程肯定不会像8086那样平滑。因为这完全是一个不同的设计思路,思想。即使,他披着一张似乎有着段加偏移量的一层皮。

好吧,让我扯的远一点。随着生产力的发展,有一个超牛B的程序,他想做其他程序的老大。让他们乖乖听话。而这个程序,就是操作系统。可惜啊,在原始社会,生产力不足。并不能让所有的人都听话。让我们暂时告别原始社会,我们来到了奴隶社会。其实计算机发展也和人类社会一样。我们出现了阶级,让我们仔细看看这个维持统治阶级工具的核心——80386体系结构。

80386以后,CPU历经多种改进,虽然速度提高了几个量级,功能上也有很多改进。但并没有重大的质的改变。所以统称为i386结构,如果除去大量的3D密集型图形图像运算,并行等之后。其实,只是相当于一个更更快速80386而已。

80386是32位的CPU。也就是ALU数据总线是32位。这里,我们终于在地址总线和数据总线一致了。都是32位。当面对地址总线的宽度达到32位。也就是CPU的寻址能力达到了232 = 4G。这的确是一个相当大的空间。为了保证这个空间的和谐。80386增加了一个叫做保护模式的一个名词。但是为了和之前的8086体系兼容,又有了实模式和虚拟86模式。

这里只是简单的介绍。实模式,没有什么其他的意义。只是比原来的8086寄存器大了。CPU快了。一些指令和操作更加方便容易了。

保护模式则是重点。事实上,没有保护模式,现代操作系统是无法构建的,在x86下。

既然我们有了这么大的一个空间,那么该如何分配呢?很容易的想法是,我们可以把地址空间平均分给各个任务。那么他们都有了各自的地址,他们只要在各自地方做就好了。但是这个同样假设这各个程序都是善良的。而且,对于各种各样的硬件又该如何做呢?他们所映射的CPU地址空间该如何保护?而且,当我们真正的运行着相当多的任务的时候,我们的内存,是否还能经得住呢?而这些问题归根到底是因为CPU的地址空间每一个任务都是可见的,那么就想通过各种各样的渠道来搞破坏。所以,为了构建操作系统的核心地位,以及各个任务之间的互不干涉。操作系统中最重要的概念登场了——虚拟存储技术。

其实,这是一个很简单的道理。统治阶级(操作系统)为了维持他的权威,他把珍贵的核心资源(CPU地址空间)和被统治阶级(用户程序)之间加了一个中间层,从而核心资源(CPU地址空间)对被统治阶级(用户程序)是透明的而统治阶级(操作系统)所独占。然后他又对所有的被统治阶级(用户程序)整了一个弥天大谎:“你们有整个4G的CPU地址空间。而且你们在跑的时候(程序运行)是独占所有资源的”。然后被统治阶级(用户程序)就在这个统治阶级(操作系统)下勾画的这个美丽的世界下安分的生活下去了,至少是绝大多数。(这里的表达不准确,这里的用户程序,其实我的意思是任务,或是说,在普通程序,我们可以写这么一个地址,在高地址空间上,只是如果我们去操作他,操作系统不让我们这么做。但是我们还是能“看”到的。感觉还是不合适,这段可以去掉:))

OK。操作系统给这个世界整个一个这么大的谎言。现在计算机的核心资源都在他的掌握下了,他的目的终于达到了。但是就和再苛刻的资本家也得给工人发工资一样。如果没有了被统治阶级,统治阶级还有什么存在意义呢?所以,操作系统也必须给用户程序一个高效的获得CPU资源的方式。也就是要给用户程序发工资。

而一个程序运行的最基本的要求就是数据。瞎话扯了这么多。该来点正经东东了。

80386CPU的内存管理支持2种,段式,和段页式。这些都为操作系统实现内存管理提供了硬件基础。

CPU的段机制,提供了一种手段。可以将系统的内存空间分成一个个较少的受保护区域。每个区域称为一个段。每个段都有自己的基地址,边界和访问权限。但是80386在实现这个的时候,不得不背上历史的负担。intel选择了在原有段寄存器的基础上构筑保护模式。并保留了原来的16位段寄存器。并添加了2个段寄存器FS,GS。但是我们看到了。光是用段寄存器来确定一个地址是不行的。因为我们需要这个地址段的长度(边界),访问权限等等。所以,这里变成了一个数据结构,而不是之前8086的那个单纯的基地址。

所以,intel在做这个的时候,改变了段寄存器的功能,使他从单一的基地址,改成了指向一个数据结构的地址(或是数据结构的指针可能好听点)。这样,CPU才能获得它足够的信息。而这,也是学过8086 再看80386最让人迷惑的地方。因为这个完全是2套东东。而且根本上没有任何关系。

让我们捋一下当一条访问内存指令的执行情况。

根据指令的性质确定使用哪一个段寄存器。 根据段寄存器内容,找到相应的段描述符结构。 找到基地址。 将指令中的发出的地址位移,检查是否越界。 根据指令的性质和段描述符中的访问权限看时候越权。 一切正常,我们相加获得实际物理地址。 CPU搞定段需要知道3个信息。

段基地址 段界限 段属性 段信息的长度是64位。段基地址32位。段界限20位,段属性12位。而这个段信息标准的叫法就是段描述符。而许许多多的段描述符组成个段描述符表。

为了能够访问段描述符表,80386中新增了2个寄存器来寻址段描述符表:GDTR和LDTR。GDTR为全局描述符表寄存器,LDTR为局部描述符表寄存器。GDTR是48位,直接指向内存的线性地址,32位的线性基地址,16位的边界描述这个表的大小。LDTR是16位寄存器,表示的是全局描述符表的索引。这说明LDT其实就是GDT中的一项而已。

段寄存器中的内容为16位。由于指向的内容改变了。所以也有了新的名字,为段选择子。

alt text

TI表示要索引的段描述表种类。TI = 0表示全局描述符表,TI = 1表示局部描述符表。由于索引只有13位,也就是说,我们的表项最多213 = 8K个描述符。RPL 表示请求特权级,用于特权检查。

我们现在仔细看看这个索引指向的内容,描述符表。

在一个多任务系统中。通常我们会同时存在很多个任务,每个任务涉及多个段,每个段都需要存放段描述符。那么描述符根据用途不同,IA-32处理器分为3种描述符表。全局描述符表GDT,局部描述符表LDT。中断描述符表IDT。IDT将放在后面中讨论。段描述符的结构比较纠结,充分体现了历史负担。这里也就不继续了,不过,这个真是一个相当“太监”的结构。

GDT表是全局的。一个系统中通常只有一个GDT。供所有任务使用。LDT和具体任务相关,每个任务都可以有一个LDT。也可以多个任务共享一个LDT。

alt text

根据上图,我们可以形象的看出,段内存管理的计算方式。讲了这么多的理论,让我们稍微动动手。

使用Windbg调试程序,可以使用dg命令来显示一个段选择子指向的段描述符详细信息。首先看下CS

alt text

Sel就是选择子(selector)。base limit就是之前的基地址,和边界。Code就是段的类型。RE = ReadOnly + Executable。Ac表示访问过

Pl表示特权级别(Privilege Level)。3的意思是用户特权。Size表示代码的长度,Bg意味32位代码。Gran表示粒度 Pg代表为内存页4K。Pres代表是否在内存中(我们之前看到了那么多的表项,8K,事实上并不是都在内存中的,当不在内存中时,访问会重新载入这个内容,所以需要记录)。Long 下的Nl表示 这个不是64位代码。

我们看到了SS DS ES 一样。

alt text

我们看到了类型是数据,并可以读写。而且我们发现。SS DS ES CS 的基地址都为0,长度都是整个内存空间大小。Intel把这种方式成为平坦模式(Flat)。我们看到了当我们通过段+偏移获得一个地址,其实基地址的作用已经没有了。limit也是最大空间4G,作用也很小了。可见,在平坦模式下,只是段管理的一个特例。我们只是关注与权限而已。

等等,少了一个。FS这个段寄存器比较特殊这里只是贴个图。具体的会在后面总结他。当然这里面的知识非常多,还有各种各样的段描述符存在。但是如果是和我一样在这些方面是新手,我觉得还是知道的少一点比较好。

alt text

我们看出,根据段内存管理下。我们把程序分成了不同类型。有代码部分,有数据部分等。但事实上,无论是windows 还是linux都没有采用段内存管理,准确说是只使用Flat模式,也就说。只是使用了权限部分来针对特权级对代码和数据保护。

Intel在80286实现保护模式,段式内存管理。但是发现了如果不支持页式管理是不行的。所以,在80386下,需要支持页式管理。也就是说,80386又背起了历史的负担,既要维护段式管理,还要实现页式管理。

之前的段式管理机制,是通过段寄存器转换加偏移形成一个32位的物理地址。这个是真正的物理地址,也就是这个是要在地址总线上跑的。也就是说应用程序获得的这个地址是真实的地址,那么也就对操作系统对内存换入换出增大了困难。而且对需要对code和data分类管理,导致程序加载过慢。而且缺乏足够的对内存管理的粒度,而究其原因,就在于它并没有真正的隔离用户程序和实际资源以及等等问题。所以,页式管理开始登场了。

本来页式管理和段式管理是不需要结合在一起的。但是在80386中。保护模式的实现是和段式管理分不开的(权限控制)。我们在查看CS的代码段描述符时,我们看出执行的这段代码的优先级是3。所以intel设计80386时,就考虑利用原有的基础再扩充。那么也就有了我们现在的基于段式管理的页式管理。也就意味着,我们需要在段式管理上再建立一个地址映射。说白了就是。这整个一套地址转译,需要将逻辑地址,通过段式管理转成线性地址,再通过页式管理最终转成真实的物理地址。那么如果我们启用了页式管理,那么段式管理的运行结果就不是之前的真实的物理地址,而成了一个中间地址或是线性地址。而这个过程。同样也是从8086跳到80386比较费劲的地方。

80386将线性地址空间划分为4KByte的页面(一般情况下)。每个页面可以被映射至物理存储空间中任意一块4KByte大小的区间。在段式管理下,连续的逻辑地址转译后在线性地址空间还是连续的。页式管理下物理空间却可以不连续。所以我们可控的粒度更小,从而更灵活。而物理空间的不连续,也就意味着我们可以更加灵活的把暂时不用的数据放到外部存储器,通常为硬盘。而这也就解决了我们多任务下,物理内存不够的情况。

当然,灵活的背后便是复杂的机制,在我们继续了解详细的页式管理过程之前。我们先看一下我们真正的需求,以及80386给我们提供了什么。

我们的首先目的是特权机制。通过特权机制来保证操作系统的权威。也就是一些指令,寄存器只能由操作系统这一级别的才能操作。而用户程序不能操作。这是段式管理已经搞定的。那么剩下的问题就是用户程序和用户程序之间互不影响。

在页式管理中,我们已经有了一个虚拟层:线性地址。事实上。每一个任务都有一个这样的虚拟地址。任务中针对地址的操作都是在这个虚拟地址上而不是真正的物理地址。我们知道。我们的数据最终和物理地址相关联才有意义。这个中间层,使得任务不知道自己确切的物理地址,也就为了保证一个任务不会被另一个任务随意修改或访问。

80386页式管理的核心是将线性地址空间划分成一个个页面,大小一般为4K。那么我们需要保存这一个个页面的映射关系。而我们知道,现在的地址空间大小是4G。那么我们剩下的就是如何管理,或是保存这些信息。我们首先发现,这个空间对我们绝大多数程序来说都太大了。所以为了减少保存这些映射的资源,80386使用了分级管理,所以,一个简单的线性地址被拆成了3个部分。

alt text

分别为Directory, Table 和Offset。

对于一般来说,页面大小为4K。为了能够找到每一个Byte,那么我们需要12位才能找到。也就是Offset = 12的原因。

我们称指向一个页面的地址(指针)为页表项,多个页表项的集合构成页表。10位table,也就意味着我们能够表示1K个pageTableEntry。那么我们总共能够表示的4MByte。

指向页表的地址(指针)为页目录项。多个页目录项的集合构成页目录。10位的Directory,也就意味着我们能够表示1K个Directory entry。那么我们总共能够表示4GByte。正好为我们的地址空间大小。

CR3寄存器,则给我们指出了Directory 的基地址。所以它又有了另一个名字,页目录基地址寄存器(PDBR)。

具体的取址这里就不描述了。因为上图已经很清楚了。

这真是一个看似完美的方案。但是现实是很残酷的。我们并没有那么多的内存。为了能够跑起那么多的程序,支持多任务,也就是意味着,我们需要在一些时候,把一些内存搬到硬盘。那么当我们访问这些页面的时候,就会产生pagefault,然后操作系统会把这部分页面再搬入到内存中。

当然,还有相当多的细节这里无法阐述。事实上,我们也不可能一下子把这些东东搞清楚,毕竟这些东西离我们还是有些远。下一篇将从应用的层面扯。当然在合适的时机,我们还需要回来。比如另一些重要的概念如中断。

Comments