imx6ull-驱动开发篇27——Linux阻塞和非阻塞 IO(上)
目录
阻塞和非阻塞
等待队列
等待队列头
等待队列项
将队列项添加/移除等待队列头
等待唤醒
等待事件
阻塞和非阻塞
IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。
当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。
对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
阻塞式 IO 如图:
应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。
应用程序可以使用如下所示示例代码来实现阻塞访问:
int fd;
int data = 0;// 以阻塞方式打开设备文件(默认行为)
fd = open("/dev/xxx_dev", O_RDWR); // 尝试从设备读取数据(阻塞式读取)
int ret = read(fd, &data, sizeof(data));
对于设备驱动文件的默认读取方式就是阻塞式的。
非阻塞 IO 如图:
应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。
如果应用程序要采用非阻塞的方式来访问驱动设备文件,可以使用如下所示代码:
int fd;
int data = 0;// 以非阻塞方式打开设备文件
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); // 尝试从设备读取数据(非阻塞式读取)
int ret = read(fd, &data, sizeof(data));
使用 open 函数打开“/dev/xxx_dev”设备文件的时候添加了参数“O_NONBLOCK”,表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了。
等待队列
等待队列头
Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。
如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t 表示。
wait_queue_head_t 结构体定义在文件 include/linux/wait.h 中,结构体内容如下所示:
// 等待队列头结构体(内核等待机制核心数据结构)
struct __wait_queue_head {spinlock_t lock; // 自旋锁,保护任务列表的并发访问struct list_head task_list; // 等待任务链表头(双向链表)
};// 类型重定义,简化使用
typedef struct __wait_queue_head wait_queue_head_t;
定义好等待队列头以后需要初始化, 使用 init_waitqueue_head 函数初始化等待队列头。
init_waitqueue_head函数原型如下:
void init_waitqueue_head(wait_queue_head_t *q)
- 参数 q 就是要初始化的等待队列头。
也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化。
示例代码如下:
/* 动态初始化(非静态变量) */
wait_queue_head_t *q = kmalloc(sizeof(*q), GFP_KERNEL);
if (q) init_waitqueue_head(q);/* 快速初始化宏 */DECLARE_WAIT_QUEUE_HEAD(my_queue); // 静态声明并初始化
等待队列项
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。
结构体 wait_queue_t 表示等待队列项,结构体内容如下:
// 等待队列条目结构(表示一个等待任务)
struct __wait_queue {unsigned int flags; // 状态标志(如WQ_FLAG_EXCLUSIVE)void *private; // 通常指向关联的进程描述符(task_struct)wait_queue_func_t func; // 唤醒回调函数struct list_head task_list; // 链表节点,用于链接到等待队列头
};// 类型重定义,简化使用
typedef struct __wait_queue wait_queue_t;
使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
DECLARE_WAITQUEUE(name, tsk)
- name 就是等待队列项的名字,
- tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current 。
在 Linux 内 核 中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏DECLARE_WAITQUEUE 就是,给当前正在运行的进程创建并初始化了一个等待队列项。
将队列项添加/移除等待队列头
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。
当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可。
等待队列项添加 API 函数如下:
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
- q: 等待队列项要加入的等待队列头。
- wait:要加入的等待队列项。
等待队列项移除 API 函数如下:
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
- q: 要删除的等待队列项所处的等待队列头。
- wait:要删除的等待队列项。
等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:
/*** wake_up - 唤醒等待队列中的所有可唤醒进程* @q: 等待队列头指针** 唤醒所有状态为TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE的进程,* 但会跳过设置了WQ_FLAG_EXCLUSIVE标志的独占进程(只唤醒一个独占进程)。*/
void wake_up(wait_queue_head_t *q)
{__wake_up(q, TASK_NORMAL, 1, NULL);
}/*** wake_up_interruptible - 只唤醒可中断睡眠的进程* @q: 等待队列头指针** 仅唤醒状态为TASK_INTERRUPTIBLE的进程,* 忽略TASK_UNINTERRUPTIBLE状态的进程。*/
void wake_up_interruptible(wait_queue_head_t *q)
{__wake_up(q, TASK_INTERRUPTIBLE, 1, NULL);
}
参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。
wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,
而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。
等待事件
除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程。
和等待事件有关的 API 函数如表:
示例代码如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/delay.h>MODULE_LICENSE("GPL");// 定义等待队列头
static wait_queue_head_t my_wait_queue;
// 定义共享资源标志
static int resource_ready = 0;// 线程函数:模拟资源生产者
static int producer_thread(void *data)
{msleep(2000); // 模拟2秒的资源准备时间resource_ready = 1;// 使用不同唤醒API演示printk(KERN_INFO "Producer: Resource ready, waking waiters\n");wake_up(&my_wait_queue); // 唤醒所有等待者(包括不可中断)// wake_up_interruptible(&my_wait_queue); // 只唤醒可中断等待者// wake_up_all(&my_wait_queue); // 明确唤醒所有等待者return 0;
}static int __init mymodule_init(void)
{struct task_struct *producer;printk(KERN_INFO "Waitqueue API Demo Module init\n");// 初始化等待队列init_waitqueue_head(&my_wait_queue);// 创建生产者线程producer = kthread_run(producer_thread, NULL, "resource_producer");/**************** 等待API演示 ****************/// 方法1:基础等待(不可中断)wait_event(my_wait_queue, resource_ready);printk(KERN_INFO "wait_event() returned\n");// 重置标志用于下一次演示resource_ready = 0;// 方法2:带超时的可中断等待int ret = wait_event_interruptible_timeout(my_wait_queue, resource_ready, HZ * 3); // 3秒超时if (ret == 0)printk(KERN_INFO "Timeout occurred!\n");else if (ret == -ERESTARTSYS)printk(KERN_INFO "Interrupted by signal!\n");elseprintk(KERN_INFO "Resource ready with %d jiffies left\n", ret);return 0;
}static void __exit mymodule_exit(void)
{// 确保所有等待者被唤醒wake_up_all(&my_wait_queue);printk(KERN_INFO "Module exit\n");
}module_init(mymodule_init);
module_exit(mymodele_exit);