写在前面 |
因为如果你想要成为一位优秀的程序员,只知道所以然,而不知道之所以然,是远远不够的。对于一个程序员来说,仅仅知道“双击程序图标,程序开始运行”这样的层次,肯定是不能写出一手高质量的代码的。我们还需要知道,加载到内存中的机器语言程序,由CPU经过怎样的解析和运行,进而使得计算机系统整体的控制和数据运算开始运行起来,最终使得程序跑起来。最起码,下面这张图,应该有些许认识:
这篇文章大概结构 |
正文开始 |
- 对程序员来说CPU是什么
CPU(Central Processing Unit)怎么描述它呢?是不是第一反应是,它是计算机的大脑。那么,对于程序员来说,CPU是什么呢?CPU是寄存器的集合体。学过计算机系统知识的我们都知道,CPU是由寄存器、控制器、运算器和时钟四部分构成的,那么为什么独独说,CPU是寄存器的集合体呢?
因为程序是把寄存器作为对象来描述的。那么我们就不得不来说一说,寄存器的主要种类和功能:种类 功能 累加寄存器(accumulator register) 存储执行运算的数据和运算后的数据 标志寄存器(flag register) 存储运算处理后的CPU的状态 程序计数器(program counter) 存储下一条指令所在内存的地址 基址寄存器(base register) 存储数据内存的起始地址 变址寄存器(index register) 存储基址寄存器的相对地址 通用寄存器(general purpose register) 存储任意数据 指令寄存器(instruction register) 存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作 栈寄存器(stack register) 存储栈区域的起始地址 而在以上列举的寄存器中,因为程序计数器存储着下一条指令所在内存的地址,所以它决定着程序流程。说到程序流程,我们应该能够想到,程序流程分为三种:顺序执行、条件分支和循环。顺序执行就不用说了,就是按照地址内容的顺序依次执行命令即可。循环就是程序重复执行同一地址的指令。而条件分支,就是根据条件执行任意地址的指令。
到这里,CPU大概就介绍完了。你会发现,CPU处理其实挺简单的,它能够做的处理非常少。少到下面一张表,就能够描述完整:类型 功能 数据转送指令 寄存器和内存、内存和内存、寄存器和外围设备之间的数据读写操作 运算指令 用累加寄存器执行算术运算、逻辑运算、比较运算和移位运算 跳转指令 实现条件分支、循环、强制跳转等 call/return指令 函数的调用/返回调用前的地址 - 数据使用二进制数表示的
对于数据是使用二进制数表示的,我觉得咱们应该是可以达成这个共识的。在这里,想说一下移位运算。移位运算指的就是,将二进制数值的各数位进行左右移位的运算。
咱们先来看看左移运算:
在十进制的基础上,我们知道,如果一个数向左移动两位,比如1向左移动两位变为100,扩大了10的2次方倍。那么同理,使用二进制来表示时,向左移动两位,那么应该扩大为原来的2的2次方倍。移位前是39,移位后为156,正好是4倍。反之,二进制数右移之后,则会变为原来的1/2,1/4,1/8…这也是为什么移位运算能够代替乘法运算和除法运算的原因。
介绍完之后左移,我们来看一下右移:
按照左移的规则,那么我们应该很容易就想到,所谓的右移就是这样:
有没有发现,为什么我右移两位之后,同样空出来的位用0补齐,它的数值大小不是原来的1/4呢?因为我们一般使用最高位来标识一个数的符号位,如果不加判断就使用0去补齐的话,那么就破坏了这种规则。这种右移方法在逻辑上是成立的,但是在算术上不成立,所以这种右移方法称为逻辑右移。接下来我们看看算术右移:
这样我们发现,使用这种方法右移之后,结果就是正确的了。关于数据是用二进制数来表示的相关内容就说到这里了。- 计算机进行小数运算时出错的原因
通过上面,我们知道计算机是使用二进制数来表示的。那么,在它运行过程中会出错也是在所难免的了。为什么这样说呢,我们来看一张表:
二进制数 对应的十进制数 0.0000 0 0.0001 0.0625 0.0010 0.125 0.0011 0.1875 0.0100 0.25 能看出来点儿什么嘛?因为计算机是使用二进制数来表示的,所以在进行小数运算时,它会出错。因为在二进制中,0.0001和0.0010是连续的,但是它们所表示的十进制数并不连续。这样的差别就造成了计算机在进行小数运算时,出错。
那么我们应该如何避免呢?首先就是会比策略,即无视这些错误。因为有的时候,这些微小的偏差是不会造成什么的问题的。但是如果非要精确到这种程度,也未尝不可。既然计算机在进行小数运算时会出错,那么我们在运算时,可以将小数转换成整数来计算,这样计算机就不会出错了。- 熟练使用有棱有角的内存
已经经历过拆装机的我们,对于内存肯定是不陌生的:
那么,内存的逻辑模型是什么呢?
如果能够了解内存的逻辑模型,就比较容易理解“数据是内存的使用方法的基础”这句话。因为数组的结构和内存的构造是一样的。所以,使用数组能够使编程工作变得更加高效。- 内存和磁盘的亲密关系
说到内存,我们就不得不说到磁盘。磁盘中存储的程序,必须要加载到内存后才能运行,这是因为负责解析和运行程序内容的CPU,需要通过内部程序计数器来指定内存地址,然后才能读出程序。基于此,内存和磁盘的关系其实是非常亲密的。
首先,磁盘缓存机制。磁盘缓存实际上是内存,这种机制的存在,加快了对磁盘访问的速度。其次是虚拟内存。虚拟内存是将磁盘的一部分作为假想的内存来使用。磁盘缓存是假想的磁盘(实际上是内存),虚拟内存是假想的内存(实际上是磁盘)有没有被绕晕~
我们都知道,程序会经常被内存不足,而导致运行缓慢。如何解决呢?首先最简单粗暴的办法就是增加内存容量。当然了这样的前提是,你得有钱,有银子~
还有另外两种办法:1)调用DLL文件实现函数共有。2)通过调用_stdcall来减小程序文件的大小
接下来说一说磁盘的物理结构,这样才能引出压缩数据。
磁盘时通过把它物理表面划分成多个空间来使用的,划分方式分为扇区方式和可变长方式两种,而Windows计算机采用的都是扇区方式。扇区方式是指,将磁盘划分为固定长度的空间,它是以簇为单位进行读写的。也就是说,只要我进行写入,不管1簇的区域有没有被填满,这1簇都不会再被别的程序使用。- 亲自尝试压缩数据
知道磁盘的存储方式之后,我们来看看数据是如何进行压缩的。
在任何情况下,文件中的字节数据都是连续存储的。
压缩数据是通过算法来实现的,RLE算法,二叉树实现哈夫曼编码之类的,具体我也不太懂,但是原理是:所有存储的内容,在计算机看来只有两种方式“0”和“1”。所有,所谓压缩数据,就是将排列的“0”与“1”尝试进行压缩。
哈夫曼算法能够大幅提升压缩比率,所以啊,数据结构还是要好好学,哈夫曼算法还是要好好理解。- 程序是在何种环境中运行的
程序都有运行环境,那么什么是运行环境?运行环境=操作系统+硬件
CPU只能解释其自身固有的机器语言,机器语言的程序称为本地代码,文本文件在任何环境下都能显示和编辑,我们称之为源代码,通过对源代码进行编译,就能得到本地代码。一般的应用软件包,就是本地代码,而不是源代码。
在这一块想说一说Java虚拟机(JVM,Java Visual Machine)。Java虚拟机的存在,使得它能够提供不依赖特定硬件及操作系统的程序运行环境,因为它是一边将java字节代码逐一转换成本地代码,一边运行的。所以java就比较火,因为它能够在任何环境下运行。
但是其实这样也有问题,那就是不同的java虚拟机之间是没办法进行完整互换的,因为想让所有的字节代码在任意java虚拟机上都能运行起来是比较苦难的。还有就是运行速度问题。因为java虚拟机每次运行时都要把字节代码变换成本机代码。所以这也是需要优化的一块。- 从源文件到可执行文件
通过上面我们知道,计算机只能运行本地代码。使用某种编程语言编写的程序就成为源代码,源代码到本地代码需要有机器来转换一下,计算机才能运行起来。
编译器负责转换源代码。编译器首先读入代码内容,然后再把源代码转换成本地代码。读入的源代码经过语法解析,句法解析,语义解析等,生成本地代码,使得计算机能够运行起来。
但是编译器转换源代码后,还是无法直接运行的,还需要进行“链接”处理。链接有静态链接库,动态链接库。就是常见的DLL文件。
到这里,才有些明白,为什么都要有DLL文件~- 操作系统和应用的关系
应用想要控制硬件,比如使用网易云音乐播放音乐时,需要让音响发出声音,那么就需要去控制硬件,但是如何控制呢?通过操作系统。
操作系统的硬件控制功能,通常是通过一些小的函数集合体的形式来提供的,这些函数及调用函数的行为统称为系统调用。- 通过汇编语言了解程序的实际构成
在这里,想说说栈。栈(stack)有“干草堆积如山”的意思。就像名称所表示的那样,数据在存储时是从内存的下层逐渐往上层累积,读出时则是按照从上往下的顺序进行的。
栈是存储临时数据的区域,它是通过push指令和pop指令进行数据的存储和读出。所以栈是“后进先出”的机制。- 硬件控制方法
相信大家对DMA(Direct Memory Access)机制来进行对硬件的控制的。通过利用DMA机制,大量数据可以在短时间内转送到主内存,是因为CPU作为中介的时间被节省了。
还记得DMAC(Direct Memory Access Center)嘛,外围设备想要获得主动权时,通过DMAC向CPU发送请求,CPU同意之后,DMA便可以自己去控制外围设备的工作。使得效率显著提高。- 让计算机“思考”
当下最火的莫过于AI(Artificial Intelligence),但是自我感觉计算机本身并不智能,它只是运行了表现人类思考方式的程序而已。所以让计算机“思考”的前提,是程序体现了人类思考方式的习惯。
最后结语 自我感觉这篇文章篇幅确实是有些长了些,但是如果分开写的话,感觉就不是一个整体了,所以就冒了一个险,写了这么长的一篇文章。如果你有耐心读到这里,请先接受我崇高的敬意~
这篇文章,内容参考《程序是怎样跑起来的》,我读完之后收获挺大的,就看这篇文章应该也能看出来。这篇博客,短短的一篇就想说明白一本书想要讲的内容,那不是天方夜谭嘛~所以,如果读完这篇文章能够激发起你想要去探索的欲望,那我的目的也就达到了。
在草稿箱里躺了好久,经过了这几天的总结,终于能够在今天发表出来了~对了,今天我感觉还是挺特殊的一天,2018102,发现了嘛,倒着看也是2018102,哈哈哈,神奇的一天
因为在刚开始上了一张总体图,所以我就不画整体的思维导图了~
最后非常感谢您的阅读~