竟态,阻塞

 产生的情况:

  1. SMP系统中任何时刻都可能出现
  2. 内核的代码是可抢占的 

设计驱动时尽可能减少资源共享

保护数据并发访问的一般方法

  1. 使用缓冲区并且避免共享变量
  2. 使用自旋锁实现互斥访问
  3. 使用原子地递增或递减锁变量

   //. . . . . . . 以上保持不变

static ssize_t ctest_write(struct file *file,char __user *buf,
size_t count,loff_t *offst)
{
static char ctestbuf[256]; //仅仅是把ctestbuf变成了static的缓冲区
int cnt;
memset(ctestbuf,0,256);
if(count<256)
cnt = count;
else
cnt = 255;
if(!copy_from_user(ctestbuf,buf,cnt)){
printk(“%s\n”,ctestbuf); //考虑一下,如果在这一行执行前发生进程间切换,如何?
return cnt;
}else{
return -1;
}
}
// . . . . . . 以下保持不变

-----------------------------------------------------------------------

大家仔细分析上面的代码,我们只是将char ctestbuf[256]改称了static char ctestbuf[256], 这样这个空间将不是存储在函数的栈上,而是存储在了静态全局区,
这样ctestbuf就变成了一个共享的内存资源。

当有多个进程对这个设备进行write操作的时候,比如P1向/dev/ctest设备写”hello,kernel”,P2向/dev/ctest写”hello,ctest”,请问最后的结果,printk是输出什么呢?



大家可以写下面的一段程序来测试:

-----------------------------------------------------------------------
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int main()
{
int fd = fopen(“/dev/ctest”,O_RDWR);
if(fd<0){
perror(“open /dev/ctest failed\n”)
exit(1);
}
if(fork()==0){ //子进程
int cnt = 0xfffff;
while(--cnt){
write(fd,”hello,kernel”,13);
}
close(fd)
}else{ //父进程
int cnt = 0xfffff;
while(--cnt){
write(fd,”hello,ctest”,12);
}
close(fd);
wait(NULL);
return 0;
}
}

-----------------------------------------------------------------------

这里的答案实际上是不确定的,如果内核不支持抢占的话,P1进程将是让printk输出”hello,kernel”, P2进程将是让printk()输出”hello,ctest”.

但是如果支持抢占的话,那么结果就很难确定了,考虑一下极端情况,如果P1进程刚刚完成了数据从user buffer 到ctestbuf的拷贝工作,此时,进行了一次进程调度(如代码中
指示的位置),调度的结果如果是让P2进程执行,并且完成了write的操作,那么ctestbuf里面的数据将会是”hello,ctest”.这样当控制权再次回到P1的时候,printk的输出结
果将是”hello,ctest”.同理P2进程也可能输出”hello,kernel”.这就是我们一直在讨论的竞争条件的情况。

 要使用一个信号量,首先应该#include <asm/semaphore.h> 或者是include <linux/semaphore.h>, 不同的体系结构使用的头文件的位置可能是不同的。我们使用struct semphore这个类型来表示信号量,有几种声明和初始化的方式,如下:

struct semaphore sem;

void sema_init(struct
semaphore *sem,int val)

操作步骤:

1. 定义一个信号量

2. 使用sema_init函数初始化它,val的值代表初始值

DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name)
这里定义的就是一个互斥体,这两个宏是由内核提供的,在内核里面常见这样的方式,不光定义了struct semaphore name;还给出了初始值,他们的初始值是多少?

void init_MUTEX(struct
semaphore *sem);

void init_MUTEX_LOCKED(structsemaphore *sem)
这里很明显是对于前面定义的sem进行初始化,而且是将之当成互斥体初始化,也就是说信号量的初始值非0既1。



在Linux的世界中,P操作对应的函数为down(),意思很明显,将信号量的值下降一点.在Linux里面,获得一个信号量的方法有三种,其意义差别很大:

void down(struct semaphore *)
int down_interruptible(struct semaphore *)

都执行P操作,将信号量的值减1,如果信号量的值小于0,它将进入blocked 状态,只是这里需要注意:第二个函数比第一个函数多了interruptible,很明显,如果使用第二个函数进入的blocked状态,那么将是一个可中断的blocked状态,那么down()进入的就是一个不可中断的状态。可中断状态与不可中断状态的区别在于对信号的处理方式不同。

如果进程是因为down()进入的blocked状态,那么将不能被杀死,因为杀死进程就是向其发送一个信号,比如kill -9 PID.

使用down_interruptible需要额外小心,如果操作被中断,该函数会返回非零值,而调用者不会拥有该信号量。对down_interruptible的正确使用需要始终检查返回值,并作出相应的响应,如果返回值为非0,通常立即返回-ERESTARTSYS,如:

if(down_interruptible(&sem)){
return –ERESTARTSYS;
}


int down_trylock(struct semaphore *)

此函数一样是执行P操作,只是这里带有try的意思,那也就是尝试获得,换句话说,它可能会获得信号量,也可能信号量此时不可用。当信号量不可用的时候,调用此函数的进程将不会等待,也就是说不会进入blocked 状态,而是恢复信号量的原有值,并继续执行。

既然此函数不会导致进程等待,而是继续执行,那么它的返回值就应该告知此函数是否获得了信号量。当返回为0时,表明获得信号量,所以在退出临界区的时候需要释放,否则将无须此操作。

if(!down_trylock(&sem)){
...
up(&sem);
}


当一个线程成功调用上述down的某个版本之后,就称为该线程“拥有”(或“拿到”、“获取”)了该信号量。这样,该线程就被赋予访问由该信号量保护的临界区的权利。

不管你用何种方式获得了semaphore,释放的操作都是一样的,这里释放的操作也就对应sempahore的V操作。
void up(struct semaphore *sem);

调用up之后,调用者不再拥有该信号量。

如读者所料,任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出现错误的情况下,经常需要特别小心;如果在拥有一个信号量时发生错误,必须在将错误状态返回给调用者之前释放该信号量。我们很容易犯忘记释放信号量的错误,而其结果(进程在某些无关位置处被挂起)很难复现和跟踪。信号量一般这样被使用,如下所示:

//定义信号量

DECLARE_MUTEX(sem);

down(&sem);//获取信号量,保护临界区



Critical section//临界区



up(&sem);//释放信号量

这样,我们可以使用semaphore让我们开篇的代码,不管是否是抢占式,还是非抢占式内核下都可安全的运行:

-----------------------------------------------------------------------

DECLARE_MUTEX(sem);

//. . . . . . . 以上保持不变

static ssize_t ctest_write(struct file *file,char __user *buf,
size_t count,loff_t *offst)
{
static char ctestbuf[256]; //仅仅是把ctestbuf变成了static的缓冲区
int cnt;
if(down_interruptible(&sem)){
return –ERESTARTSYS;
}。
memset(ctestbuf,0,256);
if(count<256)
cnt = count;
else
cnt = 255;
if(!copy_from_user(ctestbuf,buf,cnt)){
printk(“%s\n”,ctestbuf); //考虑一下,如果在这一行执行前发生进程间切换,如何?
up(&sem);
return cnt;
}else{
up(&sem);
return -1;
}
}
// . . . . . . 以下保持不变

-----------------------------------------------------------------------

驱动程序不能使用信号量, 可以使用自旋锁,不能等待时间太长,因为其他cpu被强制等待 可以看到,使用信号量,如果有一个进程持有了信号量,另一个进程就会进入睡眠等待。而很多情况并不需要进程进入等待睡眠,例如中断处理中不允许进入睡眠,或者一些情况下只是简单测试公共数据是否被其它进程占用,如果被占用,就重新测试直到可以使用,这里就只需要利用自旋锁(spinlock)。当然使用自旋锁时处理器被占用,所以自旋锁适用持有数据时间比较短的情况,而且绝对不能在持有锁时进入睡眠。

  #include <linux/spinlock.h>
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 <linux/sched.h>
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 <list.h>
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之前,在一些情况下,当驱动准备进入睡眠,即已经设置了current->state之前,可能刚好有数据到达,这个时候wake_up是不会唤醒这个还没有真正进入睡眠的进程,这样就可能造成该进程因为没有响应唤醒一直处于睡眠,这样就产生这个竞态,这个竞态也是很容易发生的。解决办法就是不使用interruptible_sleep_on(),而是直接使用它的内部实现。
例如:
#include <list.h>
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 <list.h>
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 <linux/fs.h>

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值