阻塞和非阻塞是设备访问的两种基本方式,阻塞和非阻塞驱动程序使用时,经常会用到等待队列。

阻塞和非阻塞

阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时,并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止。

对于同步调用来说,许多时候当前线程还是激活的,只是逻辑上当前函数没有返回而已。

阻塞的进程被唤醒的最大可能是通过中断,因为硬件资源获得的同时往往伴随着一个中断。

分别使用阻塞和非阻塞的方式读取串口的一个字符代码比较

/*阻塞方式*/
char buf;
fd = open("/dev/ttyS1", O_RDWR);
...
res = read(fd,&buf,1);     /* 当串口上有输入时才返回 */
if(res==1)
printf("%c\n", buf); /*非阻塞方式*/
char buf;
fd = open("/dev/ttyS1", O_RDWR| O_NONBLOCK);  //区别在这里
...
while(read(fd,&buf,1)!=1)
continue;             /* 串口上无输入也返回,所以要循环尝试读取串口 */
printf("%c\n", buf);

非阻塞方式即使没有数据也会返回,所以要读取串口应该使用循环,一次次的访问串口。

完整实例程序分析

等待队列(可以控制进程的休眠与唤醒)

等待队列机制使等待的进程暂时睡眠,当等待的信号到来时,便唤醒等待队列中进程继续执行。

等待队列的基本数据结构是一个双向链表,这个链表可以存储睡眠的进程。等待队列也与进程调度机制紧密结合,能够实现内核中异步事件通知机制。

等待队列在中断处理、进程同步、定时等场合有重要的用处。

等待队列的实现

等待队列通过等待队列头来管理

每个等待任务都被抽象成一个wait_queue,并且挂载到wait_queue_head上。

等待队列的使用

1. 定义和初始化等待队列头

wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
//或者使用宏进行初始化
DECLARE_WAIT_QUEUE_HEAD (name)

2. 定义等待队列

DECLARE_WAITQUEUE(name, tsk) //此宏用于定义并初始化一个名为name的等待队列

3. 添加和移除等待队列

void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); 
//add_wait_queue()用于将等待队列wait 添加到等待队列头q 指向的等待队列链表中
void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
//remove_wait_queue()用于将等待队列wait 从附属的等待队列头q 指向的等待队列链表中移除。

4. 等待事件

wait_event(queue, condition) //不可以被信号打断;queue,等待队列头(值传递);
                  //condition:休眠前后都要对该表达式求值,条件为假继续睡眠,为真被唤醒         
wait_event_interruptible(queue, condition) //可以被信号打断
wait_event_timeout(queue, condition, timeout)
//加上_timeout 后的宏意味着阻塞等待的超时时间,以jiffy 为单位,在第3 个参数的timeout到达时,不论condition 是否满足,均返回。
wait_event_interruptible_timeout(queue, condition, timeout)

等待第1 个参数queue 作为等待队列头的等待队列被唤醒,而且第2 个参数condition 必须满足,否则继续阻塞。

5. 唤醒等待队列

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue); void wake_up_nr(wait_queue_head_t *queue, int nr); //唤醒nr个独占等待进程,nr=0则唤醒所有的独占等待进程
void wake_up_interrupible_nr(wait_queue_head_t *queue, int nr); void wake_up_interruptible_sync(wait_queue_head_t *queue); //被唤醒后强制调度重新执行原休眠进程,前面几个实际上是没有让进程立即执行

唤醒以queue 作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。

wake_up()应该与wait_event()或wait_event_timeout()成对使用,而wake_up_interruptible()则应与wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。wake_up()可唤醒处于TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE 的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE 的进程。

进程的睡眠和唤醒过程中可能存在竞争,如:

A、B进程都等待在等待队列上,且在监视同一个唤醒条件flag,此时如果C进程调用唤醒函数wake_up_interruptible,A被唤醒,检查条件flag!=0成立,此时调度到B进程,B进程也检查到flag!=0成立,这样就会一个事件唤醒两个进程,产生竞态。

解决方法:使用原子操作

6. 在等待队列上睡眠(无条件休眠,老版本,建议不使用)[与wait_event的功能相同]

sleep_on(wait_queue_head_t *q );  //将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q 引导的等待队列被唤醒。
interruptible_sleep_on(wait_queue_head_t *q ); //将目前进程的状态置成TASK_INTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q 引导的等待队列被唤醒或者进程收到信号。

sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()应该与wake_up_interruptible()成对使用。

在内核中使用set_current_state()函数或_ _add_current_state()函数来实现目前进程状态的改变,直接采用current->state = TASK_UNINTERRUPTIBLE 类似的赋值语句也是可行的。通常而言,set_current_state()函数在任何环境下都可以使用,不会存在并发问题,但是效率要低于_ _add_current_state()。

因此,在许多设备驱动中,并不调用sleep_on()或interruptible_sleep_on(),而是亲自进行进程的状态改变和切换:

1 static ssize_t xxx_write(struct file *file, const char *buffer, size_t count,
2                 loff_t *ppos)
3 {
4   ...
5   DECLARE_WAITQUEUE(wait, current); /* 定义等待队列 */ //wait:等待队列名,
6   add_wait_queue(&xxx_wait, &wait); /* 添加等待队列 */
7
8   ret = count;
9   /* 等待设备缓冲区可写 */
10   do {
11     avail = device_writable(...);
12     if (avail < 0)
13       _ _set_current_state(TASK_INTERRUPTIBLE);/* 改变进程状态 */
14
15     if (avail < 0) {
16       if (file->f_flags &O_NONBLOCK) {/* 非阻塞 */
17         if (!ret)
18           ret = - EAGAIN;
19         goto out;
20       }
21       schedule(); /* 调度其他进程执行
22       if (signal_pending(current)) {/* 如果是因为信号唤醒 */
23         if (!ret)
24           ret = - ERESTARTSYS;
25         goto out;
26       }
27     }
28   }while (avail < 0);
29
30   /* 写设备缓冲区 */
31   device_write(...)
32   out:
33   remove_wait_queue(&xxx_wait, &wait);/* 将等待队列移出等待队列头 */
34   set_current_state(TASK_RUNNING);/*设置进程状态为TASK_RUNNING*/
35   return ret;
36 }

几个要点如下:
① 如果是非阻塞访问(O_NONBLOCK 被设置),设备忙时,直接返回“-EAGAIN”。
② 对于阻塞访问,会进行状态切换并显式通过“schedule()”调度其他进程执行;
③ 醒来的时候要注意,由于调度出去的时候,进程状态是TASK_INTERRUPTIBLE,即浅度睡眠,因此唤醒它的有可能是信号,因此,我们首先通过“signal_pending(current)”了解是不是信号唤醒的,如果是,立即返回“- ERESTARTSYS”。

设置进程休眠的内部细节

1. 分配并初始化一个wait_queue_t结构

  包括休眠进程的信息,以及期望被唤醒的相关细节

2. 设置进程的状态,将其标记为休眠状态

  TASK_INTERRUPTIBLE (wait_event()后的状态)

  TASK_UNINTERRUPTIBLE

void set_current_state(int new_state); //手动设置进程状态
current->state=TASK_INTERRUPTIBLE; //老版本内核设置方式

3. 让出处理器

if(!condition)
schedule(); //执行调度。让出处理器

阻塞方式

在阻塞驱动程序中,read实现方式:如果进程调用read,但是设备没有数据或数据不足,进程阻塞。当新数据到达,唤醒被阻塞进程。

write实现类似。

阻塞IO实例

独占等待

与普通休眠的不同

  等待队列入口设置了 WQ_FLAG_EXCLUSIVE标志时,则会被添加到等待队列的尾部。而没有这个标志的入口会被添加到等待队列的头部。

  在某个等待队列上调用wake_up时,他会唤醒第一个具有WQ_FLAG_EXCLUSIVE标志的进程之后停止唤醒其他独占进程。

使进程进入独占等待函数(有自己特定的函数)

void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);

注意:使用wait_event的变种函数都无法使用独占等待。 

支持阻塞操作的globalfifo设备驱动

使用等待队列实现同步机制

To be continue...

Linux驱动设计——阻塞和同步的更多相关文章

  1. Linux驱动设计—— 中断与时钟

    中断和时钟技术可以提升驱动程序的效率 中断 中断在Linux中的实现 通常情况下,一个驱动程序只需要申请中断,并添加中断处理函数就可以了,中断的到达和中断函数的调用都是内核实现框架完成的.所以程序员只 ...

  2. Linux驱动之异步OR同步,阻塞OR非阻塞概念介绍

    链接:https://www.zhihu.com/question/19732473/answer/20851256 1.同步与异步同步和异步关注的是消息通信机制 (synchronous commu ...

  3. Linux驱动设计——并发与竞态控制

    并发的概念:多个执行单元同时.并行被执行. 共享资源:硬件资源(IO/外设等),软件上的全局变量.静态变量等. 四种并发控制机制(对共享资源互斥的访问):原子操作.自旋锁(spinlock).信号量( ...

  4. Linux驱动设计—— 内核模块(一)

    Linux内核理论基础 组成Linux内核的5个子系统:进程调度(SCHED)/内存管理(MM)/虚拟文件系统(VFS)/网络接口(NET)/进程间通信(IPC). 进程调度(SCHED) 在设备驱动 ...

  5. Linux驱动设计——字符设备驱动(一)

    Linux字符设别驱动结构 cdev结构体 struct cdev { struct kobject kobj; struct module *owner; const struct file_ope ...

  6. Linux驱动设计——内存与IO访问

    名词解释 内存空间与IO空间 内存空间是计算机系统里面非系统内存区域的地址空间,现在的通用X86体系提供32位地址,寻址4G字节的内存空间,但一般的计算机只安装256M字节或者更少的内存,剩下的高位内 ...

  7. Linux驱动设计—— 中断与时钟@request_irq参数详解

    request_irq函数定义 /*include <linux/interrupt.h>*/ int request_irq(unsigned int irq, irq_handler_ ...

  8. Linux驱动设计——字符杂项设备

    杂项设备 linux里面的misc杂项设备是主设备号为10的驱动设备,misc设备其实也就是特殊的字符设备,可自动生成设备节点. 定义头文件<linux/miscdevice.h>   杂 ...

  9. Linux驱动设计—— 驱动调试技术

    参考博客与书籍: <Linux设备驱动开发详解> <Linux设备驱动程序> http://blog.chinaunix.net/uid-24219701-id-2884942 ...

随机推荐

  1. 记录一些容易忘记的属性 -- NSTimer

    使定时器停止的方法: 1. //将定时器的启动时间设置为很久以后的将来,到这个时间,定时器才会开始工作            [_timer setFireDate:[NSDate distantFu ...

  2. SharePoint 2013 Nintex Workflow 工作流帮助(十三)

    博客地址 http://blog.csdn.net/foxdave 工作流动作 35. Delegate Workflow Task(User interaction分组) 该操作将委托未处理的工作流 ...

  3. c#图像处理入门(-bitmap类和图像像素值获取方法) 转

    一.Bitmap类 Bitmap对象封装了GDI+中的一个位图,此位图由图形图像及其属性的像素数据组成.因此Bitmap是用于处理由像素数据定义的图像的对象.该类的主要方法和属性如下: 1. GetP ...

  4. Mainstoryboard

    页面间进行跳转 [self performSegueWithIdentifier:@"signInSuccess" sender:self] signSuccess是miansto ...

  5. druid连接池配置

    本人使用的是springMVC框架,以下是我的配置: step1,配置数据源(applicationContext-resource.xml中): <bean id="cc_ds&qu ...

  6. [转]dev C++编写windows程序遇到问题

    1.工具-编译选项-编译器-在连接器命令行加入以下命令: -mwindows 2.出现错误:undefined reference to `PlaySoundA@12' 解决办法:工具-编译选项-编译 ...

  7. IOS 作业项目(3) 霓虹灯效果

    先上效果图 #import "CHViewController.h"@interface CHViewController (){    int i;    int j;}@pro ...

  8. Android pix转换为sp

    /** * 把pix值转换为sp * * @return */ public static float px2sp(Context context, float pixValue) { final f ...

  9. mysql主从同步mysql slave_io_running:no的解决方案

    在主从同步的时候出现slave_io_running:no 问题,于是查看mysqld.log日志,发现时1042错误 解决方案: 编辑/etc/my.cnf,在:[mysqld]内添加一行:skip ...

  10. String.Format 全汇总

    C#格式化数值结果表 字符 说明 示例 输出 C 货币 string.Format("{0:C3}", 2) $2.000 D 十进制 string.Format("{0 ...