2008年(1)
分类: LINUX
2008-03-16 17:44:19
LINUX设备驱动 - 竞态、阻塞、可重入代码
*******************************************************************************************************************
1. 关于竞态
如果有两个进程同时打开设备进行写数据操作,在进程A写数据时它将新申请一快设备内存,并在设备dev数据链表追加一个新的量子,使指针指向这个新的设备内存块,而在进程B写操作也有同样操作,这样如果不做任何驱动修改,因为设备被两个进程同时打开,两个进程拥有同一个设备数据链表的信息,就会产生修改同一个数据,很明显最先操作的进程建立的数据会被后面的进程覆盖,这就是产生了竞态。
不过,不太积极的说法是,在单处理器上这种情况不会发生,因为在内核运行的代码是非抢占性的,就是说在同一时间处理器只能处理一个代码,但是多处理器系统就可能发生了。
LINUX提供了竞态的解决办法:
1)信号量semaphore, 用于互斥
#include
很简单,就是在需要避免竞态的数据块中定义一个标记,当有进程在使用时把标记设置成0,表示信号已经被占用了不能再用,所有的进程如果要访问该数据块,必须先检查该标记,如果为0,表示有进程正在占用,就必须等待。
因此在scull0的数据结构Scull_Dev中就有一个semaphore的标记。
但是信号量我们是由内核处理的,因为我们希望当进程要访问信号量时,如果信号量被使用,进程就应该交给内核,进入等待,而不是由我们自己循环的检查和等待信号量。
ok,既然又要交给内核管理,那么必须初始化信号量。
sema_init(&scull_devices.sem, 1); //;注册一个信号量,初始化为1,表示可用。
当需要获取信号量时,调用down_interruptable(&sem), 释放信号量up(&sem), up并将唤醒正在等待信号量的进程。
if (down_interruptable(&dev->sem)) return -ERESTARTSYS; //;如果失败,直接返回,不能调用up(&sem)//;data operations...
up(&dev->sem);
注意的是,要小心信号量的使用,可见,如果有个进程持有信号量,而当它释放信号量失败的话,其他进程就会一直阻塞。
另外因为使用信号量会导致进程睡眠,所以在中断处理中不能适用信号量。
2)锁
可以看到,使用信号量,如果有一个进程持有了信号量,另一个进程就会进入睡眠等待。而很多情况并不需要进程进入等待睡眠,例如中断处理中不允许进入睡眠,或者一些情况下只是简单测试公共数据是否被其它进程占用,如果被占用,就重新测试直到可以使用,这里就只需要利用自旋锁(spinlock)。当然使用自旋锁时处理器被占用,所以自旋锁适用持有数据时间比较短的情况,而且绝对不能在持有锁时进入睡眠。
#include
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; 或者spin_lock_init(&my_lock); 申明/创建一个锁
spin_lock(spinlock_t *my_lock); 获得给定的锁,如果锁被占用,就自旋直到锁可用,当spin_lock返回时调用函数即持有了该锁,直到释放spin_unlock(spinlock_t *my_lock); 释放锁
2 关于阻塞和非阻塞
2.1 关于阻塞
对read调用存在一个问题,就是当设备无数据可读时,解决的方法有两种,一是不阻塞直接读失败跳出。 二就是阻塞读操作,进程进入睡眠,等待有数据时唤醒。
这里探讨一下阻塞型IO,处理睡眠和唤醒。
睡眠就是当一个进程需要等待一个事件时,应该暂时挂起,让出CPU,等事件到达后再唤醒执行。
处理睡眠的一种方法是把进程加入等待队列:
1)首先需要申明和初始化一个等待队列项.
#include
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
如果是申明一个静态全局的等待队列,可以不用上面两个定义,直接使用
DECLARE_WAIT_QUEUE_HEAD(my_queue); //静态申明将在编译时自动被初始化
2)使用已初始化的等待队列项
在需要加入内核等待队列时,调用 interruptible_sleep_on(&my_queue); 或者sleep_on(&my_queue)
在需要唤醒时,调用wake_up_interruptible(&my_queue); 或者wake_up(&my_queue)
3)interruptible_sleep_on()的缺陷
a.引起的竞态:
要了解interruptible_sleep_on()等这些sleep_on函数可能引起的竞态,就需要多interruptible_sleep_on()的实现有个认识。
等待队列其实是一个队列链表,链表中的数据是类型wait_queue_t. 简化了的interruptible_sleep_on()内部大概是这样:
#include
wait_queue_t wait; //;定义一个等待队列
init_wait_queue_entry(&wait, current); //;初始化
current->state = TASK_INTERRUPTILBE; //;设置为休眠状态,将要进入睡眠
add_wait_queue(&my_queue, &wait); //;把我们定义的等待队列项加入到这个等待队列中
schedule(); //;真正进入睡眠
remove_wait_queue(&my_queue, &wait); //;事件到达,schedule()返回
竞态就发生在current->state = TASK_INTERRUPTIBLE和schedule()之间,在一些情况下,当驱动准备进入睡眠,即已经设置了current->state时,可能刚好有数据到达,这个时候wake_up是不会唤醒这个还没有真正进入睡眠的进程,这样就可能造成该进程因为没有响应唤醒一直处于睡眠,这样就产生这个竞态,这个竞态也是很容易发生的。解决办法就是不使用interruptible_sleep_on(),而是直接使用它的内部实现。
例如:
#include
wait_queue_t wait; //;定义一个等待队列
init_wait_queue_entry(&wait, current); //;初始化
add_wait_queue(&my_queue, &wait); //;把我们定义的等待队列项加入到这个等待队列中
while(1){
current->state = TASK_INTERRUPTILBE; //;设置为休眠状态,将要进入睡眠
if (short_head != short_tail) break; //;测试是否有数据到达,如果有,跳出
schedule(); //;真正进入睡眠
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&my_queue, &wait); //;事件到达,schedule()返回
事实上,可以不用我们做这些复杂的事情,内核定义了一个宏
wait_event_interruptible(wq, condition); 或者wait_event(wq, condition) condition就是测试的条件
b.关于排它睡眠:
存在这样一种情况,几个进程都在等待同一个事件,当事件到达调用wake_up时,等待在这个事件上的所有进程都被唤醒,但是假如该事件只需要被一个进程处理,其它进程只是被唤醒后接着又进入睡眠,这样很多进程运行,导致上下文切换,造成系统变慢。解决办法是通过直接对等待队列的链表操作, 指定排它睡眠,内核把这个队列放在其它非排它睡眠之前,当事件到达时如果遇到排它睡眠的队列,唤醒它后即结束,其它睡眠下一次被唤醒处理。事实上sleep_on的一系列函数都是对等待队列的链表操作。链表中数据项是类型为wait_queue_t的数据。
直接操作链表设置排它睡眠方法大概如下:
#include
wait_queue_t wait; //;定义一个等待队列
init_wait_queue_entry(&wait, current); //;初始化
current->state = TASK_INTERRUPTALBE | TASK_EXCLUSIVE; //;设置为排它
add_wait_queue_exclusive(queue, &wait); //;把我们定义的等待队列项加入到这个等待队列中
schedule(); //;进入睡眠
remove_wait_queue(queue, &wait); //;事件到达,schedule()返回
c.在多个队列中睡眠
interrruptible_sleep_on等函数只能在一个队列中睡眠,如果真的需要做到在多个等待队列中睡眠,只能通过直接操作等待队列链表。
这个技巧找到相关资料再看看。
2.2. 非阻塞
打开,读和写操作在设备没有准备好或没有数据时立即返回。
在LINUX的打开设备时,可以传递一个参数O_NONBLOCK, 系统的open调用如果使用了这个参数,filp->f_flags的O_NONBLOCK标记将被设置,
驱动检查到这个标记,应该实现非阻塞的open, read, write方法.
#include
3. 关于可重入代码
简单介绍,因为驱动可以被多个进程调用,互不干扰,这样驱动必须是可重入的。
可重入最简单的理解就是所有变量都是局部变量。