《程序员的自我修养》学习笔记(一):简介
1. 从Hello World说起
2. 万变不离其宗
3. 站得高,望得远
4. 操作系统做什么
不让CPU打盹
监控程序监控CPU使用——多道程序;
每个程序运行一段时间后主动让出CPU——分时系统;
多任务系统:应用程序以进程方式运行;抢占式CPU分配。
设备驱动
操作系统是对硬件抽象;
操作系统中的硬件驱动程序完成硬件细节的实现;
操作系统为硬件厂商提供一系列接口和框架。
5. 内存不够怎么办
简单分配物理内存的问题:地址空间不隔离;内存使用效率低;程序运行的地址不确定。
关于隔离:虚拟地址空间;物理地址空间。
分段(Segmentation)
把一段与程序所需的内存空间大小的虚拟空间映射到某个地址空间
解决了问题1、3
分页(Paging)
把地址空间人为地分成固定大小的页,于是产生了:虚拟页;物理页;磁盘页。
页映射
内存共享;
内存保护;
采用Memory Management Unit部件进行页映射;
CPU发出虚拟地址-->MMU转换成物理地址-->物理内存。
6. 众人拾柴火焰高
线程基础
线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。
多个线程组成进程,线程间共享
内存空间:代码段;数据段;堆。
进程级资源:打开文件;信号。
多线程的优点:
有效利用等待时间;交互与计算;程序逻辑要求;多CPU或多核计算机;与多进程相比更高效的数据共享。
线程的访问权限
可以访问进程内存所有数据:全局变量;堆;函数内的静态变量;程序代码;打开文件。
线程私有空间:栈上数据——函数参数;线程局部存储空间(Thread Local Storage)上的数据;寄存器上数据——局部变量。
线程调度与优先级
不断在处理器上切换不同的线程——线程调度。
线程状态:运行;就绪;等待。
调度方法有轮转法和优先级调度法。
优先级设置策略
a.用户设置
b.系统设置,其考虑因素有:
进入等待的频繁程度:频繁等待——IO密集型线程;很少等待,用尽时间片——CPU密集型线程。IO密集型更容易提升优先级;
等待时间长短,等待时间过长的线程将饿死。
可抢占式线程和不可抢占式线程。抢占式为当线程用尽时间片后会被强制释放CPU,而进入就绪状态。早期系统线程不可抢占,只能线程自己发命令放弃执行,主动进入就绪状态。不可抢占线程主动放弃执行的情况:1)线程试图等待某事件;2)线程主动放弃时间片。
Linux的多线程
Linux内核不存在真正意义的线程概念。Linux将所有执行实体都称为任务(Task)——具有内存空间、执行实体、文件资源。任务间可以共享内存空间。系统调用创建新的任务:
fork:复制当前进程,与原任务一起共享写时复制的内存空间;
exec:使用新的可执行映像覆盖当前可执行映像,fork与exec配合产生新任务;
clone:创建子进程并从指定位置开始执行,用于产生新线程。
线程安全
维护并发数据的一致性对多线程程序很重要
竞争与原子操作
以自增为例子说明一句高级程序代码可能被转化为多句汇编指令,从而导致错误,而单指令的操作成为原子操作,其执行不会被打乱。CPU和操作系统提供了相应的原子操作接口。
同步与锁
同步(Synchronization),即一个线程访问数据未有结束时,其他线程不得对同一个数据进行访问,它实现了数据访问的原子化。
锁(Lock)——实现同步最常见的方法,其概念为:线程访问资源或数据前首先试图获取(Acquire)锁;访问结束时释放(Release)锁;当试图获取锁时,锁已被其它进程占用,线程将等待。
二元信号量(Binary Semaphore),最简单的锁,只有占用/非占用两种状态;
信号量(Semaphore),允许多个线程并发访问资源,初始值N的信号量允许N个线程并发访问。其操作:
线程访问资源:信号量减1;如果信号量小于0,线程进入等待状态,否则继续执行;
线程释放资源:信号量加1;如果信号量小于1,唤醒一个等待中的线程。
互斥量(Mutex),资源仅同时允许一个线程访问,与二元信号量相类似。
与二元信号量不同的是,信号量在整个系统可以被任意线程获取并释放;互斥量则要求获取和释放互斥量的是同一个线程。
临界区(Critical Section),进入临界区——锁的获取;离开临界区——锁的释放。
与信号量、互斥量的区别是,信号量、互斥量在系统的任何进程里都是可见的;临界区的作用范围仅限于本进程,其他进程无法获取该锁
读写锁(Read-Write Lock),对于同一个锁,有两种获取方式:共享的(Shared),独占的(Exclusive)。当锁处于自由时,两种获取锁的方式都成功;当锁处于共享状态时,其他线程以共享的方式获取锁仍然成功;当锁处于独占状态时,其他线程必须等待其释放。
条件变量(Condition Variable),可被多个线程等待(线程等待某个事件),条件变量被唤醒(事件发生),所有线程可以一起恢复执行。
可重入与线程安全
函数被重入,表示函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。发生重入的情况有:多个线程同时执行这个函数;函数自身(可能经过多层调用后)调用自身。
函数可重入的条件:
不使用任何(局部)静态或全局的非const变量;
不返回任何(局部)静态或全局的非const变量的指针;
仅依赖与调用方提供的参数;
不依赖任何单个资源的锁;
不调用任何不可重入的函数。
过度优化
编译器和CPU优化带来两个问题:编译器为提高变量访问速度,将其缓存在寄存器,即使发生写操作也不写回主存(互斥问题);CPU动态调度,交换两条相邻指令的执行顺序(同步问题)。
使用volatile关键字试图阻止过度优化,该关键字能够。
1)阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回;
2)阻止编译器调整操作volatile变量的指令顺序。
但它不能阻止CPU动态调用换序,文中以Singleton为例说明,指出new指令包含三步操作,多线程和CPU动态调用情况下可能导致错误。需要调用CPU提供的指令barrier阻止其进行指令交换。
多线程内部情况
内核线程由多处理器或调度实现并发。而用户实际使用的是用户态线程,并不一定在操作系统内核里对于同等数量的线程,于是产生了用户线程与内核线程的三种模型
一对一模型
线程间的并发是真正的并发,一个线程受阻塞并不影响其他线程,在多处理器上有很好表现。Linux中使用clone;Windows中使用CreateThread创建。但其缺点为:内核线程的数量有限制;内核线程调度时,上下文切换的开销较大,导致用户线程执行效率低。
多对一模型
多个用户线程映射到一个内核线程上,线程切换由用户态代码实现,速度快。但如果一个用户线程受阻塞,其他线程均无法执行,而且未能利用多处理器。
多对多模型
多个用户线程映射到多于一个但少量的内核线程上。一个用户线程阻塞并不会使所有用户线程阻塞,对用户线程数量没有限制,在多处理器系统上的运行效率也有一定提升。