第一章 温故而知新
1.1-1.3 硬件结构,软件结构
最关键的三个部件:CPU、内存、I/O控制芯片
软件体系结构
应用层调用库,库调用操作系统接口,操作系统调用硬件,相当于一些硬件驱动
中断
中断指当出现需要时,CPU暂时停止当前程序的执行转而执行处理新情况的程序和执行过程。即在程序运行过程中,系统出现了一个必须由CPU立即处理的情况,此时,CPU暂时中止程序的执行转而处理这个新的情况的过程就叫做中断。
软件中断(Software Interrupt),从软件中断指令而来。在32位x86中,为了实现linux用户态到内核态的切换,linux使用软中断指令“int 0x80”来触发异常,切换CPU特权级,实现系统调用。
硬件中断 硬件中断是一个异步信号, 表明需要注意, 或需要改变在执行一个同步事件. 硬件中断是由与系统相连的外设(比如网卡 硬盘 键盘等)自动产生的. 每个设备或设备集都有他自己的IRQ(中断请求), 基于IRQ, CPU可以将相应的请求分发到相应的硬件驱动上(注: 硬件驱动通常是内核中的一个子程序, 而不是一个独立的进程). 比如当网卡受到一个数据包的时候, 就会发出一个中断. 处理中断的驱动是需要运行在CPU上的, 因此, 当中断产生时, CPU会暂时停止当前程序的程序转而执行中断请求. 一个中断只能中断一颗CPU(也有一种特殊情况, 就是在大型主机上是有硬件通道的, 它可以在没有主CPU的支持下, 同时处理多个中断). 硬件中断可以直接中断CPU. 它会引起内核中相关代码被触发. 对于那些需要花费时间去处理的进程, 中断代码本身也可以被其他的硬件中断中断. 对于时钟中断, 内核调度代码会将当前正在运行的代码挂起, 从而让其他代码来运行. 它的存在时为了让调度代码(或称为调度器)可以调度多任务.
1.4 操作系统干嘛
多道程序 一个程序在执行的过程中让CPU停下来了(比如在读写磁盘),那么就让其他的在等待的程序上去执行(内部有一个监控程序,监控CPU是否空闲)
分时系统 一段时间内,所有的程序都有机会上去执行一小段时间,然后主动让出CPU给其他程序。但是如果有个程序来了while(1)死循环,则会导致CPU一直被占用,整体像卡主了。
多任务系统 操作系统接管所有硬件资源,并且本身运行在一个受硬件保护的级别。所有程序都以进程的方式运行在比操作系统级别更低的级别。进程与进程之间相互拥有独立的地址空间,地址空间相互隔离。如果一个进程运行超出一段时间,则操作系统会暂停该进程,把CPU资源分配给其他进程。这就是所谓的抢占式,把CPU资源强制剥夺并分配给它认为目前最需要的进程。
设备驱动:
硬件多入牛毛,操作系统的开发者根本写不过来这么多的硬件,所以把这些任务交给硬件驱动程序,通常由硬件厂商的开发者来写,操作系统再公开一些接口和框架,让驱动程序去调用。驱动和操作系统内核一起运行在特权级,但它又和操作系统内核之间有一定的独立性。
以文件系统为例,发送read系统调用,文件系统收到read请求后,判断存储的扇区。然后向硬盘驱动发出读取请求,驱动向硬盘发出硬件命令。
1.5 内存不够怎么办
使用虚拟地址,虚拟地址可以映射到真实的物理地址。这样,我们只要妥善的控制这个映射过程,就可以保证任意一个程序所能访问的物理内存区域跟另外一个程序相互不重叠,以达到地址空间隔离的效果。
分段
把一段与程序所需要的内存空间大小的虚拟空间映射到某个物理地址空间
但是段还是太大了,颗粒度不够细,内存使用效率不高,为了解决这个问题,需要更细致的映射方式。
分页
分页是把地址空间人为地等分成固定大小的页,页的大小由硬件决定,比如一个4kb大小的页。
然后把进程的虚拟地址空间按页分割,把常用的数据和代码页装在到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它从磁盘里读取出来。
虚拟页 (VP)虚拟空间的页
物理页 (PP)
磁盘页(DP)
有时候多个进程的虚拟空间的有些页被映射到同一个物理页,这样就可以实现内存共享。
页错误(Page Fault):当进程需要用到磁盘页中的数据时,硬件会捕获到这个消息,然后操作系统接管进程,负责把该数据从磁盘中读出来并装入内存,然后同虚拟内存建立映射关系。
虚拟内存映射通过一个叫MMU(Memory Management Unit)的硬件来进行页映射
1.6 线程
线程的访问权限
栈 栈上的局部变量其它线程是访问不了的,一般可以认为是线程的私有数据
线程局部存储(Thread Local Storage,TLS) 操作系统为线程单独提供的私有空间
寄存器 执行流的基本数据,为线程私有
线程的三个状态:
运行 此时线程正在运行
就绪 此时线程可以立刻运行,但CPU被占用
等待 此时线程在等待某一时间(通常是I/O同步)发生,无法执行。
时间片 处于运行中线程拥有一段可以执行的时间,当时间片用尽,进程就进入就绪态,然后重新调度新的就绪线程运行。如果在时间片耗尽前就进入等待事件,那么它就进入等待态,此时调度系统会选择其他的就绪线程继续执行。
线程调度通常使用
轮转法(Round Robin) 让各个线程轮流执行一小段时间
优先级调度 (Priority Schedule) 每个线程都有各自的线程优先级(Thread Priority)
IO密集型线程(IO Bound Thread)频繁等待的线程,它执行通常只占用少量时间
CPU密集型线程(CPU Bound Thread) 很少等待,总是把时间片用尽的线程
一般IO密集型线程因为执行起来快,很快进入等待态,所以操作系统更喜欢IO密集型线程,给它更高的优先级
饿死(Starvation)在优先级调度下,一个线程的优先级较低,在它执行前,总是有较高优先级的线程要执行,因此这个低优先级线程始终无法执行。
比如一个CPU密集型线程优先级很高,其他低优先级的线程就始终无法执行,而一个IO密集型线程因为大部分时间处于等待态,所以不容易造成其它线程饿死。为了避免饿死现象,调度系统会逐步提升哪些等待了过长时间的线程。
线程优先级改变的3种方式
用户指定优先级
根据进入等待状态的频繁程度提升或降低优先级
长时间得不到执行而提升优先级
线程安全
原子操作: 单指令的操作,执行的时候不会被打断。
同步:在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。
二元信号量 一种最简单的锁,只有占用和非占用。只能被唯一一个线程独占访问,当处于占用状态时,其他所有试图获取的线程都会处于等待,直到锁被释放。
信号量 可以允许多个线程并发访问资源,一个初始值为N的信号量允许N个线程并发访问。线程访问资源的时候,将信号量-1,信号量为0就进入等待态,访问完将信号量+1,唤醒一个等待中的线程
互斥量 和二元信号量很类似,但是信号量可以被任意线程获取并释放。互斥量只能是获取它的线程负责释放。
临界区 作用范围仅限于本进程,和互斥量与信号量的区别在于,互斥量和信号量在系统的任何进程都是可见的。
读写锁 一般对数据读的多,写的少的适合用这个锁,拥有三种状态 自由、共享(Shared)、独占(Exclusive)
当处于自由态的时候,任何以共享方式获取的线程都能成功,锁会分配给多个线程。当有一个线程想以独占的方式获取时,它先等其他线程释放该锁,然后独占锁,组织其它线程获取锁,直到它释放
条件变量 类似于一个栅栏,可以被多个线程等待,比如一个线程把它卡主,然后其他线程都去等这个条件变量,然后该线程把它放开,所有线程都被唤醒并继续执行。
过度优化
有时候编译器会调换指令顺序,导致使用了锁,也不一定能保证线程安全。
Volatile
阻止编译器为了速度把变量缓存到寄存器但是不写回
阻止编译器调整Volatile变量的指令顺序
但是能阻止编译器调整顺序,也无法阻止CPU动态调度换序
比如单例模式
volatile T* pInst = 0;
T* GetInstance()
{
if (PInst == NULL)
{
lock();
if (PInst == NULL)
PInst = new T;
unlock();
}
return PInst;
}
这里PInst = new T;
实际有3步
分配内存
在内存位置调用构造函数
把内存地址赋值给
PInst
但是因为构造函数花的时间可能长一些,所以CPU可能先把内存地址(此时已经在分配好了)给PInst,相当于调换了2和3的执行顺序,那么假设此时有其他线程进来,就会拿到一个还没有构造完成的地址,容易出问题
可以使用barrier,一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。
#define barrier() __asm__ volatile ("lwsync")
volatile T* pInst = 0;
T* GetInstance()
{
if (PInst == NULL)
{
lock();
if (PInst == NULL)
T* temp = new T;
barrier();
PInst = temp;
unlock();
}
return PInst;
}
多线程内部情况
操作系统内部提供的线程支持为内核线程,用户实际使用的线程是用户态线程,两者不是对等的。存在三种对应的模型
- 一对一模型 一个用户线程对应唯一一个内核使用的线程,速度提升明显,但是受限于内核线程数量,以及线程上下文切换开销大
- 多对一模型 多个用户线程对应一个内核线程,好处是上下文切换快和增加用户线程无限制,坏处是如果某个用户态线程卡主,会导致其他共用这个内核线程的用户态线程也卡主,并且提升处理器并不会对性能有明显帮助
- 多对多模型 对用户线程数量没什么限制,并且一个线程阻塞不会影响其他线程的执行,但是提升线程数量对性能提升的幅度不如一对一模型高