因此,中断(或软中断)禁止用于防止同一CPU上中断(或软中断)对共享资源的非同步访问。而自旋锁则防止在不同CPU上的执行单元对共享资源的同时访问,以及不同进程上下文互相抢占导致的对共享资源的非同步访问。
在对称多处理器,仅仅禁止某个CPU的中断是不够的,当然我们也可以将所有CPU的中断都禁止,但这样做开销很大,整个系统的性能会明显下降。此外,即使在单处理器上,如果内核是抢占式的,也可能出现不同进程上下文同时进入临界区的情况。为此,Linux内核中提供了"自旋锁(spinlock)"的同步机制。
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"因此而得名。
进程上下文主要是异常处理程序和内核线程。内核之所以进入进程上下文是因为进程自身的一些工作需要在内核中做。例如,系统调用是为当前进程服务的,异常通常是处理进程导致的错误状态等。所以在进程上下文中引用current是有意义的。
内核进入中断上下文是因为中断信号而导致的中断处理或软中断。而中断信号的发生是随机的,中断处理程序及软中断并不能事先预测发生中断时当前运行的是哪个进程,所以在中断上下文中引用current是可以的,但没有意义。事实上,对于A进程希望等待的中断信号,可能在B进程执行期间发生。例如,A进程启动写磁盘操作,A进程睡眠后B进程在运行,当磁盘写完后磁盘中断信号打断的是B进程,在中断处理时会唤醒A进程。
内核可以处于两种上下文:进程上下文和中断上下文。在系统调用之后,用户应用程序进入内核空间,此后内核空间针对用户空间相应进程的代表就运行于进程上下文。异步发生的中断会引发中断处理程序被调用,中断处理程序就运行于中断上下文。中断上下文和进程上下文不可能同时发生。
运行于进程上下文的内核代码是可抢占的,但中断上下文则会一直运行至结束,不会被抢占。因此,内核会限制中断上下文的工作,不允许其执行如下操作:
(1) 进入睡眠状态或主动放弃CPU;
由于中断上下文不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程),所以中断上下文一旦睡眠或者放弃CPU,将无法被唤醒。所以也叫原子上下文(atomic context)。
(2) 占用互斥体;
为了保护中断句柄临界区资源,不能使用mutexes。如果获得不到信号量,代码就会睡眠,会产生和上面相同的情况,如果必须使用锁,则使用spinlock。
(3) 执行耗时的任务;
中断处理应该尽可能快,因为内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。在中断处理例程中执行耗时任务时,应该交由中断处理例程底半部来处理。
(4) 访问用户空间虚拟内存。
因为中断上下文是和特定进程无关的,它是内核代表硬件运行在内核空间,所以在中断上下文无法访问用户空间的虚拟地址
(5) 中断处理例程不应该设置成reentrant(可被并行或递归调用的例程)。
因为中断发生时,preempt和irq都被disable,直到中断返回。所以中断上下文和进程上下文不一样,中断处理例程的不同实例,是不允许在SMP上并发运行的。
(6)中断处理例程可以被更高级别的IRQ中断。
如果想禁止这种中断,可以将中断处理例程定义成快速处理例程,相当于告诉CPU,该例程运行时,禁止本地CPU上所有中断请求。这直接导致的结果是,由于其他中断被延迟响应,系统性能下降。
禁止中断包括禁止当前处理器的所有中断和禁止一条中断线两种,禁止所有中断可以使用local_irq_save和local_irq_restore函数(local_irq_disable这些都是当前CPU的终端)
禁止一个中断线可以使用disable_irq函数,只是disable当前irq.
查看使用的中断号可以用cat /proc/interrupts得到。中断系统的状态可以通过几个函数获得,irqs_disabled函数查看本地中断传递是否被禁止.
禁止本地CPU中断是确保一组内核语句被当作一个临界区处理的主要机制。这个机制的意义是:即使当硬件设备产生了一个IRQ信号时,中断禁止也让内核控制路径继续执行,因此,这就提供了一种有效的方式,确保内核控制路径中的一些中断处理程序能访问的数据结构也受到保护。
1 禁止本地中断
然而,禁止本地中断并不保护运行在另一个CPU上的中断处理程序对该数据结构的并发访问,因此,在多处理器系统上,禁止本地中断经常与自旋锁结合使用。
宏local_irq_disable()使用cli汇编语言指令关闭本地CPU上的中断(x86体系的禁止本地中断位于include/linux/Irqflags.h):
2 禁止下半部(可延迟函数)
在中断处理专题的“下半部分”一节,我们说明了可延迟函数可能在不可预知的时间执行(实际上是在硬件中断处理程序结束时)。因此,必须保护可延迟函数访问的数据结构使其避免竞争条件。
禁止可延迟函数在单个CPU上执行的一种简单方式就是禁止在那个CPU上的中断。因为没有中断处理程序被激活,因此,软中断操作就不能异步地开始。
然而,内核有时需要只禁止可延迟函数而不禁止中断。这种情况是需要通过操纵当前thread_info描述符preempt_count字段中存放的软中断计数器,可以在本地CPU上激活或禁止可延迟函数。
回忆一下,如果软中断计数器是正数,do_softirq()函数就不会执行软中断,而且,因为tasklet在软中断之前被执行,把这个计数器设置为大于0的值,由此禁止了在给定CPU上的所有可延迟函数和软中断的执行。
宏local_bh_disable给本地CPU的软中断计数器加1,而函数local_bh_enable()从本地CPU的软中断计数器中减掉1。
递减软中断计数器之后,local_bh_enable()执行两个重要的操作以有助于保证适时地执行长时间等待的线程:
1. 检查本地CPU的preempt_count字段中硬中断计数器和软中断计数器,如果这两个计数器的值都等于0而且有挂起的软中断要执行,就调用do_softirq()来激活这些软中断(见中断专题“下半部”博文)。
2. 调用preempt_check_resched函数检查本地CPU的TIF_NEED_RESCHED标志是否被设置,如果是,说明进程切换请求是挂起的,因此调用preempt_schedule()函数(参见专题前面的“内核抢占”博文)。
#define preempt_check_resched() /
do { /
if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) /
preempt_schedule(); /
} while (0)
Linux操作系统中中断上下文中的互斥
在Linux系统中,有两种不同的上下文:进程上下文、中断上下文。在中断中没有进程上下文,而具有中断上下文,因此在中断上下文中不能发生睡眠,也就是不能发生进程切换。
这就决定了在在中断上下文中不能采用同步原语(信号量,管程、同步变量等)和长期互斥原语(这会导致进程睡眠), 而只能采用短期互斥原语(例如自旋锁)。
曾经,中断处理程序并不具有自己的栈。相反,它们共享所中断进程的内核栈。内核栈的大小是两页,具体地说,在32位体系结构上是8KB,在64位体系结构上是16KB.
现在。中断处理程序有了自己的栈,每个处理器一个,大小为一页。这个栈就称为中断栈,尽管中断栈的大小是原先共享栈的一半,但平均可用栈空间大得多,因为中断处理
程序把这一整页占为己有。
互斥
如果一个中断处理程序的代码访问或者更新了由非中断的代码(通常称为基准代码)使用的同一数据结构,那么就会出现竞争条件。
幸运的是,得到允许的以内核态执行的进程会临时禁止中断。因此,只要基准代码要更新一个与中断处理程序共享的数据结构,那么就
首先禁止中断,执行临界段,然后再重新允许中断。禁止和允许中断的动作就实现了互斥。
在采取中断互斥时,必须使用函数显示地把编码写入算法中。
MP(多CPU系统)上
在MP系统上,中断可以在任何处理器上出现。从最低限度上来说,每个进程会接收时钟中断,但也可能接收I/O中断。在MP系统上,例程
SPL(禁止中断)所提供的保护并不充分,因为它们执行影响执行它们的处理器上的中断优先级。中断可能会在另一个处理器上出现,如果设备驱动程序
正在别处运行,那么会造成一个竞争条件。因为中断处理程序代表另一个进入内核的入口点。
当基准驱动程序代码和中断处理程序之间共享数据结构时,UP可以通过屏蔽中断来防止出现竞争条件的技术,在多线程MP内核中还不充分。
临界段要在一个处理器上执行,执行屏蔽中断例程只会屏蔽在那个处理器上出现的中断。如果在别的处理器上出现中断,那么立即就会有
两个处理器同时访问、而且可能更新临界资源。既然这些临界段需要短期互斥,那么可以使用自旋锁来对数据进行保护。
如果不考虑中断处理程序和基准代码之间的互斥,则Linux中的中断处理程序是无须重入的。当一个给定的中断处理程序正在执行时,相应的中断线
在所有处理器上都会被屏蔽掉,以防止同一中断线上接收另一个新的中断。通常情况下,所有的其他中断都是打开的,所以这些不同中断线上的其他中断
都能处理,但当前中断线总是被禁止的。由此可以看出,同一个中断处理程序绝不会被同时调用以处理嵌套的中断。这极大地简化了中断程序的编写。
自旋锁(spinlock) 解释得经典,透彻
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
该宏声明一个自旋锁x并初始化它。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。
该宏用于静态初始化一个自旋锁。
该宏用于判断自旋锁x是否已经被某执行单元保持(即被锁),如果是,返回真,否则返回假。
该宏用于等待自旋锁x变得没有被任何执行单元保持,如果没有任何执行单元保持该自旋锁,该宏立即返回,否则将循环在那里,直到该自旋锁被保持者释放。
该宏尽力获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则不能立即获得锁,立即返回假。它不会自旋等待lock被释放。
该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放,这时,它获得锁并返回。总之,只有它获得锁才返回。
该宏获得自旋锁的同时把标志寄存器的值保存到变量flags中并失效本地中断。
该宏类似于spin_lock_irqsave,只是该宏不保存标志寄存器的值。
该宏在得到自旋锁的同时失效本地软中断。
该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放。
该宏释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的值。它与spin_lock_irqsave配对使用。
该宏释放自旋锁lock的同时,也使能本地中断。它与spin_lock_irq配对应用。
该宏释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对使用。
因此如果能够立即获得锁,它等同于spin_lock_irqsave,如果不能获得锁,它等同于spin_trylock。如果该宏获得自旋锁lock,那需要使用spin_unlock_irqrestore来释放。
该宏如果获得了自旋锁,它也将失效本地软中断。如果得不到锁,它什么也不做。因此,如果得到了锁,它等同于spin_lock_bh,如果得不到锁,它等同于spin_trylock。如果该宏得到了自旋锁,需要使用spin_unlock_bh来释放。
该宏用于判断自旋锁lock是否能够被锁,它实际是spin_is_locked取反。如果lock没有被锁,它返回真,否则,返回假。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。
spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问。
自旋锁的选择
参考:《Linux Kernel Development》3ed_CN p-149 扩展
如下内容中没有涉及读写自旋锁,尝试锁的获取等,只包含一般情形;如下是我的总结,如有误,请指出:
自旋锁在不同情况下的选择:1、进程上下文与进程上下文之间:
spin_lock()
2、进程上下文与中断上半部之间:
spin_lock_irqsave() or spin_lock_irq()
3、进程上下文与中断下半部之间:
spin_lock_bh()
4、中断上半部与中断上半部之间:
spin_lock_irqsave() or spin_lock_irq()
5、中断上半部与中断下半部之间:
spin_lock_irqsave() or spin_lock_irq()
6、中段下半部与中断下半部之间:
(1)软中断与软中断之间 (同类型或不同类型的软中断可以同时运行在一个系统的多个处理器上,在同一个核上当然不能同时运行)
spin_lock()
(2) 不同类型的tasklet之间(此处只写明是不同类型的tasklet之间,因为同类型的tasklet不可能同时运行)
spin_lock()
在不同的核之间使用spin_lock就可以了,至于其它的全是对同一个核施加的限制;
补充
2013-9-7 21:00
关于读写锁rwlock_t和顺序锁seqlock_t的理论.
参考:《Operating System Concepts》9ed p_220
《CSAPP》2_ed p_1004
分别阐述了 Readers-Writers Problem first readers-writers problem. The first readers-writers problem, which favors readers,requires that no reader be kept waiting unless a writer has already been granted permission to use the object. In other words, no reader should wait simply because writer is waiting. second readers-writers problem. The second readers-writers problem, which favors writers, requires that once a writer is ready to write, it performs its write as soon as possible. Unlike the first problem, a reader that arrives after a writer must wait, even if the writer is also waiting. 根据rwlock_t和seqlock_t的各自实现与用法,可以看出rwlock_t数据结构用于解决first readers-writers problem,seqlock_t数 据结构用于解决second readers-writers problem。
Linux设备驱动程序:中断处理之顶半部和底半
设备的中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽可能地短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。
为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux 将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottom half)。 顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,可以服务更多的中断请求。现在,中断处理工作的重心就落在了底半部的头上,它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部则相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。
顶半部是实际响应中断的过程,也就是用request_irq注册的中断例程。而底半部会在稍后比较安全的时间内执行的过程。即底半部的中断都是打开的。比较典型的情况是顶半部保存设备的数据到一个设备特定的缓存区并调度它的底半部,然后退出;这个过程是非常迅速的。然后底半部执行其他必要的工作,例如唤醒进程、启动另外的I/O操作等等。这种方法允许在底半部工作期间,顶半部还可以继续为新的中断服务。
尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为 Linux设备驱动中的中断处理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。其他操作系统中对中断的处理也采用了类似于Linux系统的方法,真正的硬件中断服务程序都应该尽可能短。因此,许多操作系统都提供了中断上下文和非中断上下文相结合的机制,将中断的耗时工作保留到非中断上下文去执行。
Linux 系统实现底半部的机制主要有tasklet,工作队列和软中断。Linux 的中断处理分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时操作。tasklet 和工作队列都是调度中断底半部的良好机制,tasklet 基于软中断实现。内核定时器也依靠软中断实现。内核中的延时是忙等待或者睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对延迟时间的要求并不是很精确的情况下,睡眠等待通常是值得推荐的。
(1)tasklet (首选机制),它非常快,但是所有的tasklet 代码必须是原子的;始终工作在中断期间运行。
(2)工作队列,它可能有更高的延时,但允许休眠。工作在一个特殊内核进程的上下文中运行。
NAPI用于在禁止中断时以轮询模式操作接口。主要用于网络带宽很高的情况下。对于传统的driver,当接口收到一个包的情况下,就会产生一个中断。但是,当宽带接口每秒钟收到上千个包时,使用中断方式会使系统性能全面下降。所以,为了提高Linux在宽带系统上的性能,使用了一种基于轮询方式的接口。该接口在kernel中的定义:int (*poll)(struct net_device *dev, int *quota)。
netpoll的目的是让内核在网络和I/O子系统尚不能完整可用时,依然能发送和接收数据包。主要用于网络控制台(net console)和远程内核调试(KGDBoE)中。实现netpoll功能,主要是要实现kernel中的poll_controller函数,该函数定义:void (*poll_controller)(struct net_device *dev)。该函数的作用是在缺少设备中断的情况下,还能对控制器做出响应。几乎所有的poll_controller函数都定义成如下形式:
void my_poll_controller(struct net_device *dev) {
disable_device_interrupt(dev);
call_interrupt_handler(dev->irq, dev);
enable_device_interrupt(dev);
}
所以,poll_controller只是模拟了来自指定设备的中断。
volatile描述符,要求编译器不要对其作优化处理,对它的读写都需要从内存中访问。