设备驱动中的阻塞与非阻塞IO

阻塞和非阻塞I/O是设备访问的两种不同的模式,驱动程序可以灵活地支持这两种用户空间对设备的访问方式。在Linux设备驱动下,可以使用等待队列或轮询操作实现对I/O的阻塞和非阻塞访问。

阻塞操作是指在执行设备操作时,若不能获取资源,则挂起进程直到满足可以操作的条件后在进行操作。被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待条件满足。而非阻塞操作的进程不能进行设备操作时,并不挂起,它要么放弃,要么不停地查询,直到可以进行操作。也就是说,阻塞操作在不能立即获取资源时,会使进程挂起,将CPU资源让出来供其他进程占用,直到自己操作条件满足时申请并占用CPU资源,而非阻塞在进行设备造作不能获取资源时,会立即返回错误信息或是一直占用CPU系统资源等待条件的满足。(占着茅坑不拉屎)

但是应用程序需要阻塞或非阻塞的访问资源操作,取决应用需要,所以作为驱动代码,就要做到即支持非阻塞操作,又支持阻塞操作。上述说道,在阻塞操作等待时,会将CPU资源“让”出来,等到条件满足时在进行设备操作,于是就需要一个地方能唤醒休眠的进程。在Linux驱动程序中,可以使用 等待队列 来实现阻塞进程的唤醒。

等待队列的操作如下:

//定义  等待队列头部
wait_queue_heat_t my_queue;     //wait_queue_heat_t 是  __wait_queue_head结构体的一个typedef。
//初始化  等待队列头部
init_waitqueue_head(&my_queue);  //宏 DECLARE_WAIT_QUEUE_HEAD(name) 可以作为定义并初始化等待队列的快捷方式

//定义 等待队列元素
DECLARE_WAITQUEUE(name,tsk); //该宏用于定义并初始化一个名为name的等待队列元素

//添加或移除等待队列
void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait); //将等待队列元素wait 添加到等待队列头部q 指向的双向链表中
void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);

//等待事件
wait_event(queue,condition);//条件condition满足时唤醒queue为头部的等待队列,否则继续阻塞
wait_event_interruptible(queue,condition);//可以被中断信号打断
wait_event_timeout(queue,condition,timeout);//当阻塞事件timeout达到时,无论condition是否满足,都返回
wait_event_interruptible_timeout(queue,condition,timeout);

//唤醒等待队列
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的进程。
  
//等待队列上睡眠
sleep_on(wait_queue_head_t *q);//将目前的进程状态制成TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,之后把他挂到等待队列头部q指向的双向链表,直到资源可获得,q队列指向连接的进程被唤醒
interruptible_sleep_on(wait_queue_head_t *q);
//sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()应该与 wake_up_interruptible()成对使用。

设备驱动中的并发控中的驱动代码做些许修改

//添加头文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <linux/device.h>
 
static int major=0,minor=0;
 
struct mymodule_dev{
    struct cdev cdev;  //定义cdev结构体
    unsigned char buf[512];
    struct mutex mutex;
    unsigned int current_len;//buf中实际数据长度
    wait_queue_head_t r_wait;//定义读写等待队列头部
    wait_queue_head_t w_wait;
};
 
struct mymodule_dev *mydev;
struct class *my_class;
struct device *my_device;
 
static int mymodule_open(struct inode *inode,struct file *filp)
{
    filp->private_data = mydev; //file->private_data用来保存mydev的地址,已提供给其他函数使用
    return 0;
}
 
static int mymodule_release(struct inode *inode,struct file *filp)
{
    return 0;
}
 
static ssize_t mymodule_read(struct file *filp,char __user *buf,size_t count,loff_t *ppos)
{
    int ret = 0;
    struct mymodule_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->buf,count)){
        ret = -EFAULT;
        goto out;
    }else{
        memcpy(dev->buf,dev->buf+count,dev->current_len-count);
        dev->current_len -= count;
        printk(KERN_INFO"Read %d bytes,current_len %d\n",count,dev->current_len);
        wake_up_interruptible(&dev->w_wait);//唤醒可能阻塞的写进程
        ret = count;
    }
out:
mutex_unlock(&dev->mutex);
out2:
remove_wait_queue(&dev->w_wait,&wait); //将元素移除等待队列
set_current_state(TASK_RUNNING); //设置进程状态
return ret;
}
 
static ssize_t mymodule_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos)
{
    int ret = 0;
    struct mymodule_dev *dev = filp->private_data;
    DECLARE_WAITQUEUE(wait,current);
    mutex_lock(&dev->mutex);
    add_wait_queue(&dev->w_wait,&wait);
    while(dev->current_len == 512){
        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 > 512 - dev->current_len)
        count = 512 - dev->current_len;
    //用户空间->内核空间
    if(copy_from_user(dev->buf+dev->current_len,buf,count)){
        ret = -EFAULT;
        goto out;
    }else{
        dev->current_len += count;
        printk(KERN_INFO"Writen %d bytes,current_len: %d\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;
}
 
//file_operation设备驱动文件操作结构体
static struct file_operations mymodule_fops = {
    .owner = THIS_MODULE,
    .open = mymodule_open,
    .release = mymodule_release,
    .read = mymodule_read,
    .write = mymodule_write,
};
 
//初始化并添加cdev结构体
static void mymodule_cdev_setup(struct mymodule_dev *dev)
{
    int err,devno=MKDEV(major,minor);
    //初始化
    cdev_init(&dev->cdev,&mymodule_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &mymodule_fops;
    //注册,添加
    err = cdev_add(&dev->cdev,devno,1);
    if(err)
        printk(KERN_NOTICE"error %d adding mymodule",err);
}
 
//模块加载
int __init mymodule_init(void)
{
    //申请设备号
    int result;
    dev_t devno = MKDEV(major,minor);
    if(major)
        result = register_chrdev_region(devno,1,"mymodule");
    else{
        result = alloc_chrdev_region(&devno,minor,1,"mymodule");
        major = MAJOR(devno);
        minor = MINOR(devno);
    }
    if(result<0)
        return result;
    //动态申请设备结构体内存
    mydev = kmalloc(sizeof(struct mymodule_dev),GFP_KERNEL);
    if(!mydev){
        result=-ENOMEM;
        goto fail_malloc;
    }
    memset(mydev,0,sizeof(struct mymodule_dev));
    //初始化互斥体
    mutex_init(&mydev->mutex); 
   //cdev字符设备的初始化和添加
    mymodule_cdev_setup(mydev);

    //初始化等待队列长度
    init_waitqueue_head(&mydev->r_wait);
    init_waitqueue_head(&mydev->w_wait);
    
    //注册设备节点
    my_class = class_create(THIS_MODULE,"mymodule_t");
    my_device = device_create(my_class,NULL,MKDEV(major,minor),NULL,"mymodules");
 
    return 0;
fail_malloc:unregister_chrdev_region(devno,1);
return result;  
}
//模块卸载
void __exit mymodule_exit(void)
{
    device_destroy(my_class,MKDEV(major,minor));
    class_destroy(my_class);
    //删除cdev结构体
    cdev_del(&mydev->cdev);
    kfree(mydev);
    //注销设备号
    unregister_chrdev_region(MKDEV(major,minor),1);
}
 
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_AUTHOR("HaoRan");
MODULE_LICENSE("GPL");

编译并加载到虚拟开发板中,开一个后台进程   cat /dev/mymodules &

在使用 echo 'I want to be a greet Chinese Linux kernel Developer' > /dev/mymodules 测试所写驱动代码。或使用echo 'I want to be a greet Chinese Linux kernel Developer' > /dev/mymodules | cat /dev/mymodules 测试。

下面我们对代码做简单的分析:

在大的方面还是那么几点:

  1. linux模块编程的加载和卸载函数,以及GPL协议
  2. 文件操作结构体file_operations 的填充和相关函数模型的实现
  3. 字符设备结构体cdev的实现与初始化,加载
  4. file_operations结构体中函数的具体实现
  5. 共享资源并发访问保(互斥体 mutex)的实现
  6. 阻塞与非阻塞的实现

在细节方面,其他的代码就不分析了,可以看前面的文章,这里分析一下读写函数:

就此驱动程序的愿意是写函数获取用户程序输入的信息保存到一个内核数组中,在用户程序调用读函数是,在把内核数组中的内容传输到用户空间,所以,就写函数而言,会调用 copy_from_user 从用户空间获取数据。但在获取数据之前,需要判断设备是否可以读写,即定义得buf是空还是满;

如果设备驱动可以读写,则判断是阻塞还是非阻塞(file 结构体中数据的属性),如果是非阻塞,设备忙时直接返回 -EAGAIN ,阻塞访问时,会调用__set_current_state(TASK_INTERRUPTIBLE)进行进程状态切换并显示“schedule()”调度其他进程执行,醒来的时候要注意,由于调度出去的时候,进程状态是TASK_INTERRUPTIBLE,即浅度睡眠, 所以唤醒它的有可能是信号,因此,我们首先通过signal_pending(current)了解是不是信号唤醒的,如果是,立即返回“-ERESTARTSYS”。如果用户空间数据没有达到内核申请的内核空间,则copy数据。

内核驱动支持阻塞和非阻塞的操作,阻塞I/O等待设备可访问后访问,那非阻塞I/O怎么知道其是否可访问了,若不可访问不就直接返回错误了吗?这时,设备驱动的轮询(poll)操作就可以帮助用户了解到设备驱动无阻塞I/O是否支持访问。在用户程序中,使用select() 和 poll() 系统调用查询无阻塞I/O是否支持访问。select() 和 poll() 函数最终会调用设备驱动中的poll()函数,或是epoll()函数,即poll()的扩展。

在应用程序中广泛用到的是select() 系统调用,其原型是:

int select(int numfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

其中,readfds,writefds、exceptfds分别是被select() 监视的读,写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的fd加1.readfds文件集中任何一个文件变得可读,selsct()返回,同理,writefds文件集中的任何一个文件变得可写,select()也返回。timeout参数是指向struct timeval类型的指针,他可以使select()在等待timeout时间后任然没有文件描述符准备好则超时返回。

下列操作用来设置、清除、判断文件描述符集合:

//清除一个文件描述符集合
FD_ZERO(fd_set *set);
//将一个文件描述符加入到文件描述集合中
FD_SET(int fd,fd_set *set);
//将一个文件描述符从文件描述符集合中清除
FD_CLR(int fd,fd_set *set);
//判断文件描述符是否被置位
FD_ISSET(int fd,fd_set *set);

poll() 的功能和实现原理与select() 相似,其函数原型为:

int poll(struct pollfd *fds,nfds_t nfds,int timeout);

当多路复用的文件数量庞大、I/O流量频繁的时候,select和poll 函数一般不太适用,宜用epoll() 函数,其不会随着fd的数目增长而降低效率。epoll() 的用户空间编程接口包括:

//创建一个epoll句柄,size是要监听的fd个数,创建好后本身占用一个fd,所以使用完后必须调动close()
int epoll_create(int size);

//告诉内核监听什么类型事件,第一个参数是epoll句柄,第二个参数表示动作,第三个参数是要监听的fd,第四
个参数是要监听的事件类型
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
//第二个参数动作包括
EPOLL_CTL_ADD  //注册新的fd到epfd
EPOLL_CTL_MOD  //修改已注册的fd的监听事件
EPOLL_CTL_DEL  //从epfd中删除一个fd
//第四个参数event包括
EPOLLIN  //表示对应文件描述符可读
EPOLLOUT //对应文件描述符可写
EPOLLPRI //对应文件描述符有紧急的数据可读
EPOLLERR //对应文件描述符发生错误
EPOLLHUP //对应文件描述符被挂断
EPOLLET  //将epoll设为边沿触发模式
EPOLLONESHOT //意味着一次性监听


//等待事件的发生
int epoll_wait(int epfd,struct epoll_event *event,int manevents,int timeout);

设备驱动中的poll() 函数的原型是

unsigned int(*poll)(struct file *filp,struct poll_table *wait);
//头文件为 #include <linux/poll.h>
//第一个参数是file结构体指针,第二个参数是轮询表指针、这个函数进行两项工作
// 1. 对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头部添加到poll_table中
// 2. 返回是否能对设备进行无阻塞读写访问的掩码


viod poll_wait(struct file *filp,wait_queue_head_t *queue,poll_table *wait);

poll_wait()函数的名称非常容易让人产生误会,以为它和wait_event()等一样,会阻塞地等待某 事件的发生,其实这个函数并不会引起阻塞。poll_wait()函数所做的工作是把当前进程添加到wait参数 指定的等待列表(poll_table)中,实际作用是让唤醒参数queue对应的等待队列可以唤醒因select()而睡眠的进程。

驱动程序poll()函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、 POLLERR、POLLNVAL等宏的位“或”结果。每个宏的含义都表明设备的一种状态,如POLLIN(定义为 0x0001)意味着设备可以无阻塞地读,POLLOUT(定义为0x0004)意味着设备可以无阻塞地写。

在上述驱动代码中添加poll函数以及ilctl函数

#define BUF_CLEAR 0x1

static unsigned int mymodule_poll(struct file *filp,struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    struct mymodule_dev *dev = filp->private_data;

    mutex_lock(&dev->mutex);

    poll_wait(filp,&dev->r_wait,wait);
    poll_wait(filp,&dev->w_wait,wait);

    if(dev->current_len !=0)
        mask |= POLLIN | POLLRDNORM;

    if(dev->current_len != 512)
        mask |= POLLOUT | POLLWRNORM;

    mutex_unlock(&dev->mutex);
    return mask;
}

static long mymodule_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
{
    struct mymodule_dev *dev = filp->private_data;
    switch(cmd)
    {
        case BUF_CLEAR:
            mutex_lock(&dev->mutex);
            memset(dev->buf,0,512);
            mutex_unlock(&dev->mutex);
            printk(KERN_INFO"globalmem is set to zero\n");
            break;
        default:
            return - EINVAL;
    }
    return 0;
}

//file_operation设备驱动文件操作结构体
static struct file_operations mymodule_fops = {
    .......
    .poll = mymodule_poll,
    .compat_ioctl = mymodule_ioctl,
};

在用户空间年编写test_module.c代码测试设备的轮询

#include <stdio.h>
#include <sys/select.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#define CLEAR 0x1

int main()
{
    int fd,num;
    char rd_ch[20];
    fd_set rfds,wfds; //读写文件描述集

    fd = open("/dev/mymodules",O_RDONLY | O_NONBLOCK);//非阻塞方式打开
    if(fd != -1){
        if(ioctl(fd,CLEAR,0)<0)
            printf("ioctl command filed\n");
        while(1){
            FD_ZERO(&rfds);
            FD_ZERO(&wfds);
            FD_SET(fd,&rfds);
            FD_SET(fd,&wfds);

            select(fd+1,&rfds,&wfds,NULL,NULL);
            if(FD_ISSET(fd,&rfds))
                printf("Poll monitor:can be read!\n");
            if(FD_ISSET(fd,&wfds))
                printf("Poll monitor:can be write!\n");
        }
    }else
        printf("open file\n");
    return 0;
}

编译后执行,在无任何输入的时候显示

如果输入echo 'hello test' > /dev/mymodules | ./poll_test    显示

 如果输入echo 'hello test ... mymodule' > /dev/mymodules | ./poll_test  即输入的内容大于内核代码申请的内存空间512,则显示

发布了45 篇原创文章 · 获赞 7 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_42005898/article/details/104773611
今日推荐