嵌入式linux驱动重入问题,LINUX设备驱动 - 竞态、阻塞、可重入代码

本文详细介绍了Linux设备驱动中如何处理竞态条件,包括使用信号量和自旋锁。同时,讨论了阻塞与非阻塞IO,特别是阻塞IO中的等待队列和排它睡眠。最后,提到了驱动代码的可重入性对于多进程并发访问的重要性。
摘要由CSDN通过智能技术生成

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. 关于可重入代码

简单介绍,因为驱动可以被多个进程调用,互不干扰,这样驱动必须是可重入的。

可重入最简单的理解就是所有变量都是局部变量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值