📄 (ldd) ch09-中断处理(下)(转载).txt
字号:
数据项;覆盖缓冲区的tail,printk是这么实现的(参见第4章的“消息是如何记录的”
一节);或者阻塞生产者,scullpipe是这么实现的;或者分配一个临时的附加的缓冲区
作为主力缓冲区的候补。最好的解决方案取决于数据的重要性和其它一些具体情况下的
问题,所以我就不在这讨论了。
虽然循环缓冲区看来解决了并发访问的问题,但当read函数进入睡眠时仍有出现
竞争条件的可能。下面的代码给出short中这个问题出现的位置:
while (short_head==short_tail) {
interruptible_sleep_on(&short_queue);
interruptible_sleep_on(&short_queue);
/* ... */
}
执行这个语句时,新数据有可能在while条件被测试是否为真后和进程进入睡眠
前到达。中断中携带的信息就无法被进程及时读取;因此即使此时head != tail进程也
将进入睡眠,直到下一项数据到达时它才会被唤醒。
我并没有为short实现正确的锁,因为short_read的源码在第8章的“驱动程序样
例”一节中就包括了,当时还没有讨论到这一点。而且,short处理的数据也不值得我们
为它这么做。
尽管short收集的数据并不重要,而且在连续的两条指令时间间隔内发生中断的
可能性小到可以忽略,但是有些时候你还是不能在还有待处理的数据时冒险地进入睡眠
。
。
但这个问题一般来说还是值得对它进行特别的处理的,我们将它留到本章后面的
“无竞争地进入睡眠”一节,那里我将会更详细地进行讨论。
值得注意的是,循环缓冲区只能处理生产者和消费者的情形。程序员必须经常地
通过更复杂的数据结构来解决并发访问的问题。生产者/消费者的情形实际上是这些问题
中最简单的一种;其它的数据结构,比如象链接表,就不能简单地使用循环缓冲区的实
现方案。
禁止中断
获得对共享数据独占访问的通用方法是调用cli来禁止处理器的中断报告。当数
据项(例如链接表)在中断时间内要被修改并且是被生存于正常的计算流中的函数修改时
,那么随后的函数在访问这些数据前就必须先禁止中断。
这种情况下,竞争条件会发生在读共享数据项的指令和使用刚获得与数据有关的
信息的指令之间。例如,如果链接表在中断时间内被修改过了,那么下面的循环在读这
信息的指令之间。例如,如果链接表在中断时间内被修改过了,那么下面的循环在读这
个表时就可能会失败。
for (ptr=listHead; ptr; ptr=ptr->next)
/* do somthing */;
在ptr已经被读取后但在使用它之前,一个中断可能会改变了ptr的值。如果发生
了这种情况,你一使用ptr就会有问题,因为这个指针当前的值与链接表已经没有关系了
。
一个可能的解决的方法就是在整个关键循环期间都将中断禁止。虽然禁止中断的代码早
在第2章的“ISA内存”一节中就已经引入了,但仍值得在这里再重复一遍:
unsigned long flags;
unsigned long flags;
save_flags(flags);
cli();
/* 临界区代码 */
restore_flags(flags);
实际上,在驱动程序的方法中,可以就用简单的cli/sti对来替代,因为你可以
认为当进程进入系统调用时中断会被打开。但是,在要被其它代码所调用的代码中,你
不得不使用更安全的save_flags/restore_flags解决方法,因为此时无法确定中断标志
位(IF) 当前的值。
使用锁变量
共享数据变量的第三种方法是使用使用原子指令进行访问的锁。当两个无关的实
体(比如象中断处理程序和read系统调用,或者是SMP对称多处理器计算机中的两个处理
器)需要并发地对共享的数据项进行访问时,它们必须先申请锁。如果得不到锁,它就必
须等待。
Linux内核开放了两套函数来对锁进行处理:位操作和对“原子性”数据类型的
访问。
位操作
经常的,我们要使有单个位的锁变量或者要在中断时间内更新设备状态位-而进
程可能正在访问它们。内核为此提供了一套原子地修改和测试位的函数。因为整个操作
是单步完成的,因此不会介入任何中断。
原子性的位操作运行的很快,因为它们通常不禁止中断,使用单条机器指令来完
成相应操作。这些函数与体系结构相关,在头文件<asm/bitops.h>中声明。即使在SMP机
器上它们也能保证是原子的,因此是推荐的保持处理器间一致性的方式。
不幸的是,这些函数的数据类型也是体系结构相关的。nr参数和返回值在Alpha
上是unsigned long类型,而在其它体系结构上是int类型。下面的列表描述了1.2到2.1.
37各版的位操作形式。但该列表在2.1.38版中有了改变,详情可参见第17章“近期发展
”的“位操作”一节。
”的“位操作”一节。
set_bit(nr, void *addr);
这个函数用于设置addr指向的数据项的第nr个位。该函数作用在一个unsigned long上,
即使addr指向void。返回的是该位原先的取值-0或非零。
clear_bit(nr, void *addr);
这个函数用于清除addr指向的unsigned long数据中的指定位。它的语义和set_bit类似
。
change_bit(nr, void *addr);
这个函数用于切换指定位,其它方面和前面的set_bit和clear_bit函数类似。
test_bit(nr, void *addr);
这个函数是唯一一个不必是原子的为操作;它只是简单地返回该位当前的值。
当这些函数用于访问和修改共享的位时,你只要调用它们即可。而使用位操作来
管理控制共享变量访问的锁变量,则更复杂些,需要举一个例子。
要访问共享数据项的代码段可以使用set_bit或clear_bit来试着原子地获取锁。
通常是象下面的代码段这样实现的;假定锁位于地址addr的第nr位上。并且假定当锁空
闲时该位为0,锁忙时该位非零。
/* 试着设置锁 */
while (set_bit(nr,addr)!=0)
wait_for_a_while();
/* 做你的工作 */
/* 释放锁,并检查... */
if (clear_bit(nr,addr)==0)
something_wnt_wrong(); /* 已经被释放了:出错 */
这种访问共享数据的方式的毛病是竞争双方都必须要等待。如果其中一方是中断
处理程序,那么这一点就较难保证了。
原子性的整数操作
内核程序员经常需要在中断处理程序和其它函数间共享整数变量。我们刚才已经看到对
位的原子访问还不足以保证一切都能运行正常(对前面的例子来说,如果一方是一个中断
位的原子访问还不足以保证一切都能运行正常(对前面的例子来说,如果一方是一个中断
处理函数的话,那就必须使用cli)。
实际上,防止竞争条件的需要是如此迫切,以致于内核的开发者为这个问题专门实现了
一个头文件:<asm/atomic.h>。这个头文件比较新,Linux 1.2中就没有提供。因此,需
要向后兼容的驱动程序是不能使用的。
atomic.h中提供的函数比刚才介绍的那些位操作功能更强大。atomic.h中定义了一种新
的数据类型,atomic_t,只能通过原子操作来访问它。
atomic_t目前在所有支持的体系结构上都被定义为int。下面的操作是为这个数据类型所
定义的,能保证SMP机器上的所有处理器是原子地对它进行访问。这些操作都非常快,因
为它们都尽可能编译成单条的机器指令。
void atomic_add(atomic_t i, atomic_t *v);
void atomic_add(atomic_t i, atomic_t *v);
将v指向的原子变量加上i。返回值是void类型,大部分时候没有必要知道新值。网络部
分的代码使用这个函数来更新套接字在内存使用上的统计信息。
void atomic_sub(atomic_t i, atomic_t *v);
从*v里减去i。在最新的2.1版的内核中这两个函数的参数i都声明成int类型,但这种改
变主要是出于美观的需要,并不对源代码造成影响。
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
对原子变量加减1。
int atomic_dec_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
该函数是在1.3.84版的内核里加入的,用于跟踪引用计数。仅当变量*v在减1后取值为0
时返回值为0。
如上所述,只能使用上面这些函数来访问atomic_t类型的数据。如果你将原子数据项传
递给了一个要求参数类型为整型的函数,编译时就会得到警告。不用说,可以读取原子
数据项的当前值并将它强制转换成其它数据类型。
无竞争地进入睡眠
在讨论进入睡眠的问题中我们曾忽略了一个竞争条件。这个问题实际上要比中断
驱动的I/O问题更普遍,而有效的解决方案需要对sleep_on的实现内幕有些了解。
这种特别的竞争条件发生在检查进入睡眠的条件和对sleep_on的实际调用之间。
下面的测试代码和前面使用的代码是一样的,但我觉得还是值得再在这里列出:
while (short_head==short_tail){
while (short_head==short_tail){
interruptible_sleep_on(&short_queue);
/* ... */
}
如果要安全地进行比较和进入睡眠,你必须先禁止中断报告,然后测试条件并进
入睡眠。因此,比较中被测试的变量不会被修改。内核允许进程在发出cli指令后就进入
睡眠。而在将进程插入它的等待队列之后,在调用shcedule之前,内核只要简单地重新
打开中断报告就可以了。
这里给出的例子代码使用了while循环,由该循环来进行信号处理。如果有阻塞
的信号向进程发出报告,interruptible_sleep_on就返回,再次进行while语句中的测试
。
下面是一种可能的实现:
while (short_head==short_tail){
cli();
if (short_head==short_tail)
interuptible_sleep_on(&short_queue);
sti();
/* ... 信号解码 .... */
}
如果中断是在cli后发生的,那么这个中断在当前进程进入睡眠前都会处于待处
理状态。而当中断最终报告给处理器时,进程已经进入了睡眠,可以被安全地唤醒。
理状态。而当中断最终报告给处理器时,进程已经进入了睡眠,可以被安全地唤醒。
在这个例子中,我可以使用cli/sti,是因为设计的这段范例代码存在于read方
法内的;否则我们必须使用更为安全的save_flags,cli,和restore_flags函数。
如果在进入睡眠之前你不想禁止中断,那么还有另一种方法来完成与上面相同的
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -