Linux驱动(六)设备驱动中的阻塞与非阻塞IO
我们在Linux学习(二十三)IO模型中了解了LINUX中IO模型,IO模型最简单的可以分为阻塞IO和非阻塞IO。并且学习了一个用如何使用阻塞操作和非阻塞操作。而应用层之所以能实现阻塞操作和非阻塞操作,都是因为底层实现了阻塞操作和非阻塞操作。我们这一节就来看看底层是如何实现的。阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。而非阻塞操作的进程在不能进行
我们在Linux学习(二十三)IO模型中了解了LINUX中IO模型,IO模型最简单的可以分为阻塞IO和非阻塞IO。并且学习了一个用如何使用阻塞操作和非阻塞操作。而应用层之所以能实现阻塞操作和非阻塞操作,都是因为底层实现了阻塞操作和非阻塞操作。我们这一节就来看看底层是如何实现的。
阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。而非阻塞操作的进程在不能进行设备操作时,并不挂起,要么放弃,要么不停地查询,直到可以操为止。
举个很简单的例子,要从串口读数据read(fd,&buf,1);如果此时串口没有数据如何处理呢?应用层在打开串口设备时,可以设置该设备是阻塞操作还是非阻塞操作的open("/dev/ttyS1",O_RDWR | O_NONBLOCK); O_NONBLOCK代表非阻塞,没有这一项代表阻塞模式。应用层提供了阻塞和非阻塞模式,很显然,底层的read函数就要实现阻塞和非阻塞两种情况。
文件结构体指针struct file 中变量f_flags来表示该设备是阻塞模式还是非阻塞模式。
等待队列
在linux驱动程序中,可以使用等待队列来实现阻塞进程的唤醒。
1、定义等待队列头部
wait_queue_head_t my_queue;
2、初始化等待队列头部
init_waitqueue_head(&my_queue);
DECLARE_WAIT_QUEUE_HEAD(name) 这个可以作为定义并初始化等待队列头部
3、定义等待队列元素
DECLARE_WAITQUEUE(name, tsk)
4、添加/移除等待队列
void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
5、 等待事件
wait_event(queue, condition) // queue 等待队列, condition 唤醒条件, 可以是一个表达式
wait_event_interruptible(queue, condition) // 等待事件,可以被中断所打断
wait_event_timeout(queue, condition, timeout)//等待事件发生 超时可自动唤醒
wait_event_interruptible_timeout(queue, condition, timeout)//等待事件,可以被中断所打断,超时可自动唤醒
上述四个函数都属于等待事件函数,condition是判断条件,如果条件成立,则不休眠,例如应用层读串口数据,串口此时有数据,也就是条件成立。那正常给应用即可。如果无数据,条件不成立,就会进入休眠,interruptible表示在休眠期间可以被信号打断,timeout表示休眠期间,若超时就会被唤醒。
6、唤醒队列
void wake_up(wait_queue_head_t *queue); // 唤醒等待队列,对应wait_event或wait_event_timeout
void wake_up_interruptible(wait_queue_head_t *queue); //唤醒等待队列 对应wait_event_interruptible或wait_event_interruptible_timeout。
我们先来看看等待如何使用把
1、定义初始化队列头
2、、等待事件,条件不满足,阻塞(切换到其他进程)
3、被唤醒后继续执行
这几步的实现有手动和自动两种方式,自动的更简单一些。我们来介绍一下自动方式,在模块初始化中初始化等待队列,read函数中,判断一下是否有数据,无数据则阻塞等待。write函数中,写完成后,唤醒等待队列
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <asm/atomic.h>
#include <linux/spinlock.h>
#include <linux/semaphore.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>
MODULE_LICENSE("GPL");
dev_t devno;
int major = 0;
int minor = 0;
int count = 1;
#define KMAX 1024
char kbuf[KMAX] = {};
int counter = 0; //用它记录kbuf中实际存储的字节数量
struct cdev *pdev;
struct class * pclass;
struct device * pdevice;
struct semaphore sem_r;
struct semaphore sem_w;
wait_queue_head_t wq; //创建一个等待队列头 +++++++++++++++++++++++++++++++++++
int demo_open(struct inode * inodep, struct file * filep)
{
printk("%s,%d\n", __func__, __LINE__);
return 0;
}
int demo_release(struct inode *inodep, struct file *filep)
{
printk("%s,%d\n", __func__, __LINE__);
return 0;
}
// read(fd, buff, N) --> ... --> demo_read()
ssize_t demo_read(struct file * filep, char __user * buffer, size_t size, loff_t * offlen)
{
// 应用程序,读数据时,发现没有资源,那么此时阻塞等代
if(counter == 0)
{
if(filep->f_flags & O_NONBLOCK) //设备是阻塞还是非阻塞模式++++++++++++++++++++++++
{
return -EAGAIN;
}
if(wait_event_interruptible(wq,counter != 0)) //阻塞模式,是否有数据可读+++++++++++++++
{
return -ERESTARTSYS;
}
}
down_interruptible(&sem_r);
if(size > counter)
{
size = counter;
}
if(copy_to_user(buffer, kbuf, size) != 0)
{
printk("Failed to copy_to_user.\n");
return -1;
}
counter = 0;
up(&sem_w);
return size;
}
// write(fd, buff, n) --> ... --> demo_write();
ssize_t demo_write(struct file *filep, const char __user *buffer, size_t size, loff_t * offlen)
{
down_interruptible(&sem_w);
if(size > KMAX)
{
return -ENOMEM;
}
if(copy_from_user(kbuf, buffer,size) != 0)
{
printk("Failed to copy_from_user.\n");
return -1;
}
printk("kbuf:%s\n", kbuf);
counter = size;
up(&sem_r);
// 唤醒等待队列
wake_up_interruptible(&wq); //写入了数据,fifo不为空,可以唤醒读中的等待队列++++++++++++++++++++++++
return size;
}
struct file_operations fops = {
.owner =THIS_MODULE,
.open = demo_open,
.release = demo_release,
.read = demo_read,
.write = demo_write,
};
static int __init demo_init(void)
{
int ret = 0;
printk("%s,%d\n", __func__, __LINE__);
ret = alloc_chrdev_region(&devno,minor,count, "xxx");
if(ret)
{
printk("Failed to alloc_chrdev_region.\n");
return ret;
}
printk("devno:%d , major:%d minor:%d\n", devno, MAJOR(devno), MINOR(devno));
pdev = cdev_alloc();
if(pdev == NULL)
{
printk("Failed to cdev_alloc.\n");
goto err1;
}
cdev_init(pdev, &fops);
ret = cdev_add(pdev, devno, count);
if(ret < 0)
{
printk("Failed to cdev_add.");
goto err2;
}
pclass = class_create(THIS_MODULE, "myclass");
if(IS_ERR(pclass))
{
printk("Failed to class_create.\n");
ret = PTR_ERR(pclass);
goto err3;
}
pdevice = device_create(pclass, NULL, devno, NULL, "hello");
if(IS_ERR(pdevice))
{
printk("Failed to device_create.\n");
ret = PTR_ERR(pdevice);
goto err4;
}
sema_init(&sem_r, 0);
sema_init(&sem_w, 1);
// 初始化等待队列+++++++++++++++++++++++++++++++++++++++++
init_waitqueue_head(&wq);
return 0;
err4:
class_destroy(pclass);
err3:
cdev_del(pdev);
err2:
kfree(pdev);
err1:
unregister_chrdev_region(devno, count);
return ret;
}
static void __exit demo_exit(void)
{
printk("%s,%d\n", __func__, __LINE__);
device_destroy(pclass, devno);
class_destroy(pclass);
cdev_del(pdev);
kfree(pdev);
unregister_chrdev_region(devno, count);
}
module_init(demo_init);
module_exit(demo_exit);
应用层,一个进程读,一个进程写,写进程在写数据之前先延迟5s,此时读进程是无法读取数据的,直到写数据完成,讲读进程唤醒。大家可是试一下将该设备文件改成非阻塞模式,看看会有什么不同。
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#define N 128
int main(int argc, const char *argv[])
{
int fd;
char buf[N] = {};
char wbuf[N] = "This is a write test.";
pid_t pid;
//fd = open("/dev/hello", O_RDWR|O_NONBLOCK);
fd = open("/dev/hello", O_RDWR);
if(fd < 0)
{
perror("Failed to open.");
return -1;
}
else
{
printf("open success.\n");
}
if((pid = fork()) < 0)
{
perror("Failed to fork.");
return -1;
}
else if(pid == 0)
{
if(read(fd, buf, N) < 0)
{
perror("Failed to read");
return -1;
}
printf("buf:%s\n", buf);
}
else
{
sleep(5);
write(fd, wbuf, strlen(wbuf)+1);
printf("Wrote done.\n");
}
close(fd);
return 0;
}
我们来看一下内核的处理过程,我们在自动模式中只定义了一个等待队列头,我们使用wait_event系列函数时,就会创建一个等待队列元素DECLARE_WAITQUEUE(name, tsk),加入到等待队列中。等待队列结构体都包含什么呢?我们来看看
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE0x01
void *private; //指到当前进程结构体
wait_queue_func_t func; //唤醒回调函数
struct list_head task_list; // 一个循环双链表
};
private指向当前进程结构task_struct,唤醒时知道时要唤醒哪一个进程。
func :唤醒时的回调函数。
队列插入完成后,如下图。
这两个变量的值就是在DECLARE_WAITQUEUE完成赋值的
#define DEFINE_WAIT_FUNC(name, function)\
wait_queue_t name = {\
.private= current, \ //设置为当前进程
.func = function,\ //回掉函数
.task_list= LIST_HEAD_INIT((name).task_list),\
}
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
current代表的就是当前进程,唤醒时的回调函数就是autoremove_wake_function。
好,现在看看wait_event_interruptible
#define wait_event_interruptible(wq, condition) \
({ \
int __ret = 0;\
if (!(condition))\
__wait_event_interruptible(wq, condition, __ret);\
__ret; \
})
继续向下追
#define __wait_event_interruptible(wq, condition, ret)\
do { \
DEFINE_WAIT(__wait);\ 初始化一个等待队列元素
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);\ //加入等待队列设置进程状态
if (condition)\
break; \
if (!signal_pending(current)) {\
schedule();\ //进程调度
continue; \
} \
ret = -ERESTARTSYS;\
break; \
} \
finish_wait(&wq, &__wait);\
} while (0)
对于wait_event和 wait_event_interruptible就是在prepare_to_wait中设置的参数TASK_INTERRUPTIBLE不一样
void
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if (list_empty(&wait->task_list))
__add_wait_queue(q, wait); //添加到等待队列中
set_current_state(state); //修改当前进程状态
spin_unlock_irqrestore(&q->lock, flags);
}
这个函数主要是 1、添加到等待队列头中,这里和添加/移除等待队列实际上是一个东西
2、设置 一下当前进程状态
执行完prepare_to_wait后回到__wait_event_interruptible中,判断condition条件是否满足,直接break,然后执行finish_wait(&wq, &__wait);还原进程状态。若不满足,则schedule()出让CPU控制权。
这里还有一点,为什么这里用的for(;;)来完成进程调度和休眠的呢?这里我们要理解一点,唤醒是将等待队列中的所有进程都唤醒,但是每个进程设置的condition条件是不一样的,如果判断到底是不是唤醒了当前进程呢?那就再判断一下condition条件呗,如果确实满足了,那真的是当前进程被唤醒了,然后使用finish_wait结束休眠。如果不满足,说明还需要继续等待,再次调用schedule()出让CPU控制权。(在这之前需要判断一下谁把我唤醒的,if (!signal_pending(current)),如果是被信号唤醒的,不用进程调度,直接返回一个错误码)
上面是休眠和唤醒后的过程,那看看如果完成唤醒的。
#define wake_up_interruptible(x)__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
void __wake_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key);
spin_unlock_irqrestore(&q->lock, flags);
}
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
if (curr->func(curr, mode, wake_flags, key) && //唤醒函数回调
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
还记得curr->func是什么,前面提到了,这是唤醒回调函数。
执行的是autoremove_wake_function
休眠时将当前进程加入到了等待队列当中,在唤醒时自然要将其从唤醒队列当中移除。
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int ret = default_wake_function(wait, mode, sync, key);
if (ret)
list_del_init(&wait->task_list); //将当前进程从等待队列当中移除
return ret;
}
唤醒函数继续
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,
void *key)
{
return try_to_wake_up(curr->private, mode, wake_flags);
}
在这里将指定的进程唤醒。
总结一下:
休眠的基本步骤:
1、当前进程加入等待队列头指定的队列中
2、修改当前的进程状态
3、调度
几个关键函数:
wait_event_interruptible->>>>__wait_event_interruptible->>>DEFINE_WAIT->>>prepare_to_wait->>>schedule->>finish_wait
唤醒时的步骤:
1、将指定进程从等待队列头指定的队列中删除
2、修改指定进程的状态
3、唤醒指定的进程
几个关键函数
__wake_up->>>__wake_up_common->>>autoremove_wake_function->>>default_wake_function-->try_to_wake_up
更多推荐
所有评论(0)