CSAPP读书笔记
第1章 计算机系统漫游
计算机系统是由 硬件 和 系统软件 组成的, 它们共同作用运行应用程序
所有的计算机系统都有相似的硬件和软件组件, 它们又执行着相似的功能
1.1 信息就是位+上下文
源程序实际上是一个由值 0 和 1 组成的位(又称为比特)序列, 8个位组成一组, 称为字节
系统里所有的信息: 磁盘文件, 内存中的程序, 内存中存放的数据, 网络上传送的数据, 都是由一串比特表示的
1.2 程序被其他程序翻译成不同的格式
在Unix系统上, 从源文件到目标文件的转化是由 编译器驱动程序 完成的:
linux> gcc -o hello hello.c
gcc编译器驱动程序读取源文件hello.c, 并把它翻译成可执行文件hello
翻译过程分成四个阶段, 如下图所示
四个阶段的程序 预处理器, 编译器, 汇编器, 链接器 构成编译系统
- 预处理阶段: 预处理器根据以字符"#"为开头的命令, 修改原有的c程序. 最后得到一个新的c程序, 以 .i 为文件拓展名
- 编译阶段: 编译器将文本文件 hello.i 翻译成文本文件 hello.s , 它包含了一个汇编语言程序
- 汇编阶段: 汇编器将hello.s 翻译成机器语言指令, 把这些指令打包成一种叫做可重定位目标程序的格式, 并把结果保存在目标文件hello.o中. hello.o是二进制文件, 打开后只能看到乱码
- 链接阶段: hello程序中调用了一个printf函数, 而printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中. 链接器负责把hello.o和printf.o合并, 最后得到hello文件, 它是一个可执行文件, 可以被加载到内存中, 由系统执行
1.3 了解编译系统如何工作是大有益处的
- 优化程序性能
- 理解链接时出现的错误
- 避免安全漏洞
1.4 处理器读并解释储存在内存中的指令
要想在Unix系统上运行可执行文件, 我们将它的文件名输入到shell的应用程序上
linux> ./hello
hello world
linux>
shell是一个命令行解释器, 它输出一个提示符, 等待输入一个命令行, 然后执行这个命令
1.4.1 系统的硬件组成
1. 总线
贯穿整个系统的是一组电子管道, 称为 总线
通常总线被设计成传送定长的字节块, 也就是 字
字中的字节数是基本系统参数, 不同机器不尽相同
目前大多数机器的字长大多为4个字节(32位) 或 8个字节(64位)
2. I/O设备
I/O设备 是系统与外部世界的联系通道. 每个I/O设备都通过一个 控制器 或 适配器 与 I/O总线 相连. 控制器和适配器之间的区别主要是在它们的封装方式. 控制器是I/O设备本身或主板上的芯片组, 而适配器是插在主板插槽上的卡
3. 主存
主存 是一个临时存储设备, 在处理器执行程序时, 用来存放程序和程序处理的数据.
4. 处理器
中央处理单元(CPU), 简称处理器 , 是解释 (或执行) 存储在主存中指令的引擎. 处理器的核心是一个大小为一个字的存储设备(或 寄存器 ), 称为 程序计数器(PC) . PC都指向主存中某条机器指令(即含有该条指令的地址)
处理器看上去是由一个非常简单的指令执行模型来操作的, 这个模型是由指令集架构决定的. 在这个模型中, 指令按照严格的顺序执行, 而执行一条指令包含一系列的步骤. 处理器从程序计数器指向的内存处读取指令, 解释指令中的位, 执行该指令指示的简单操作, 然后更新PC, 使其指向下一条指令
操作围绕着 主存, 寄存器文件, 算术/解释逻辑单元(ALU) 进行.
- 加载: 从主存中复制一个字节或者一个字到寄存器, 以覆盖寄存器原来的内容
- 存储: 从寄存器复制一个字节或者一个字到主存的某个位置, 以覆盖这个位置上原来的内容
- 操作: 把两个寄存器的内容复制到ALU, ALU对这两个字进行算术运算, 并把结果存放到一个寄存器中, 以覆盖寄存器原来的内容
- 跳转: 从指令中提取一个字, 并将这个字复制到程序计数器(PC)中, 以覆盖PC中原来的值
1.4.2 运行hello程序
初始时, shell程序执行它的指令, 等待我们输入一个命令. 当我们在键盘上输入字符串".\hello"后, shell程序将字符逐一读入寄存器, 再把它放到内存中
当我们在键盘上敲回车键时, shell程序就知道我们已经结束了命令的输入. 然后shell执行一系列指令来加载可执行的hello文件, 这些指令将hello目标文件中的代码和数据从磁盘复制到主存.
一旦目标文件hello中的代码和数据加载到主存, 处理器就开始执行hello程序的main程序中的机器语言指令. 这些指令将"hello, world\n"字符串中的字节从主存复制到寄存器文件, 再从寄存器文件中复制到显示设备
1.5 高速缓存至关重要
根据机械原理, 较大的存储设备要比较小的存储设备运行得慢, 而快速设备的造价远高于同类的低速设备
一个寄存器文件只能存储几百字节的信息, 而主存中可存放几十亿字节. 然而, 处理器从寄存器文件中读数据比从主存中读数据几乎快100倍. 随着半导体技术的进步, 处理器与主存之间的差距还在拉大
针对处理器和主存之间的差距, 系统设计者采用了更小更快的存储设备, 称为 高速缓存存储器(简称为cache或高速缓存) , 作为暂时的集结区域, 存放处理器近期可能会需要的信息
1.6 存储设备形成层次结构
层次结构中, 从上到下, 设备的访问速度越来越慢, 容量越来越大, 造价越来越便宜.
1.7 操作系统管理硬件
操作系统可以看成时应用程序和硬件之间插入的一层软件
操作系统有两个基本功能:
- 防止硬件被失控的应用程序滥用
- 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备
文件是对I/O设备的抽象表示, 虚拟内存是对主存和硬盘I/O设备的抽象表示, 进程则是对处理器, 主存和I/O设备的抽象表示
1.7.1 进程
进程 是操作系统对一个正在进行的程序的一种抽象. 在一个系统上可以同时运行多个进程, 而每个进程都在独占地使用硬件.
并发运行 是说一个进程的指令和另一个进程的指令是交错进行的
操作系统保持跟踪进程运行所需的所有状态信息, 这种状态, 也就是 上下文 . 当操作系统决定要把控制权从当前进程转移到一个新进程时, 就会发生 上下文转换 , 即保存当前进程的上下文, 恢复新进程的上下文, 然后把控制权传递到新进程
从一个进程到另一个进程的转换是由 操作系统内核 管理的
1.7.2 线程
一个进程实际上可以由多个被称为 线程 的执行单元组成, 每个线程都运行在进程的上下文中, 并共享代码和全局数据
1.7.3 虚拟内存
虚拟内存 是一种抽象概念, 它为每个进程提供了一种假象, 即每个进程都在独占地使用主存. 每个进程看到的内存都是一致的, 称为 虚拟内存空间
在Linux中, 地址空间最上面的区域是保留给操作系统的代码和数据
- 程序代码和数据. 对于所有的进程来说, 代码是从同一固定地址开始, 紧接着的是和C全局变量相对应的数据地址
- 堆. 代码和数据区后紧接着的是运行时栈. 当调用malloc和free这种C标准函数时, 堆可以在运行时动态扩展和收缩
- 共享库. 大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域
- 栈. 位于用户虚拟地址空间顶部的是用户栈, 编译器用它来实现函数调用
- 内核虚拟内存. 地址空间顶部的区域是为内核保留的, 不允许应用程序读写这个区域的内容获知直接调用内核代码定义的函数. 相反, 它们必须调用内核来执行这些操作
1.7.4 文件
每个I/O设备, 包括磁盘, 键盘, 显示器, 甚至网络, 都可以看成是文件. 系统内所有输入输出都是通过使用一小组称为 Unix I/O 的系统函数调用读写文件实现的