目录
一,CPU、内存、总线、设备驱动
1,计算机基本结构
计算机可以分成CPU、内存、IO设备、其他部件。
总线(BUS)是用来连接所有部件的。
后来人们设计了高速设备处理的北桥芯片、低速设备处理的南桥芯片。
北桥PCI连接CPU、内存等,南桥ISA连接低速设备,
系统总线是PCI BUS,低速设备采用ISA总线汇总到南桥,高速设备有各自的总线汇总到北桥。
2,设备驱动
对于硬件设备,操作系统中的硬件驱动程序来实现具体操作,向上提供统一的访问模式,软件开发只需要调用接口。
设备驱动由设备厂商开发,操作系统开发者为设备厂商提供接口,按照该接口开发的驱动就可以在该操作系统上使用。
3,Cache
4,DMA访问
二,进程、线程、锁1,进程和线程
每个进程都有自己的独立内存空间。
线程,也可以成为轻量级进程,一个进程由一个到多个线程组成。
2,线程调度
CPU的运行被切割成时间片,每个时间片运行一个线程,不断切换运行线程。
线程至少有3个状态:运行running、就绪ready、等待waiting
3,Linux系统的进程和线程
Linux 对多线程的支持绩为贫乏, 事实上, 在 Linux 内核中并不存在真正意义上的线程概念。Linux 将所有的执行实体 (无论是线程还是进程) 都称为任务 (Task), 每一个任务概念上都类似于一个单线程的进程, 具有内存空间、执行实体、文件资源等。不过, Linux 下不同的任务之间可以选择共享内存空间, 因而在实际意义上, 共享了同一个内存空间的多个任务构成了一个进程, 这些任务也就成了这个进程里的线程。在 Linux 下, 用以下方法可以创建一个新的任务, 如表 1-2 所示。
4,原子操作、同步、锁
一个独立的不被其他操作影响的操作叫原子操作。
线程是共享数据的,为了避免同时读写导致数据异常,需要对数据进行访问同步,即一个线程访问的时候另外一个线程不能访问,这样的访问就是原子操作。
同步的最常见方法就是锁。
常见的锁:信号量、互斥量、临界区、读写锁、条件变量。
二元信号量 (Binary Semaphore) 是最简单的一种锁, 它只有两种状态: 占用与非占用。它适合只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时, 第一个试图获取该二元信号量的线程会获得该锁, 并将二元信号量置为占用状态, 此后其他的所有试图获取该二元信号量的线程将会等待, 直到该锁被释放。
对于允许多个线程并发访问的资源, 多元信号量简称信号量 (Semaphore), 它是一个 很好的选择。一个初始值为N 的信号量允许 N 个线程并发访问。线程访问资源的时候首先获取信号量, 进行如下操作:
- 将信号量的值减 1 。
- 如果信号量的值小于 0 , 则进入等待状态, 否则继续执行。
访问完资源之后, 线程释放信姿量, 进行如下操作:
- 将信号量的值加 1 。
- 如果信号量的值小于 1 , 唤醒一个等待中的线程。
互斥量 (Mutex) 和二元信号量很类似, 资源仅同时允许一个线程访问, 但和信号量不同的是, 信号量在整个系统可以被任意线程获取并释放, 也就是说, 同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。而互斥量则要求哪个线程获取了互斥量, 哪个线程就要负责释放这个锁, 其他线程越刞代㽶去释放互斥量是无效的。
临界区 (Critical Section) 是比互斥量更加严格的同步手段。在术语中, 把临界区的锁的获取称为进入临界区, 而把锁的译放称为离开临界区。临界区和互斥量与信号量的区别在于, 互斥量和信号量在系统的任何进程里都是可见的, 也就是说, 一个进程创建了一个互斥量或信号量, 另一个进程试图去获取该锁是合法的。然而, 临界区的作用范围仅限于本进程, 其他的进程无法获取该锁。除此之外, 临界区具有和互斥量相同的性质。
条件变量 (Condition Variable) 作为一种同步手段, 作用类似于一个栅栏。对于条件变量, 线程可以有两种操作, 首先线程可以等待条件变量, 一个条件变量可以被多个线程等待。其次, 线程可以唤醒条件变量, 此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。也就是说, 使用条件变量可以让许多线程一起等待某个事件的发生, 当事件发生时(条件变量被唤醒), 所有的线程可以一起恢复执行。
5,可重入
一个函数被重入, 表示这个函数没有执行完成, 由于外部因素或内部调用, 又一次进入该函数执行。一个函数要被重入, 只有两种情况:
(1) 多个线程同时执行这个函数。
(2) 函数白身 (可能是经过多层调用之后) 调用自身。
函数可重入指的是,该函数重入之后,不会产生任何不良后果。
可重入函数的特点:
- 不使用任何(局部)静态或全局的非 const 变量。
- 不返回任何(局部)静态或全局的非 const 变量的指针。
- 仅依赖于调用方提供的参数。
- 不依赖任何单个资源的锁 (mutex 等)。
- 不调用任何不可重入的函数。
6,过度优化 VS 线程安全
(1)一些复杂的优化行为,会让锁失效。
案例一:将变量写入寄存器而不写回
编译器为了提高访问速度,将变量x写入寄存器而不写回,可能导致最终x的值是1,本来应该是2的。
案例二:交换指令顺序
编译器可能会为了提高效率而交换相邻两条无关指令的顺序,CPU的动态调度功能也会交换指令执行的顺序。
这样,最终结果可能就是r1=r2=0,本来是不可能这样的。
(2)使用volatile关键字,可以阻止编译器为了提高访问速度,将变量x写入寄存器而不写回,可以阻止编译器操作volatile变量的指令顺序。
所以,volatile可以解决案例一,但是无法解决案例二,因为CPU有动态调度。
(3)单例模式的double-check
案例三:还是交换指令顺序
这个是单例模式,申请一个对象,双重if判断是为了降低lock的调用,提高效率。
而(2)(3)的顺序是可以颠倒的,所以如果函数并发调用,就会有一个正在调用构造函数的对象的指针,被提供给调用方,从而引发错误。
CPU通常会提供一条屏障指令,用来组织指令交换顺序的时候穿过屏障,有的叫barrier,有的叫lwsync
例如,可以这样来解决上面的问题:
三,并发并行、中断自陷、同步异步1,并发、并行
2,中断、自陷
3,同步、异步
四,系统设计、调度管理1,多CPU系统设计
2,多道程序设计 multiprogramming
3,分时系统
将主机执行时间分成一个个时间片。