CSAPP读书笔记
最近花了半个月时间重读CSAPP第三版,本文把一些关键点记录下来。
高级语言如何运行
所有的高级语言,经过一系列编译过程后,都会转化成机器码,也就是指令和数据,最终在计算机内部执行。能够支持哪些指令,是由CPU的ISA决定的。典型的编译过程如下,汇编语言和机器指令可以认为是一一对应的
最终的可执行二进制文件,可看作一组0和1的序列,由指令和数据组成。在现代常见的计算机体系中,对应低电平和高电平,从而最终驱动计算机执行(由一系列门电路组成),当今一个典型计算机系统的硬件组成如下
之前一直有一个疑问,即“0和1如何转换成高低电平?”。比如计组这门课上,大家是对着0和1手工拨动开关,但是如今编程并没有去“拨开关”的动作,0和1是怎么转换成电路的打开与闭合的呢?实际上在硬件层面,从来就没有所谓的0和1,编译完成后,ELF文件在硬盘中保存的形态已经是高低电平。至于具体如何转换,取决于存储硬件的实现,比如说对于某些ROM,是通过直接烧断电路的方式。于是当ELF文件从硬盘加载到内存执行时,就已经是一系列的电路操作而已。
所谓的0和1,只是高低电平在逻辑上的映射。如果真的用“螺丝刀”把硬件拆开,无论是CPU、内存、硬盘或者任何其他IO设备,根本没有0和1的存在。0和1只存在于人脑之中,真正在计算机里运行的,只有电路。
存储器层次结构
不同存储技术的访问时间差异极大。速度越快的技术每字节的成本就更高,而且容量较小。因此,存储有一个简单的铁律:越快越贵。如果不计成本,理论上可以制造出极大的cache,但是不具有现实意义。因此,现代计算机系统采用的组织存储器的方法,称为存储器层次结构(memory hierarchy)。在这个层次结构中,上层的存储(更快,但更小)作为下一层存储的缓存
异常处理
异常一部分由硬件实现,一部分由操作系统实现,所以具体细节随着计算机系统的不同而有所区别,但基本思想是类似的。
当处理器正在执行当前指令I1,状态发生了一个重要的变化,状态变化称为事件。事件可能与当前正在执行的指令I1直接相关,比如发生虚拟内存缺页,或发起一个系统调用;也可能与当前指令I1无关,比如一个I/O请求完成。
在任何情况下,当处理器检测到事件发生,会通过异常表(exception table),跳转到异常处理程序(exception handler)。然后异常处理程序完成处理后,根据事件类型,会发生以下3种情况之一:
- 处理程序将控制返回给I1
- 处理程序将控制返回给I1的下一条指令
- 处理程序终止被中断的程序
异常分类
interrupt,是由I/O设备信号引起的,异步发生
trap,当用户程序请求发起一个系统调用syscall时,会触发此异常事件
fault,最典型的就是虚拟内存缺页错误
abort,此类异常会导致程序终止,比如内存奇偶校验出错等
进程
进程是计算机科学中最深刻,最成功的概念之一。
进程是一个执行中程序的实例。每个程序都运行在某个进程的上下文中。上下文包括程序的代码和数据、虚拟地址空间(VAS)、寄存器内容、PC、环境变量、打开文件描述符等
虚拟内存
每个进程都会拉起一块独立的虚拟地址空间(Virtual Address Space)。此虚拟内存空间,通过内存映射(mmap)的方式,映射到硬盘上的某个文件。
该映射文件是分页的,每个虚拟页都对应同等大小的物理页。虚拟页保存在硬盘里,物理页保存在内存里。当CPU通过MMU寻址,发现物理页尚不存在时,操作系统就会发起一个页错误异常(fault),将控制交给异常处理程序。异常处理程序将虚拟页加载到物理页,然后让CPU重新访问,此时CPU就会得到需要的物理页。
具体打开VAS,其中的分布如下
操作系统与库
linux提供了一组标准IO接口,比如open等,属于linux提供的系统调用。同时,C语言标准库也封装了一系列用于IO操作的标准函数,比如fopen等。这类C标准函数,最终实际上会调用open等系统调用。
因此,如果用户程序依赖了open函数,那么就不具备跨平台的特性。反之,如果依赖的是fopen函数,那么就具备一定的跨平台能力。因为C标准库在各操作系统平台都提供了fopen的实现(包括不同操作系统平台的编译器)。
linux文件
linux把所有能够读出或写入字节的对象,如IO、网络、硬件设备等,都抽象为“文件”,也就是一组字节序列,可以从中读出字节,也可以往其中写入字节,仅此而已。
因此在linux平台做网络开发,本质上也是对文件的操作,只是需要掌握socket API
并发
一旦逻辑控制流在时间上重叠,这种现象就称为并发(concurrency)。并发出现在系统的不同层面上,包括硬件异常处理、进程、linux信号处理等
使用应用级并发的程序称为并发程序(concurrent program),现代操作系统一般提供了三种基本的构造并发程序的方法
进程
每个逻辑控制流都是单独的进程,由内核来调度。由于每个进程有独立的虚拟地址空间,所以需要显式的进程间通信(IPC)机制来与其他流交互
I/O多路复用
该形式中,在同一个进程上下文中显式地调度逻辑流。由于程序是单独的进程,所有流共享同一个虚拟地址空间。典型的这类程序比如nginx和node.js
线程
线程是运行在单一进程上下文中的一组逻辑流,由内核进行调度,但共享同一个虚拟地址空间
linux通过Posix线程API(Pthreads),定义了大约60个函数,如线程创建、终止、回收等,对线程提供了系统级的支持。