序
我知道CPU和内存是计算机的核心, 毕竟所有的运算最后都得通过他们俩来完成, CPU从内存里要取一条指令, 做计算,然后再写回内存, 如此周而复始。
但是这俩货却瞧不起我,说这是什么年代了,还在用机械式操作, 读写数据的时候, 还得一个磁头在多个盘片上滑来滑去,找来找去, 速度慢的要死。
内存说:“CPU比我快100倍, 比你快100万倍,整个系统的速度都被你给拖慢了。”
这是典型的五十步笑百步。
他俩还嘲笑我很娇气,得真空、密闭、不能有浮尘、运行时不能震动, 一动就坏了。
但这俩二货总是会忘记他俩的最大问题,所以我只用一句就把他们俩给噎死: 你们俩断电了怎么办?
还有我的容量都是按TB, 甚至PB 来算的, 就你们俩那点容量,还笑我?
还有,没有我来存储程序,你们从哪儿得到程序, 难道要像牵牛星(如下图)一样, 手工拨动一排开关来输入程序吗?
其实我也很纳闷, 为什么你们人类造不出来一个能够断电存储的, 大容量的, 访问速度快的,当然还要便宜的硬盘来 ,你们不都上天了吗?要登陆火星了吗? 这些基础的材料怎么还无法突破?
我憧憬着这么一天的来临, 如果能制造出来了, CPU就可以直接访问硬盘了, 内存就一边凉快去吧。
在制造出来之前, 你们必须得容忍CPU-内存-硬盘之间的速度不匹配, 并且想出办法来解决这种速度的不匹配, 比如用缓存、 直接内存访问、 多进程/线程切换等等方法。
内部结构
其实我的内部是长这个样子的:
看到没有,我有很多个盘片像串糖葫芦一样被串在一个主轴上, 主轴带着他们疯狂的旋转。
每个盘片都有很多一圈一圈的磁道, 每个磁道又分为一个一个的扇区。
多个盘片上的同一位置的磁道组成了一个柱面 (需要发挥一下你的想象力)
最后每个盘片上都有可以读写数据的磁头。
所以,如果你想访问我的数据,可以说: 把0柱面, 0磁头, 1扇区的数据给我拿来。
我就把磁头挪到您指定的柱面,对每个磁盘来讲其实就是指定的磁道, 所以这叫“寻道时间”
然后再旋转磁盘,让磁头指向您指定的扇区,这才能开始读取数据, 这叫“旋转时间”,转速快的硬盘能更快的旋转到特定扇区, 所以性能会更好些。
文件
当然,对于绝大部分人来说,都不想去了解什么柱面,磁头,扇区这些非人的术语,所以我为懒人们专门提供了一个叫做逻辑块的方式,你看到磁盘就是有一个个“块”组成的,编号为1, 2, 3, ..n 。
想取哪一块就取哪一块, 比如你说:把第1024号的“块" 的数据给我取过来, 我在内部就把1024转化成柱面,磁头,扇区, 按照上面说的方法寻道,旋转,读取数据。
但是这还远远不够,比方说你想写个文档,输入了很多字和图片,最后想存到我这个硬盘上,你该怎么操作?
一种方法是这样的:
你: 硬盘,给我找20个空闲的磁盘块, 我想存我的文档
我:空闲的磁盘块编号是1024, 2048, 2049 ,3000 ,......
你: 把这些文字和图片存到这些磁盘块上
我: 好的,存完了, 你得记住这些块啊, 这样下次才能读取。
你: 拿一支笔把这些磁盘块编号都记到本子上
过了几天......
你: 硬盘, 把1024,2048,2049,3000这些数据给我取出来,我要编辑。
我: 好的,这是你的数据
没有人喜欢这种方式, 太折磨人了!每个人喜欢这么做:
打开word -> 新建一个文件-> 输入文字和图片-> 保存到C盘“我的文档”目录下。
这个所谓的“文件”和“目录”就是我的杰作啊,你再也不需要和烦人的磁盘块打交道, 只需要记住你的文件名和路径, 一切工作交由我和操作系统老大来搞定。
我和老大商量好了,文件对人类来说是最小存储单位, 你想存任何东西,无论多么小,非得建个文件不可。
此外为了让这个世界整洁有序, 多个文件可以放到一个目录(其实也是个特殊的文件)里, 目录之上还可以有目录,形成一个树的结构。
文件这个东西是个伟大的发明, 我估计你们还得再用100年。
文件的存放
我日常的主要工作就是对目录和文件做操作,当然需要操作系统老大的配合,好吧,其实是老大在主导。
这其中最重要的一个问题怎么去记录各个文件都用到哪些磁盘块?
内存给我支了一招:你可以采用连续记录的方式啊, 就像这样。
文件1 占据磁盘块 1-3
文件2 占据磁盘块 8-12
文件3 占据磁盘块 15-20
内存说: 这种方法在随机访问文件是效率极好,因为你只要知道了开头和长度,就像数组一样可以随便访问, 就像CPU访问我一样, 只要给出地址,立刻就能定位到指定的位置。
我仔细想了想, 内存出的是一个损招, 比如说内存磁盘块 4 -7 ,以及13-14怎么没用?
那是因为之前那里也有文件,后来被删除了,留下了空洞, 如果之后没有大小合适的文件过来,他们就永远空在那里了。
对我来说这是严重的浪费, 这是我不能容忍的。
我说:“小样你以为我看不出来啊,你不就是嫉妒我容量大,让我浪费一点嘛”
内存坏笑了一下又说: 不喜欢也没关系嘛,试试采用链式啊:
这个文件从第一块磁盘开始,形成一个链 1->9->18->8->3 , 每一块空闲的磁盘都会得到充分的利用, 效率非常高。
我心想: 这些码农说的数据结构和算法还真是有用啊, 这里也用上链表了。
可是这种方式随机的访问效果太差, 每次都得从第一块开始,沿着绳子往后找,太痛苦了。
现在内存已经嘲笑我慢了,用这种很慢的办法,还不得笑死我?
操作系统老大说:“别听内存在那里BB了, 用索引式!”
例如第16号磁盘块专门用来存放文件属性以及该文件所使用的磁盘块。
老大把这个磁盘块叫做inode,通过它可以轻松的找到这个文件所使用的所有磁盘块, 无论是顺序访问还是随机访问都很快。
唯一的缺点是得用额外的磁盘块单独存放inode 。
我觉得挺好,没有十全十美的东西,折中达到平衡最重要 ! 就是它了!
我问老大:每个文件都需要有个inode来描述,每个目录是不是也需要一个?
”这是自然,和文件一样,每个目录也是一个inode, 其中有目录的属性,还有存放这个目录内容的磁盘块号,在磁盘块中才真正的存放着目录下的内容“
“举个例子来说吧: 有人要读取 /tmp/test.log这个文件, 查找次序是这样的:根目录inode-根目录磁盘块-tmp目录inode-tmp目录磁盘块-test.log的 inode-读取磁盘块”
(点击看大图)
内存说: “我赛,这也太绕了吧,比CPU访问我的数据麻烦多了, 硬盘,你要小心点, 这要是操作不当的很容易出乱子的。”
我心想内存这次没坑我,他提醒的对, 这操作确实有点复杂,读数据的时候还行, 如果是修改,尤其是删除就很容易出事,例如想删除上面的文件/tmp/test.log,需要这些步骤:
(1) 在目录中删除文件
(2) 释放inode 到空闲的节点池, 这样可以复用
(3) 将磁盘块释放到空闲的磁盘块池
在操作某一步的时候出现系统崩溃,那我这些个目录和文件就凌乱了, 可能会出现空间无法释放的情况。
老大说: “这确实比较烦, 不过也能解决,听说过数据库是怎么办的吗: 记录日志! ”
”就是把要做的事记录下来?“
“是的, 在做操作之前, 记录要做的事情,形成日志, 把他们成功写入磁盘以后再正式动手操作, 等到所有步骤都搞完,才可以擦除日志项。 你想想,如果执行到某一步崩溃, 系统重启时检查日志项,就知道哪些没做,哪些已经做了, 对于没做的日志,重新来一遍就是了” 。
其实说的很轻松,实施起来还是挺难的, 重新执行就意味着那些操作一定是可以重复执行, 并且不会带来破坏才行。
(码农翻身注:这叫做日志(Journal)文件系统)
管理空闲块
目录和文件的存储问题解决了, 接下来我需要一个大管家,把那些没有使用的、空白的、数量上亿的磁盘块给管理起来,只有这样, 新的文件来的时候,才能分配空间存储。
操作系统老大给我推荐了两位, 第一位主张是链式大法好, 无非就是把空闲磁盘块组成一个链表(又是链表!) ,但是我心里盘算了一下: 如果磁盘块号是32位的,每个块都得花费我32位的空间,如果我有5亿个空闲块, 那仅仅为了记录他们就要占用接近2G的磁盘空间! 这浪费可是有点大啊。
还有一位主张位图法, 这个方法更简单, 对每个磁盘块,如果已经被使用,那就标记为1, 没被使用就是0。 这样整个磁盘块就形成了一个由0和1组成的一个大位图。
由于每个磁盘块只用一个位来表示, 非常节省空间, 这个方案我喜欢!
文件系统
扯了这么多,是时候看一看全局了, 在你们程序员的眼中, 其实我是长这个样子的(拿你们崇拜的Linux ext2为例):
(点击看大图)
我这个硬盘主要由MBR(Master Boot Record)和各个磁盘分区组成。
MBR中的有 引导代码 和 磁盘分区表, 分区表中记录了每个分区的起始位置,以及哪个磁盘分区是活动分区, 这样系统就会找到它, 然后装载这个分区中的引导块,并执行之。
引导块将会装载存存储在本分区的操作系统。 需要注意的是,每个分区都有一个引导块,不管这个分区有没有操作系统,这是各大厂商的约定,是一种标准。
每个分区除了必须的引导块之外,又被分成多个块组。
在每个块组中你能看到熟悉的磁盘块位图和inode位图, 不用解释估计你也知道是干嘛的。
还有inode表(当然是存放文件和目录的inode 了)和真正的数据块。
对了, 我的磁盘分区表只有64个字节, 而每个分区项占用16个字节, 所以只能容纳4个分区。
如果你想用多于4个分区, 你就需要把其中一个设为扩展分区, 然后在其中继续划分成逻辑分区,想划几个就划分几个。
一般情况下,大家都喜欢把我划分成一个主分区+一个扩展分区, 在扩展分区中再需要划分。
(完)
二等公民
这个世界存在阶级歧视的, 我确信。
CPU和内存很明显是第一等公民, 这一对好基友占据着二环内最核心、最金贵的土地,居高临下对外发号施令, 从各处“抢劫”二进制数据到自己的地界来。
其他的都被归类到二等公民, 居住在5环以外, 统称为输入输出(I/O)设备。
有些设备居无定所,通过USB临时接入到计算机,待个三五天就走,典型的“北漂”。
哦对了,我必须得说明, 硬盘的地位有点特殊,虽然它也是个I/O设备, 但是它存储着所有的程序和数据,包括操作系统老大!
虽然经常被CPU和内存嘲笑, 但硬盘应该属于1.5 等子民, 住在三环以里。
(码农翻身注: 参见《我是一块硬盘》)
我是一个键盘,和我的兄弟鼠标一样,是个典型的输入设备, 像我这样的I/O设备多如牛毛, 比如显卡,声卡,网卡,打印机,扫描仪, CD-ROM等等。
操作系统老大把我们这些二等公民笼统的划分为两类:块设备和字符设备。
硬盘,CD-ROM,U盘是典型的块设备, 数据存储在固定大小的块中, 每个块都有一个地址,就样门牌号那样。
像我,鼠标,打印机很明显就是字符设备,哪有什么块结构? 就是用一个个字符组成的流而已,也没有什么地址。
还有一种分法是存储设备(硬盘),传输设备(网卡,调制解调器)、人机交互设备(我和鼠标,显示器也算), 不管怎么划分,我们二等公民的身份都无法更改,也住不到二环去, 那里房价实在太贵了。
夜深人静的时候,我和二等公民朋友们经常探讨这个经久不衰的问题: 咱都是人,为啥就住不到二环去?
鼠标说: 这都是命啊, 计算机刚发明的时候,只有最基本的计算功能和存储功能,哪有什么显卡,声卡,网卡? CPU和内存的祖先占据了二环,并且一直在那里经营至今, 现在都不知道是多少代了。
我说: 唉,也是, 我们的祖先还是出现晚了, 没有占据好地界儿。
网卡说: 这个不对的, 关键是我们没本事,干不了CPU的活啊。
“那算啥, 我的GPU运算速度已经很厉害了, 很多超级计算机还用我做运算部件呢,知道不?” 显卡说。
鼠标说: 别想那么多,其实二环生活也不轻松,你看看一开机,CPU阿甘和内存就忙的不可开交, 累得要死。 像我和键盘, 尤其是你键盘, 除了码农写程序, 半天都不用一下, 还是知足吧。
总线 和 端口
虽然我们是住在五环外的二等公民,但是CPU还得和我们打交道, 那CPU是怎么和我们联系的?
一种办法就是CPU和每个I/O设备之间都扯一根线, 有多少个设备就扯多少根,组成了一个以CPU为中心的星型布局, 很明显这样太麻烦了, 尤其是来了新设备怎么办?
后来我们采用了“总线”这个概念, 大家都挂到这一条“总线”上, CPU想找谁了,就在上面吼一声。
当然这种方式也有缺点, 当一个人在总线上吼叫的时候会霸占总线, 其他人都得等待。
还有这么多设备, CPU怎么知道谁是谁?
首先肯定得给每个设备编号,比方说硬盘(更准确一点是,硬盘控制器)的编号是320 , 图形控制器的编号是 3D0 , 这个编号就被称为 IO端口。
有时候CPU会更懒, 他和内存商量好, 把我们这些IO端口映射到内存中去, 这样CPU访问我们的时候,就像访问内存地址一样了。 称为 内存映射I/O
轮询(程序式I/O)
现在CPU知道了我们在哪儿,还知道我们的编号, 接下来就可以和我们通信了。
这时候我们又遇到了那个老问题: CPU太快,而我们I/O设备太慢, 毕竟机械设备的速度是无法和电子设备相媲美的。
比如说有个进程要读取硬盘上的文件, CPU代表该进程向磁盘控制器发出指令:
CPU: 硬盘硬盘, 把你第1023689号磁盘块的内容给我拿过来
硬盘: 好的
CPU :弄好了没有?
硬盘: 还没有
CPU : 弄好了没有?
硬盘: 还没有还没有
CPU : 到底弄好没有? 快点
硬盘:都说过没弄好了 !
CPU : 弄好了没有?
硬盘 : @#¥%……&&
CPU一直霸占着总线,不厌其烦的问硬盘弄好了没有, 别的啥事也不做, 这叫做轮询 , 或者叫做程序控制的I/O
由于CPU比硬盘快百万倍, 很明显CPU被浪费了。
中断
大家都觉得这样不合适, 因为CPU忙着和硬盘“卿卿我我”, 把别的I/O都抛弃了, 像我,鼠标即使有什么数据等着CPU读取,他也不搭理我们。
后来就改成了这种方式:
CPU: 硬盘硬盘, 把你第1023689号磁盘块的内容给我拿过来, 你弄好了以后告儿我一声。
硬盘: 我怎么告诉你啊
CPU: 我有一个中断请求线, 你弄完了可以往这个地方发信号, 我每次执行完一个指令都会检查。
硬盘 :好的
(CPU干别的事儿去了,当前进程A阻塞,另外一个就绪的进程B开始执行)
过了不知道多少纳秒。。。。。
硬盘: CPU, 数据好了,赶紧过来取走。
CPU : 稍等,我把当前的进程B给保存了, 然后就去处理。
(CPU执行中断处理程序, 读取数据.....)
这就是“中断”方式,有了中断以后, 这些平时都不怎么露面的I/O设备都跳出了抢着发中断, “调戏”CPU, CPU乱成了一团, 系统也乱成了一团。
CPU不胜其烦, 后来专门找了一个叫中断控制器的家伙专门负责协调, 这家伙确实厉害,一上场就说:
只有我才能给CPU发中断, 你们的中断请求统统发给我啊,我来裁决谁的优先级高,谁能“调戏”CPU。
这样一来系统清净了。
(码农翻身注: 这种“中断”的方式,其实就是一种异步的、事件驱动的处理思想,在计算机软硬件上应用非常广泛, 例如Node.js, AJAX等等 )
DMA
但是我知道, CPU和内存才是系统的核心, CPU运算时候只认内存这个好基友,所以所有的数据不管是谁产生的, 不管是1.5等公民硬盘,还是2等公民键盘,鼠标等, 数据统统都得搬到内存去。
这就给我们带来了一个挑战: 数据的搬运。
中断的方式对于小数据量传输是有效的, 像我是一个键盘,每次你按下一个键以后, 我就会发出一个中断告诉CPU,CPU就能发出指令,把这个一个键对应的字符搬到内存。
但是对于大数据量传输尤其是像硬盘这样的, CPU还得花费大量的时间和精力不断的发出指令,让磁盘控制器把数据从硬盘搬到内存去, 这相当于又陷入了程序式I/O的陷阱。
对于类似这样的情况,我们也有办法处理, 就是用一个DMA 控制器, 使用这个专用的处理器进行I/O设备和内存之间直接的数据传输, 脏活累活都被这个DMA给处理了。
CPU: 硬盘硬盘, 把你第1023689号磁盘块的内容发送到内存的xyz 地址去,弄完了告诉我, 我去干别的事儿去了。
硬盘 : 好咧, DMA, 我这儿有数据要传输, 数据一共有4096个字节, 要传输到内存的xyz地址去。
DMA : 没问题 !
(DMA控制器开始哼哧哼哧的干活,把这4096个字节复制到xyz地址)
DMA : CPU, 数据已经在内存中了, 可以用了啊
CPU : 怪不得刚才有时候我没法使用总线, 是不是你小子霸占着啊?
DMA : 我不用总线怎么搬运数据到内存? 我也没有挪用几个时钟周期啊, 再说了你还能使用你的一级缓存和二级缓存不是?
CPU : 好吧, 看在你帮我干了这么多苦活累活,就算了吧。
键盘的工作原理
说了这么多别人的事儿, 也该说说我自己了。
我和鼠标一样,是个非常简单的I/O 设备, 每次你按下和释放键盘上某个键的时候, 我就会产生一个扫描码,例如你按下A , 扫描码是"1E" , 释放A ,扫描码是"9E"
我把这个扫描码放到0x60端口, 然后向CPU发中断, 当然得通过中断控制器了。
CPU 会调用中断处理程序, 读这个0x60端口,取到扫描码, 翻译成ASCII码就可以使用了。
那么问题来了, 假设系统中有好几个进程都在等待键盘的输入, 这个中断处理程序获得 ASCII 码发给谁呢? 怎么发过去呢?
这个问题留给你了, 欢迎给我回复留言,正确答案有奖励啊。
微信公众号【黄小斜】大厂程序员,互联网行业新知,终身学习践行者。关注后回复「Java」、「Python」、「C++」、「大数据」、「机器学习」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「笔试」、「面试」、「面经」、「计算机基础」、「LeetCode」 等关键字可以获取对应的免费学习资料。