自学内容网 自学内容网

(Linux驱动学习 - 7).阻塞IO和非阻塞IO

一.阻塞IO和非阻塞IO定义

1.阻塞IO

        当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式IO就会将应用程序对应的线程挂起,直到设备资源可以获取为止。

        在应用程序中,用户调用 open 函数默认是以阻塞式打开设备文件的。

fd = open("/dev/xxx_dev",O_RDWR);        //阻塞式打开

ret = read(fd,&data,sizeof(data));        //读取数据

2.非阻塞IO

        非阻塞IO对设备驱动进行操作的时候,如果不能获取到设备资源,那么应用程序要么一直轮询等待,直到设备资源可以使用,要么就直接放弃

        在应用程序中,若想以非阻塞式打开设备文件,则需要在调用 open 函数时,使用 O_NONBLOCK 标识

fd = open("/dev/xxx_dev",O_RDWR | O_NONBLOCK);

二.等待队列

        阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将
CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完
成唤醒工作。

         Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要
在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体
wait_queue_head_t 表示, wait_queue_head_t 结构体定义在文件 include/linux/wait.h 中,结构体内;

1.等待队列头结构体

struct __wait_queue_head
{
    spinlock_t lock;
    struct list_head task_list;
};

typedef struct __wait_queue_head wait_queue_head_t;

2.初始化等待队列头 - init_waitqueue_head

函数原型:

void init_waitqueue_head(wait_queue_head_t *q);

/* 也可以使用 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化 */

功能

        初始化一个等待队列头;

参数

        q:要初始化的等待队列的指针;

3.初始化一个等待队列项 - DECLAER_WAITQUEUE

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;

使用 DECLARE_WAITQUEUE 定义并初始化一个等待队列项

宏原型

DECLARE_WAITQUEUE(name,tsk);

功能

        定义并初始化一个等待队列项;

参数

        name:等待队列项的名字;

        tsk:表示这个等待队列项属于哪个进程(任务),一般设置为 current , current 是一个全局变量, 表示当前进程

4.将队列项添加至等待队列头 - add_wait_queue

函数原型

void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);

功能

        当设备不可访问的时候,将进程对应的等待队列项添加到等待队列头中,就可以让进程进入休眠态;

参数

        q:等待队列头的指针;

        wait:等待队列项的指针;

5.将队列项从等待队列头中移除 - remove_wait_queue

函数原型

void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);

功能

        当设备可以访问以后,将进程对应的等待队列项等待队列头中移除

参数

        q:等待队列头的指针;

        wait:等待队列项的指针;

6.唤醒进入休眠的进程 - wake_up / wake_up_interruptible

函数原型

/* 可唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程 */
void wake_up(wait_queue_head_t *q);


/* 只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程 */

void wake_up_interruptible(wait_queue_head_t *q);

功能

        将这个等待队列头中的所有进程都唤醒

参数

        q:要唤醒的等待队列头的指针

7.进程以等待事件自动唤醒

三.轮询

        如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,
也就是轮询。 poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。

当应用程序调用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。我们先来看一下应用程序中使用的 select、 poll 和 epoll 这三个函数。
 

1.select 函数(最大数量为1024)

/*
*    @description:        监视文件描述符集的读、写、异常变化
*    @param - nfds    :   所要监视的这三类文件描述符集合中,最大文件描述符加1
*    @param - readfds :   监视指定文件描述符集的读变化
*    @param - writes  :   监视指定文件描述符集的写变化
*    @param - exceptfds:  监视指定文件描述符集的异常
*    @param - timeout :   此变量用来判断是否超时
*    @return          :   超时则返回(0),发生错误则返回(-1),(其他值)表示可以进行操作的文件描述符的个数      
*/
int select(int nfds,
            fd_set *readfds,
            fd_set *writefds,
            fd_set *exceptfds,
            struct timeval *timeout);
/* 用于将 fd_set 变量的所有位都清零 */
void FD_ZERO(fd_set *set);


/* 向 fd_set 变量添加一个文件描述符 */
void FD_SET(int fd,fd_set *set);


/* 向 fd_set 变量中删除一个文件描述符 */
void FD_CLR(int fd,fd_set *set);


/* 用于测试一个文件是否属于某个集合 */
int FD_ISSET(int fd,fd_set *set);

应用程序使用示例:

void main(void)
{
    int ret, fd;             /* 要监视的文件描述符 */
    fd_set readfds;         /* 读操作文件描述符集 */
    struct timeval timeout; /* 超时结构体 */

    fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */

    FD_ZERO(&readfds);     /* 清除 readfds */
    FD_SET(fd, &readfds); /* 将 fd 添加到 readfds 里面 */

    /* 构造超时时间 */
    timeout.tv_sec = 0;
    timeout.tv_usec = 500000; /* 500ms */

    /* 只关心文件是否可读 */
    ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
    switch (ret) 
    {
        case 0: /* 超时 */
            printf("timeout!\r\n");
            break;
        case -1: /* 错误 */
            printf("error!\r\n");
            break;
        default: 
            /* 可以读取数据 */
            if(FD_ISSET(fd, &readfds)) 
            { 
                /* 判断是否为 fd 文件描述符 */
                /* 使用 read 函数读取数据 */
            }
            break;
    }
}

2.poll 函数(没有最大数量限制)

函数原型

/*
*    @description:        监视文件描述符集合的事件
*    @param - fds    :    要监视的文件描述符集合及要监视的事件
*    @param - nfds_t :    poll函数要监视的文件描述符的数量
*    @param - timeout:    超时时间,单位为 ms
*    @return         :    超时则返回(0),发生错误返回(-1),成功则返回发生事件的文件描述符的数量
*/
int poll(struct pollfd *fds,nfds_t nfds,int timeout);


struct pollfd
{
    int fd;            //文件描述符
    short events;      //关心的事件
    short revents;     //返回的事件
};

可监视的事件如下

POLLIN         有数据可以读取。
POLLPRI        有紧急的数据需要读取。
POLLOUT        可以写数据。
POLLERR        指定的文件描述符发生错误。
POLLHUP        指定的文件描述符挂起。
POLLNVAL       无效的请求。
POLLRDNORM     等同于 POLLIN

应用程序示例代码:

void main(void)
{
    int ret;
    int fd;              /* 要监视的文件描述符 */
    struct pollfd fds;

    fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */

    /* 构造结构体 */
    fds.fd = fd;
    fds.events = POLLIN;            /* 监视数据是否可以读取 */
    ret = poll(&fds, 1, 500);       /* 轮询文件是否可操作,超时 500ms */
    if (ret) 
    { 
    /* 数据有效 */
        //......
    /* 读取数据 */
        //......
    } 
    else if (ret == 0) 
    { /* 超时 */
        //......
    } 
    else if (ret < 0) 
    { /* 错误 */
        //......
    }
}

3.epoll 函数

        传统的 selcetpoll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且
poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此, epoll
应运而生, epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。

(1).创建一个 epoll 句柄

/**
 * @descrption:         创建一个 epoll 句柄
 * @param - size    :   从Linux2.6.8开始,此参数已经没有意义了,随便传入一个大于 0 的数就可以
 * @return          :   成功时返回(epoll 句柄),失败则返回(-1)
 */
int epoll_create(int size)

(2).epoll 操作指令 - epoll_ctl

函数原型:

/**
 * @description:        向 epoll 句柄中添加要监视的文件描述符以及关心的事件
 * @param - epfd    :   要操作的 epoll 句柄
 * @param - op      :   要对 epfd 进行的操作
 * @param - fd      :   要监视的文件描述符
 * @param - event   :   要监视的事件类型
 */
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)


/* epoll_event 结构体 */
struct epoll_event
{
    uint32_t events;        //epoll 事件
    epoll_data_t data;      //用户数据
};

其中 op 对应的操作可以为以下所示:

EPOLL_CTL_ADD         向 epfd 添加文件参数 fd 表示的描述符。
EPOLL_CTL_MOD         修改参数 fd 的 event 事件。
EPOLL_CTL_DEL         从 epfd 中删除 fd 描述符。

要监视的事件类型可以为以下所示:

EPOLLIN             有数据可以读取。
EPOLLOUT            可以写数据。
EPOLLPRI            有紧急的数据需要读取。
EPOLLERR            指定的文件描述符发生错误。
EPOLLHUP            指定的文件描述符挂起。
EPOLLET             设置 epoll 为边沿触发,默认触发模式为水平触发。
EPOLLONESHOT        一次性的监视,当监视完成以后还需要再次监视某个 fd,那么就需要将
                    fd 重新添加到 epoll 里面

(3).等待事件发生 - epoll_wait

函数原型

/**
 * @description:                等待事件发生
 * @param - epfd        :       epoll 句柄
 * @param - events      :       指向 epoll_events 结构体的数组,当有事件发生时,Linux内核会填写 events,用户可通过 events 判断发生了哪些事件
 * @param - maxevents   :       events 数组的大小,必须 > 0
 * @param - timeout     :       超时事件,单位为ms
 * @return              :       超时则返回(0),失败则返回(-1),成功则返回就绪的文件描述符的数量    
 */
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout)


4.Linux 驱动程序下的 poll 操作函数

(1).驱动程序下的 poll 函数

函数原型

/**
 * @description:            驱动程序中的 poll 函数
 * @param - filp     :      文件描述符
 * @param - wait     :      结构体 poll_table_struct 类型指针,由应用程序传递进来的,一般将此参数传递给 poll_wait 函数  
 * @return           :      POLLIN 有数据可以读取。
                            POLLPRI 有紧急的数据需要读取。
                            POLLOUT 可以写数据。
                            POLLERR 指定的文件描述符发生错误。
                            POLLHUP 指定的文件描述符挂起。
                            POLLNVAL 无效的请求。
                            POLLRDNORM 等同于 POLLIN,普通数据可读
 */
unsigned int (*poll)(struct file *filp,struct poll_table_struct *wait)

(2).将应用程序添加到 poll_table 中 - poll_wait

函数原型

/**
 * @description:            将应用程序添加到 poll_table 中
 * @param - filp    :       应用程序传递的文件描述符
 * @param - wait_address:   要添加到 poll_table 中的等待队列头
 * @param - p       :       poll_table ,就是 file_operations 中 poll 函数的 wait 参数
 */
void poll_wait(struct file *filp,wait_queue_head_t *wait_address,poll_table *p)

四.利用等待队列实现阻塞IO

1.设备树

(1).流程图

(2).设备树代码

2.驱动部分

(1).流程图

(2).驱动代码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>



#define IMX6UIRQ_CNT    1                       /* 设备号个数 */
#define IMX6UIRQ_NAME   "blockio"               /* 设备名字 */
#define KEY0VALUE       0X01                    /* KEY0按键值 */
#define INVAKEY         0XFF                    /* 无效的按键值 */
#define KEY_NUM         1                       /* 按键数量 */



/* 中断 IO 描述结构体 */
struct irq_keydesc
{
    int gpio;                               /* gpio */
    int irqnum;                             /* 中断号 */
    unsigned char value;                    /* 按键对应的键值 */
    char name[10];                          /* 名字 */
    irqreturn_t (*handler)(int,void *);     /* 指向中断服务函数的函数指针 */
};

/* imx6uirq 设备结构体 */
struct imx6uirq_dev
{
    dev_t devid;                            /* 设备号 */
    struct cdev cdev;                       /* cdev */
    struct class *class;                    /* 类 */
    struct device *device;                  /* 设备 */
    int major;                              /* 主设备号 */
    int minor;                              /* 次设备号 */
    struct device_node *nd;                 /* 设备结点 */
    atomic_t keyvalue;                      /* 有效的按键值 */
    atomic_t releasekey;                    /* 标记是否完成一次完整的按键动作 */
    struct timer_list timer;                /* 定义一个定时器 */
    struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键中断信息描述数组 */
    unsigned char curkeynum;                /* 当前的按键号 */

    wait_queue_head_t r_wait;               /* 读等待队列头 */
};


/* irq 设备 */
struct imx6uirq_dev imx6uirq;



/**
 * @description:                KEY0 按键中断服务函数,开启定时器,延时 10 ms,定时器用于按键消抖
 * @param - irq     :           中断号
 * @param - dev_id  :           设备结构
 * @return          :           中断执行结果
 */
static irqreturn_t key0_handler(int irq,void *dev_id)
{
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;

    /* 表示按键中断触发(双边沿触发) (因为按键平时为上拉状态) */
    dev->curkeynum = 0;
    dev->timer.data = (volatile long)dev_id;

    /* 2.让定时器回调函数 10 ms 后触发 */
    mod_timer(&dev->timer,jiffies + msecs_to_jiffies(10));          

    return IRQ_RETVAL(IRQ_HANDLED);
}


/**
 * @description:                定时器服务函数,用于按键消抖,定时器到了以后再次读取按键值,
 *                              如果按键还是处于按下的状态就表示按键动作有效
 * @param - arg     :           设备结构体变量
 * @return          :           无
 */
void timer_function(unsigned long arg)
{
    unsigned char value;
    unsigned char num;
    struct irq_keydesc *keydesc;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

    /* 获取上一刻的按键状态 */
    num = dev->curkeynum;
    keydesc = &dev->irqkeydesc[num];

    /* 读取当前 gpio 的状态 */
    value = gpio_get_value(keydesc->gpio);
    /* 若此刻 gpio 的状态为低电平 , 则消抖后确认按键确实是按下了 (因为这个定时器服务函数是在按键中断之后触发的) */
    if(0 == value)
    {
        atomic_set(&dev->keyvalue,keydesc->value);
    }
    /* 若此刻 gpio 的状态为高电平 , 则消抖后确认按键确实是松开了 (因为这个定时器服务函数是在按键中断之后触发的) */
    else
    {
        atomic_set(&dev->keyvalue, 0x80 | keydesc->value);      //最高为置 1 
        atomic_set(&dev->releasekey, 1);                        //标记松开按键
    }

    /* 若完成了一次按键动作,则唤醒进程 */
    if(atomic_read(&dev->releasekey))
    {
        wake_up_interruptible(&dev->r_wait);
    }
}



/**
 * @description:                按键 IO 初始化
 * @param           :           无
 * @return          :           成功返回(0),返回其他则为失败
 */
static int keyio_init(void)
{
    unsigned char i = 0;
    int ret = 0;

    /* 1.获取设备结点 */
    imx6uirq.nd = of_find_node_by_path("/key");
    if(NULL == imx6uirq.nd)
    {
        printk("key node not find!\r\n");
        return -EINVAL;
    }

    /* 2.获取 KEY 的 GPIO 编号 */
    for(i = 0;i < KEY_NUM;i++)
    {
        imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd,"key-gpio",i);
        /* 若获取 gpio 编号失败 */
        if(imx6uirq.irqkeydesc[i].gpio < 0)
        {
            printk("can not get key%d\r\n",i);
        }
    }

    
    for(i = 0;i < KEY_NUM;i++)
    {
        memset(imx6uirq.irqkeydesc[i].name,0,sizeof(imx6uirq.irqkeydesc[i].name));
        sprintf(imx6uirq.irqkeydesc[i].name,"KEY%d",i);

        /* 3.申请 GPIO */
        gpio_request(imx6uirq.irqkeydesc[i].gpio,imx6uirq.irqkeydesc[i].name);

        /* 4.设置 IO 为输入 */
        gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);

        /* 5.获取 GPIO 的中断号 */
        imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd,i);

#if 0
        /* 若使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应中断号 */
        imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
#endif

        printk("key : %d ; gpio = %d , irqnum = %d\r\n",i,imx6uirq.irqkeydesc[i].gpio,imx6uirq.irqkeydesc[i].irqnum);
    }


    /* 6.申请中断 */
    imx6uirq.irqkeydesc[0].handler = key0_handler;
    imx6uirq.irqkeydesc[0].value = KEY0VALUE;
    for(i = 0;i < KEY_NUM;i++)
    {
        ret = request_irq(imx6uirq.irqkeydesc[i].irqnum,
                          imx6uirq.irqkeydesc[i].handler,
                          IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                          imx6uirq.irqkeydesc[i].name,
                          &imx6uirq);
    }
    
    
    /* 7.创建定时器 */
    init_timer(&imx6uirq.timer);
    imx6uirq.timer.function = timer_function;


    /* 8.初始化等待队列头 */
    init_waitqueue_head(&imx6uirq.r_wait);

    return 0;
}


/**
 * @description:            打开设备
 * @param - inode   :       传递给驱动的 inode
 * @param - filp    :       设备文件
 * @return          :       0 为成功 , 其他为失败
 */
static int imx6uirq_open(struct inode *inode,struct file *filp)
{
    /* 设置私有数据 */
    filp->private_data = &imx6uirq;

    return 0;
}


/**
 * @description:            从设备读取数据
 * @param - filp    :       文件描述符
 * @param - buf     :       返回给用户空间的数据缓冲区
 * @param - cnt     :       要读取的字节数
 * @param - offt    :       相对于文件首地址的偏移量
 * @return          :       成功读取的字节数,如果为负值,则表示失败
 */
static ssize_t imx6uirq_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt)
{
    int ret = 0;
    unsigned char keyvalue = 0;
    unsigned char releasekey = 0;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;


    /* 定义一个等待队列 */
    DECLARE_WAITQUEUE(wait,current);

    /* 若没有按键按下,则添加到等待队列头,此进程进入休眠 */
    if(0 == atomic_read(&dev->releasekey))
    {
        add_wait_queue(&dev->r_wait,&wait);         //添加到等待队列头
        __set_current_state(TASK_INTERRUPTIBLE);    //设置任务状态为可被信号打断
        schedule();                                 //进行一次任务切换
        
        /* 若此进程是由信号唤醒,则返回错误信息 */
        if(signal_pending(current))
        {
            ret = -ERESTARTSYS;
            goto wait_error;
        }

        /* 若唤醒时不是有信号唤醒,而是以中断唤醒,则继续往下读取键值 */
        __set_current_state(TASK_RUNNING);          //设置为运行状态
        remove_wait_queue(&dev->r_wait,&wait);      //将等待队列移除

    }


    /* 读取消抖后的键值 */
    keyvalue = atomic_read(&dev->keyvalue);
    releasekey = atomic_read(&dev->releasekey);

    /* 如果有按键按下 */
    if(releasekey)
    {
        if(keyvalue & 0x80)
        {
            keyvalue &= ~0x80;      //将 keyvalue 最高为清零
            ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue));
        }
        else
        {
            goto data_error;
        }

        /* 清除按下标志 */
        atomic_set(&dev->releasekey,0); 
    }
    else
    {   
        goto data_error;
    }

    return 0;

wait_error:
    set_current_state(TASK_RUNNING);        //设置任务为运行状态
    remove_wait_queue(&dev->r_wait,&wait);  //将等待队列移除
    return ret;

data_error:
    return -EINVAL;
}


/* 设备操作函数 */
static struct file_operations imx6uirq_fops = 
{
    .owner = THIS_MODULE,
    .open = imx6uirq_open,
    .read = imx6uirq_read,
};


/**
 * @description:            驱动入口函数
 * @param           :       无
 * @return          :       无
 */
static int __init imx6uirq_init(void)
{
    /* 1.创建设备号 */
    if(imx6uirq.major)          //若定义了设备号
    {
        imx6uirq.devid = MKDEV(imx6uirq.major,0);
        register_chrdev_region(imx6uirq.devid,IMX6UIRQ_CNT,IMX6UIRQ_NAME);
    }
    else                        //若没有定义设备号
    {
        alloc_chrdev_region(&imx6uirq.devid,0,IMX6UIRQ_CNT,IMX6UIRQ_NAME);
        imx6uirq.major = MAJOR(imx6uirq.devid);
        imx6uirq.minor = MINOR(imx6uirq.devid);
    }

    /* 2.初始化cdev */
    cdev_init(&imx6uirq.cdev,&imx6uirq_fops);

    /* 3,添加一个cdev */
    cdev_add(&imx6uirq.cdev,imx6uirq.devid,IMX6UIRQ_CNT);

    /* 4.创建类 */
    imx6uirq.class = class_create(THIS_MODULE,IMX6UIRQ_NAME);
    if(IS_ERR(imx6uirq.class))
    {
        return PTR_ERR(imx6uirq.class);
    }

    /* 5.创建设备 */
    imx6uirq.device = device_create(imx6uirq.class,NULL,imx6uirq.devid,NULL,IMX6UIRQ_NAME);
    if(IS_ERR(imx6uirq.device))
    {
        return PTR_ERR(imx6uirq.device);
    }

    /* 6.初始化按键 */
    atomic_set(&imx6uirq.keyvalue,INVAKEY);
    atomic_set(&imx6uirq.releasekey,0);
    keyio_init();

    return 0;
}


/**
 * @description:            驱动出口函数
 * @param           :       无
 * @return          :       无
 */
static void __exit imx6uirq_exit(void)
{
    unsigned int i = 0;

    /* 1.删除定时器 */
    del_timer_sync(&imx6uirq.timer);

    /* 2.释放 中断 与 GPIO */
    for(i = 0;i < KEY_NUM;i++)
    {
        free_irq(imx6uirq.irqkeydesc[i].irqnum,&imx6uirq);
        gpio_free(imx6uirq.irqkeydesc[i].gpio);
    }

    /* 3. 注销字符设备驱动 */
    cdev_del(&imx6uirq.cdev);
    unregister_chrdev_region(imx6uirq.devid,IMX6UIRQ_CNT);
    device_destroy(imx6uirq.class,imx6uirq.devid);
    class_destroy(imx6uirq.class);
}


module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kaneki");



(3).应用程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


int main(int argc, char *argv[])
{
    int fd,ret;
    char *filename;
    unsigned char data;

    if(2 != argc)
    {
        printf("Usage : ./%s <devpath>",argv[0]);
        return -1;
    }

    filename = argv[1];

    fd = open(filename,O_RDONLY);
    if(0 > fd)
    {
        perror("open error");
        return -1;
    }

    while(1)
    {
        ret = read(fd,&data,sizeof(data));
        if(0 > ret)
        {
            
        }

        else
        {
            if(data)        /* 读取到数据 */
            {
                printf("key value = %#X\r\n",data);
            }
        }
    }

    close(fd);

    return ret;
}

Makefile:   

KERNELDIR := /home/linux/IMX6ULL/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH :=$(shell pwd)
obj-m := blockio.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

(4).实验现象

实验现象

        可以看到,应用程序在 while(1) 里面一直都在调用 read ,但占用 CPU 的资源很少

五.利用 poll 实现非阻塞 IO 

1.设备树

(1).流程图

(2).设备树代码

2.驱动部分

(1).流程图

(2).驱动代码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>



#define IMX6UIRQ_CNT    1                       /* 设备号个数 */
#define IMX6UIRQ_NAME   "nonblockio"               /* 设备名字 */
#define KEY0VALUE       0X01                    /* KEY0按键值 */
#define INVAKEY         0XFF                    /* 无效的按键值 */
#define KEY_NUM         1                       /* 按键数量 */



/* 中断 IO 描述结构体 */
struct irq_keydesc
{
    int gpio;                               /* gpio */
    int irqnum;                             /* 中断号 */
    unsigned char value;                    /* 按键对应的键值 */
    char name[10];                          /* 名字 */
    irqreturn_t (*handler)(int,void *);     /* 指向中断服务函数的函数指针 */
};

/* imx6uirq 设备结构体 */
struct imx6uirq_dev
{
    dev_t devid;                            /* 设备号 */
    struct cdev cdev;                       /* cdev */
    struct class *class;                    /* 类 */
    struct device *device;                  /* 设备 */
    int major;                              /* 主设备号 */
    int minor;                              /* 次设备号 */
    struct device_node *nd;                 /* 设备结点 */
    atomic_t keyvalue;                      /* 有效的按键值 */
    atomic_t releasekey;                    /* 标记是否完成一次完整的按键动作 */
    struct timer_list timer;                /* 定义一个定时器 */
    struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键中断信息描述数组 */
    unsigned char curkeynum;                /* 当前的按键号 */

    wait_queue_head_t r_wait;               /* 读等待队列头 */
};


/* irq 设备 */
struct imx6uirq_dev imx6uirq;



/**
 * @description:                KEY0 按键中断服务函数,开启定时器,延时 10 ms,定时器用于按键消抖
 * @param - irq     :           中断号
 * @param - dev_id  :           设备结构
 * @return          :           中断执行结果
 */
static irqreturn_t key0_handler(int irq,void *dev_id)
{
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;

    /* 表示按键中断触发(双边沿触发) (因为按键平时为上拉状态) */
    dev->curkeynum = 0;
    dev->timer.data = (volatile long)dev_id;

    /* 2.让定时器回调函数 10 ms 后触发 */
    mod_timer(&dev->timer,jiffies + msecs_to_jiffies(10));          

    return IRQ_RETVAL(IRQ_HANDLED);
}


/**
 * @description:                定时器服务函数,用于按键消抖,定时器到了以后再次读取按键值,
 *                              如果按键还是处于按下的状态就表示按键动作有效
 * @param - arg     :           设备结构体变量
 * @return          :           无
 */
void timer_function(unsigned long arg)
{
    unsigned char value;
    unsigned char num;
    struct irq_keydesc *keydesc;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

    /* 获取上一刻的按键状态 */
    num = dev->curkeynum;
    keydesc = &dev->irqkeydesc[num];

    /* 读取当前 gpio 的状态 */
    value = gpio_get_value(keydesc->gpio);
    /* 若此刻 gpio 的状态为低电平 , 则消抖后确认按键确实是按下了 (因为这个定时器服务函数是在按键中断之后触发的) */
    if(0 == value)
    {
        atomic_set(&dev->keyvalue,keydesc->value);
    }
    /* 若此刻 gpio 的状态为高电平 , 则消抖后确认按键确实是松开了 (因为这个定时器服务函数是在按键中断之后触发的) */
    else
    {
        atomic_set(&dev->keyvalue, 0x80 | keydesc->value);      //最高为置 1 
        atomic_set(&dev->releasekey, 1);                        //标记松开按键
    }

    /* 若完成了一次按键动作,则唤醒进程 */
    if(atomic_read(&dev->releasekey))
    {
        wake_up_interruptible(&dev->r_wait);
    }
}



/**
 * @description:                按键 IO 初始化
 * @param           :           无
 * @return          :           成功返回(0),返回其他则为失败
 */
static int keyio_init(void)
{
    unsigned char i = 0;
    int ret = 0;

    /* 1.获取设备结点 */
    imx6uirq.nd = of_find_node_by_path("/key");
    if(NULL == imx6uirq.nd)
    {
        printk("key node not find!\r\n");
        return -EINVAL;
    }

    /* 2.获取 KEY 的 GPIO 编号 */
    for(i = 0;i < KEY_NUM;i++)
    {
        imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd,"key-gpio",i);
        /* 若获取 gpio 编号失败 */
        if(imx6uirq.irqkeydesc[i].gpio < 0)
        {
            printk("can not get key%d\r\n",i);
        }
    }

    
    for(i = 0;i < KEY_NUM;i++)
    {
        memset(imx6uirq.irqkeydesc[i].name,0,sizeof(imx6uirq.irqkeydesc[i].name));
        sprintf(imx6uirq.irqkeydesc[i].name,"KEY%d",i);

        /* 3.申请 GPIO */
        gpio_request(imx6uirq.irqkeydesc[i].gpio,imx6uirq.irqkeydesc[i].name);

        /* 4.设置 IO 为输入 */
        gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);

        /* 5.获取 GPIO 的中断号 */
        imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd,i);

#if 0
        /* 若使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应中断号 */
        imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
#endif

        printk("key : %d ; gpio = %d , irqnum = %d\r\n",i,imx6uirq.irqkeydesc[i].gpio,imx6uirq.irqkeydesc[i].irqnum);
    }


    /* 6.申请中断 */
    imx6uirq.irqkeydesc[0].handler = key0_handler;
    imx6uirq.irqkeydesc[0].value = KEY0VALUE;
    for(i = 0;i < KEY_NUM;i++)
    {
        ret = request_irq(imx6uirq.irqkeydesc[i].irqnum,
                          imx6uirq.irqkeydesc[i].handler,
                          IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                          imx6uirq.irqkeydesc[i].name,
                          &imx6uirq);
    }
    
    
    /* 7.创建定时器 */
    init_timer(&imx6uirq.timer);
    imx6uirq.timer.function = timer_function;


    /* 8.初始化等待队列头 */
    init_waitqueue_head(&imx6uirq.r_wait);

    return 0;
}


/**
 * @description:            打开设备
 * @param - inode   :       传递给驱动的 inode
 * @param - filp    :       设备文件
 * @return          :       0 为成功 , 其他为失败
 */
static int imx6uirq_open(struct inode *inode,struct file *filp)
{
    /* 设置私有数据 */
    filp->private_data = &imx6uirq;

    return 0;
}


/**
 * @description:            从设备读取数据
 * @param - filp    :       文件描述符
 * @param - buf     :       返回给用户空间的数据缓冲区
 * @param - cnt     :       要读取的字节数
 * @param - offt    :       相对于文件首地址的偏移量
 * @return          :       成功读取的字节数,如果为负值,则表示失败
 */
static ssize_t imx6uirq_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt)
{
    int ret = 0;
    unsigned char keyvalue = 0;
    unsigned char releasekey = 0;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

    /* 定义一个等待队列 */
    DECLARE_WAITQUEUE(wait,current);

    /* 1.判断是不是以非阻塞访问 */
    if(filp->f_flags & O_NONBLOCK)
    {
        /* 若没有按键按下,没有则返回错误信息,按下了则继续往后读取键值 */
        if(0 == atomic_read(&dev->releasekey))
            return -EAGAIN;
    }


    /* 读取消抖后的键值 */
    keyvalue = atomic_read(&dev->keyvalue);
    releasekey = atomic_read(&dev->releasekey);

    /* 如果有按键按下 */
    if(releasekey)
    {
        if(keyvalue & 0x80)
        {
            keyvalue &= ~0x80;      //将 keyvalue 最高为清零
            ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue));
        }
        else
        {
            goto data_error;
        }

        /* 清除按下标志 */
        atomic_set(&dev->releasekey,0); 
    }
    else
    {   
        goto data_error;
    }

    return 0;


data_error:
    return -EINVAL;
}



/**
 * @description:            poll 函数,用于处理非阻塞访问
 */
unsigned int imx6uirq_poll(struct file *filp,struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

    /* 将应用程序添加到 poll_table 中 */
    poll_wait(filp,&dev->r_wait,wait);


    /* 若按键按下则返回事件 */
    if(atomic_read(&dev->releasekey))
    {
        mask = POLLIN | POLLRDNORM;        //返回POLLIN
    }

    return mask;
}


/* 设备操作函数 */
static struct file_operations imx6uirq_fops = 
{
    .owner = THIS_MODULE,
    .open = imx6uirq_open,
    .read = imx6uirq_read,
    .poll = imx6uirq_poll,
};


/**
 * @description:            驱动入口函数
 * @param           :       无
 * @return          :       无
 */
static int __init imx6uirq_init(void)
{
    /* 1.创建设备号 */
    if(imx6uirq.major)          //若定义了设备号
    {
        imx6uirq.devid = MKDEV(imx6uirq.major,0);
        register_chrdev_region(imx6uirq.devid,IMX6UIRQ_CNT,IMX6UIRQ_NAME);
    }
    else                        //若没有定义设备号
    {
        alloc_chrdev_region(&imx6uirq.devid,0,IMX6UIRQ_CNT,IMX6UIRQ_NAME);
        imx6uirq.major = MAJOR(imx6uirq.devid);
        imx6uirq.minor = MINOR(imx6uirq.devid);
    }

    /* 2.初始化cdev */
    cdev_init(&imx6uirq.cdev,&imx6uirq_fops);

    /* 3,添加一个cdev */
    cdev_add(&imx6uirq.cdev,imx6uirq.devid,IMX6UIRQ_CNT);

    /* 4.创建类 */
    imx6uirq.class = class_create(THIS_MODULE,IMX6UIRQ_NAME);
    if(IS_ERR(imx6uirq.class))
    {
        return PTR_ERR(imx6uirq.class);
    }

    /* 5.创建设备 */
    imx6uirq.device = device_create(imx6uirq.class,NULL,imx6uirq.devid,NULL,IMX6UIRQ_NAME);
    if(IS_ERR(imx6uirq.device))
    {
        return PTR_ERR(imx6uirq.device);
    }

    /* 6.初始化按键 */
    atomic_set(&imx6uirq.keyvalue,INVAKEY);
    atomic_set(&imx6uirq.releasekey,0);
    keyio_init();

    return 0;
}


/**
 * @description:            驱动出口函数
 * @param           :       无
 * @return          :       无
 */
static void __exit imx6uirq_exit(void)
{
    unsigned int i = 0;

    /* 1.删除定时器 */
    del_timer_sync(&imx6uirq.timer);

    /* 2.释放 中断 与 GPIO */
    for(i = 0;i < KEY_NUM;i++)
    {
        free_irq(imx6uirq.irqkeydesc[i].irqnum,&imx6uirq);
        gpio_free(imx6uirq.irqkeydesc[i].gpio);
    }

    /* 3. 注销字符设备驱动 */
    cdev_del(&imx6uirq.cdev);
    unregister_chrdev_region(imx6uirq.devid,IMX6UIRQ_CNT);
    device_destroy(imx6uirq.class,imx6uirq.devid);
    class_destroy(imx6uirq.class);
}


module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kaneki");



(3).应用程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <sys/time.h>





int main(int argc, char *argv[])
{
    int fd,ret;
    char *filename;
    unsigned char data;
    struct pollfd fds;          //要监视的文件描述符集合和要监视的事件
    fd_set readfds;             //定义读操作文件描述符集
    struct timeval timeout;     //超时时间


    if(2 != argc)
    {
        printf("Usage : ./%s <devpath>",argv[0]);
        return -1;
    }

    filename = argv[1];

    fd = open(filename,O_RDONLY | O_NONBLOCK);
    if(0 > fd)
    {
        perror("open error");
        return -1;
    }


    /* 构造结构体 */
    fds.fd = fd;            //设置要监视的文件描述符
    fds.events = POLLIN;    //设置要监视的事件


    while(1)
    {
        ret = poll(&fds,1,500);
        /* 若数据有效 */
        if(ret)
        {
            ret = read(fd,&data,sizeof(data));
            if(0 > ret)
            {
                /* 读取错误  */
            }
            else
            {
                if(data)
                {
                    printf("key value = %d\r\n",data);
                }
            }
        }
        /* 若超时 */
        else if(0 == ret)
        {
            /* 用户自定义超时处理 */
        }
        else if(0 > ret)
        {
            /* 用户自定义错误处理 */
        }
    }

    close(fd);

    return ret;
}

(4).实验现象

        同样可以发现,利用 poll 也可以使应用程序占用 CPU 资源很少


原文地址:https://blog.csdn.net/kaneki_lh/article/details/142724296

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!