第9章 内核同步方法
- 总述
1)应用程序和内核都应该防止共享资源被并发访问
2)保护共享资源的困难:单处理器时,只有在中断发生的时候,或在内核代码显式地请求重新调度,执行另一个任务时,数据才可能并发访问,比较简单。内核支持多处理器后,内核代码完全可以同时运行在两个或更多的处理器上,如果不加保护,运行在两个不同处理器上的内核代码完全可能在同一时刻里并发访问共享数据。Linux内核已经支持抢占式内核,即调度程序可以在任何时刻抢占正在运行的内核代码,重新调度其他的进程执行。
9.1 临界区和竞争条件
1)临界区:访问和操作共享数据的代码段。
2)避免在临界区中并发访问:必须保证这些代码原子地执行,即代码在执行结束前不可被打断。
3)竞争条件:两个执行线程处于同一个临界区
4)同步:避免并发和防止竞争条件
9.2 加锁
1)简单的竞争条件:原子操作
2)复杂的竞争条件:加锁
需求:处理一个队列上的所有服务请求,
实现:假设用一个链表表示,链表中的每一个节点代表一个请求。有两个函数来操作此队列,一个函数将请求添加到队列尾部,另一个函数从队列头删除情况,然后处理它。
问题:并发访问问题。内核各个部分都会调用到这两个函数,内核会频繁地将在队列中加入请求,从队列中删除和处理请求。对请求队列的操作要用到多条指令。如果一个线程试图读取队列,而正好另一个线程正在处理该队列,那么读取线程就会发现队列此刻正在处于不一致状态。如果运行并发访问该队列,就会产生危害。当共享资源是一个分支的数据结构时,竞争条件往往会使该数据结构遭到破坏。体系结构可以提供简单的原子指令,实现算术运算和比较之类的原子操作,提供专门的指令用于不定长度的临界区进行保护,不太现实。
解决方法:锁机制,保证一个时刻只能有一个线程持有锁。
锁机制:种类多样,区别是当锁被争用时的行为表现:一些锁被占用时会简单地执行忙等待,而有些锁会使当前任务睡眠直到锁可用为止。
9.2.1 什么造成了并发执行
1)用户空间为何需要同步:用户程序会被调度程序抢占和重新调度;用户进程可能在任何时刻被抢占,而调度程序完全可能选择一个高优先级的进程到处理器上执行,所以就有可能在一个程序正在处于临界区时,就被非自愿地抢占了,如果新调度的进程随后也进入同一个临界区,前后两个进程相互之间就会产生竞争。另外,因为信号处理是异步发生的,所以,即使是单线程的多个进程共享文件,或者一个程序内部处理信号,也有可能产生竞争。
2)单处理器是伪并发执行,多处理器的机器,两个进程就可以正在的在临界区中同时执行,这种类型是真并发。它们都可以造成竞争条件。
3)内核造成并发执行的原因
A)中断
B)软中断和tasklet
C)内核抢占
D)睡眠以及用户空间的同步
E)对称处理器—两个或多个处理器可以同时执行代码
F)内核代码操作某资源时系统产生中断,而且该中断处理程序还要访问这一资源;内核代码在访问一个共享资源期间可以被抢占;内核代码在临界区里睡眠;两个处理器同时访问同一个共享数据;
3)发现潜在并发执行,有意识的采取措施来防止并发执行;辨认真正需要共享的数据和相应的临界区是比较困难的。
4)加锁基本原则:编写代码开始阶段就要设计恰当的锁
5)中断安全码:在中断处理程序中避免并发访问的安全代码
6)SMP安全码:在对称多处理器的机器中避免并发访问的安全代码
7)抢占安全码:在内核抢占时能避免并发访问的安全代码
9.2.2 保护什么
关键:找出哪些数据需要保护
方法:寻找哪些代码不需要保护。如果有其他执行线程可以访问内核数据结构,那就给这些数据加上某种形式的锁;如果任何其他什么东西能看到它,那么就锁住它。给数据加锁而不是代码。
保护对象:执行线程的局部数据仅仅被它本身访问,不需要保护;数据只会被特定的进程访问,也不需要加锁;内核数据结构需要加锁;
配置选项:SMP与UP
A)Linux内核可在编译时配置,可以针对特定机器进行内核裁剪。
B)CONFIG_SMP配置选项控制是否支持SMP,CONFIG_PREEMPT是否允许内核抢占的配置选项
C)许多加锁问题在单处理器上是不存在的,配置选项不同,实际编译时包含的锁就不同
编写内核代码时,要考虑
A)数据是不是全局的?除了当前线程外,其他线程能不能访问它?
B)数据会不会在进程上下文和中断上下文共享?它是不是要在两个不同的中断处理程序总共享?
C)当前进程是否会睡眠(阻塞)在某些资源上,如果是,它会让共享数据处于何种状态?
D)怎样防止数据失控?
E)函数又在另一个处理器上被调用将会发生什么?
F)要对这些代码做什么?
总之,几乎访问所有的内核全局变量和共享数据都需要某种形式的同步方法
9.3 死锁
1)产生条件:一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用。所有线程都在相互等待,且它们永远不会释放已经占用的资源,导致任何线程都无法继续,便发生了死锁。
2)实例:
A)四路交通阻塞问题
B)自死锁:如果一个执行线程试图去获取一个自己已经持有的锁,它将不得不等待锁被释放,但因为它正在忙着等待这个锁,所以自己永远也不会有机会释放锁,最终结果就是死锁。
3)预防死锁
A)加锁顺序是关键。使用嵌套的锁时必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁。
B)防止发生饥饿。A不发生,B要一直等下去吗?
C)不要重复请求同一个锁
D)越复杂的加锁方案越有可能造成死锁
9.4 争用和扩展性
1)锁的争用:当锁正在被占用时,有其他线程试图获得该锁。锁的作用是使程序以串行方式对资源进行访问,使用它也降低系统的性能。高度争用的锁会成为系统的瓶颈,严重降低系统性能。
2)扩展性:对系统可扩展程序的度量。
3)2.0内核引入多处理支持,Linux对集群处理器的可扩展性大大提高,一个时刻只能有一个任务在内核中执行;2.2版本,加锁机制发展到细粒度加锁后,便取消了这种限制。2.6内核,加锁粒度很细,可扩展性很好。
4)设计所的开始阶段要考虑到保证良好的扩展性。
9.5 小结
1)编写SMP安全代码,要考虑如何加锁
2)恰当的同步(加锁)既要满足不死锁、可扩展,还要清晰、简洁。
3)编写内核代码、新的系统调用还是重写驱动程序,要考虑保护数据不被并发访问。