计算机底层原理杂谈(白话文)
简单说一下写这篇文章的缘由。首先这个不是教学类型的,是我Java实在学不下去了,因为好多计算机底层原理都不是很清楚,每次学新东西都由于想不明白底层原理困惑,所以下决心停止学习Java的新东西,开始搞明白底层。一开始搞的所谓的底层是“Java虚拟机”,然后又C语言汇编语言什么的,其实是想图快,尽快接近现在做的事情。后来发现不行,这事快不了,所以干脆就从物理层面用导线灯泡集成芯片开始动手做一个cpu开始吧。其实也没多久,大概三个多月吧,从我之前写的【从零开始自制cpu】系列的学习文章也可以看到开始时间。cpu做好之后(其实后来由于老是烧坏各种电路没做完),开始看操作系统,现在刚开始看一点。总结起来就是我认为的学习路线应该是:
自制个cpu--微机原理--简单数据结构与算法--计算机组成原理--汇编语言--操作系统--C语言--编译原理--复杂数据结构与算法--计算机网络--Java--Java虚拟机--源码研究--多线程linux高并发Spring负载均衡各种以这些评判一个人Java水平的东东...
我认为Java应该在那样一个遥远的地方,好多人却以它为起点了。我现在粗略地看到操作系统,当然不断地在补前面的知识。这篇文章想借此机会跟大家说说计算机底层的理解,学的知识很碎,大家顺便帮我挑挑错误,如果对你有帮助那就更好了。我用大白话说,想到哪说到哪,这样更容易展现出漏洞和错误,希望大家在评论区里积极吐槽~
----------------------------------------华丽的正文分割线------------------------------------------
CPU就是由一堆导线将一堆部件连接在一块的东西,每个部件里面又是由一堆导线和一堆更小的部件连接在一块。把整个cpu看成一个部件,那它和RAM,磁盘等外设又是用一堆导线连接在一块,我感觉就像套娃似的。然后每个部件从外部看就是暴露了一堆针脚,可以用导线连上,给他们传递要么高电平要么低电平的电流,你比如说下面这款74LS173(3态输出端的4位D型寄存器),简单说就是一个能存4位数据的东西,可以用来做寄存器。
看这么多阵脚,我感觉分三类理解就行了,别管他们是数据输入、数据输出、信号控制啥的,他们都是一类,反正要给他们要么高要么低的电信号。还有一类就是接+接-的vcc和gnd,目的是让这个部件有“电”。还有一个是时钟clock,这你给他一个一会高一会低来回变的电信号就行了,目的一般是让它电平上升沿的时候这个部件“触发”一个什么效果,对这个寄存器来说,就是存了个数据或者读出了数据,当然有的部件不需要时钟信号。
CPU以及它关联的其他设备,全都是一个个这样的“部件”,这些部件别管它多复杂,最终暴露出来的都是一个时钟、一个接正接负、一堆其他的乱七八糟的针脚。有的部件用来存东西,有的用来做运算比如加法器,有的用来计数比如程序计数器,有的部件用来做各种翻译呀转换呀什么的,比如地址译码什么的。最终它们都连接在一个大boss上那就是时钟信号发生装置,反正它的作用就是特别快速地输出一个高低高低高低电平来回转换的电信号。所以现在我总结出cpu就是一个boss时钟信号,一堆小弟部件,一堆导线把部件有规律地连在一块,最终都连在大boss上。
那这样一堆部件是怎么运行的呢?首先第一个部件是程序计数器pc,它就是靠时钟信号不断累加,输出的针脚从0000,0001,0002一直往上加。当然你可以一个时钟周期(就是高低电平来一下)就+1,但这样除非你其他部件一个时钟周期内就能把一条指令执行完,一般不行,那怎么办呢,就再弄一个计数器,比如只能从0加到5。这个计数器每次都从0开始,然后没到5之前都把程序计数器的一个状态针脚变成“不触发加”的状态,这样程序计数器就不变了。然后各个部件在5个时钟周期内把指令执行完,然后程序计数器再加1。这就是单指令周期,每个指令都用5个时钟周期执行完。当然这不好,你可变化呀,让这个计数器从0加到一个动态的值,这个值根据指令类型改变,这就多指令周期了。然后程序计数器一直往上加也不好,给他弄几个针脚,再弄一个状态,可以直接设置一个新值,这就实现程序跳转了。一个指令周期可以动态改变,还可以强行设置程序计数器的值,这差不多就够了。
第二个部件是寄存器堆,其实就是一个存东西的地方,跟内存呀硬盘呀一样,只不过离cpu近,就光用距离除以电流流速来说都能说明它比较快。所以一些运算出来的中间结果呀,甚至我从内存中读出来要做加法或者要写入内存的数据都先放寄存器里面。寄存器都一样的你存哪个都行,只不过为了统一,别一个人一个样,把寄存器分门别类弄了些专用的功能,地址数据就放你地址数据该存的寄存器,表示状态的数据就放在你状态该存的寄存器,仅此而已。寄存器正因为有不同人给它赋予定义才麻烦,就比如IO接口中的端口,就是寄存器而已,只不过比如像硬盘接口,你往它3号端口写个011101啥的,他就表示你要读数据了,然后硬盘把数据放到4号寄存器等着你读。
第三个部件是算术逻辑单元,你可以先假设它就是个只能执行加法的部件,8个针脚数据1,8个针脚数据2,再来8个针脚表示这俩数据的和,完事。
第四个部件我不想叫它控制单元,我感觉我一开始困惑的就是因为这种叫法,我感觉它更像是一种布线的方式,只不过抽象地说出来逼格高,更容易写成教材。简单说就是几个针脚接收指令,另几个针脚输出各种不同高低电平信号,连接在其他部件的针脚上起到一些控制作用。你比如我输入一个“写入内存”的指令,那我输出的针脚肯定有一个是接在内存的“是否写入”这个针脚上,这不就控制了么。
总结起来,其实部件就那么几种,存储部件:寄存器呀,RAM ROM呀;控制部件:就是联系所有部件的控制它们可读可写可加这种逻辑的;算数部件:数学运算用的;发动机部件:这我给命名成发动机部件吧,就是时钟信号产生,还有程序计数器,这些都是将整个部件激活、发动的感觉,没有它们就没了源动力。外设部件:也可以叫IO部件,注意千万不要把硬盘理解成存储部件,它跟网口、鼠标、键盘是一样的,都是IO,你能从磁盘中读数据,你也能从键盘中读数据,当它们接到IO接口上时,全都视为同一个东西了。
我拿键盘举个例子,不管谁家生产的键盘,都要接在我一个叫“键盘接口”的东西上,这个键盘接口中有5个端口,1号2号3号4号5号,其实就是寄存器,接口上面的寄存器就叫端口。我这个键盘生产商可以写个说明书,告诉大家你们听好啦,1号端口就是我的按键数据,我按了键盘中的A,我就往1号端口中写00100011,你cpu读到了怎么处理我就不管啦。当然我很好心,我给你2号端口也搞一个数据,为0的时候说明我没按键,为1的时候我就按键了,这够可以了吧。这时候cpu就可以处理了,我去读这个2号端口的数据,就像我读内存数据一样,读到了我发现它是1,那我知道键盘按键了,我接着读1号端口的数据,然后各种处理最终给显示器接口中的一堆端口写上一堆奇奇怪怪的数据,显示器读到这些数据后又做了一堆处理最终在屏幕上亮了几个灯泡,亮出了一个A。这里面cpu不断读2号端口看键盘有没有操作就叫用轮询IO的方式检查设备,读了1号端口的数据做各种处理最终给显示器接口写入数据,就是驱动程序。最后显示器读这些数据显示到屏幕上,那这是另一个设备的物理细节了,它里面也有个像我们这个cpu的东西就不去细究了。
完美,不过上面的过程又有些问题,如果io设备很多,轮询io的方式就很没效率了,最好是io有动作的时候主动通知cpu。那可以这样做,比如键盘有动作,我不是往我2号端口写数据了,而是往你cpu中一个寄存器中写一个号码,cpu读到这个寄存器中有数据了,通过查它的号码找到对应驱动程序的内存地址,执行这个程序。这个过程就叫中断,而查询号码去找程序的地方,叫做中断向量表。这里其实我真的也不想叫它中断,因为又是这个词让我困惑好久。因为cpu是通过增加一个时钟周期专门检测是否有中断信号产生,也就是说如果没有任何中断信号,这个时钟周期也是需要空跑一次的。所以你看,从更物理的时钟周期的层面看,这个中断方式仍然是轮询,只不过轮询的单位不是指令,而是时钟。
这说法完美,不过上面还有个问题,就是像键盘这样的还好,因为它确实需要执行一段特殊的驱动程序去完成功能。但想磁盘这种,单纯的就是读出数据写到内存或者读内存数据写到磁盘,这种操作很低级但是很耗时间,如果每次都是通过中断然后数据通过cpu先传到寄存器在一个个传到内存,那就让cpu太大材小用了。这种重复的耗时的劳动,最好别占用cpu,直接从硬盘通过某个设备到内存就好了,这个设备就叫做IO控制器DMA。硬盘接收到cpu的读请求后,向dma发请求信号。dma完成了从硬盘写入内存操作后,再向cpu发一个中断信号,简单执行一下数据处理完的中断程序就行了,至于数据传输的过程,cpu可以做其他更高级的事情。
完美,不过上面的又有一些问题,就是你虽然不占用我cpu时间,但你占用总线啊,我们是公用一条总线传输数据的,你传输数据占用总线的时候,我cpu就占不了了。或者你等我cpu不用总线的时候你在再用,这个叫做dma的时钟周期窃取。但这样也不好,我他妈就希望你离我越远越好,别占我cpu时间也别占我的地方,让专门一个可以执行简单指令的设备和你公用一条单独的总线去完成这件事,我称之为low版cpu,他就是io通道。
再说说IO端口地址问题,cpu如何指定一个端口呢,可以用一个部分表示地址,另一个部分表示是IO地址还是内存地址。还有一种方式是,将io端口也加入到内存一样的地址范围中,然后访问一个端口跟访问一个内存地址没什么差别。所以上述到就是IO端口的两种编址方式,第一种是独立编址,采用端口映射io,第二种是统一编制,采用内存映射io,现在基本都是内存映射。整个io这一块大体的骨架就是这样子的,你看刚刚所说的中断呀,dma呀这些,我觉得可以理解为操作系统,或者说由于使用cpu的需求倒逼出的产物。当然所有这些都可以用软件来实现,但当需求足够大的时候可以让cpu为操作系统做出一些改变的,这并不是cpu原本就是这个样子。其实上面提到的中断,是外部中断,当然也可以是内部中断,就是指令自己去出发一个中断。这是根据中断源的不同分的。当然本质是一样的,都是往一个寄存器或者几个寄存器里写数,cpu一个时钟周期专门查看一下这个寄存器,然后查下中断向量表找到对应的程序执行一下,执行完了恢复之前的pc再继续往下进行。
整个io差不多就是这样的骨架,所以你看为什么操作系统关注io,关注内存管理,关注多进程,因为没啥别的东西可关注了,cpu原本能做的事情太简单了,所谓操作系统也好,dma这些新增的硬件也好,没有什么技术上高端的事情,或者说在计算机底层,高端的本质就是复杂和麻烦,这也回答了我好久之前写过的一篇《究竟什么是技术》。你包括我的第一张74LS173的针脚图,如果你看了我说的什么“部件”巴拉巴拉明白了,你可以说你懂,当然你把cpu主要部件的针脚图都看了,都记住了并且在面包板或者焊接版上接过了,你也可以说你懂。但这层次就不同了,所谓理解得深不深,其实就在于细节。
再说说内存地址管理,或者说寻址方式,当然你可以在指令中的地址就表示绝对的地址,不经过任何转换直接到内存或者相应的设备中输入这个地址信号然后读数据。你或者把俩地址拼一块,形成一个新地址。再或者你形成新地址后再通过某种方式转换映射一下,或者再映射一下。等等,操作系统对内存的管理就是这些,全都是细节。我只是简单入了个门,最开始cpu是绝对地址寻址,就是我指令中的地址直接输入到某个部件的地址线上。第一个搞事情的是8086cpu,也就是x86架构的鼻祖cpu,它有16位数据线,但有20位地址线。当然你可以只用16位地址线但当时恰好人们觉得地址不够用了,然后又各种原因不能弄成32位的cpu,于是乎寻址的时候就把一个寄存器当作段地址数据,另一个当作段内地址,其实别管那么多,就是应给凑成了20位地址罢了,这样寻址范围就扩大了。但这设计好不好?美其名曰段地址和段内地址,其实这很麻烦,如果cpu位数够,没人给自己找这种麻烦,以至于后来的32位cpu为了兼容以前的拍脑门设计,即便是寻址空间已经够了但还是采用这种段方式。但后来又说操作系统变得复杂了,倒逼着cpu弄出实模式和保护模式,每个段也有自己的权限呀长度呀等等各种标志了,这样段寄存器这样的设计就硬生生变得有用起来了,指向一张段表记录这些标志型的数据。
段的长度是可以改变的,我们先假定它大小固定这样好说明,假如我内存一共能容纳10个段,然后我硬盘能容纳1000个段,我段表就记录我内存中的这10个段对应着硬盘中的哪段数据。然后呢我编程的时候,地址范围写成硬盘那样大,然后有个专门的硬件mmu用来把我程序中的地址通过查表翻译成内存中的地址,如果没有,那就把硬盘中的那个地址的数据放在内存中,最终翻译成的还是内存中的地址。这里就用到了虚拟地址的概念,我程序中的地址是虚拟地址,帮我查表翻译娜硬盘到内存的装置叫MMU,查的表叫做段表,最终翻译成的内存地址是物理地址。用段的方式来管理这个虚拟内存就叫做段式内存管理。就酱。
段式的管理好处是长度可变比较灵活,不好的地方是你比如你A段和C段原来是B段,大小是1000,现在不用了这个地方挪出来了,你新来个从硬盘调来的数据,大小是1001,是不是很膈应人。看着放进去正好却偏偏差一个,于是乎你只能把整个C段往前娜。要是心来个数据是900,其他地方都放不下只能放在这,那剩下的100就很尴尬。这个尴尬的100就叫做内存碎片。根据角度不同,如果你说这个100是由于那个900挤进来了剩下的空间,那就叫内部碎片,如果你说这个100太小新来的程序放不进来,那就叫外部碎片。这块我觉得通过段式叫外部页式叫内部不好,希望大家来讨论下。那为了解决这个问题就有了页式管理,页就是个概念而已,你愿意叫他不变的段也行。硬盘被分为固定大小的物理页,操作系统逻辑上页分为了同等数量同等大小的逻辑页,然后同样有个叫页表的东西记录了逻辑页和物理页的对应关系,然后和段一样当请求的一个页不在页表中,准确说是页表中标志了这个页不在内存,那就把硬盘中的页调进来,这个过程就叫缺页中断。这个页式当然页有好有坏,然后又有个和稀泥的办法就是段页式管理。一句话,怎么的都行,现在操作系统基本都是页,完事。
再说说进程的部分,哎不说了,这块是在连入门都不算,完全不懂就不bb了,留在下一篇吧。
其实上面从某个地方突然就从计算机组成原理的画风转成操作系统了,下面简单说说操作系统为啥出现。当然一开始那个cpu和外设已经可以做想做的任何事了,你完全可以纯手工的方式去把内存中的一块区域一个个地写入代码嘛,然后程序计数器搞一个初值,电一通跑起来。但这太恶心了,于是有了卡片机,再来个卡片机读入的程序事先写好,这样你就不用手工操作内存了,你制作好卡片就好,反正是方便了一点,但本质一样。这叫做手工操作系统,也可以叫没有操作系统。后来发现即使是完全相同的工作,仍然需要每次取出纸片再放进去,比如有两个卡片1和2,有个程序是执行1122121这种顺序,那就需要一个人来来回回放纸,这不科学。于是有了批处理操作系统,人可以事先把1和2加载到内存,然后弄一个c卡片来负责调用这个1和2卡片,这就是调度程序,也可以叫监督程序。这就叫做批处理操作系统。再后来可以交替执行多个任务,一个任务遇到io操作就切换,这叫多道程序系统。但一个作业扔进去之后就不受用户管了,没有交互,于是有了分时操作系统,可以有多个终端使用cpu通过命令的方式并得到响应。但如果某些特别操作需要立刻相应可能就没法做到,于是通过引入中断和严格的中断时间控制做到了实时操作系统。再后来就是我们现在的操作系统啦。其实对这些叫法和定义我并不是特别理解,所以你可以发现我讲的其实挺乱的,这里也希望有大佬给个好的解释。
今天就写到这吧,想着这些天好像学了很多东西,但自己写出来发现把所有肚子里东西吐出来就这么点,没什么系统就是想到哪写到哪能串的尽量串一下了,工作之余偷偷溜走写的。再有这里面的地址呀数据呀好多就是举例,不是准确的值,为了方便理解而已,主要我也不愿意花时间搞个准确的放在这样一篇随便写的文章,以后会把某些地方具体拿出来讲。希望各位大佬给出批评指正或者吐槽探讨,感激不尽!
公众号 - 低并发编程