探索计算机的结构与核心概念

在我们的生活与工作中所使用到的计算机都是基于冯诺依曼结构实现的,冯诺依曼结构又称冯诺依曼模型或普林斯顿结构,它是一种将程序指令存储器和数据存储器合并在一起的计算机设计概念结构.

冯诺依曼结构起源于EDVAC(Electronic Discrete Variable Automatic Computer)离散变量自动电子计算机,当时冯诺依曼以技术顾问的身份加入EDVAC项目组,负责总结和详细说明EDVAC的逻辑设计,直到1945年6月发表了一份长达101页的报告,这就是计算机史上著名的"101页报告",该报告明确规定用二进制替代十进制运算,并将计算机分成五大组件,这一卓越的思想为电子计算机的逻辑结构设计奠定了基础,已成为计算机设计的基本原则.

冯诺依曼结构

冯诺依曼结构具有以下特点:

  1. 数据由一个贯穿整个结构的总线来进行传输.

  2. 存储器是按地址访问、线性编址的空间

  3. 指令由操作码和地址码组成

  4. 数据以二进制编码

  5. 一个冯诺依曼结构的计算机必须有存储器,控制单元,运算单元,输入输出设备.

冯诺依曼结构将CPU与存储器分开的做法也并非十全十美,CPU和内存、硬盘等设备的数据传输速度不匹配成了整体效率的瓶颈,CPU会在等待数据输入的时间中空置,许多技术都是为了解决这个瓶颈,例如DMA(直接内存访问),在CPU中建立高速缓冲区等.

本文作者为: SylvanasSun(sylvanas.sun@gmail.com).转载请务必将下面这段话置于文章开头处(保留超链接).
本文首发自SylvanasSun Blog,原文链接: https://sylvanassun.github.io/2017/09/08/2017-09-08-ComputerStructure/

现代计算机结构


现代计算机是基于冯诺依曼结构的电子计算机.所谓电子计算机,就是是一种利用电子学原理,根据一系列指令对数据进行处理的机器.

晶体管是组成现代电子计算机的最原始的部件(集成电路中含有数以亿计的晶体管),它是一种半导体材料(导电性可受控制,范围可从绝缘体至导体之间),晶体管可以通过电流的变化,实现电路的切换,这种特性非常适合组成各种逻辑门(与或非)与表示二进制数据.值得一提的是,早期使用继电器实现逻辑门的计算机体积甚至大到要一整个屋子才能放下.

现代计算机的硬件结构如下图,虽然多了很多其他的硬件但与冯诺依曼结构的概念是一致的:

总线


总线是一组贯穿所有硬件结构的电子管道,它携带数据并负责在各个部件间交互传递.总线传送的数据通常为一个定长的字节块,这个字节块的长度即是总线的位宽,总线位宽越大,数据传输的性能就越高,在32位机器中总线位宽为4个字节,64位机器中为8个字节.

有意思的是总线的英文单词是bus,如果把主板想象成一座城市,那么总线就像是城市中的公共汽车,它按着多种固定线路不停地来回传输数据.

I/O设备


I/O(输入/输出)设备是计算机与外部进行联系的桥梁,每个I/O设备都要通过一个控制器或者适配器来与I/O总线相连.

控制器与适配器的区别只在于它们的封装方式,它们的功能都是为了让I/O设备与I/O总线进行连接:

  • 控制器是I/O设备本身或者主板上自带的芯片组

  • 适配器则是插在主板上的外部设备,

在图中,I/O设备包含鼠标、键盘(输入设备)、显示器(输出设备)、磁盘、网络.

内存


内存也叫主存,它是一个临时的存储设备,存储了运行时的数据(程序与程序处理的数据),以供CPU进行处理.内存是由一组DRAM(动态随机存取存储器)芯片组成的,DRAMRAM(随机存取存储器)的一种,另一种为SRAM(静态随机存取存储器),SRAMDRAM速度更快,但造价也更贵,通常用来实现为高速缓存区.

32位操作系统中的CPU的最大寻址空间只有2^32字节,换算下来最高内存上限为4GB,但由于CPU还要对BIOS和其他硬件等进行寻址(这些优先级更高),所以用户实际可用的内存只有3GB左右.

64位操作系统的CPU最大寻址空间足足有2^64字节,也就是16EB(1024GB等于1TB,1024TB等于1PB,1024PB等于1EB),这已经是一个无法想象的数字了,不过这也不一定是够用的,毕竟谁又能知道未来的数据量会有多庞大呢?

内存具有以下特点:

  • 随机存取: 当存储器中的数据被写入或读取时,所需要的时间与数据所在的位置无关(从逻辑上,可以把内存看成一个线性的字节数组,每个字节都有其唯一的地址(索引),这些地址是从零开始的).
  • 易失性: 如果电源突然断开,RAM中的数据就会全部丢失(磁盘可以将数据持久化地永久保存下来,就算断电也不会丢失数据).
  • 依赖刷新: RAM使用电容器来存储数据,当电容器充满电之后表示1,未充电则表示0.由于电容器或多或少有漏电的情形,若不作特别处理,电荷会渐渐随时间流失而使数据发生错误.刷新是指重新为电容器充电,弥补流失了的电荷.DRAM的读取即有刷新的功效,但一般的定时刷新并不需要作完整的读取,只需作该芯片的一个列选择,整列的数据即可获得刷新,而同一时间内,所有相关记忆芯片均可同时作同一列选择,因此,在一段期间内逐一做完所有列的刷新,即可完成所有存储器的刷新.需要刷新正好解释了随机存取存储器的易失性.
  • 对静电敏感: RAM与集成电路一样,对环境的静电荷非常敏感,静电会干扰存储器内电容器的电荷,导致数据流失,甚至烧坏电路.

CPU


Intel I7 CPU

Central Processing Unit中央处理单元,简称CPU或处理器,CPU包含了冯诺依曼结构中的控制器与运算器,它是解释或执行存储在内存中的指令的引擎.CPU好比计算机的大脑,从通电开始,直到断电,CPU一直在不断地执行内存中存储的指令.如果没有CPU,那么计算机就会是一台不会动的死机器了.

所谓指令就是进行指定操作的操作码,而指令集架构就是这些操作码的集合,至于微架构是一套用于执行指令集的微处理器设计方法,多个不同微架构的CPU可以使用同一套指令集,一些常见的指令如下:

  • 加载: 从内存中复制数据(多少个字节取决于总线位宽)到寄存器,以覆盖寄存器中原来的内容.
  • 存储: 从寄存器复制数据到内存中的某个位置,以覆盖这个位置上原有的内容.
  • 操作: 把两个在寄存器中的数据复制到ALU,ALU对这2个数据进行算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原有的内容.
  • 跳转: 从指令本身中抽取数据(地址),将它复制到程序计数器中,以覆盖程序计数器原有的内容.

下面以一个简单的算术问题1 + 1来大致了解一下CPU的工作流程:

  1. 这两个变量首先会被存储在内存中.

  2. CPU从内存中读取指令并刷新程序计数器(每执行完一个指令都要刷新程序计数器).

  3. CPU执行加载指令,通过总线将这两个变量传输(复制)到寄存器.

  4. CPU执行运算指令,从寄存器中复制这两个变量进行算术运算,并将结果存到寄存器.

  5. CPU执行存储指令,寄存器通过总线将结果存储回内存(覆盖原有位置).

寄存器


寄存器是CPU中的一个存储部件,可以认为它是容量很小但速度飞快的内存,寄存器是与ALU直接交互的存储设备(不管数据是在内存还是高速缓冲区,最终都要存到寄存器才能与ALU交互).

CPU架构中,拥有多个寄存器,它们分别拥有各自的用途(指令寄存器,整数寄存器,浮点数寄存器等),且寄存器的数量和它的大小都与指令集架构和机器支持的位宽相关联(例如x86-64指令集架构(64位指令集架构)中支持64位的通用寄存器与64位整数运算,而x86指令集架构只能支持32位和16位).

程序计数器


程序计数器用于指示将要执行的指令序列,并且不断刷新指向新的指令地址,根据CPU的实现不同,程序计数器可能会指向正在运行的指令地址也可能会是下一个指令的地址.

高速缓冲


由于寄存器与内存的速度相差过大,为了避免性能上的浪费,在寄存器与内存之间建立数据的缓存区是很有必要的.

高速缓存是一个比内存更小但更快的存储设备,且使用SRAM实现,现在的CPU一般都配有三级缓存,L1缓存速度最快但存储的容量也最小,L2要比L1慢但存储的容量也更大,以此类推(上一层的存储器作为下一层存储器的高速缓存,也就是说,寄存器就是L1的高速缓存,L1则是L2的高速缓存,L2L3的高速缓存...)....

CPU发起向内存加载数据的请求时,会先从缓存中查找,如果缓存未命中,才会从内存加载数据,并更新缓存.高速缓存之所以如此有效,主要是利用了局部性原理,即最近访问过的内存位置以及周边的内存位置很容易会被再次访问.而高速缓存中就存储着这些经常会被访问的数据.

DMA


DMA全称为Direct Memory Access直接内存访问,它允许其他硬件可以直接访问内存中的数据,而无需让CPU介入处理.一般会使用到DMA的硬件有显卡、网卡、声卡等.

DMA会导致发生缓存不一致的问题,需要额外的进行同步操作保证数据安全.例如,当CPU从内存中读取数据后,会暂时将新数据写入缓存中,但还没有将数据更新回内存,如果在这期间发生了DMA,就会读取到旧的数据.

缓存一致性问题
缓存一致性问题

流水线


流水线又称管线,是现代CPU中必不可少的优化技术,它将指令的处理过程拆分为多个步骤,并通过多个硬件处理单元并行执行这些步骤.

管线的具体执行过程很像工厂中的流水线(指令就像在流水线传送带上的产品,各个硬件处理单元就像是在流水线旁进行操作的工人),因此而得名为流水线.

流水线虽然提高了整体的吞吐量,但也是有其缺点的,这是由于流水线依赖于分支预测,如果CPU预测的分支是错误的,那么整个流水线上的所有指令都要取消,然后重新向流水线填充指令,这项操作是很耗费性能的.

超线程


超线程是一种允许一个CPU执行多个控制流的技术,它复制了CPU中必要的硬件资源(程序计数器、寄存器),来让其在同一时间内处理两个线程的工作.

通过超线程技术,可以让一个CPU核心去执行两个线程,所以一个带有4核(实体核心)的CPU实际上可以执行8个线程(逻辑线程).

多核


多核CPU是指将多个核心(也就是CPU)集成到一个集成电路芯片上.每个核心都可以独立的执行指令,也就是真正意义上的并行执行.

每个核心都拥有独立的寄存器,程序计数器,高速缓存等组件,一般还会有一个所有核心共享的缓存,它是直接与内存连通的缓冲区.

多核CPU与多处理器不同,多处理器是将多个CPU封装在多个独立的集成电路芯片中,而多核CPU是所有核心都封装在同一个集成电路芯片中.

操作系统


操作系统是用于管理计算机硬件与软件的程序,可以把操作系统看成是应用程序与硬件之间插入的一层软件,所有应用程序对硬件的操作尝试都必须通过操作系统.

操作系统需要负责管理与配置内存、调度系统资源的优先次序、管理进程与线程、控制I/O设备、操作网络与管理文件系统等事务.可以说操作系统是整个计算机系统中的灵魂所在.

System Call

操作系统的内核是操作系统最核心的地方,它是代码和数据的一个集合.当应用程序需要操作系统的某些操作时,会执行一条系统调用(system call)指令,这时,控制权会被移交到内核,由内核执行被请求的操作并返回到应用程序.大多数系统的交互式操作都需要在内核完成,例如I/O、进程管理等.

虚拟内存


虚拟内存是计算机系统内存管理的一种技术,它为每个进程提供了一个假象,即每个进程都在独占地使用内存(一个连续的地址空间),而实际上,它通常被分割为多个物理内存碎片,还有部分暂时存储在磁盘存储器上,在需要时进行数据交换.使用虚拟内存会使程序的编写更加容易,对真实的物理内存的使用也会更加有效率.

进程的虚拟地址空间

每个进程所能看到的虚拟地址空间大致如上图所示,每个区域都有它专门的作用.

  • 内核虚拟内存: 这个区域是为操作系统内核保留的,它不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数(只有操作系统内核才有权限).
  • 共享库: 以c语音为例,共享库是用来存放的是像C标准库这样的共享库的代码和数据的区域.
  • 程序代码和数据: 对于所有进程来说,代码都是从同一固定地址开始,紧接着的是其相对应的数据位置.这片区域就是用来存放代码和数据的.
  • 堆: 堆内存是指应用程序在运行时进行分配的内存区域,堆可以在运行时动态地扩展和收缩.像malloc()free()这样的函数就是在堆内存中进行分配空间与释放,而类似Java这种更高一级的语言提供了自动内存管理和垃圾回收,不需要程序员手动地分配与释放堆内存空间.
  • 栈: 栈同样也是可以动态地扩展和收缩,它是一个后进先出的容器,主要用于函数调用.当一个函数调用时会在栈中分配空间,当调用结束时,这个函数所占用的内存空间会一起释放,无需程序员关心.

进程与线程


进程


进程是操作系统对一个正在运行的程序的一种抽象,它是程序的执行实体,是操作系统对资源进行调度的一个基本单位,同时也是线程的容器.

进程跟虚拟内存一样,也是操作系统提供的一种假象,它让每个程序看上去都是在独占地使用CPU、内存和I/O设备.但其实同一时间只有一个进程在运行,而我们能够边听歌边上网边码代码的原因其实是操作系统在对进程进行切换,一个进程和另一个进程其实是交错执行的,只不过计算机的速度极快,我们无法感受到而已.

操作系统会保持跟踪进程运行所需的所有状态信息,这种状态,被称为上下文(Context),它包含了许多重要的信息,例如程序计数器和寄存器的当前值等.当操作系统需要对当前进程进行切换时(转移到另一个进程),会保存当前进程的上下文,然后恢复新进程的上下文,这时控制权会移交到新进程,新进程会从它上次停下来的地方开始执行,这个过程叫做上下文切换.

操作系统的进程空间可以分为用户空间与内核空间,也就是用户态与内核态.它们的执行权限不同,一般的应用程序是在用户态中运行的,而当应用程序执行系统调用时就需要切换到内核态,由内核执行.

线程


线程是操作系统所能调度的最小单位,它被包含在进程之中,且一个进程中的所有线程共享进程的资源,一个线程一般被指为进程中的一条单一顺序的控制流.

线程都运行在进程的上下文中,虽然线程共享了进程的资源,但每条线程都拥有自己的独立空间,例如函数调用栈、寄存器、线程本地存储.

线程的实现主要有以下三种方式:

  • 使用内核线程实现: 内核线程就是由操作系统内核直接支持的线程,这种线程由内核来完成线程切换调度,内核通过调度器对线程进行调度,并将线程的任务映射到各个处理器上.应用程序一般不会直接使用内核线程,而是使用内核线程的一个接口: 轻量级进程,每个轻量级进程都由一个内核线程支持,所以它们的关系是1:1的.这种线程的实现方式的缺点也很明显,应用程序想要进行任何线程操作都需要进行系统调用,应用程序会在用户态和内核态之间来回切换,消耗的性能资源较多.

  • 使用用户线程实现: 这种方式将线程完全实现在用户空间中,相关的线程操作都在用户态中完成,这样可以避免切换到内核态,提高了性能.但正因为没有借助系统调用,操作系统只负责对进程分配资源,这些复杂的线程操作与线程调度都需要由用户线程自己处理实现,提高了程序的复杂性.这种实现方式下,一个进程对应多个用户线程,它们是1:N的关系.

  • 混合实现: 这是一种将内核线程与用户线程一起使用的实现方式.在这种实现下,即存在用户线程,也存在轻量级进程.用户线程依旧是在用户空间中建立的(相关的线程操作也都是在用户空间中),但使用了轻量级进程来当作用户线程与内核线程之间的桥梁,让内核线程提供线程调度和对处理器的映射.这种实现方式下,用户线程与轻量级进程的数量比例是不定的,它们是N:M的关系.

文件


文件也是一个非常重要的抽象概念,它向应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的I/O设备.计算机文件系统通过文件与树形目录的抽象概念来屏蔽磁盘等物理设备所使用的数据块(chunk),让用户在使用文件的时候无需关心它实际的物理地址,用户也不需要管理磁盘上的空间分配,这些都由文件系统负责.

所谓文件其实也就是一串字节序列,一个文件想要长期存储,就必须要存放于某种存储设备上,如本地磁盘、U盘.

网络


如果用图论的方式来看待网络,其实网络就是一张无向图(需要双向通信),每台计算机都是图中的一个节点(指计算机网络),图的边就是计算机之间互相通信的连接.简单的说,计算机网络其实就是多台计算机进行通信的系统.

网络其实也可以看作是一个I/O设备,当系统从内存中复制一串字节到网络适配器时,数据流经过网络传输到达另一台机器上(这其实就是输出操作),系统也可以读取从其他机器传输过来的数据,并把数据复制到内存中(输入).

互联网(Internet)是计算机网络中的一种(如果按区域划分还有局域网、广域网等),互联网是网络与网络之间组成的巨大的国际网络,这些网络之间以TCP/IP协议相连,连接了全世界上几十亿的设备.

我们日常生活中用浏览器上网浏览网页,其实使用的是万维网(World Wide Web),它是运行在互联网之上提供的一个服务,万维网是一个基于超文本链接组成的系统,并且通过http协议进行访问.

OSI模型


OSI模型全称为开放式系统互联通信参考模型(Open System Interconnection Reference Model),是由国际标准化组织提出的一个试图使各种计算机在世界范围内进行互联通信的标准框架.

OSI模型中,数据经过每一层都会添加该层的协议头(物理层除外),当一个数据从一端发送到另一端时,需要经过层层封装.

  • 应用层: 应用层直接和应用程序通信并提供常见的网络应用服务.常见的应用层协议有:HTTP,HTTPS,FTP,TELNET,SSH,SMTP,POP3等.

  • 表示层: 表示层为不同终端的上层用户提供数据和信息正确的语法表示变换方法.该层定义了数据格式及加解密,

  • 会话层: 会话层负责在数据传输中设置和维护网络中两台电脑之间的通信连接.但会话层不参与具体的传输,它只提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制.

  • 传输层: 传输层将数据封装成数据包,提供端对端的数据通信服务.它还提供面向连接的数据流支持、可靠性、流量控制、多路复用等服务.最著名的传输层协议有TCPUDP.

  • 网络层: 网络层提供路由和寻址的功能,使两终端系统能够互连且决定最佳路径,并具有一定的拥塞控制和流量控制的能力.网络层将网络表头(包含网络地址等数据)加到数据包中,网络层协议中最出名的就是IP协议.

  • 数据链路层: 数据链路层在两个网络实体之间提供数据链路连接的创建、维持和释放管理.它将数据划分为数据帧从一个节点传输到临近的另一个节点,这些节点是通过MAC(主机的物理地址)来进行标识的.

  • 物理层: 物理层是OSI模型中最低的一层,物理层主要负责传输数据所需要的物理链路创建、维持、拆除,而提供具有机械的,电子的,功能的和规范的特性.简单来说,物理层负责了物理设备之间的通信传输.

TCP/IP


TCP协议全称为传输控制协议(Transmission Control Protocol),由于它是基于IP协议之上的,所以也有人称作为TCP/IP协议.

TCP协议是位于传输层的协议,它与同样位于传输层的UDP协议差别很大,它保证了数据包在传输时的安全性(丢包重传),而UDP则只负责发送数据,不保证数据的安全.

TCP为了保证不发生丢包,给每个包标记了一个序号,同时序号也保证了接收端在接收数据包时的顺序.然后接收端对已成功收到的包发回一个相应的确认(ACK);如果发送端在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传.TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和.

TCP协议在连接建立与终止时需要经过三次握手与四次挥手,这个机制主要都是为了提高可靠性.

三次握手
  1. 客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态,等待服务器端确认.

  2. 服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态.

  3. 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态.

  4. 服务器接收到客户端发送的SYN报文,三次握手完成,连接建立.

四次挥手
  1. 某一端首先调用close,称该端执行“主动关闭”(active close).该端发送一个FIN报文,表示数据发送完毕(我们称它为A端).

  2. 另一端接收到这个FIN信号执行 “被动关闭”(passive close ),并回应一个ACK报文.(我们称它为B端)

  3. 一段时间后,B端没有数据发送的任务了,这时它将调用close关闭套接字,然后向A端发送一个FIN信号.

  4. A端接收到FIN信号,开始进行关闭连接,并对B端返回一个ACK.

  5. B端接收到来自A端的ACK信号,进行关闭连接,四次挥手完毕.

TCP/IPOSI模型抽象成了四层,下图为以HTTP为例的一个数据发送过程.

分组交换


数据包在网络中进行传输时使用了分组交换.分组交换也称为包交换,它将用户通信的数据划分成多个更小的等长数据段,在每个数据段的前面加上必要的控制信息作为数据段的首部,每个带有首部的数据段就构成了一个分组.首部指明了该分组发送的地址,当交换机收到分组之后,将根据首部中的地址信息将分组转发到目的地,这个过程就是分组交换.能够进行分组交换的通信网被称为分组交换网.

分组交换的本质就是存储转发,它将所接受的分组暂时存储下来,在目的方向路由上排队,当它可以发送信息时,再将信息发送到相应的路由上,完成转发.其存储转发的过程就是分组交换的过程.

数据的表示


计算机编程语言拥有多种数据类型, 例如intchardouble等.但不管是什么类型的数据,在计算机中其实都只是一个字节序列(以8位二进制为一个字节).每个机器中对字节序列的排序不大相同,有一些机器按照从最高有效字节到最低有效字节的顺序存储,这种规则被称为大端法;还有一些机器将最低有效字节排在最前面,这种规则被称为小端法.

计算机使用补码来表示数值,一个数的最高有效位为符号位(以整数为例,整数占有4字节32位,最高位即最左位,剩下31位用于表示数字,所以整数的有效范围为-2^31 ~ 2^31 - 1),如果符号位为1,则代表这个值为负,如果符号位为0,则代表这个值为正.负数的补码即是它的反码(在保持符号位不变的前提下按位取反)+1,正数的补码不需要做其他操作,就是它本身的值.

当将一个较小类型的值强转为较大类型时(如byte强转为int),将会发生符号扩展,较小类型不包含的位会以符号位来进行填充(还是以byte为例,当它强转为int时,高24位会被填充为最高有效位中的数值,如果最高有效位为1,那么高24位都会为1,这时byte原来要表示的值将产生变化,要避免这种情况,可以使用一个低8位为1高24位为0的数,将它与强转后的结果进行&操作,来保留低8位,并消除高24位中的1).

对一个数进行移位操作时,也需要按规则填充丢失的位数.移位操作分为算术移位与逻辑移位,算术移位会填充符号位,而逻辑移位全部填充0.

  • 当进行左移操作时,右边空出的位用0补充,高位左移溢出则舍弃该高位.

  • 当进行右移操作时,左边空出的位用符号位来补充(正数补0,负数补1),右边溢出则舍弃.如果使用逻辑移位(Java中为>>>),左边空出的位会用0来补充.

读到这里,可能有人会有疑问,为什么计算机非得使用补码?这主要因为,计算机中没有减法器只有加法器,而减去一个数其实就是加上一个负数,使用补码进行计算会很方便快速.

我们假设一个指定n为长度的二进制序列,那么它将会有2^n个可能的值,加减法运算都存在上溢出与下溢出的情况,实际上都等价于模(≡) 2^n的加减法运算.

把范围想象成一个时钟,假设现在时针指向数字3,若要得出6小时前时针指向的数字是几,有两种方法:

  1. 将时针逆时针拨动6格.

  2. 将时针顺时针拨动12 - 6 = 6格.

这里的12就是模,3小时-6小时 = 3小时 + (12 - 6)小时.

例如以下例子,模为2^8 = 256

  • 一个8位无符号整数的值的范围是0到255.因此4+254将上溢出,结果为2: (4 + 254) ≡ 258 ≡ 258 - 256 ≡ 2

  • 一个8位有符号整数的值的范围是−128到127,则126+125将上溢出,结果为-5: (126+125) ≡ 251 ≡ 251 - 256 ≡ -5

浮点数


浮点数是一种对于实数的近似值数值表现法,由一个有效数字(即尾数)加上幂数来表示,通常是乘以某个基数的整数次指数得到.但浮点数计算通常伴随着因为无法精确表示而进行的近似或舍入.

在计算机使用的浮点数被电气电子工程师协会(IEEE)规范化为IEEE-754,任意一个二进制浮点数V都可以表示成下列形式:

  • V = (-1)^s * M * 2^E

  • ${(-1)}^s$表示符号位,当s=0,V为正数;s=1,V为负数.

  • M 表示有效数字,$1≤M<2$.

  • $2^E$表示指数位.

这种表示方式有点类似于科学计数法,在计算机中,通常使用2为基数的幂数来表示.IEEE-754同时还规定了单精度(float)与双精度(double)的区别:

  • 32位的单精度浮点数,最高1位是符号位s,接着的8位是指数E,剩下的23位是有效数字M.

  • 64位的双精度浮点数,最高1位是符号位s,接着的11位是指数E,剩下的52位为有效数字M.

函数调用


当调用一个函数时,系统会在栈上分配一个空间,存放了函数中的局部变量、函数参数、返回地址等,这样的一个结构被称为栈帧.

函数中的数据的存活状态是后进先出的,而栈正好是满足这一特性的数据结构,这也是为什么计算机使用栈来当作函数调用的存储结构.

int main() {
  sayHello();
  return 0;
}

void sayHello() {
  hello_world();
}

void hello_world() {
  print("Hello,World");
}


 main()  sayHello()  hello_world()  print()
   -                                main()
   |
   +>     -                            sayHello()
   .      |
   .      +>   -                              hello_world()
   .      .    |
   .      .    +>   -                                  print()
   .      .    .    |
   .      .    +   <-                       return from print()
   .      .    |
   .      +   <-                        return from hello_world()
   .      |
   +     <-                        return from sayHello()
   |
   -                             return from main()

x86-64架构中,栈是向低地址方向生长的,寄存器%rsp指向栈顶,当一个函数被调用时,将会执行pushq指令,栈帧入栈,栈指针减小(向下生长),当函数返回后,将会执行popq指令,栈帧出栈,释放空间,栈指针增加.如果不断有函数进行调用,栈就会不断向下生长,最终会产生Stack Overflow.

计算机编程语言


计算机编程语言是用来定义计算机程序的语言,它以一种标准化的语法规则来向计算机发出指令.最早的编程语言是在计算机发明之前产生的,当时是用来控制提花织布机及自动演奏钢琴的动作.如今已经有上千种不同的编程语言,不管是哪种语言,尽管它们的特性各有不同,但写程序的核心都是条件判断、循环、分支(这些也是机器指令的核心).

编程语言依赖于编译器或解释器(所以也分为编译型语言与解释型语言),如果没有对应的编译器/解释器来对语法与语义进行分析并生成对应的机器语言,那么我们所写的代码其实都只是普通的文本字符(编译器/解释器也会对源代码进行一系列优化提高性能).

编译型语言通过编译器直接将源代码翻译成机器语言并生成一个可执行文件(机器语言是不兼容的,如果要到另一台机器上运行,就需要对源代码重新编译);解释型语言通过解释器动态地翻译源代码并直接执行(性能上会比编译型语言直接运行可执行文件要差);虽然大多数的语言既可被编译又可被解译,但大多数仅在一种情况下能够良好运行.

Java的编译机制比较特殊,它将Java源代码编译成JVM字节码(通过虚拟机来达到一次编译在所有平台可用),然后JVM对字节码进行解释执行,但对于较热的代码块(频繁调用的函数等),JVM会通过JIT即时编译技术将这些频繁使用的代码块动态地编译成机器语言,提高程序的性能.

推荐阅读更多精彩内容