麦克雷 Mavom.cn
标题:
驱动程序开发之等待队列
[打印本页]
作者:
aabbss
时间:
昨天 16:47
标题:
驱动程序开发之等待队列
一、等待队列简介
在内核里面,等待队列是有很多用处的,尤其是在中断处理、进程同步、定时等场合。可以通过等待队列来实现阻塞进程的唤醒。它是以队列为基础的数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问等。
等待队列的类型在内核源码的头文件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;
(1)定义和初始化
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
直接定义并初始化。init_waitqueue_head()函数会将自旋锁初始化为未锁,等待队列初始化为空的双向循环链表,也可以用下面的宏函数直接完成定义和初始化的工作:
DECLEAR_WAIT_QUEUE_HEAD(my_queue);
(2)等待事件:
等待事件通常用如下编程接口实现:
(, 下载次数: 0)
上传
点击文件名下载附件
(3)唤醒队列:
唤醒队列通常用如下编程接口实现:
(, 下载次数: 0)
上传
点击文件名下载附件
二、 阻塞型字符设备驱动
调用read时没有数据可读,但是以后可能会有;或者一个进程试图向设备写入数据,但是设备暂时没有准备好接收数据。应用程序通常不关心这种问题,应用程序只是调用read或write并得到返回值。驱动程序应当(缺省地)阻塞进程,使它进入睡眠,直到请求可以得到满足。
在阻塞型驱动程序中,read实现方式如下:如果进程调用read,但设备没有数据或数据不足,则进程阻塞。当新数据到达后,唤醒被阻塞进程。
在阻塞型驱动程序中,write实现方式如下:如果进程调用write,但设备没有足够空间供其写入数据,则进程阻塞。当设备中的数据被读走后,缓冲区真空出部分空间,则唤醒进程。
阻塞方式是文件读写操作的默认方式,但应用程序员可以通过使用O_NONBLOCK标志来人为的设置读写操作位非阻塞方式(该标志定义在<linux/fcntl.h>)中,在打开文件是指定。
三、综合案例
1、程序功能如下:
(1)、通过读写定位标志在内存上实现一个先入先出缓冲设备,为该设备添加读、写等操作。
(2)、该驱动的读操作在设备无数据可读是发生阻塞;写操作在设备没有空间可写时发生阻塞。
(3)、编程应用程序测试该驱动.
2、实训步骤:
字符驱动源代码如下:
在头文件memdev.h中为mem_dev结构添加新的成员:
struct mem_dev
{
bool canRead; /*设备可读标识*/
bool canWrite; /*设备可写标识*/
char *data;
unsigned long size;
unsigned long rpos; /*读定位标识*/
unsigned long wpos; /*写定位标识*/
unsigned long nattch;
wait_queue_head_t rwq;
wait_queue_head_t wwq;
struct semaphore sem; /*抢占式内核时需要添加*/
};
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
/*获取次设备号*/
int num = MINOR(inode->i_rdev);
if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];
/*将设备描述结构指针赋值给文件私有数据指针*/
filp->private_data = dev;
dev->nattch++;
return 0;
}
在文件操作结构体中仅实现如下操作:
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
(1)在模块初始化代码中添加对设备结构体中新成员的初始化
for (i=0; i < MEMDEV_NR_DEVS; i++) {
mem_devp
.size = MEMDEV_SIZE;
mem_devp
.data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp
.data, 0, MEMDEV_SIZE);
mem_devp
.canRead = false; /*一开始设备没有数据可供读*/
mem_devp
.canWrite = true; /*一开始设备有空间可供写*/
/*初始化读写指针*/
mem_devp
.rpos = 0;
mem_devp
.wpos = 0;
/*初始化等待队列*/
init_waitqueue_head(&(mem_devp
.rwq));
init_waitqueue_head(&(mem_devp
.wwq));
/*设备文件被打开的次数*/
mem_devp
.nattch = 0;
/*初始化信号量*/
sema_init(&mem_devp
.sem, 1);
}
(2)实现有阻塞功能的读操作:
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned int count;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*printk("<1> [Before mem_read: rpos=%lu, wpos=%lu, canR=%d, canW=%d]\n",
dev->rpos, dev->wpos, dev->canRead, dev->canWrite);*/
/* 这里使用while的好处是可以保证是因为有数据可读而跳出的,但
* 使用while又带来了另外一个问题:无法通过中断信号跳出循环*/
if (!dev->canRead) {
if (filp->f_flags & O_NONBLOCK) {
return -EAGAIN;
}
wait_event_interruptible(dev->rwq, dev->canRead);
}
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
if (dev->rpos < dev->wpos) {
count = dev->wpos - dev->rpos;
count = count>size ? size : count;
} else {
count = MEMDEV_SIZE - dev->rpos - 1;
if (count >= size) {
count = size;
} else {
if (copy_to_user(buf, (void*)(dev->data + dev->rpos), count))
ret = - EFAULT;
ret += count;
dev->rpos = 0;
buf += count;
size -= count;
count = dev->wpos>size ? size: dev->wpos;
}
}
/*读数据到用户空间*/
if (copy_to_user(buf, (void*)(dev->data + dev->rpos), count)) {
ret = - EFAULT;
} else {
dev->rpos += count;
ret += count;
}
if (ret) {
dev->canWrite = true; /*有空间可写*/
wake_up(&(dev->wwq)); /*唤醒写等待队列*/
if (dev->rpos==dev->wpos)
dev->canRead = false; /*无数据可读*/
}
/*printk("<1> [After mem_read: rpos=%lu, wpos=%lu, canR=%d, canW=%d]\n",
dev->rpos, dev->wpos, dev->canRead, dev->canWrite);*/
up(&dev->sem);
return ret;
}
(3)实现有阻塞功能的写操作。写操作时读操作的逆过程,和读操作十分相似,写的时候可以对照读操作:
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned int count;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*printk("<1> [Before mem_write: rpos=%lu, wpos=%lu, canR=%d, canW=%d]\n",
dev->rpos, dev->wpos, dev->canRead, dev->canWrite);*/
/* 没有空间可写,则进入睡眠 */
if (!dev->canWrite) {
if (filp->f_flags & O_NONBLOCK) {
return -EAGAIN;
}
wait_event_interruptible(dev->wwq, dev->canWrite);
}
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
if (dev->rpos > dev->wpos) {
count = dev->rpos - dev->wpos;
count = count>size ? size : count;
} else {
count = MEMDEV_SIZE - dev->wpos - 1;
if (count >= size) {
count = size;
} else {
if (copy_from_user(dev->data + dev->wpos, buf, count))
ret = - EFAULT;
ret += count;
dev->wpos = 0;
buf += count;
size -= count;
count = dev->rpos>size ? size: dev->rpos;
}
}
/*从用户空间写入数据*/
if (copy_from_user(dev->data + dev->wpos, buf, count)) {
ret = - EFAULT;
} else {
dev->wpos += count;
ret += count;
}
if (ret) {
dev->canRead = true; /*有空间可写*/
wake_up(&(dev->rwq)); /*唤醒读等待队列*/
if (dev->rpos==dev->wpos)
dev->canWrite = false; /*无数据可写*/
}
/*printk("<1> [After mem_write: rpos=%lu, wpos=%lu, canR=%d, canW=%d]\n",
dev->rpos, dev->wpos, dev->canRead, dev->canWrite);*/
up(&dev->sem);
return ret;
}
经过这样的修改,设备基本可以满足实验要求了。在模块初始化时,驱动将读写定位标识(rpos,wpos)都赋值为0,并且让设备可读标志为假(表示无数据可读),让设备可写标志位真(表示有空间可写)。在进行一次写操作时,写定位标识wpos会向前移动所写字节数,在成功写入数据后,写操作会将设备可读标识置为真,同时调用wake_up唤醒读等待队列。如果写操作完成时发现wpos等于rpos,则说明整个内存区域都已经写满,这时将可写标志置为假,那么下次再调用写操作时就会发生阻塞了。读操作与写操作类似,在此补再赘述。
完善设备驱动release操作。从读写操作可以看出,设备的读写定位标识是全局的,因此不会因为重新打开文件而变化。这就带来了问题:每次打开设备文件,读写定位标识等设备属性都会因为前面的操作而变的不确定,显然这是不合理的。合理的做法是:在所有打开设备文件的进程都关闭设备文件时,该设备的读写定位标识以及设备可读可写标识得到相应的更新,这样就不会在重新使用设备时,得到不确定的设备属性了。鉴于此需要修改mem_release操作:
int mem_release(struct inode *inode, struct file *filp)
{
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
dev->nattch--;
if (0==dev->nattch) {
/*使读写位置标志重新指到开头, 并修改可读可写标志*/
dev->rpos = 0;
dev->wpos = 0;
dev->canRead = false;
dev->canWrite = true;
/*printk("<1> [release and renew dev]");*/
}
return 0;
}
完成驱动程序后,下面的事情就是为驱动程序编写测试程序了。下面的测试程序主要是为了验证读阻塞和写阻塞的情况。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MEMDEV_SIZE 4096
int main(void)
{
int fd, ret;
char buf[MEMDEV_SIZE];
printf("One processes read, Another write ...\n");
if (-1==(ret=fork())) {
printf("fork() error\n");
_exit(EXIT_FAILURE);
} else if (0 == ret) { /*child process*/
if (-1==(fd=open ("/dev/memdev0", O_RDWR))) {
printf("open() error\n");
_exit(EXIT_FAILURE);
}
printf("Child sleep 5s ...\n\n");
sleep(5);
strcpy(buf, "Here is the CHILD writing ...");
ret = write(fd, buf, strlen(buf));
printf("Child write1(%d bytes): %s\n", strlen(buf), buf);
sleep(5);
printf("Child sleep 10s ...\n\n");
sleep(10);
ret = write(fd, buf, strlen(buf));
printf("Child write2(%d bytes): %s\n", ret, buf);
sleep(5);
ret = write(fd, buf, sizeof(buf));
printf("Child write3(%d bytes -- buf is full)\n", ret);
printf("Child try to write4, but there is no space, blocking ...\n");
ret = write(fd, buf, strlen(buf));
buf[ret] = 0;
usleep(30000);
printf("After Father read4, Child write4(%d bytes): %s\n", ret, buf);
close(fd);
_exit(EXIT_SUCCESS);
} else { /*father process*/
if (-1==(fd=open ("/dev/memdev0", O_RDWR))) {
printf("open() error");
_exit(EXIT_FAILURE);
}
printf("Father try to read, but there is no data, blocking ...\n");
int ret=0;
ret = read(fd, buf, 20);
usleep(30000);
printf("After Child write1, Father can read:\n");
buf[ret] = 0;
printf("Father read1(%d bytes): %s\n", ret, buf);
ret = read(fd, buf, 20);
buf[ret] = 0;
printf("Father read2(%d bytes): %s\n", ret, buf);
printf("Father read3, no data to read, blocking ...\n");
ret = read(fd, buf, 50);
usleep(30000);
buf[ret] = 0;
printf("After Child write2, Father read3(%d bytes): %s\n", ret, buf);
printf("Father sleep 10s ...\n\n");
sleep(10);
ret = read(fd, buf, 20);
printf("Father read4(%d bytes)\n", ret);
wait(NULL);
close(fd);
}
_exit(EXIT_SUCCESS);
}
代码编写好后,通过make编译该驱动模块,和应用程序,通过nfs加载驱动模块、创建设备节点、执行应用程序,查看程序的执行效果是否和预期的效果一样?请仔细分析这些打印结果。
总结:应该掌握Linux内核编程中等待队列的使用方法和编程接口,以及利用等待队列实现阻塞型字符设备驱动的原理和流程。下面列出一个阻塞型设备驱动的实现要素:
1. 需要一个等待队列作为容器,将需要阻塞的进程放入。
2. 需要一个全局的条件标识,该标识为真时表示条件允许字符设备进行相关操作而无需阻塞。
3. 而该条件为假时,设备相关操作通过等待事件函数wait_event,将当前进程设置为睡眠状态并放入等待队列中,使其阻塞。
4. 被阻塞的进程会直到其他进程的对应操作使其重新具备完成原操作的条件时被唤醒(wake_up),并且全局条件标识被对应操作设置为真。原阻塞进程被唤醒后将从等待队列中移除。
5. 阻塞进程被唤醒后接着完成剩下的设备操作。
欢迎光临 麦克雷 Mavom.cn (https://mavom.cn/)
Powered by Discuz! X3.5