1.对程序员来说 CPU 是什么

  1. 程序是指计算机每一步动作的一组指令,程序是由指令和数据组成的
  2. 硬盘和磁盘等媒介上保存的程序被拷贝到内存后才能运行
  3. 内存地址是在内存中保存指令和数据存储位置的数值,是一个整数值
  4. 机器语言是 CPU 可以直接识别并使用的语言,可以理解为一个个电路状态的矩阵转换,高级编程语言必须编译为机器语言才能被CPU 处理
  5. CPU 范围更大表示计算机的中央处理单元,包含了微处理器芯片,但很多时候两者表达的意思作等同处理
  6. 程序运行流程:
    1. 高级语言编写程序
    2. 将程序编译后转换成机器语言的 EXE 文件
    3. 程序运行时,在内存中生成 EXE 文件的副本
    4. CPU 解释并运行程序内容
  7. 时钟信号(clock puzzle)的频率越高,CPU 的运行速度越快
  8. CPU 和内存是由许多晶体管组成的电子部件,通常称为 IC(Integrated Circuit,集成电路)
  9. CPU 构成:
    1. 寄存器:暂存指令、数据等处理对象,可理解为内存的一种;CPU 内部会有 20-100 个不同种类的寄存器
    2. 控制器:负责吧内存上的指令、数据读入寄存器,并根据指令的执行结果控制整个计算机
    3. 运算器:负责运算从内存读入寄存器的数据
    4. 时钟:负责发出 CPU 时钟信号(有些计算机的时钟信号位于 CPU 的外部)
      CPU的 4 个构成部分
  10. 在汇编语言中,是把寄存器作为对象来描述的,所以需要了解 CPU 中的寄存器部分,寄存器中的存储的内容既可以是指令也可以是数据,其中数据分为“用于运算的数值”和“表示内存地址的数值”(内存地址是一个整数值)。数据种类不同,存储改数值的寄存器也不同。下面是寄存器的主要种类和功能:
    1. 累加寄存器(accumulator register):存储执行运算的数据和运算后的数据
    2. 标志寄存器(flag register):存储运算处理后的 CPU 的状态,比如标志运算后的结果是负数、零还是正数,以及是否溢出和奇偶校验的结果,标志寄存器的数值会根据运算结果自动设定
    3. 程序计数器(program counter):存储下一条指令所在内存的地址
    4. 基址寄存器(base register):存储数据内存中的起始地址
    5. 变址寄存器(index register):存储基址寄存器的相对地址
    6. 通用寄存器(general purpose register):存储任意数据
    7. 指令寄存器(instruction register):存储指令,CPU 内部使用,程序员无法进行读写
    8. 栈寄存器(stack register):存储栈区域的起始地址
  11. 对程序员来说,CPU 是具有各种功能的寄存器的集合体,其中程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器都只有一个,其他的寄存器可有多个:CPU 是寄存器的几何体
  12. 只有一行的有用程序是很少见的,机器语言的程序也是如此,操作系统将程序的机器语言加载到内存后,把程序第一个指令的地址设置到程序计数器中,并且程序计数器的值会自动加1,即 CPU 执行完程序的第一条指令后,会自动加载执行下一条指令。程序计数器决定着程序的执行流程。
  13. 函数的调用地址也是通过把程序计数器的值设定为函数的存储地址来实现的(但是函数执行完之后还需要跳转回来),机器语言的 call 指令是调用函数的,它在将函数的入口地址设定到程序计数器之前,call 指令会把调用函数后要执行的指令地址存储在名为栈的主存内,当函数处理完毕后,再通过函数的出口来执行 return 命令,return 命令的功能是把保存在栈中的地址设定到程序计数器中。
  14. CPU 会把基址寄存器+变址寄存器的值解释为实际查看的内存地址,变址寄存器的值就相当于高级编程语言程序中数组的索引功能
  15. CPU 的处理其实很简单,机器语言指令的主要类型和功能:
    1. 数据传送指令:寄存器和内存、内存和内存、寄存器和外围设备之间的数据读写操作
    2. 运算指令:用累加器执行算术运算、逻辑运算、比较运算和移位运算
    3. 跳转指令:实现条件分支、循环、强制跳转等
    4. call/return 指令:函数的指令/返回调用钱的地址

2.数据是用二进制数表示的

  1. 集成电路(IC)的引脚,只有直流电压 0V 和 5V 两个状态,也就说 IC 的一个引脚智能表示两个状态,IC 的这个特性决定了计算机的信息数据只能用二进制数来处理,二进制数不少专门为 IC 设计的,但是和 IC 的特性非常吻合。
  2. 计算机所处理的信息的基本单位是 8 位二进制数,被称为一个字节(byte);字节是基本单位,内存和磁盘都使用字节来存储和读写数据,位是最小单位。
  3. 计算机只存储信息,如何区分它是数值、文字还是某种图片的格式,取决于程序的编写方式。
  4. 二进制数表示负数值时,一般会把最高位作为符号位来使用
  5. 正数符号位为 0,而其他部分正常表示,负数使用补数来表示,负数使用补数来表示只会计算机的减法就可以变为加法,只需要加法器即可无需减法器了,在计算机运算时位溢出部分会被忽略
  6. 补数是在正数的基础上取反再+1 得到的,为了理解为什么补数这么定义,只需要使用 0-正数=负数 的二进制运算便明了了,由于 0 不够减便像更高位借位,减去后得到的结果和被减数比较,便会发现 负数=正数的取反+1。
  7. 算术右移对于正数用 0 补位,对于用补码表示的负数用 1 补位
  8. 符号扩充:8 位的二进制数扩充到 16 位和 32 位时,用符号位扩充到高位即可,如果是正数扩充 0,如果是补码表示的负数扩展 1,如 1111 1111 扩充到 16 位是:11111111 11111111
  9. 算术运算:将二进制表示的信息进行算术四则运算
  10. 逻辑运算:将数值处理为单纯的 0 和 1 的罗列,逻辑运算有 NOT/AND/OR/XOR 四种

3.计算机进行小数运算时出错的原因

  1. 浮点数是指把小数用“符号 尾数 * 基数的指数次幂”这种形式来表示
  2. 计算机可以准确的用二进制数来表示正式,但无法准确的用二进制数来表示小数,这是因为整数不是偶数就是奇数,偶数部分用2的幂次方相加总能表示出来,而2的0次幂是1也解决了奇数的问题,但是2的-1次幂、2的-2次幂等却不能表示所有连续的小数
  3. 双进度浮点型(double)用 64 位,单精度浮点型(float)用 32 位来表示全体小时
  4. 浮点数是指用符号、尾数(小数点后面的数字)、基数和指数这四部分来表示的小数:+-m*n^e,因为计算机内部使用的是二进制数,所以基数自然就是 2,所以实际的数据中往往缺省了基数 2,只用符号、尾数、指数这三部分即可表示浮点数,所以 double 的64位和 float 的32位的数据,会被分为 3 部分来使用,分别表示符号位、尾数部分和指数部分。
  5. 如十进制的 91.25 用二进制浮点数是 1001.001,将其表示成科学计数方式为 1.001012^3,在计算机中,任何一个数都可以表示成 1.xxxxx2^n 这样的形式,其中 xxxxx 部分就是尾数部分,n 表示指数部分,而其中因为最高位的 1 在二进制浮点数的科学计数法里面最高位总是 1,所以在存储的时候实际上并不保存这一位,这使得 float 的 23bit 的尾数部分可以表示 24bit 的精度,double 中的 52bit 的尾数可以表达 53bit 的精度:
    浮点数的内部构造(IEEE 的规定)
  6. Excess 系统:计算机内可以同时存储正数和负数而不用符号位的一种方法,在 Excess 系统中存在一个幻数,是寻址空间的中位值,比中位值大的表示正数,比中位值小的表示负数。在计算机的浮点数表示中,指数部分的负数就是使用 Excess 系统来表示的。对于 1-256 个数来说,幻数取 2^7-1即 127,所以 126 表示-1,128 表示+1。
  7. 二进制数很方便,但是位数表多后在程序里看起来比较麻烦,这个时候就有人想到了用 16 进制来表示,在数值的开头加上 0x 表示 16 进制数。

4.熟练使用有棱有角的内存

  1. 高级编程语言中的数据类型表示的是占据内存区域的大小和存储在该内存区域的数据类型
  2. 物理内存是以字节为单位进行数据存储的
  3. 内存实际上是一种名为内存 IC 的电子元件,内存 IC 中有电源(VCC +5V,GND 0V)、地址信号(A0A9)、数据信号(D0D7)、控制信号(RD(read), WR(write))等用于输入输出的大量引脚(IC 的引脚),通过为其指定地址,来进行数据的读写
    内存 IC
  4. 内存 IC 的物理机制实质上很简单,总体来讲其内部有大量可以存储 8 位数据(1 byte)的地方,通过地址来指定这些场所之后即可进行数据的读写。内存 IC 读写
  5. 将多字节数据的低位字节存储在内存低位地址的方式称为低字节序,于此相反,把数据的高位字节存储在内存低位的方式称为高字节序。
  6. 理解指针的关键点就是要弄清楚数据类型这个概念,指针也是一种变量,它所表示的不是数据类型的值,而是存储着数据的内存的地址,通过使用指针,就可以对任意指定地址的数据进行读写。
  7. C 语言中定义指针:char *d; 这里的 * 表示的是指针,而用 char 来指定,其实表示的就是从指针 d 存储的地址中一次能够读写的数据字节数(换成自定义类型也一样,类型在内存中占有的内存在编译时是可知的)。内存指针
  8. 数组是高效使用内存的基础,数组所有和内存地址的变换工作是由编译器自动实现的,CPU 是通过利用基址寄存器(保存数组的起始位置)和变址寄存器(数组的索引)来指定内存地址的。数组和物理内存的构造是一样的,所以数组被广泛的应用。
  9. 队列一般是以环状缓冲区(ring buffer)的方式来实现的:queue
  10. 链表使元素的追击和删除更容易,在数组的各个元素中,除了数据的值之外,通过为其附带上下各元素的索引,即可实现链表。数据的值和下一个元素的索引组合在一起,就构成了数组的一个元素。
  11. 二叉查找树使数据搜索更有效,二叉查找树是在链表的基础上往数组追加元素时,考虑到数据的大小关系,将其分成左右两个方向的表现形式。binar tree为了处理二叉查找树,其实数组的每个元素中只要有数据的值和两个索引信息就可以了。使用二叉查找树的便利之处在于可以使数据的搜索更有效率。
  12. 只要在程序开发中多花些心思,我们就可以熟练使用内存、实现栈处理、链表处理、二叉查找树处理等,为什么要进行这些处理?就是为了使用计算机的物理内存来对现实世界中的存储模型进行建模,而数组是进行这些处理的基础。

5.内存和磁盘的亲密关系

  1. 内存的基本单位是 1 byte,磁盘的一个扇区是 512 byte,扇区是磁盘保存数据的物理单位。内存利用电为存储信息,断电后内容消失,磁盘利用磁来存储信息,断电后信息不丢失
  2. 虚拟内存:把磁盘中的一部分作为假想内存来使用。借助虚拟内存,内存容量不足的计算也可以运行很大的程序
  3. 存储程序方式(程序内置方式):程序保存在存储设备中,通过有序地被读到内存以实现运行。程序要加载到内存后才开始运行:程序加载
  4. 磁盘缓存(把磁盘中读出的数据存储到内存空间中)加快了磁盘访问速度。缓存在两种访问介质速度不一样的地方经常存在,如 低速的web 请求缓存在相对高速的磁盘中。可以理解为磁盘缓存是假想的磁盘,正如虚拟内存是假想的内存一样。虚拟内存 把运行的程序分成一定大小的页(Page,比如4K大小),并以页为单位在内存和磁盘间进行置换,从磁盘装载到内存叫 Page In,从内存写回到磁盘叫 Page Out
  5. 虚拟内存能解决内存不够的问题,但伴随着频繁的 Page In/Out 会让程序运行变得缓慢。把应用程序变小可以避免频繁的换页操作:通过 DLL(Dynamic Link Library) 文件实现函数共有、通过调用 _stdcall(标准调用) 来减少程序文件的大小、
  6. 在 C 语言中,在调用函数后由调用方负责执行栈清理处理指令,将不需要的函数参数从栈上清理出去,而这是通过设置栈的写入位置前进多少位来清理的,相当于重置写入指针的位置。另外,C 语言函数的返回值是通过寄存器而非栈来返回的。栈清理处理,比起在程序调用方进行,在反复被调用的函数一方进行时,程序整体要小一些,这就是 _stdcall。在函数前加上 _stdcall 就可以把栈清理处理变为在被调用函数一方进行。int myFunc(int a, int b) => int _stdcall MyFunc(int a, int b),具体是可以节省在汇编语言层面视角的清理栈指令的内存占用。
  7. 磁盘、磁道、扇区。扇区是磁盘进行物理读写的最小单位,在 windows 中一般 1 个扇区是 512 字节,不过在软件方面对磁盘进行读写的单位是扇区整数倍,称为蔟。不管是硬盘还是软盘,不同的文件是不能存储在同一个蔟中的(否则会导致只有一方的文件不能被删除),因此不管是多么小的文件都会占用一簇的空间,而且文件占用内存的大小始终都是蔟的整数倍。
  8. 一个优秀的程序,不仅要运行速度快,还要内存占用小。(Rust!)

6.亲自尝试压缩数据

  1. 文件存储的基本单位是 1 个字节,文件是字节数据的集合体,文件是将数据存储在磁盘等存储媒介中的一种形式,在任何情况下,文件中的字节数据都是连续存储的。
  2. RLE(Run Length Encoding) 行程长度压缩算法,是把数据按线性序列分成 2 种,一种是连续的重复数据块,另一种是连续的不重复数据块,然后表示成 数据块*出现 次数的压缩方式。RLE 是一种可逆的压缩,AAABBC 压缩后是 A3B2C1 或者 A3B2C。RLE 算法经常被用于传值 FAX 等,因为传真机是把文字和图形都作为黑白图像来发送的。
  3. 非8位数据的读写
  4. 摩尔斯编码:把一般文本中出现频率高的字符用短编码来表示。这里的出现频率是根据印刷行业的印刷活字数目来确定的
  5. 哈夫曼编码:比摩尔斯编码更进一步,根据要压缩文本中字符出现的频率来决定短编码。在哈夫曼算法压缩过的文件中,存储着哈夫曼编码信息和压缩过的数据huffman
  6. 哈夫曼树的编码顺序:huffman-encoding-tree
  7. windows的标准图像数据形式为 BMP(Bitmap),是完全未压缩的。

7.程序是在何种环境中运行的

  1. 应用的运行环境,指的是操作系统和计算机本身(硬件)的种类。不同的硬件种类需要不同的操作系统,应用是为了在特定操作系统上运行而作成的。
  2. 运行环境 = 操作系统 + 硬件
  3. 程序 = 指令 + 数据 = 算法 + 数据结构
  4. CPU 只能解释其固有的机器语言,不同的 CPU 能解释的机器语言种类是不同的
  5. 从操作系统方面来看,Java 虚拟机是一个应用,而从 Java 应用方面来看,Java 虚拟机就是运行环境。
  6. 程序的运行环境中,存在名为 BIOS(Basic Input/Output System) 的系统。BIOS 存储在 ROM(Read Only Memory) 中,是预先内置在计算机主机内部的程序。BIOS 除了键盘、磁盘、显卡等基本控制程序外,还有启动“引导程序”的功能。引导程序是存储在启动驱动器起始区域的小程序。操作系统的启动驱动器一般是硬盘,不过有时也可以是 CD-ROM 或软盘。(磁盘分为硬盘和软盘,硬盘一般是装在主机内的不可移动的,容量较大;而软盘是可以移动的,比如 U 盘)开机后,BIOS 会确认硬件是否正常运行,没有问题的话就会启动引导程序(启动驱动器(一般是硬盘)起始区域的小程序)。引导程序的功能是把在硬盘等记录的 OS 加载到内存中运行。虽然启动应用是 OS 的功能,但 OS 并不能自己启动自己,而是通过引导程序来启动。
  7. Bootstrap的原意是指靴子上不的“拔靴带”,BIOS 这样小的程序(拔靴带)可以带动(启动)操作系统(靴子)这样的大程序,所以由此得名。操作系统运行以后,程序员就不用关注 BIOS 及引导程序了。

8.从源文件到可执行文件

  1. source code vs native code(本地代码,即机器语言,native 有“母语的”意思,对 CPU 来说其母语就是机器语言)
  2. CPU 只能解释native code,即使是不同编程语言的 source code,最后都被转换成同一种语言了(机器语言)。而机器语言对应的是 0/1 的组合构成的电平变化指令,这是数字硬件电路能处理的,通过读取和译码过程产生相应的状态跳转和基本计算
  3. 编译器负责将 source code 转换成 native code,词法解析、语义解析等
  4. 编译器也是程序的一种,也需要运行环境。
  5. 仅靠编译是无法得到可执行文件的,编译器生成 native code 后,会生成目标文件“*.obj”,还需要进行“链接”处理(使程序使用到的系统类型填充进来,即把多个目标文件结合并生成 1 个 EXE 文件,其实包括了给 native code 添加“启动”的部分,可以理解为引导头),这就是链接器。windows-compile-link
  6. 在 native code 的程序中,给变量及函数分配的地址是虚拟地址。在程序运行时,虚拟地址会转换成实际的内存地址,其中链接器会在 native code 的头部,追加转换内存所需的必要信息,这个信息称为 再配置信息,这个再配置信息,就称为了变量和函数的相对地址,表示的是相对于基点地址的偏移量。在进行链接后的 native code 里面,虽然变量和函数是在不同位置分散记述的,但是在链接后的可执行文件中,变量和函数就会变成一个连续排列的组。这样各个变量的内存地址就可以用相对于变量组起始位置这一基点的偏移量来表示。而各组基点的内存地址则是在程序运行时被分配的。link-exe
  7. 程序加载时会生成栈和堆。可执行文件的内容分为再配置信息、变量组合函数组,除此之外当程序加载到内存后,还会额外生成 2 个组:栈和堆。栈是用来存储函数内部的局部变量,以及函数调用时所用的参数的内存区域;而堆是用来存储程序运行时的任意数据及对象的内存区域。
  8. 内存中的程序,就是由用于变量的内存空间、用于函数的内存空间、用于栈的内存空间、用于对的内存空间这 4 部分构成的。program-memory
  9. 栈和堆:两者的内存空间都是程序在运行得到分配的,不同在于用法,栈中对数据的存储和舍弃的代码是由编译器自动生成的,因此程序员不需要参与,需要时会自动申请,不需要时会自动释放;而堆中的内存空间则要程序员来明确进行申请分配或释放。
  10. 栈和堆的大小,可以由程序任意指定。在高级编程语言中,编译器会自动生成指定栈和堆大小的代码,并将其附加到程序中。
  11. 垃圾回收:对堆内存空间不再需要的数据和对象进行清理

9.操作系统和应用的关系

  1. 操作系统的原型是监控程序,监控程序的主要功能是程序的加载和运行
  2. 调用操作系统功能称为 system call,即系统调用
  3. GUI 全称是 Graphical User Interface(图形用户界面)
  4. What You See Is What You Get(所见即所得)
  5. 利用计算机运行程序大部分是为了提供处理效率,程序员的工作就是编写各种各样的应用来提高业务效率
  6. 操作系统(Operating System)也称为基础软件,操作系统是计算机运行时不可或缺的控制程序,以及在控制程序下运转的位其他软件运行提供操作环境的软件的统称。在操作系统上运行的程序也叫“应用程序”