![b73630570f1b0cc09f93a3d033746f0e.png](https://i-blog.csdnimg.cn/blog_migrate/a4d5b1ff620061f6e92f90c0a68b9258.jpeg)
本文的目的是给新入门的软件工程师介绍计算机体系结构,内容很基础。
可编程计算机
计算机即为可以执行数学计算的机器。
比如说现在某计算机可以计算A类数学问题,那么当我们遇到A类问题的某个实例 i 时,我们只需要将 i 编码成该计算机可以理解形式,并将其输入到计算机,便可以获取该计算机的输出的已编码的运算结果。
上面涉及到了几个概念:计算机、某类数学问题A、A问题的某个实例i、i问题的编码、计算机的输入、计算机的输出、已编码的计算结果。
这些概念比较重要,特别是问题的编码与结果的编码,因为我们必须将一个问题具体化为计算机可以理解的形式。对于解一元一次方程 0=ax+b 而言,问题的输入可以具体化为2个小数a、b,输出可以编码为一个小数x。
这样看来计算机仿佛是一个不需要吃饭,只需要通电的天才工具人,我们只需要把问题扔给他,他就会把结果给我们,已经没有程序员什么事了。
但事情没这么简单,现实世界中的问题类型无穷无尽,我们不可能针对每一种问题都设计制造一台对应的计算机,也不能设计出一台什么问题都能解决的计算机(参考可计算理论)。相反,我们总是使用通用计算机,它可以按程序员输入的计算流程进行计算,解决相应的问题,这样便使得它拥有了解决不同问题的能力。程序员输入的计算流程叫做程序,程序员编写程序的过程叫做编程,而执行程序的计算机被称为可编程计算机。
可编程计算机好比是一个可以按“问题指南”进行工作的工具人,它的职责是严格按照程序的指令进行计算,而程序员的职责便是写出完备的、高效率的程序,让计算机面对对应问题的任何实例时,都能正确运算,并在指定时间内给出结果。
物理上的计算机
计算机早期发展史如下:
1642年法国哲学-数学家Blaise Pascal 发明了世界上第一台手摇式机械式计算机,利用齿轮传动原理制成,能做加减法。
1889年,美国科学家赫尔曼·何乐礼研制出以电力为基础的电动制表机,用以储存计算资料。
1930年,美国科学家范内瓦·布什造出世界上首台模拟电子计算机。
1933年,美国数学家D.N.Lehmer造出一台电气计算机用来分解1-1000万之间的所有自然数为素数因子。
1946年2月14日,由美国军方定制的世界上第一台电子计算机“电子数字积分计算机”(ENIAC Electronic Numerical And Calculator)在美国宾夕法尼亚大学问世。
参考计算机发展历史 和 计算机发展史。
这里我并不打算详细介绍电子计算机的组成原理,因为程序员一般对这个层面的知识不太感兴趣(除非是底层系统开发人员)。因为程序员一般会在逻辑上的计算机上进行编程,这样的好处有2点:
- 逻辑上的计算机更加简单、整洁,在它上面写程序更加简单。
- 避免与物理计算机相耦合,我们不希望自己的程序依赖于某些计算机独有的特性,以至于它无法在其他计算机上运行。
现在应该能理解为什么程序员不会修电脑了。。。
逻辑上的计算机
这里必须搬出大名鼎鼎的冯洛伊曼构架了,如下图所示。
![ab3b7f06b47a23eb70ee5ee7c4ee9e0c.png](https://i-blog.csdnimg.cn/blog_migrate/a4ae98df77b6f2ed442bba4ef04da0be.jpeg)
这里把运算器和控制器看做CPU整体,以方便理解。
在启动程序时,操作系统会首先将程序载入内存,然后让cpu从程序的第一条指令开始,依次执行后面的指令(程序指令有循环、跳转、分支等结构)。程序员编写的指令会将问题的编码载入内存,并开始执行对应的计算流程,这个过程中产生的中间计算结果也都保存到内存上。最后当计算完成时,程序中的指令会将结果写入输出设备。
可以发现,该结构以内存为中心,只不过以CPU作为工具对它进行倒腾,以得到问题的结果。
这里CPU好比是工具人,内存好比是工具人手里拿着的草稿纸。在执行程序的时候,我们会给工具人一张纸作为输入,上面写着问题实例的编码和对应的程序指令。工具人在拿到输入的纸后会立马把它抄到自己的草稿纸上,然后按照草稿纸上的指令在草稿纸上运算,最后在草稿纸上得出结果,并依据指令把结果写到输出的纸上。
另外需要注意的是,CPU上也有一些被称之为寄存器的存储单元,比如PC(program count)寄存器保存了需要执行的下一条指令在内存中的位置,这些寄存器也属于程序的存储体系。
操作系统
在正式介绍程序内存结构之前,这里必须先简单介绍一下操作系统。操作系统一般包括操作系统内核以及附带的实用程序,这里仅介绍内核的功能。
我们希望计算机能同时运行多个程序,比如在写代码时还能听歌,但CPU工具人这个二愣子在设定好PC寄存器后就会不停往下执行,使得其他程序没有执行机会,这里就需要操作系统内核介入了。CPU每过一段时间就会产生一个时钟中断信号,这个信号会使CPU保存自己当前的状态到内存并转而执行对应的中断程序,中断程序会调度不同程序使它们均有执行机会。而中断程序则是操作系统内核的一部分,在开机时便由操作系统设置好了。
上面的例子只是不同程序共享cpu的例子,实际上运行在同一台计算机上的程序几乎共享所有东西,包括CPU、内存、硬盘、键盘、打印机等等。我们希望自己的程序不受其他程序干扰,逻辑上自己独占整个计算机,使得程序编写更加简单,而这都离不开操作系统对计算机资源的管理。
为了使得操作系统有管理其他程序的能力,CPU在运行时至少有2种权限级别,分别称为内核态和用户态,其中操作系统运行在内核态,其他普通程序运行在用户态。CPU中的一些指令只有在内核态才能执行,比如访问输入输出设备,这使得普通程序在访问共享资源时必须先向操作系统申请(主动触发某个中断),操作系统会访问共享资源并把结果返回给用户程序。
以内存的访问为例。内存好比是一个很大的快递柜,数据好比是快递包裹,访问内存好比在快递柜的某个小柜子里里面存取包裹,所以在访问内存时必须指定快递柜的编号,即内存的地址。那么问题来了,如果2个程序需要访问同一个地址怎么办?问题的解决得益于操作系统的内存管理,实际上程序使用的地址为独享的虚拟地址,在程序访问内存时,CPU会根据操作系统设置的转换规则将虚拟地址转换为物理地址,这样一来就避免了不同程序内存地址的冲突。
程序运行时的内存结构
终于可以开始分析程序结构了。已知目前计算机体系结构是以内存为中心,那么在分析程序运行时结构时,也主要分析其内存结构,即程序的指令数据,全局数据、中间计算数据在内存中的布局及其变化。
对于一个问题的计算流程而言,我们需要部分数据在任何时候都存在且可以访问到,我们称存储它的变量为全局变量;还有某些数据只在某个子计算流程才用到,子流程结束就不需要了,我们称之为局部变量;还有一些数据比较特殊,它需要根据具体的问题实例动态分配大小,我们姑且称之为“动态变量”。加上存储程序指令的区域一共需要4个存储区域,如下图所示:
![8db57e8f85d4204d19360cb4e8844faf.png](https://i-blog.csdnimg.cn/blog_migrate/4c64b0f68a65b2a82db078b69e799741.png)
严格来说,上图为c/c++程序在Linux系统上运行时的内存布局,其他程序或系统可能会有区别。上述内存地址空间为程序的虚拟地址空间,其中代码段存放指令,已初始化数据段和未初始化数据段存放全局变量,栈存放局部变量,堆存放动态变量。
堆和栈的内存分配会随着程序的运行而发生变化。以栈为例,每当调用一个程序函数时,为了分配此函数所需的局部变量,栈空间会往下添加一个保存该函数局部变量的栈帧,同理,当函数退出时也会删除当前的栈帧。而堆则麻烦一些,程序一般会先向操作系统申请,然后自己管理这些内存使用和释放。
栈帧操作的逻辑比较简单,其操作代码一般由编译器生成。而堆内存管理则是问题相关的,比较复杂,一般需要程序员自己编写,这也导致了堆内存分配和释放是程序bug的重灾区。
计算机编程语言
计算机编程语言用来描述计算机程序的计算流程,其大致可分为3类:机器语言,汇编语言,高级语言。
- 机器语言是计算机可以直接执行的二进制代码,基本没人会用这种方式写程序。
- 汇编语言依然十分贴近计算机底层,只不过用人类易于使用的字符串符号代表指令和内存地址,目前可能仅用于驱动或其他系统软件开发。汇编语言需要由汇编器翻译成机器代码才能由计算机执行。
- 高级语言目前使用的主流语言,其不使用任何CPU指令描述程序,而是使用加减乘除等数学运算,用户可以定义各种抽象变量,不需要直接和内存地址打交道。高级语言更贴近人类的思维方式,且与具体机器无关,其一般需要通过编译器编译后才能被机器执行。
目前使用较多的高级语言有c、c++、java、python、PHP、js等等,它们均有自己擅长的使用领域。
编译器
程序编译技术比较复杂,如果不是从事编译器开发的程序员,对其有个概念就ok。下面放2张图,看看就好。
c++代码编译流程如下所示:
![aebf6d40e8db5bcd7f2c6828a5bf8eb1.png](https://i-blog.csdnimg.cn/blog_migrate/b36848152c35c726b691f0c516193eaa.jpeg)
编译器内部计算流程如下所示:
![4c750015aec9e2b19871b27851c5ac62.png](https://i-blog.csdnimg.cn/blog_migrate/9ec1748bc27382be763e4812990020ba.jpeg)
原生代码还是解释性代码
如果某语言编写的程序代码最终被编译为二进制机器指令,其可以由物理计算机直接执行,那么我们称其为原生代码。高级语言的源代码一般可移植到不同机器平台,但编译后的原生二进制机器代码不可移植。
如果某语言写的代码直接被解释器执行,或编译为平台无关的字节码后再由虚拟机执行,那么我们称该语言编写的代码为解释性代码。
原生代码的代表是c、c++,其优点如下:
- 执行比较快。
- 可以用于开发操作系统、驱动等底层软件。
解释性代码的代表有java、python、JavaScript等,其优点如下:
- 更加灵活,运行时特性更丰富。
- 支持堆内存自动回收。解释器或虚拟机可以追踪堆内存对象的引用情况,以做到智能回收,大幅降低程序员的编程负担。
- 可执行文件可移植,即不同平台运行得到的结果一致。
如果没有必须选择原生代码的理由,建议选择解释性语言。如果觉得性能不够,完全可以将耗时的那部分代码剥离出来,并使用原生代码编写,最后在解释性代码中调用原生代码。
软件开发的难点
本部分比较重要,是程序员吃饭的本事之一。参考下述链接:
yishun:软件开发的难点及一些应对方法zhuanlan.zhihu.com![aa26167a342a33dffc4cdec27bd2f6cc.png](https://i-blog.csdnimg.cn/blog_migrate/86d444f0ccf8d708d02bf22d5aa87848.jpeg)
结语
本文自低向上的介绍了一个程序员应该了解的计算机体系结构知识,如有错误或不足,还请指出。