等待队列是内核中实现进程调度的一个十分重要的数据结构,其任务是维护一个链表,链表中每一个节点都是一个PCB(进程控制块),内核会将PCB挂在等待队列中的所有进程都调度为睡眠状态,直到某个唤醒的条件发生。应用层的阻塞IO与非阻塞IO的使用我已经在Linux I/O多路复用一文中讨论过了,本文主要讨论驱动中怎么实现对设备IO的阻塞与非阻塞读写。显然,实现这种与阻塞相关的机制要用到等待队列机制。本文的内核源码使用的是3.14.0版本

设备阻塞IO的实现

当我们读写设备文件的IO时,最终会回调驱动中相应的接口,而这些接口也会出现在读写设备进程的进程(内核)空间中,如果条件不满足,接口函数使进程进入睡眠状态,即使读写设备的用户进程进入了睡眠,也就是我们常说的发生了阻塞。In a word,读写设备文件阻塞的本质是驱动在驱动中实现对设备文件的阻塞,其读写的流程可概括如下:

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

//定义等待队列头
wait_queue_head_t waitq_h;
//初始化,等待队列头
init_waitqueue_head(wait_queue_head_t *q);
//或
//定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(waitq_name);

上面的几条选择中,最后一种会直接定义并初始化一个等待头,但是如果在模块内使用全局变量传参,用着并不方便,具体用哪种看需求。

我们可以追一下源码,看一下上面这几行都干了什么:

//include/linux/wait.h
35 struct __wait_queue_head {
36 spinlock_t lock;
37 struct list_head task_list;
38 };
39 typedef struct __wait_queue_head wait_queue_head_t;

wait_queue_head_t

--36-->这个队列用的自旋锁

--27-->将整个队列"串"在一起的纽带

然后我们看一下初始化的宏:

 55 #define __WAIT_QUEUE_HEAD_INITIALIZER(name) {                           \
56 .lock = __SPIN_LOCK_UNLOCKED(name.lock), \
57 .task_list = { &(name).task_list, &(name).task_list } }
58
59 #define DECLARE_WAIT_QUEUE_HEAD(name) \
60 wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

DECLARE_WAIT_QUEUE_HEAD()

--60-->根据传入的字符串name,创建一个名为name的等待队列头

--57-->初始化上述task_list域,竟然没有用内核标准的初始化宏,无语。。。

2. 将本进程添加到等待队列

为等待队列添加事件,即进程进入睡眠状态直到condition为真才返回。_interruptible的版本版本表示睡眠可中断,_timeout版本表示超时版本,超时就会返回,这种命名规范在内核API中随处可见。

void wait_event(wait_queue_head_t *waitq_h,int condition);
void wait_event_interruptible(wait_queue_head_t *waitq_h,int condition);
void wait_event_timeout(wait_queue_head_t *waitq_h,int condition);
void wait_event_interruptible_timeout(wait_queue_head_t *waitq_h,int condition);

这可是等待队列的核心,我们来看一下

wait_event

   └── __wait_event

            └── ___wait_event

            ├── abort_exclusive_wait

            ├── finish_wait

            ├── prepare_to_wait_event

            └── ___wait_is_interruptible

244 #define wait_event(wq, condition)                                       \
245 do { \
246 if (condition) \
247 break; \
248 __wait_event(wq, condition); \
249 } while (0)

wait_event

--246-->如果condition为真,立即返回

--248-->否则调用__wait_event

194 #define ___wait_event(wq, condition, state, exclusive, ret, cmd)        \
195 ({ \
206 for (;;) { \
207 long __int = prepare_to_wait_event(&wq, &__wait, state);\
208 \
209 if (condition) \
210 break; \
212 if (___wait_is_interruptible(state) && __int) { \
213 __ret = __int; \
214 if (exclusive) { \
215 abort_exclusive_wait(&wq, &__wait, \
216 state, NULL); \
217 goto __out; \
218 } \
219 break; \
220 } \
222 cmd; \
223 } \
224 finish_wait(&wq, &__wait); \
225 __out: __ret; \
226 })

___wait_event

--206-->死循环

--207-->进程进入睡眠

--209-->进程被wake_up唤醒,再次检查条件,如果条件为真,跳出循环,执行finish_wait(),wait_event()返回;如果醒来发现条件仍然不满足, 则执行下一个循环进入睡眠, 周而复始...

--212-->如果进程睡眠的方式是interruptible的,那么当中断来的时候也会abort_exclusive_wait被唤醒

--222-->如果上面两条都不满足,就会回调传入的schedule(),即继续睡眠

3. 无条件睡眠

wait_event是睡在一个条件上, 内核还提供了下面的API进行无条件的睡眠, 只要被wake_up了就会醒来

//在等待队列上睡眠
sleep_on(wait_queue_head_t *wqueue_h);
sleep_on_interruptible(wait_queue_head_t *wqueue_h);

4. 唤醒

条件不满足, wait_event就不会返回, 当前调用该接口的进程就会进入睡眠, 为了唤醒这个进程, 通常在另外一个接口或中断处理程序中满足条件并调用wake_up, 另外一个进程调用这个接口的时候,就会唤醒所有睡在这个条件上(这个等待队列头)的进程, 这个这样其实也实现了两个进程之间的"通信"

//唤醒等待的进程
void wake_up(wait_queue_t *wqueue);
void wake_up_interruptible(wait_queue_t *wqueue);

模板

struct wait_queue_head_t xj_waitq_h;
static ssize_t demo_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
if(!condition) //条件可以在中断处理函数或另外的接口中置位
wait_event_interruptible(&xj_waitq_h,condition);
}
static ssize_t demo_write(struct file *, const char __user *, size_t, loff_t *)
{
condition = 1;
wake_up(&xj_waitq_h);
}
static file_operations fops = {
.read = demo_read,
.write= demo_write,
};
static __init demo_init(void)
{
init_waitqueue_head(&xj_waitq_h);
}

IO多路复用的实现

对于普通的非阻塞IO,我们只需要在驱动中注册的read/write接口时不使用阻塞机制即可,这里我要讨论的是IO多路复用,即当驱动中的read/write并没有实现阻塞机制的时候,我们如何利用内核机制来在驱动中实现对IO多路复用的支持。下面这个就是我们要用的API

int poll(struct file *filep, poll_table *wait);
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)

当应用层调用select/poll/epoll机制的时候,内核其实会遍历回调相关文件的驱动中的poll接口,通过每一个驱动的poll接口的返回值,来判断该文件IO是否有相应的事件发生,我们知道,这三种IO多路复用的机制的核心区别在于内核中管理监视文件的方式,分别是数组链表,但对于每一个驱动,回调的接口都是poll。

模板

struct wait_queue_head_t waitq_h;
static unsigned int demo_poll(struct file *filp, struct poll_table_struct *pts)
{
unsigned int mask = 0;
poll_wait(filp, &wwaitq_h, pts);
if(counter){
mask = (POLLIN | POLLRDNORM);
}
return mask;
} static struct file_operations fops = {
.owner = THIS_MODULE,
.poll = demo_poll,
};
static __init demo_init(void)
{
init_waitqueue_head(&xj_waitq_h);
}

Linux驱动技术(五) _设备阻塞/非阻塞读写的更多相关文章

  1. Linux驱动技术(五) _设备阻塞/非阻塞读写【转】

    转自:http://www.cnblogs.com/xiaojiang1025/p/6377925.html 等待队列是内核中实现进程调度的一个十分重要的数据结构,其任务是维护一个链表,链表中每一个节 ...

  2. Linux驱动技术(八) _并发控制技术

    为了实现对临界资源的有效管理,应用层的程序有原子变量,条件变量,信号量来控制并发,同样的问题也存在与驱动开发中,比如一个驱动同时被多个应用层程序调用,此时驱动中的全局变量会同时属于多个应用层进程的进程 ...

  3. 迅为4412开发板Linux驱动教程——总线_设备_驱动注册流程详解

    本文转自:http://www.topeetboard.com 视频下载地址: 驱动注册:http://pan.baidu.com/s/1i34HcDB 设备注册:http://pan.baidu.c ...

  4. 迅为4412开发板Linux驱动教程——总线_设备_驱动注冊流程具体解释

    视频下载地址: 驱动注冊:http://pan.baidu.com/s/1i34HcDB 设备注冊:http://pan.baidu.com/s/1kTlGkcR 总线_设备_驱动注冊流程具体解释 • ...

  5. Linux驱动技术(二) _访问I/O内存

    ARM是对内存空间和IO空间统一编址的,所以,通过读写SFR来控制硬件也就变成了通过读写相应的SFR地址来控制硬件.这部分地址也被称为I/O内存.x86中对I/O地址和内存地址是分开编址的,这样的IO ...

  6. Linux驱动技术(一) _内存申请

    先上基础,下图是Linux的内存映射模型,其中体现了Linux内存映射的几个特点: 每一个进程都有自己的进程空间,进程空间的0-3G是用户空间,3G-4G是内核空间 每个进程的用户空间不在同一个物理内 ...

  7. Linux驱动技术(七) _内核定时器与延迟工作

    内核定时器 软件上的定时器最终要依靠硬件时钟来实现,简单的说,内核会在时钟中断发生后检测各个注册到内核的定时器是否到期,如果到期,就回调相应的注册函数,将其作为中断底半部来执行.实际上,时钟中断处理程 ...

  8. Linux驱动技术(四) _异步通知技术

    异步通知的全称是"信号驱动的异步IO",通过"信号"的方式,放期望获取的资源可用时,驱动会主动通知指定的应用程序,和应用层的"信号"相对应, ...

  9. Linux驱动技术(六) _内核中断

    在硬件上,中断源可以通过中断控制器向CPU提交中断,进而引发中断处理程序的执行,不过这种硬件中断体系每一种CPU都不一样,而Linux作为操作系统,需要同时支持这些中断体系,如此一来,Linux中就提 ...

随机推荐

  1. Linux 软件包安装管理

    转自:http://www.cnblogs.com/Quains/archive/2012/01/03/2311049.html 本文主要是记录下RedHat系列的软件包管理. 内容分为以下三个部分: ...

  2. C语言实现求字符串子集问题

    这个问题在于实现Apriori算法的时候需要求各个频繁集的关联规则,而这时需要在求得最大的频繁集中求各个频繁集的真子集.然后在实现这一步的时候被卡主了,第一反应是用递归完成,但是面对数据挖掘中庞大的项 ...

  3. Spring自学教程-jabc编程详解、RowMapper使用(三)

    一.JDBC 1.JDBC的编程特点 静态代码+动态变量 = jdbc编程.在spring中动态变量可以用注入的形式给予.这样的编程方式适合包装成模板.静态代码构成了模板,而动态变量则是需要传入的参数 ...

  4. CodeForces 631B Print Check

    对于每一个格子,看是行最后画还是列最后画.预处理一下就可以了. #include<stdio.h> #include<string.h> int n,m,k; +]; +]; ...

  5. 改变导航栏title字体的大小和颜色

    方法一:自定义视图的方法 就是在导航向上添加一个titleView,可以使用一个label,再设置label的背景颜色透明,字体什么的设置就很简单了. //自定义标题视图 UILabel *title ...

  6. tp框架 使用ajax

    <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8&quo ...

  7. IOS第三方数据库--FMDB 分类: ios技术 2015-03-01 09:38 57人阅读 评论(0) 收藏

    iOS中原生的SQLite API在使用上相当不友好,在使用时,非常不便.于是,就出现了一系列将SQLite API进行封装的库,例如FMDB.PlausibleDatabase.sqlitepers ...

  8. STL基础知识

    一,STL的组成 1.什么是STL STL(Standard Template Library)标准模板库的简称,是由惠普开发的一系列软件的总称,STL现在是C++的一部分,已经被构建于编译系统之内, ...

  9. ubuntu 16.04 php 安装curl方法

    先查看自己的php是否已经安装了curl.方法如下:1.在web服务器目录( Ubuntu下的通常为 /var/www )新建test.php文件2.编辑文件,键入下面一行代码:<?php ph ...

  10. CentOS标准目录结构

    原博:http://www.centoscn.com/CentOS/2014/0424/2861.html/ 最高层root --- 启动Linux时使用的一些核心文件.如操作系统内核.引导程序Gru ...