注:本文是《Linux设备驱动开发详解:基于最新的Linux 4.0内核 by 宋宝华 》一书学习的笔记,大部分内容为书籍中的内容。

书籍可直接在微信读书中查看:Linux设备驱动开发详解:基于最新的Linux4.0内核-宋宝华-微信读书 (qq.com)

字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标等。对于用户而言,使用文件系统的操作接口open()、close()、read()、write()等进行访问。

阻塞与非阻塞I/O

这里的I/O指的是input/output,是应用程序对驱动设备的输入/输出操作。

1 简介

阻塞操作:执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起进程进入睡眠状态,被从调度队列中移走,直到等待的条件被满足。

非阻塞操作:进程在不能进行设备操作时,不挂起,那么轮询,要么放弃,直到可以操作为止。

阻塞与非阻塞的用户态访问的区别:

2 实现代码

以阻塞与非阻塞方式读取串口一个字符的代码为例进行说明。

阻塞的方式在打开文件的时候没有O_NONBLOCK标记。

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);

除了在打开文件时可以指定阻塞还是非阻塞方式外,在打开文件之后,可以通过ioctl()和fcntl()来改变读写的方式,比如从阻塞变为非阻塞或者从非阻塞变为阻塞。

3 等待队列(重点)

阻塞访问最大的好处是当设备不可操作时进程可以进入睡眠,让出CPU。

在Linux驱动程序中,可以使用等待队列(Wait Queue)来实现阻塞进程的唤醒,一般在中断中完成唤醒。

等待队列作为Linux内核中的一个基本单位,以队列为数据结构,与调度机制紧密结合,可以用来同步对系统资源的访问。

3.1 相关操作

定义在:include/linux/wait.h

3.1.1 等待队列头

1)定义等待队列头部:

struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t; wait_queue_head_t my_queue;

2)初始化等待队列头部

void init_waitqueue_head(&my_queue);

也可以使用宏DECLARE_WAIT_QUEUE_HEAD()定义并初始化等待队列头部:

#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

3.1.2 等待队列项

等待队列头是一个等待队列的头部,每个访问设备的进程是一个队列项,当设备不可用时需要将这些进程对应的等待队列项添加到等待队列里里面。

wait_queue_t结构体表示等待队列项:

struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
}; typedef struct __wait_queue wait_queue_t;

1)定义等待队列元素

定义并初始化一个名为name的等待队列元素:

/* 参数name:等待队列项的名字
* 参数tsk:表示等待队列项属于哪个任务,一般设置为current(表示当前进程)
*/
#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

3.1.3 添加/移除等待队列

当设备不可访问时需要将进程添加到对应的等待队列项添加道等待队列头中,当设备可以访问后再将进程对应的等待队列项从等待队列中移除。

/* 参数q:等待队列项要加入的等待队列头
* 参数wait:要加入的等待队列项
*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); /* 参数q:要删除的等待队列项所在的等待队列头
* 参数wait:要删除的等待队列项
*/
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

3.1.4 等待事件

除了主动唤醒,也可以设置等待队列等待某个事件,当事件满足就自动唤醒等待队列中的进程。相关的函数如下:

函数 说明
wait_event(wq, condition) 等待以wq为等待队列头的等待队列被唤醒,前提是condition条件满足(为真),否则一直阻塞。函数会将进程设置为TASK_UNINTERRUPTIBLE。
wait_event_timeout(wq, condition, timeout) 和wait_event类似,但可以添加超时时间,以jiffies为单位。返回值为0表示超时时间到而且condition为假;如果返回值为1表示condition为真,条件满足。
wait_event_interruptible(wq, condition) 和wait_event类似,但设置进程状态为TASK_INTERRUPTIBLE,可以被信号打断。
wait_event_interruptible_timeout(wq, condition, timeout) 和wait_event_timeout类似,但设置进程状态为TASK_INTERRUPTIBLE,可以被信号打断。

3.1.5 唤醒队列

唤醒以queue作为等待队列头部的队列中的所有的线程。

void wake_up(wait_queue_head_t *queue);

void wake_up_interruptible(wait_queue_head_t *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的进程。

3.1.6 在等待队列上睡眠

sleep_on(wait_queue_head_t *q);
interruptible_sleep_on(wait_queue_head_t *q);

sleep_on()函数的作用就是将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,之后把它挂到等待队列头部q指向的双向链表,直到资源可获得,q队列指向链接的进程被唤醒。

interruptible_sleep_on()与sleep_on()函数类似,其作用是将目前进程的状态置成TASK_INTERRUPTIBLE,并定义一个等待队列元素,之后把它附属到q指向的队列,直到资源可获得(q指引的等待队列被唤醒)或者进程收到信号。

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

3.2 使用模板

使用等待队列的模板,判断设备是否可写,如果不可写且为阻塞I/O,则进程睡眠并挂起等待队列。

static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, loff_t *ppos)
{
...
DECLARE_WAITQUEUE(wait, current); /* 定义等待队列元素 */
add_wait_queue(&xxx_wait, &wait); /* 添加元素到等待队列 */ /* 等待设备缓冲区可写 */
do {
avail = device_writable(...);
if (avail < 0) {
if (file->f_flags & O_NONBLOCK) { /* 非阻塞 */
ret = -EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态 */
schedule(); /* 调度其他进程执行 */
if (signal_pending(current)) { /* 如果是因为信号唤醒 */
ret = -ERESTARTSYS;
goto out;
}
}
} while (avail < 0); /* 写设备缓冲区 */
device_write(...);
out:
remove_wait_queue(&xxx_wait, &wait); /* 将元素移出xxx_wait指引的队列 */
set_current_state(TASK_RUNNING); /* 设置进程状态为TASK_RUNNING */
return ret;
}

这段代码对理解进程状态切换很重要,代码中有如下几个要点:

1)如果是非阻塞访问(O_NONBLOCK被设备),设备忙时,直接返回-EAGAIN。

2)如果是阻塞访问,调用__set_current_state(TASK_INTERRUPTIBLE)进行进程状态切换并通过schedule()调度其它进程执行。

3)醒来的时候,由于调度出去的时候进程状态是TASK_INTERRUPTIBLE(浅度睡眠),所以唤醒的可能是信号,所以先通过signal_pending判断是否为信号唤醒,如果是,立即返回-ERESTARTSYS。

DECLARE_WAITQUEUE和add_wait_queue这两个动作的效果如下图所示:

在wait_queue_head_t指向的链表上,新定义的wait_queue元素被插入,这个新元素绑定了一个task_struct数据结构(当前做xxx_write的current,也是DECLARE_WAITQUEUE使用"current"作为参数的原因)。

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

把globalmem的全局内存当作一个FIFO,只有FIFO中有数据的时候(有进程把数据写到FIFO而且没有读进程读空),读进程才把数据读出,而且读取数据后从globalmem的全局内存中被拿掉;只有当FIFO不是满时(有一些空间未被写或写满后读进程从这个FIFO中读出了数据),写进程才能往这个FIFO中写入数据。

在globalfifo中,读FIFO将唤醒写FIFO的进程(如果之前的FIFO正好是满的),而写FIFO也将唤醒读FIFO的进程(如果之前的FIFO是空的)。

完整代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h> /* 直接使用立即数当作命令不合理,暂定 */
#define MEM_CLEAR 0x1
#define GLOBALFIFO_MAJOR 230
#define GLOBALFIFO_SIZE 0x1000 static int globalfifo_major = GLOBALFIFO_MAJOR;
module_param(globalfifo_major, int, S_IRUGO); /* 设备结构体 */
struct globalfifo_dev {
struct cdev cdev;
unsigned int current_len; /* 当前FIFO中有效数据的长度 */
unsigned char mem[GLOBALFIFO_SIZE];
struct mutex mutex;
wait_queue_head_t r_wait;
wait_queue_head_t w_wait;
}; struct globalfifo_dev *globalfifo_devp; static int globalfifo_open(struct inode *inode, struct file *filp)
{
/* 使用文件的私有数据作为获取globalfifo_dev的实例指针 */
filp->private_data = globalfifo_devp;
return 0;
} static int globalfifo_release(struct inode *inode, struct file *filp)
{
return 0;
} /**
* 设备ioctl函数
* @param[in] filp:文件结构体指针
* @param[in] cmd: 命令,当前仅支持MEM_CLEAR
* @param[in] arg: 命令参数
* @return 若成功返回0,若出错返回错误码
*/
static long globalfifo_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct globalfifo_dev *dev = filp->private_data; switch (cmd) {
case MEM_CLEAR:
mutex_lock(&dev->mutex);
dev->current_len = 0;
memset(dev->mem, 0, GLOBALFIFO_SIZE);
mutex_unlock(&dev->mutex);
printk(KERN_INFO "globalfifo is set to zero\n");
break; default:
return -EINVAL;
}
return 0;
} /**
* 读设备
* @param[in] filp:文件结构体指针
* @param[out] buf: 用户空间内存地址,不能在内核中直接读写
* @param[in] size: 读取的字节数
* @param[in/out] ppos: 读的位置相当于文件头的偏移
* @return 若成功返回实际读的字节数,若出错返回错误码
*/
static ssize_t globalfifo_read(struct file *filp,
char __user *buf, size_t size, loff_t *ppos)
{
int ret = 0;
unsigned long count = size;
struct globalfifo_dev *dev = filp->private_data; DECLARE_WAITQUEUE(wait, current); mutex_lock(&dev->mutex);
add_wait_queue(&dev->r_wait, &wait); while (dev->current_len == 0) {
if (filp->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
goto out;
} __set_current_state(TASK_INTERRUPTIBLE);
mutex_unlock(&dev->mutex); schedule();
if (signal_pending(current)) {
ret = -ERESTARTSYS;
goto out2;
} mutex_lock(&dev->mutex);
} if (count > dev->current_len)
count = dev->current_len; /* 内核空间到用户空间缓存区的复制 */
if (copy_to_user(buf, dev->mem, count)) {
ret = -EFAULT;
goto out;
} else {
memcpy(dev->mem, dev->mem + count, dev->current_len - count);
dev->current_len -= count;
printk(KERN_INFO "read %lu bytes(s) from %u\n", count, dev->current_len);
wake_up_interruptible(&dev->w_wait);
ret = count;
} out:
mutex_unlock(&dev->mutex); out2:
remove_wait_queue(&dev->r_wait, &wait);
set_current_state(TASK_RUNNING);
return ret;
} /**
* 写设备
* @param[in] filp:文件结构体指针
* @param[in] buf: 用户空间内存地址,不能在内核中直接读写
* @param[in] size: 写入的字节数
* @param[in/out] ppos: 写的位置相当于文件头的偏移
* @return 若成功返回实际写的字节数,若出错返回错误码
*/
static ssize_t globalfifo_write(struct file *filp,
const char __user *buf, size_t size, loff_t *ppos)
{
int ret = 0;
unsigned long count = size;
struct globalfifo_dev *dev = filp->private_data; DECLARE_WAITQUEUE(wait, current); mutex_lock(&dev->mutex);
add_wait_queue(&dev->w_wait, &wait); while (dev->current_len == GLOBALFIFO_SIZE) {
if (filp->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUPTIBLE); mutex_unlock(&dev->mutex); schedule();
if (signal_pending(current)) {
ret = -ERESTARTSYS;
goto out2;
}
mutex_lock(&dev->mutex);
} if (count > GLOBALFIFO_SIZE - dev->current_len)
count = GLOBALFIFO_SIZE - dev->current_len; /* 用户空间缓存区到内核空间缓存区的复制 */
if (copy_from_user(dev->mem + dev->current_len, buf, count)) {
ret = -EFAULT;
goto out;
} else {
dev->current_len += count;
printk(KERN_INFO "written %lu bytes(s) from %u\n", count, dev->current_len);
wake_up_interruptible(&dev->r_wait);
ret = count;
} out:
mutex_unlock(&dev->mutex); out2:
remove_wait_queue(&dev->w_wait, &wait);
set_current_state(TASK_RUNNING);
return ret;
} /**
* 文件偏移设置
* @param[in] filp:文件结构体指针
* @param[in] offset: 偏移值大小
* @param[in] orig: 起始偏移位置
* @return 若成功返回文件当前位置,若出错返回错误码
*/
static loff_t globalfifo_llseek(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
switch (orig) {
case 0: /* 从文件头位置设置偏移 */
if (offset < 0) {
ret = -EINVAL;
break;
}
if ((unsigned int)offset > GLOBALFIFO_SIZE) {
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case 1: /* 从当前位置设置偏移 */
if ((filp->f_pos + offset) > GLOBALFIFO_SIZE) {
ret = -EINVAL;
break;
}
if ((filp->f_pos + offset) < 0) {
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break; default:
ret = -EINVAL;
break;;
}
return ret;
} static const struct file_operations globalfifo_fops = {
.owner = THIS_MODULE,
.llseek = globalfifo_llseek,
.read = globalfifo_read,
.write = globalfifo_write,
.unlocked_ioctl = globalfifo_ioctl,
.open = globalfifo_open,
.release = globalfifo_release,
}; static void globalfifo_setup_cdev(struct globalfifo_dev *dev, int index)
{
int err, devno = MKDEV(globalfifo_major, index); /* 初始化cdev */
cdev_init(&dev->cdev, &globalfifo_fops);
dev->cdev.owner = THIS_MODULE;
/* 注册设备 */
err = cdev_add(&dev->cdev, devno, 1);
if (err)
printk(KERN_NOTICE "Error %d adding globalfifo%d", err, index);
} /* 驱动模块加载函数 */
static int __init globalfifo_init(void)
{
int ret;
dev_t devno = MKDEV(globalfifo_major, 0); /* 获取设备号 */
if (globalfifo_major)
ret = register_chrdev_region(devno, 1, "globalfifo");
else {
ret = alloc_chrdev_region(&devno, 0, 1, "globalfifo");
globalfifo_major = MAJOR(devno);
} if (ret < 0)
return ret; /* 申请内存 */
globalfifo_devp = kzalloc(sizeof(struct globalfifo_dev), GFP_KERNEL);
if (!globalfifo_devp) {
ret = -ENOMEM;
goto fail_malloc;
}
globalfifo_setup_cdev(globalfifo_devp, 0); mutex_init(&globalfifo_devp->mutex); init_waitqueue_head(&globalfifo_devp->r_wait);
init_waitqueue_head(&globalfifo_devp->w_wait); return 0; fail_malloc:
unregister_chrdev_region(devno, 1);
return ret;
}
module_init(globalfifo_init); /* 驱动模块卸载函数 */
static void __exit globalfifo_exit(void)
{
cdev_del(&globalfifo_devp->cdev);
kfree(globalfifo_devp);
/* 释放设备号 */
unregister_chrdev_region(MKDEV(globalfifo_major, 0), 1);
}
module_exit(globalfifo_exit); MODULE_AUTHOR("MrLayfolk");
MODULE_LICENSE("GPL v2");

Makefile:

KVERS = $(shell uname -r)

# Kernel modules
obj-m += globalfifo.o # Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0 build: kernel_modules kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

编译、插入ko,并进行测试:

$ make
$ insmod globalfifo.ko
$ mknod /dev/globalfifo c 230 0 //创建设备节点
$ cat /dev/globalfifo & //读进程在后台运行
$ echo "I want to be" > /dev/globalfifo //写进程对FIFO进行写数据
$ I want to be //cat读进程会立即打印

Linux设备驱动--阻塞与非阻塞I/O的更多相关文章

  1. Linux设备驱动中的阻塞和非阻塞I/O

    [基本概念] 1.阻塞 阻塞操作是指在执行设备操作时,托不能获得资源,则挂起进程直到满足操作所需的条件后再进行操作.被挂起的进程进入休眠状态(不占用cpu资源),从调度器的运行队列转移到等待队列,直到 ...

  2. Linux设备驱动中的阻塞和非阻塞I/O <转载>

    Green 博客园 首页 新随笔 联系 订阅 管理 Linux设备驱动中的阻塞和非阻塞I/O   [基本概念] 1.阻塞 阻塞操作是指在执行设备操作时,托不能获得资源,则挂起进程直到满足操作所需的条件 ...

  3. 蜕变成蝶~Linux设备驱动中的阻塞和非阻塞I/O

    今天意外收到一个消息,真是惊呆我了,博客轩给我发了信息,说是俺的博客文章有特色可以出本书,,这简直让我受宠若惊,俺只是个大三的技术宅,写的博客也是自己所学的一些见解和在网上看到我一些博文以及帖子里综合 ...

  4. Linux设备驱动中的IO模型---阻塞和非阻塞IO【转】

    在前面学习网络编程时,曾经学过I/O模型 Linux 系统应用编程——网络编程(I/O模型),下面学习一下I/O模型在设备驱动中的应用. 回顾一下在Unix/Linux下共有五种I/O模型,分别是: ...

  5. Linux 设备驱动--- 阻塞型字符设备驱动 --- O_NONBLOCK --- 非阻塞标志【转】

    转自:http://blog.csdn.net/yikai2009/article/details/8653697 版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[-] 阻塞 阻 ...

  6. 《linux设备驱动开发详解》笔记——8阻塞与非阻塞IO

    8.1 阻塞与非阻塞IO 8.1.0 概述 阻塞:访问设备时,若不能获取资源,则进程挂起,进入睡眠状态:也就是进入等待队列 非阻塞:不能获取资源时,不睡眠,要么退出.要么一直查询:直接退出且无资源时, ...

  7. linux设备驱动归纳总结(三):5.阻塞型IO实现【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-60025.html linux设备驱动归纳总结(三):5.阻塞型IO实现 xxxxxxxxxxxxxx ...

  8. 【Linux开发】linux设备驱动归纳总结(三):5.阻塞型IO实现

    linux设备驱动归纳总结(三):5.阻塞型IO实现 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...

  9. 七、设备驱动中的阻塞与非阻塞 IO(一)

    7.1 阻塞与非阻塞 IO 阻塞操作是指在执行设备操作的时候,若不能获取资源,则挂起进程直到满足可操作的条件后再进行操作.被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足. 非 ...

  10. linux驱动编写之阻塞与非阻塞

    一.概念 应用程序使用API接口,如open.read等来最终操作驱动,有两种结果--成功和失败.成功,很好处理,直接返回想要的结果:但是,失败,是继续等待,还是返回失败类型呢?  如果继续等待,将进 ...

随机推荐

  1. xxx,一个神奇的 Python 库

    前几天,我在<技术周刊的转变:如何平衡热爱与现实?>一文里写过国内 Python 自媒体圈在近几年的两个现象(仅个人观感,无科学数据支撑): Python 广告投放出现断崖式萎缩 Pyth ...

  2. 手动给docusaurus添加一个搜索

    新版博客用docusaurus重构已经有些日子了,根据docusaurus的文档上也申请了Algolia,想一劳永逸的解决博客的搜索问题.但是流水有意,落花无情. algolia总是不给我回复,我只能 ...

  3. 学习 XQuery:XML数据查询的关键

    XQuery 是 XML 数据的查询语言,类似于 SQL 是数据库的查询语言.它被设计用于查询 XML 数据. XQuery 示例 for $x in doc("books.xml" ...

  4. CF1995E Long Inversions(贪心 + 差分)

    见代码 点击查看代码 //背景:夸一下自己,CF1955A~E均是自己想出来的,这道题思路是自己想的,唯一没想到用差分数组维护 //原理:贪心(想最左边的1开始,不可能对其左边包括自己操作了,否则又要 ...

  5. MyBatis源码之前言—JDBC编码存在的问题和Mybatis的介绍

    MyBatis源码之前言-JDBC编码存在的问题和Mybatis的介绍 为了方便操作,我们在sjdwz_test数据库下建立一张表: CREATE TABLE `t_student` ( `id` b ...

  6. UWP 写入图片 Exif 信息

    本文告诉大家如何在 UWP 中,保存图片的时候,写入 Exif 信息,也就是如照片的 相机型号 制造商 光圈值等信息的写入 在 UWP 中,保存图片或照片需要用到图片编码器,在使用编码器写入前可以设置 ...

  7. 记录一个HttpClient超时连接配置不生效的问题排查过程

    现象 首先有一个被服务由于内存有限,导致巨卡.导致调用他的服务出现线程阻塞.jstack打印线程池如下所示: 开始排查解决问题 第一步:检查代码看是否超时设置是否正确,因为感觉超时设置正确不可能阻塞. ...

  8. RT-Thread 堆区大小设置

    一.利用栈区的空间作为堆区 看过我之前的笔记的小伙伴都知道,以前我是通过申请栈区的空间使用的,感兴趣的小伙伴可以看我之前的笔记,RT-Thread移植到stm32. 在board.c文件文件中的代码如 ...

  9. SpringBoot项目预加载数据——ApplicationRunner、CommandLineRunner、InitializingBean 、@PostConstruct区别

    0.参考.业务需求 参考: https://www.cnblogs.com/java-chen-hao/p/11835120.html#_label1 https://zhuanlan.zhihu.c ...

  10. 史上功能最全的Java权限认证框架!

    大家好,我是 Java 陈序员.权限认证是我们日常开发绕不过的话题,这是因为我们的应用程序需要防护,防止被窜入和攻击. 在 Java 后端开发中,实现权限认证有很多种方案可以选择,一个拦截器.过滤器也 ...