一、三个关键部件
CPU、内存、I/O控制芯片
二、发展史
- 早期,CPU和内存同频,直接连接在总线上,I/O设备等低速设备通过I/O控制器与总线相连。
- CPU核心频率远超内存,CPU采用倍频的方式和系统总线通信。系统总线和内存同频。
- 北桥连接高速组件(内存、图形设备),南桥连接键盘、鼠标、USB等低速设备。
- SMP:每个CPU在系统相互独立,地位功能完全一致。
- 多核处理器:多个核心共享缓存。
三、计算机系统软件体系结构
"Any problem in computer science can be solved by another layer of indirection."
- 层次之间相互通信的协议称为接口。
- 系统调用接口以软件中断的方式提供。
- 硬件是接口的定义者,操作系统内核层是接口的使用者。
四、操作系统
两大功能
- 提供抽象接口
- 管理硬件资源(CPU、存储器、I/O设备)
CPU调度
- 多道程序:
- CPU闲置时分配给别的程序。
- 程序调度之间无优先级。
- 重要任务可能无法得到快速响应。
- 分时系统:
- 每个程序运行一段时间后主动让出CPU。
- 依赖程序主动让出CPU。
- 多任务系统:
- 操作系统接管所有硬件资源。
- 操作系统拥有最高权限,CPU由操作系统统一分配。
- 程序以进程的方式运行,每个进程拥有独立的地址空间。
- 进程之间有优先级,操作系统可以强制剥夺CPU权限分配给高优先级进程。
硬件抽象
- 操作系统为开发者提供统一的接口。
- 硬件生成厂商按照操作系统提供的框架提供硬件驱动程序。
内存分配
-
物理内存直接分配:
- 地址空间不隔离
- 内存使用效率低: 需要将整个程序装入内存。 会有大量的内存换入换出。
- 程序运行内存地址不确定
-
虚拟地址映射
每个进程有自己独立的虚拟地址空间。 真实的物理地址映射由操作系统管理。
虚拟地址映射方法
- 分段
- 把程序所需的内存空间映射到等大的物理空间。
- 地址空间隔离、虚拟空间地址固定都是从0x00000000开始。
- 无法解决换入换出,内存不足情况依然需要整块换出。
- 分页
- 地址空间(虚拟地址和物理地址)人为的划分为等大的页(4KB或4MB)。 32位虚拟地址空间为4GB,划分为1 048 576个4KB的页。
- 以页为单位在虚拟页、物理页、磁盘页之间交换和存取数据。
MMU(Memory Management Unit)复制处理虚拟地址的映射。
线程
线程是程序执行流的最小单元。线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。 一个进程的线程共享程序的内存空间(包括代码段、数据段、堆等)以及一些进程级的资源(打开的文件等)。
| 线程私有 | 线程之间共享(进程所有) |
|---|---|
| * 局部变量 | 全局变量 |
| * 函数的参数 | 堆上的数据 |
| * 线程局部存储(TLS) | 函数里的静态变量 |
| * 寄存器(执行流的基本数据) | 程序代码 |
| 打开的文件 |
线程调度
线程状态:
- 运行
- 就绪
- 等待
调度算法: 优先级调度、轮转法 高优先级的线程会更早的执行。IO密集型线程会比CPU密集型更容易得到优先级的提升。 IO密集型线程会频繁的进入等待状态,让出CPU时间。 饿死:一个线程的优先级较低,始终无法得到执行。 为了避免饿死,调度系统会提升那些等待了很长时间得不到执行的线程。线程在时间片用尽之后会被强制剥夺继续执行的权利,称为抢占。
Linux的多线程
Linux将所有的执行实体(无论线程还是进程)都称为任务。 fork产生一个新的任务,新任务和原任务执行相同的任务镜像,同时读取一段内存,在任意一个任务对内存发生修改时复制一份以单独使用(写时复制)。 新任务通过调用exec来执行新的可执行文件。
线程安全
一个操作被编译为汇编代码后不止一条指令,它在执行的时候就可能会被线程调度打断,导致共享的全局变量和堆数据可能被其他线程改变。 例如:i++被clang编译器翻译为三条指令。 原子操作:单指令的操作,不会被打断。
同步与锁
多个线程同时读取并不会导致线程安全问题,只有非原子型操作对数据进行修改时才需要同步机制来避免出错。 线程在访问数据之前需要先获取锁,访问结束后释放锁。在锁被占用的时候,线程会等待,直到锁重新可用。
- 信号量:
- 获取信号量,信号量值-1。
- 如果信号量的值小于0,线程阻塞,否则继续执行。
- 访问资源结束,释放信号。
- 信号量的值+1。
- 信号量的值大于1,唤醒一个阻塞的线程。
- 信号量可以由一个线程获取另一个线程释放。
- 互斥量:
- 类似于二元信号量,资源只允许被一个线程访问。
- 哪个线程获取互斥量,哪个线程释放。
- 临界区:
- 信号量和互斥量进程间可见。
- 临界量作用范围仅限于本进程,其他进程无法获取该锁。
- 读写锁:
- 自由状态:可以被获取并进入共享或独占状态。
- 共享状态:可以以共享方式被获取,等到所有其他线程都释放了可以被独占获取并进入独占状态。
- 独占状态:阻止任何其他线程获取该锁。
- 条件变量:
- 线程可以等待条件变量。
- 线程可以唤醒条件变量。
- 条件变量被唤醒,所有等待该条件变量的线程都恢复执行。
DCL, Double Check Lock Singleton
volatile T* pInst = 0; # volatile 阻止编译器将变量缓存到寄存器而不写回
T* GetInstance()
{
if(pInst == NULL)
{
lock(); # 只在初始化的时候加锁
if(pInst == NULL)
pInst = new T;
unlock();
}
return pInst;
}
new 操作做了什么
-
(1)为对象分配内存
-
(2)调用构造函数初始化对象
-
(3)返回内存地址 编译器优化后,步骤(2)和步骤(3)的执行顺序会颠倒,所以DCL单例也不是线程安全的。