操作系统导论-研读笔记
第1部分 虚拟化
第2部分 并发
第26章 并发:介绍
线程和进程的区别
- 本质区别:进程是操作系统进行资源分配和调度的基本单位,线程是处理器任务调度和执行的基本单位。
- 资源开销:每个进程都有独立的地址空间,彼此隔离,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
- 包含关系:一个进程至少有一个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 影响关系:一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮。
临界区、竞态条件、不确定性、互斥执行
- 临界区( critical section)是访问共享资源的一段代码,资源通常是一个变量或数据结构。
- 竞态条件( race condition)出现在多个执行线程大致同时进入临界区时,它们都试图更新共享的数据结构,导致了令人惊讶的(也许是不希望的)结果。
- 不确定性( indeterminate)程序由一个或多个竞态条件组成,程序的输出因运行而异,具体取决于哪些线程在何时运行。这导致结果不是确定的( deterministic),而我们通常期望计算机系统给出确定的结果。
- 为了避免这些问题,线程应该使用某种互斥( mutual exclusion)原语。这样做可以保证只有一个线程进入临界区,从而避免出现竞态,并产生确定的程序输出
第28章 锁
锁的作用
锁为程序员提供了最小程度的调度控制。我们把线程视为程序员创建的实体,但是被操作系统调度,具体方式由操作系统选择。锁让程序员获得一些控制权。通过给临界区加锁,可以保证临界区内只有一个线程活跃。锁将原本由操作系统调度的混乱状态变得更为可控。
锁的评价标准
- 第一是锁是否能完成它的基本任务,即提供互斥(mutual exclusion)。最基本的,锁是否有效,能够阻止多个线程进入临界区;
- 第二是公平性(fairness)。当锁可用时,是否每一个竞争线程有公平的机会抢到锁?用另一个方式来看这个问题是检查更极端的情况:是否有竞争锁的线程会饿死(starve),一直无法获得锁?
- 最后是性能(performance),具体来说,是使用锁之后增加的时间开销。有几种场景需要考虑。一种是没有竞争的情况,即只有一个线程抢锁、释放锁的开支如何?另外一种是一个 CPU 上多个线程竞争,性能如何?最后一种是多个 CPU、多个线程竞争时的性能。
硬件对锁的支持
-
测试并设置指令(test-and-set instruction), 也叫作原子交换(atomic exchange)
伪代码如下:
利用这种指令实现的一种自旋锁(spin lock)如下:
-
比较并交换指令(compare-and-swap)
伪代码如下:
利用这种指令实现自旋锁只需要将lock()函数替换为以下:
-
链接的加载(load-linked)和条件式存储指令(store-conditional)
链接的加载指令和典型加载指令类似,都是从内存中取出值存入一个寄存器。关键区别来自条件式存储(store-conditional)指令,只有上一次加载的地址在期间都没有更新时,才会成功,(同时更新刚才链接的加载的地址的值)。成功时,条件存储返回 1,并将 ptr 指的值更新为 value。失败时,返回 0,并且不会更新值。
伪代码如下:
利用这种指令实现的自旋锁如下:
-
获取并增加( fetch-and-add)指令
伪代码如下:
利用这种指令实现的ticket锁如下:
提高锁的性能
问题:如果临界区的线程发生上下文切换而不能完成,其他线程只能一直自旋,等待被中断的(持有锁的)进程重新运行
- 在要自旋的时候放弃CPU
使用操作系统提供的yield()调用让线程中运行态变为就绪态,从而允许其他线程运行,避免继续自旋浪费时间片 - 使用队列:休眠替代自旋
核心思想是在lock()函数中发现锁被其他线程占用时,将自己的threadID添加到一个队列,并利用park()系统调用让自己休眠;在unlock()函数中根据队列的threadID利用unpark()系统调用去唤醒对应的线程
第29章 基于锁的并发数据结构
- 并发计数器
- 并发链表
- 并发队列
- 并发散列表
第30章 条件变量
关键问题
多线程程序中,一个线程等待某些条件是很常见的,例如父线程等待子线程结束、生产者和消费者分别等待缓冲区的空和满。简单的方案是自旋直到条件满足,这是极其低效的,某些情况下甚至是错误的。那么,线程应该如何等待一个条件?
定义
线程可以使用条件变量( condition variable),来等待一个条件变成真。条件变量是一个显式队列,当某些执行状态(即条件, condition)不满足时,线程可以把自己加入队列,等待( waiting)该条件。另外某个线程,当它改变了上述状态时,就可以唤醒一个或者多个等待线程(通过在该条件上发信号),让它们继续执行。
条件变量的两种操作
- 条件变量有两种相关操作: wait()和 signal()。线程要睡眠的时候,调用 wait()。当线程想唤醒等待在某个条件变量上的睡眠线程时,调用 signal()。
- 调用 signal 和 wait 时要持有锁( hold the lock when calling signal or wait)
- 多线程程序在检查条件变量时,使用 while 循环总是对的。 if 语句可能会对,这取决于发信号的语义。因此,总是使用 while,代码就会符合预期。
第31章 信号量
关键问题
如何使用信号量代替锁和条件变量?什么是信号量?什么是二值信号量?用锁和条件变量来实现信号量是否简单?不用锁和条件变量,如何实现信号量?
信号量的定义
信号量是有一个整数值的对象,可以用两个函数来操作它。在 POSIX 标准中,是sem_wait()和 sem_post()
信号量的作用
- 信号量用作锁(二值信号量)
- 信号量用作条件变量