深入理解计算机系统笔记

深入理解计算机系统笔记

编码

反码和源码对于一个数字 0 有两种编码方式 补码编码有唯一性,符号位为1表示负数,符号位为0表示非负数 减法可以转化为加法,电路设计更简单高效 相加进位最高位的进位可以被省去

编译的本质

graph LR
A(C预处理器扩展源代码)-->B(编译器产生两个文件的汇编代码)
B-->C
C(汇编器将汇编代码转变为二进制目标代码)-->D(链接器将目标文件与标准Unix库函数合并,产生最终的可执行文件)

机器字

字表示16位数据类型,32位为双字,64位为4字

截断

高精度转低精度直接截断 低精度转高精度,符号位扩展补全

浮点数的表示方法

IEEE 754 标准所定义的单精度浮点数的长度为 32 位 | 符号 1 位 | 阶码 8 位 | 小数位 23 位 |

局部性原理

空间局部性:如果一个存储器被引用一次,在不久的将来引用附近的一个存储器位置 时间局部性:被引用过的一次的存储器位置在不久的将来被多次引用

程序加载

任何Unix程序都可以调用execve函数来调用加载器 加载器将可执行目标文件中的代码和数据从磁盘拷贝到相应的进程地址空间上,然后跳转到程序的第一条指令来运行程序

fork -> exec 全流程 父 shell fork 出一个子进程,是父进程的一个复制品,子进程使用 exec 调用加载器,加载器删除子进程已有的虚拟存储段,并初始化新的代码,数据,堆栈段。堆栈段被初始化为零值,除了一些头部信息,加载过程中没有任何从磁盘到存储器的数据拷贝,直到CPU引用一个被映射的虚拟页,才会进行拷贝,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到存储器

动态链接

给每个共享库实现分配一个专用地址空间块,然后要求加载器总是在这个地址加载共享库

异常控制流

异常是一种形式的异常控制流,它一部分是由硬件实现的,一部分是由操作系统实现的

异常处理(需要在内核模式下)

每种类型的异常都分配了一个唯一的非负整数的异常号

类型:CPU的计算异常,操作系统内核(系统调用,外部IO信号)

系统启动的时候,操作系统会分配和初始化一张异常表的跳跃表 表的起始地址放在专门的寄存器中

异常类型

中断: IO,网络适配器,磁盘,时钟通过向处理器的一个管脚发信号,并把异常号放在总线上来触发中断

陷阱: 有意的异常,系统调用运行在内核模式中

故障和终止

地址空间

地址空间底部的四分之三是预留给用户程序的,包括通常的文本、数据、堆和栈段 顶部的四分之一是预留给内核的(比如执行系统调用时,使用的代码、数据和栈)

| 内核虚拟存储器 | | 用户栈 | <- esp 栈指针 |共享库存储器映射区| <- brk 系统调用(malloc实现) | 运行时堆 | | bss段 未初始化 | | data段 已初始化 | | 只读段 |

上下文切换

  1. 保存当前进程的上下文
  2. 恢复某个先前被抢占进程所保存的上下文
  3. 将控制传递给这个新恢复的进程

时间片轮转是通过定时器中断来让调度器重新调度的

fork

父进程和新创建的子进程之间最大的区别在于它们有不同的PID

进程通信

信号:更高层的软件形式的异常。不同的信号代表不同的系统事件,它允许进程中断其他进程 主要用途是将不可见的底层异常通知到上层的进程 ctrl-c SIGINT信号给前台进程 SIGKILL 强制终止进程 SIGCHLD 子进程终止或暂停的时候,内核会发送这个信号给父进程 可以选择性阻塞接收 通过ps命令查看其带有defunct的标志判断僵尸进程

虚拟存储器

  1. 他将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,高效的使用了主存
  2. 它为每个进程提供了一致的地址空间,从而简化了存储器管理
  3. 他保护了每个进程的地址空间不被其他进程破坏

虚拟地址 -> MMU(包含了TLB高速缓存)-> 物理地址 以页的方式来组织主存,页表由操作系统进行维护,没有命中 TLB 高速缓存就需要去内存里查表,查不到就缺页中断,由操作系统负责调入页

标准库的调用:将不同进程中的虚拟页面映射到相同的物理页面,使得多个进程共享同一份代码

fork 之后两个进程中的任意一个对虚拟存储器进行了写操作之后,写时拷贝机制就会被触发,创建新的页面

execve的替换过程:

  1. 删除已存在的用户区域
  2. 映射私有区域
  3. 映射共享区域
  4. 设置程序计数器

栈空间

栈的分配本质? 两个 CPU 指令,把数据 push 到栈空间,释放只需要移动栈指针即可

系统预先分配的一段连续地址空间,可以由编译器自行管理,X86中,函数的调用会产生一个新的栈帧,大小由编译器计算好,只要栈指针向下移没有触及保护页就一直能获取栈空间,直到栈溢出,栈空间比堆空间小,适合分配已知字节长度的变量 线程栈的大小是固定的,但是主线程的栈空间比子线程的要大很多 如果访问主存的时候访问的是保护页,那么CPU就会产生页错误 segement fault

堆的本质!!

  • 一个动态存储分配器维护着一个进程的虚拟存储器区域,称为堆 heap
  • 对于每个进程,内核维护着一个变量 brk(break),指向堆的顶部
  • 堆是一个请求二进制零的区域

为什么要用堆? 知道程序实际运行时,才知道数据结构的大小

分配器将堆视为一组不同大小的块(block)的集合来维护,使用双向链表连起来,要么是已分配,要么是空闲

分配策略:首次,下次,最佳 合并分割空闲快,请求失败:合并,请求成功:计算,如果可以就切分成一半

操作系统会默认分配一部分堆内存,通过 brk 我们可以动态扩大或缩小堆,mmap 在不映射文件的情况下可以作为堆空间来使用,但是申请是按页的倍数来分配的

glibc的堆分配算法: 对于小于64字节的空间申请是采用类似于对象池的方法;对于大于512字节的空间申请则是采用的最佳适配算法(操作系统课本有讲);对于大于64字节而小于512字节的,它会根据情况采取上述方法中的最佳折中策略;对于大于128KB的申请,它会使用mmap机制直接向操作系统申请空间

因为申请堆内存会产生系统调用,因此带运行时的程序会事先就向操作系统申请一大段堆空间,长时间不用再收缩,避免重复进行堆空间的申请产生不必要的开销

C语言而言,进程初始化的时候只有栈,没有堆,只有调用了 malloc 之后系统才会分配堆空间,如果堆的使用没有触及到 brk 的地址,内核就不会参与堆的分配,因为够用

垃圾回收

垃圾回收将存储器视为一张有向可达图,一组根节点和一组堆节点 C的垃圾回收是保守的垃圾回收,每个可达块都被正确地标记为可达,而一些不可达节点却可能被错误地标记为可达

系统IO

输出:主存到IO 输入:IO拷贝到主存

RIO 自带的两种函数:

  • 无缓冲的输入输出函数,直接在内存和文件之间传送数据(尤其用于网络读写)
  • 带缓冲的输入函数(线程安全的 bufio)

共享文件: 每个进程维护一张描述符表 所有进程共享打开文件表 所有进程共享v-node 表 fd0 文件1 offset 文件inode fd1 文件2 offset fd2

close 其实是将该文件的描述符关闭,内核才能安全地删除打开文件表中的表项