1.计算机体系架构的演变
1.0 早期的计算机硬件结构
- 早期计算机硬件采用的是总线型结构
- CPU的核心频率不高,与内存的频率一致
- 由于I/O设备的速度与CPU、内存相比慢很多。为了协调I/O设备与总线之间的速度,也为了让CPU能够与I/O设备进行通信,一般每个设备配有一个I/O控制器。
2.0 CPU倍频
- 随着CPU核心频率的提升,导致内存跟不上CPU的速度,于是产生了与内存频率一致的系统总线,而CPU采用倍频的方式与系统总线进行通信。
3.0 北桥与南桥
- 北桥(PCI Bridge):协调CPU、内存和高速的图形设备进行高速地交换数据。
- 南桥(PCI Bridge):专门处理低速设备,磁盘、USB、键盘、鼠标等设备都连接在南桥上,由南桥将它们汇总后连接到北桥上。
2.设备驱动
- 当成熟的操作系统出现以后,硬件逐渐被抽象成了一系列概念。在UNIX中,硬件设备的访问形式跟访问普通的文件形式一样;在Windows中,图形硬件被抽象成了GDI,声音和多媒体设备被抽象成了DirectX对象;磁盘被抽象成了普通文件系统。
- 繁琐的硬件细节交给从操作系统,程序员无需关注。
3.Linux的多线程
- Linux将所有的执行实体都称为任务,每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。不过Linux下不同的任务之间可以选择共享内存空间,因而共享了同一个内存空间的多个任务构成了一个进程。
- 在Linux下,用以下方法可以创建一个新的任务。
系统调用 | 作用 |
---|---|
fork | 复制当前进程 |
exec | 使用新的可执行映像覆盖当前可执行映像 |
clone | 创建子进程并从指定位置开始执行 |
- 写时复制(Copy on Write, COW):
fork产生新任务的速度非常快,因为fork并不复制原任务的内存空间,而是和原任务一样共享一个写时复制。
写时复制: 指的是两个任务可以同时自由地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响到其他的任务使用。
4.可重入(reentrant)
- 一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。
- 一个函数要成为可重入的,必须具有如下几个特点:
1)不使用任何(局部)静态或全局的非const变量;
2)不返回任何(局部)静态或全局的非const变量的指针;
3)仅依赖于调用方提供的参数;
4)不依赖于任何单个资源的锁(mutex等);
5)不调用任何不可重入的函数
可重入是并发安全的强力保障,一个可重入函数可以在多线程环境下放心使用。
5.volatile阻止过度优化
-
CPU与编译器发展出了动态调度,在执行程序的时候,为了提高效率有可能交换指令的顺序。
-
换序导致的问题:
volatile T* pInst = 0; T* GetInstance() { lock(); if (pInst == NULL) pInst = new T; unlock(); } return pInst;
其中pInst = new T 包含了3个步骤:
1) 分配内存;
2)在内存的位置上调用构造函数;
3)将内存的地址赋值给pInst。
这里,2)和 3)的顺序是可以颠倒的。即,完全有可能出现:pInst的值已经不是NULL,但对象仍然没有构造完毕。这时候如果出现另外一个对GetInstance 的并发调用,此时pInst == NULL为false,所以这个调用会直接返回尚未构造完全的地址(pInst)以提供给用户使用。 -
volatile基本可以做到两件事:
1)阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;
2)阻止编译器调整操作volatile变量的指令顺序。
6.三种线程模型
1.一对一模型
- 用户线程具有了和内核线程一致的优点,线程之间的并发是真正的并发,一个线程因为某原因阻塞时,其他线程执行不会受到影响。
- 一般直接使用API或系统调用创建的线程均为一对一的线程。
- 缺点:许多操作系统线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。
2.多对一模型
- 缺点:如果其中一个用户线程阻塞,内核里的线程也随之阻塞,则所有的线程都会阻塞。
3.多对多模型
- 优点:一个用户线程阻塞并不会使得所有的用户线程阻塞。