《Linux原理与结构》课件第9章.ppt
- 【下载声明】
1. 本站全部试题类文档,若标题没写含答案,则无答案;标题注明含答案的文档,主观题也可能无答案。请谨慎下单,一旦售出,不予退换。
2. 本站全部PPT文档均不含视频和音频,PPT中出现的音频或视频标识(或文字)仅表示流程,实际无音频或视频文件。请谨慎下单,一旦售出,不予退换。
3. 本页资料《《Linux原理与结构》课件第9章.ppt》由用户(kld)主动上传,其收益全归该用户。163文库仅提供信息存储空间,仅对该用户上传内容的表现方式做保护处理,对上传内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知163文库(点击联系客服),我们立即给予删除!
4. 请根据预览情况,自愿下载本文。本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
5. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007及以上版本和PDF阅读器,压缩文件请下载最新的WinRAR软件解压。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Linux原理与结构 Linux 原理 结构 课件
- 资源描述:
-
1、1 1第九章 互斥与同步9.1 基础操作基础操作9.2 自旋锁自旋锁9.3 序号锁序号锁9.4 RCU机制机制9.5 信号量信号量9.6 信号量集合信号量集合2 2虽然进程有各自独立的虚拟地址空间,一般情况下不会出现交叉引用的问题,但由于它们运行在同一个计算环境中,共用同一个内核,不可避免地会发生一些相互作用,如竞争独占资源、访问共享对象、协调多方动作等。独占资源(如处理器、外部设备等)同时只能由一个进程使用,对独占资源竞争的结果是一个进程获得,其它进程等待。共享对象(如共用的数据结构等)允许多个进程访问,但访问操作应是原子的,不应出现交叉重叠。相互协作的进程(如生产者与消费者进程)之间需要协
2、调动作,以便保持步调一致。3 3为了使进程之间能够和谐共处、有序竞争,操作系统需要提供保证机制,大致包括互斥(Mutual Exclusion)与同步(Synchronization)。互斥是一种竞争机制,用于保护共享的独占资源。同步是一种协调机制,用于统一进程的步调。互斥是排外的、封闭的,表示资源属于自己,不许别的进程再动,因而多使用锁(Lock)。同步是开放的、合作的,在自己完成某个动作或用完某个资源之后会主动通知合作者或唤醒等待者,因而常使用信号灯或信号量(Semaphore)。Linux内核提供了多种互斥与同步机制,如自旋锁、序号锁、RCU、信号量、信号量集合等。除信号量集合之外,其余
3、机制都是内核自己使用的,4 4用于保护被所有进程共用的内核资源、协调核内进程的动作。可以说没有互斥与同步机制,就无法保证内核的和谐与稳定。5 5为了实现多处理器或并发环境中的互斥与同步机制,内核需要提供一些基础操作,如格栅操作、原子操作、抢占屏蔽操作、进程等待操作等。格栅操作用于设定一些特殊的控制点,以保证点后指令不会在点前指令完全完成之前开始执行,从而控制指令的执行顺序。原子操作是对单个内存变量的不可分割的基本操作(如变量加、减等),用于保证某些特殊内存操作的完整性。抢占屏蔽操作用于控制进程的调度时机,如禁止某些场合下的进程调度等。进程睡眠与等待操作用于管理进程等待队列,以便在条件成熟时唤醒
4、其中的进程。9.1 基基 础础 操操 作作6 69.1.1 格栅操作格栅操作正常情况下,编译或汇编器按序生成程序代码,处理器也按序执行程序代码,不会出现乱序现象。然而,为了提高执行速度,目前的汇编和编译器通常会对程序进行优化,如调整指令顺序等,处理器在执行指令期间也会采取一些加速措施,如高速缓存、乱序发射、并行执行等,因而进程对内存的实际访问顺序可能与程序的预定顺序不一致。大部分情况下,指令顺序的调整不会改变程序的行为,但也有一些例外,如将临界区内的指令移到临界区外执行就可能产生难以预料的后果。为了保证程序行为的一致性,Intel处理器提供了特殊指令、GCC编译器提供了优化格栅、Linux操作
5、系统提供了内存格栅操作,用于控制指令的执行顺序。7 7优化格栅用于防止编译器的过度优化,以保证所生成代码的正确性。Linux实现的优化格式是宏barrier,定义如下:#define barrier()_asm_ _volatile_(:memory)宏barrier告诉编译器三件事:将要插入一段汇编代码(_asm_)、不要将这段代码与其它代码重组(_volatile_)、所有的内存位置都已改变(memory),因而此前保存在寄存器中的所有内存单元的内容都已失效,此后对内存的访问需要重新读入,不能用已有的寄存器内容对程序进行优化。8 8内存格栅用于保证指令的执行顺序。在Intel处理器中,有些
6、指令是串行执行的,可以作为内存格栅,如I/O指令、带lock前缀的指令、写控制寄存器的指令等。另外,Intel处理器还专门引入了三条格栅指令,其中lfence(读格栅)保证其后的读操作不会在其前的读操作完全完成之前开始,sfence(写格栅)保证其后的写操作不会在其前的写操作完全完成之前开始,mfence(读写格栅)保证其后的读写操作不会在其前的读写操作完全完成之前开始。Linux提供了多个内存格栅宏,它们其实就是对上述三条指令的包装,如下:#define mb()asm volatile(mfence:memory)/读写内存格栅9 9#define rmb()asm volatile(lf
7、ence:memory)/读内存格栅#define wmb()asm volatile(sfence:memory)/写内存格栅因而,格栅就是屏障,只有当前面的指令完全执行完之后其后的指令才会开始执行。在程序中插入格栅可以保证程序的执行顺序,虽然会影响程序的执行性能。10 109.1.2 原子操作原子操作由于机器指令的功能过于简单,一个稍微复杂的操作通常都必须用多条指令完成。如操作x=x+3通常被翻译成三条指令:将x的值读入到某个寄存器、将寄存器的值加3、将寄存器的内容写回x。考虑如下情形:(1)在第一条指令完成之后、第三条指令完成之前出现了中断;(2)在中断返回之前处理器再次访问了变量x,如
8、执行了x=x+5。此时,x值的变化会出现不一致性,其结果是x+3而不是x+8。11 11出现上述问题的原因是操作的原子性(不可分割)被破坏了。由于中断的出现,一个完整操作的中间被随机地插入了其它操作。这一问题的解决方法比较简单,关闭中断即可避免这种操作插入现象。更复杂的情况出现在多处理器环境中,此时,一个变量可能被多个处理器同时访问。如两个处理器同时执行x=x+3操作,其结果将是x+3而不是x+6。引起这一问题的原因仍然是操作的原子性被破坏了(在x上同时施加了两个操作)。这一问题的解决需要处理器的支持,并需要操作系统提供相应的手段。12 12Intel处理器提供了lock前缀,用于将一条内存访
9、问指令(如add、sub、inc、dec等)转化成原子指令。如果某条指令之前有lock前缀,那么在执行该指令期间处理器会锁住内存总线,以保证不会出现多个处理器同时访问一个内存变量的现象。为了保证C语言操作的原子性,Linux定义了原子类型atomic_t并提供了一组该类型上的原子操作。原子类型实际就是整型,多被嵌入在其它结构中,定义如下:13 13typedef struct int counter;atomic_t;在Intel处理器上,原子加法操作由atomic_add实现,其实现代码如下:static inline void atomic_add(int i,atomic_t*v)asm
10、 volatile(LOCK_PREFIX addl%1,%0 :+m(v-counter):ir(i);14 14在多处理器平台上,宏LOCK_PREFIX就是lock前缀,用于保证加法操作的原子性,即保证在指令add执行期间不会有其它处理器访问变量v。与原子加法操作的实现思路类似,Linux还实现了多个其它的原子操作,如表9.1所示。15 15表9.1 Linux的原子操作16 169.1.3 抢占屏蔽操作抢占屏蔽操作在当前进程未主动放弃处理器的情况下,如果系统中出现了更值得运行的进程,那么应该将处理器从当前进程手中强行收回,以便让新就绪的进程尽快运行,这一过程称为抢占调度。抢占使处理器的
11、分配更加合理,也可提升系统的反应能力,但会增加设计的复杂性。抢占条件通常由中断处理程序创造,抢占时机通常在中断返回之前。如果中断之前的处理器运行在用户态,那么在从核心态返回用户态之前进行抢占调度不会引起不一致性问题。17 17但如果中断之前的处理器运行在核心态,那么在中断返回之前进行的抢占调度就有可能导致内核数据的不一致性。如中断之前处理器正在修改内核中的某个链表,那么在修改完成之前的抢占调度有可能破坏该链表的一致性。早期的Linux禁止内核态抢占。新版本的Linux允许内核态抢占,但需要某种机制来控制抢占的时机。控制内核态抢占的一种方法是关、开中断。在修改某些关键数据结构之前将中断关闭,在修
12、改完成之后再将中断打开。但关、开中断的代价较高,因而Linux又引入了抢占计数preempt_count(位于进程的thread_info结构中,如图4.8所示),每个进程一个,表示该进程当前是否可被抢占。18 18操作preempt_disable()将当前进程的抢占计数加1,从而禁止抢占该进程。操作preempt_enable()将当前进程的抢占计数减1,而后试图进行抢占调度。调度的条件是:当前进程上设置了TIF_NEED_RESCHED标志且当前进程的抢占计数是0且当前处理器的中断未被屏蔽。上述两个操作都带有优化格栅,保证能使后面的程序看到它们的操作结果。由操作preempt_disab
13、le()和preempt_enable()括起来的区域允许中断,但不可抢占。19 19在从中断返回到核心态之前,如果当前进程的抢占计数不是0,即使其上设置了TIF_NEED_RESCHED标志,善后处理程序也不会进行进程调度(如图4.3所示)。20209.1.4 睡眠与等待操作睡眠与等待操作正在运行的进程可以主动请求睡眠(sleep_on(),从而进入等待状态。进程可以在不可中断等待状态下睡眠,也可以在可中断等待状态下睡眠。进程可以预定一个睡眠时间,也可以不预定睡眠时间(长眠)。Linux将睡眠进程挂在自己指定的等待队列中。等待队列由类型wait_queue_head_t定义,如下:struc
14、t _wait_queue_head spinlock_tlock;/保护锁 struct list_headtask_list;/队列;21 21typedef struct _wait_queue_headwait_queue_head_t;睡眠者进程先将自己包装在一个_wait_queue结构中,而后挂在指定的等待队列上,最后请求调度(执行函数schedule(),放弃处理器,进入等待状态。struct _wait_queue unsigned int flags;/标志,总是1 void*private;/指向睡眠者进程的task_struct结构 wait_queue_func_t
15、func;/睡眠到期后的处理函数2222 struct list_head task_list;/队列节点;typedef struct _wait_queuewait_queue_t;如果进程预定了睡眠时间,Linux会为其启动一个定时器。当定时器到期时,Linux会将睡眠的进程唤醒(将其状态改为TASK_RUNNING并加入就绪队列)。如果进程在到期之前被唤醒,睡眠操作会返回剩余的睡眠时间。未预定睡眠时间的进程只能被显式唤醒(wake_up()。唤醒操作顺序执行等待队列中各2323_wait_queue结构中的func操作,唤醒等待的进程。唤醒者进程可以指定被唤醒进程的状态和一次可唤醒的进
16、程数量。除了睡眠之外,进程还可以等待某个事件(又称为条件变量)。等待者进程可以将自己置于不可中断等待状态或可中断等待状态,且可以预定一个最长等待时间。Linux用结构completion描述这种条件等待队列,如下:struct completion unsigned intdone;/0表示等待的事件未发生,0表示已发生 wait_queue_head_twait;/等待队列;2424欲等待某事件的进程通过wait_for_completion()类的操作将自己置为等待状态并挂在等待队列中,而后请求调度,放弃处理器。此后进程将一直等待,直到自己等待的事件发生(done大于1并被唤醒)或等待了足
17、够长的时间。等待事件的进程可被操作complete()或complete_all()唤醒。操作complete()将done加1并唤醒队列中的第一个进程,操作complete_all()将done加0 x7FFFFFFF而后唤醒队列中的所有进程。2525原子操作只能保证一条指令的原子性,因而仅能实现单个变量的互斥使用。若要实现一个数据结构的互斥使用,就需要保证一个程序片段的原子性,需要更复杂的互斥手段。自旋锁就是其中之一。9.2 自自 旋旋 锁锁2626在操作系统中,一次只允许一个进程使用的资源称为独占资源或临界资源(Critical resource),访问临界资源的程序片段称为临界区(Cr
18、itical section)。自旋锁的主要作用是保证对临界资源的互斥使用,或者说保证临界区的原子性。27279.2.1 自旋锁的概念自旋锁的概念实现进程互斥的方法很多,如纯软件方法(Peterson算法)、纯硬件方法(开关中断)等,但最直观、最简洁的方法是锁(Lock)。锁用于保护使用时间很短的临界资源,每种临界资源都需要一把保护锁。进程在使用临界资源之前需要先申请到该资源的保护锁。获得锁的进程可以使用资源,但在用完之后应尽快将锁释放。在一个特定的时间点上,最多只有一个进程能获得某个特定的锁。在无法立刻获得锁时,进程忙等测试,因而又将这种保护锁称为自旋锁(spinlock)。由自旋锁保护的临
19、界区的格式如下:2828如果加锁操作成功,进程可立刻进入临界区,否则将在加锁操作中自旋等待。自旋锁一般用在多处理器环境中。申请锁的进程和持有锁的进程运行在不同的处理器上,申请锁的进程通过简单的忙等测试等待持有锁的进程释放自旋锁。在单处理器环境中,自旋锁退化成了空语句,或简单的开、关中断操作。自旋锁不能嵌套使用。持有自旋锁的进程再次申请同一个自旋锁会使程序进入死循环,导致死锁(无限期地等待自己解锁)。2929为了实现自旋锁,至少需要定义一个锁变量和一组锁操作。锁变量用于记录锁的状态(忙、闲),锁操作用于测试和修改锁的状态,实现加锁和解锁语义。锁变量必须是共享的,使用同一个自旋锁的所有进程或处理器
20、看到的应该是同一个锁变量,因而锁通常用在内核中。当然,对锁状态的修改操作必须是原子的。30309.2.2 经典自旋锁经典自旋锁Linux用类型spinlock_t(或结构spinlock)描述自旋锁。在Intel处理器上,结构spinlock的定义如下:typedef struct arch_spinlock unsigned int slock;/记录锁状态 arch_spinlock_t;typedef struct raw_spinlock arch_spinlock_t raw_lock;raw_spinlock_t;31 31typedef struct spinlock struc
21、t raw_spinlockrlock;spinlock_t;由此可见,自旋锁实际是一个经过多层包装的无符号整型变量,其值的意义取决于加锁与解锁算法的设计。在Linux的演变过程中,自旋锁的实现算法也在不断演变。(1)算法1把锁状态看成一个整数,1表示锁闲,0表示锁忙。加锁操作将锁状态减1,如果减后的值不是负数,则获得锁,否则循环测试,直到锁的状态变成正数。解锁操作将锁状态置1。减1与置1操作都是原子的。算法1的实现流程如图9.1所示。3232图9.1 加锁解锁算法13333(2)算法2把锁状态看成一个位图,只用它的第0位,0表示锁闲,1表示锁忙。加锁操作在把第0位的值取出的同时将其置1。如果
22、取出的值是0,则获得锁,否则循环测试,直到第0位变成0。解锁操作将第0位清0。位的检测与设置(清除)操作都是原子的,由带lock前缀的指令BTS(位检测与设置)和BTR(位检测与清除)实现。上述两个算法实现了自旋锁的语义,可以保证在一个特定的时刻最多只有一个进程能获得自旋锁,但都存在着无序竞争的问题。3434假如在进程1持有锁sl期间,进程2和进程3先后申请了该锁,那么当进程1释放sl时,应该是进程2先获得,进程3后获得。但上述两种实现无法保证这一点。为此,Linux又给出了算法3。(3)算法3把锁状态看成两个无符号整数(8位或16位),一个为申请序号req,一个为使用序号use,两个序号的初
23、值都是0。加锁操作获得锁的当前申请序号同时将其加1,而后比较自己获得的申请序号和锁的使用序号,如两者相等,则获得锁,否则忙等测试,直到锁的使用序号与自己的申请序号相等为止。解锁操作将锁的使用序号加1。算法3的实现流程如图9.2所示。3535图9.2 加锁解锁算法33636当申请序号等于使用序号时,锁空闲。当申请序号大于使用序号时,锁繁忙(req-use-1是等待自旋锁的进程数)。当有多个进程等待同一自旋锁时,它们将按申请的顺序获得锁,先申请者先得,后申请者后得,避免了无序竞争。算法3优于算法1和算法2。3737然而算法3不能避免持有锁的进程被抢占。一旦持有者进程被抢占,它将停留在自己的临界区中
24、,无法及时执行解锁操作,会导致等待该锁的其它进程(或处理器)长时间自旋。因而,完整的加、解锁实现应包括抢占屏蔽,在加锁操作中屏蔽抢占,在解锁操作中使能抢占,如下:void spin_lock(spinlock_t*lock)preempt_disable();/抢占屏蔽 arch_spin_lock(lock-rlock.raw_lock);3838void spin_unlock(spinlock_t*lock)arch_spin_unlock(lock-rlock.raw_lock);preempt_enable();/抢占使能即使进程在持有锁期间被设置了TIF_NEED_RESCHED标
25、志,抢占调度也不会立刻发生。事实上,抢占调度会被推迟到解锁之时,此时进程已退出了临界区。39399.2.3 带中断屏蔽的自旋锁带中断屏蔽的自旋锁增加了抢占屏蔽的自旋锁已经比较可靠,但仍然可能让等待锁的进程长期自旋,原因是持有者进程可能被中断。若持有锁的进程被中断,处理器将转去执行中断处理程序,从而会延长其它进程的等待时间。更严重的是,如果中断处理程序再次申请同一个自旋锁,则会导致锁的嵌套使用,使系统无法继续运行(死锁)。为解决这一问题,Linux又提供了带中断屏蔽的自旋锁,如下:void spin_lock_irq(spinlock_t*lock)4040 local_irq_disable(
展开阅读全文