阻塞和非阻塞IO


1.阻塞和非阻塞IO

1.1 简介

阻塞IO:当应用程序申请不到内核资源时,程序进入阻塞状态直至被唤醒

代码如下图所示

非阻塞IO:当申请不到内核资源时,会轮询访问,此时CPU忙

代码如下图所示

1.2 等待队列

等待队列头

阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。
Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t 表示,wait_queue_head_t 结构体定义在文件include/linux/wait.h 中,结构体内容如下所示:

定义好等待队列头以后需要初始化,使用 init_waitqueue_head 函数初始化等待队列头,函数原型如下:
void init_waitqueue_head(wait_queue_head_t *q)
参数 q 就是要初始化的等待队列头。
也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化。

等待队列项

等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体 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;

使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
DECLARE_WAITQUEUE(name, tsk)
name 就是等待队列项的名字,tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current , 在 Linux 内核中current相当于一个全局变量,表示当前进程。因此宏DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。

将队列项添加/移除等待队列头

当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,等待队列项添加 API 函数如下:

函数参数和返回值含义如下:
q:等待队列项要加入的等待队列头。
wait:要加入的等待队列项。
返回值:无。
等待队列项移除 API 函数如下:

函数参数和返回值含义如下:
q:要删除的等待队列项所处的等待队列头。
wait:要删除的等待队列项。
返回值:无。

等待唤醒

当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:

参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。

等待事件

除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒
待队列中的进程,和等待事件有关的 API 函数如表所示:

1.3 轮询

如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。poll、epoll 和 select 可以用于处理轮询,应用程序通过 select、epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。我们先来看一下应用程序中使用的 select、poll 和 epoll 这三个函数。

select


函数参数和返回值含义如下:
nfds:所要监视的这三类文件描述集合中,最大文件描述符加 1。
readfds、writefds 和 exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些描述符、需要满足哪些条件等等,这三个参数都是 fd_set 类型的,fd_set 类型变量的每一个位都代表了一个文件描述符。readfds 用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时。可以将 readfs设置为 NULL,表示不关心任何文件的读变化。writefds 和 readfs 类似,只是 writefs 用于监视这些文件是否可以进行写操作。exceptfds 用于监视这些文件的异常。
比如我们现在要从一个设备文件中读取数据,那么就可以定义一个 fd_set 变量,这个变量要传递给参数 readfds。当我们定义好一个 fd_set 变量以后可以使用如下所示几个宏进行操作:

FD_ZERO 用于将 fd_set 变量的所有位都清零,FD_SET 用于将 fd_set 变量的某个位置 1,也就是向 fd_set 添加一个文件描述符,参数 fd 就是要加入的文件描述符。FD_CLR 用于将 fd_set变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述符。FD_ISSET 用于测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符。
timeout:超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时间使用结构体 timeval 表示,结构体定义如下所示:

当 timeout 为 NULL 的时候就表示无限期的等待。
返回值:0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作;-1,发生错误;其他值,可以进行操作的文件描述符个数。
使用 select 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:

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

poll

在单个线程中,select 函数能够监视的文件描述符数量有最大的限制,一般为 1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用 poll 函数,poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制,Linux 应用程序中 poll 函数原型如下所示:

函数参数和返回值含义如下:
fds:要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd类型的,pollfd 结构体如下所示:

fd 是要监视的文件描述符,如果 fd 无效的话那么 events 监视事件也就无效,并且 revents返回 0。events 是要监视的事件,可监视的事件类型如下所示:

revents 是返回参数,也就是返回的事件,由 Linux 内核设置具体的返回事件。
nfds:poll 函数要监视的文件描述符数量。
timeout:超时时间,单位为 ms。
返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述符数量;0,超时;-1,发生错误,并且设置 errno 为错误类型。
使用 poll 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:

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) { /* 错误 */
		......
	}
}

epoll

传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此,epoll应运而生,epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。应用程序需要先使用 epoll_create 函数创建一个 epoll 句柄,epoll_create 函数原型如下:
int epoll_create(int size)
函数参数和返回值含义如下:
size:从 Linux2.6.8 开始此参数已经没有意义了,随便填写一个大于 0 的值就可以。
返回值:epoll 句柄,如果为-1 的话表示创建失败。
epoll 句柄创建成功以后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件,epoll_ctl 函数原型如下所示:

函数参数和返回值含义如下:
epfd:要操作的 epoll 句柄,也就是使用 epoll_create 函数创建的 epoll 句柄。
op:表示要对 epfd(epoll 句柄)进行的操作,可以设置为:

fd:要监视的文件描述符。
event:要监视的事件类型,为 epoll_event 结构体类型指针,epoll_event 结构体类型如下所示:


一切都设置好以后应用程序就可以通过 epoll_wait 函数来等待事件的发生,类似 select 函数。epoll_wait 函数原型如下所示:

函数参数和返回值含义如下:
epfd:要等待的 epoll。
events:指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events,调用者可以根据 events 判断发生了哪些事件。
maxevents:events 数组大小,必须大于 0。
timeout:超时时间,单位为 ms。
返回值:0,超时;-1,错误;其他值,准备就绪的文件描述符数量。
epoll 更多的是用在大规模的并发服务器上,因为在这种场合下 select 和 poll 并不适合。当设计到的文件描述符(fd)比较少的时候就适合用 selcet 和 poll,本章我们就使用 sellect 和 poll 这两个函数。

1.4 poll操作函数

当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations 操作集中的 poll 函数就会执行。所以驱动程序的编写者需要提供对应的 poll 函数,poll 函数原型如下所示:

wait:结构体 poll_table_struct 类型指针,由应用程序传递进来的。一般将此参数传递给poll_wait 函数。
我们需要在驱动程序的 poll 函数中调用 poll_wait 函数,poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中,poll_wait 函数原型如下:

2.阻塞IO实验

2.1 驱动代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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 *);    /* 中断服务函数 */
};

/* irq设备结构体 */
struct imx6uirq_dev {
    dev_t               devid;          /* 设备号 */
    struct 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;          /* 定义一个(读)等待队列头 */
};

struct imx6uirq_dev  irqDev; /* 定义LED结构体 */

/* @description : 中断服务函数,开启定时器,延时 10ms,
* 定时器用于按键消抖。
* 两个参数是中断处理函数的必须写法
* @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;

    /* 采用定时器削抖,如果再定时器时间内还是这个值,说明是真的按下了,在定时器中断中处理 */
    /* 这里设置为0是简易处理,因为只有一个按键 */
    /* 有其他按键要再建一个中断处理函数,并把curkeynum改成相应的按键值 */
    /* 注意不能所有按键用一个中断函数,第一是一起按的时候会出错 */
    /* 第二,无法用curkeynum判断使用的是第几个按键 */
    dev->curkeynum = 0;
    /* 传递给定时器的参数,注意要强转,在中断处理函数里面再转回来 */
    dev->timer.data = (volatile long)dev_id;
    /* mod_timer会启动定时器,第二个参数是要修改的超时时间 */
    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; 

    /* 因为只有一个按键,这里是0 */
    num = dev->curkeynum;
    keydesc = &dev->irqkeydesc[num]; 
    value = gpio_get_value(keydesc->gpio); /* 读取 IO 值 */

    if(value == 0){ /* 按下按键 */
        atomic_set(&dev->keyvalue, keydesc->value);
    }
    else{ /* 按键松开 */
        /* 这种情况是按下再松开的松开,使用keyValue加上releaseKey */
        /* 没按下的话, releasekey一直为0*/
        atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
        atomic_set(&dev->releasekey, 1); /* 标记松开按键 */ 
    }

    /* 中断处理之后可以唤醒进程 */
    if(atomic_read(&dev->releasekey)) { /* 完成一次按键过程,没按下还是唤不醒 */
    /* wake_up(&dev->r_wait); */
        wake_up_interruptible(&dev->r_wait);
    } 
}

/*
* @description : 按键 IO 初始化
* @param : 无
* @return : 无
*/
static int keyio_init(void)
{
    unsigned char i = 0;

    int ret = 0;

    /* 1.获取key节点 */
    irqDev.nd = of_find_node_by_path("/key");
    if (irqDev.nd== NULL){
        printk("key node not find!\r\n");
        return -EINVAL;
    }
    /* 对每个按键都提取 GPIO */
    for (i = 0; i < KEY_NUM; i++) {
        irqDev.irqkeydesc[i].gpio = of_get_named_gpio(irqDev.nd, "key-gpios", i);
        if (irqDev.irqkeydesc[i].gpio < 0) {
            printk("can't get key%d\r\n", i);
        }
    }

    /* 初始化 key 所使用的 IO,并且设置成中断模式 */
    for (i = 0; i < KEY_NUM; i++) {
        /* 先对每一个IO命名 */
        /* 先对命名清0 */
        memset(irqDev.irqkeydesc[i].name, 0, sizeof(irqDev.irqkeydesc[i].name)); 
        /* 给IO命名 */
        sprintf(irqDev.irqkeydesc[i].name, "KEY%d", i); 
        /* 请求GPIO */
        gpio_request(irqDev.irqkeydesc[i].gpio, irqDev.irqkeydesc[i].name);
        
        /* 设置GPIO为输入 */
        gpio_direction_input(irqDev.irqkeydesc[i].gpio); 

        /* 获取中断号,以下为两个方法,都可以获取到 */
        /* 从interrupts属性里面获取 */
        /* 注意i是根据设备树里面设置了多少个就是多少个,都会获取到 */
        /* 下面的方法是通用的获取中断号的函数 */
        irqDev.irqkeydesc[i].irqnum = irq_of_parse_and_map(irqDev.nd, i);
#if 0
        /* 此方法是gpio获取中断号的方法 */
        irqDev.irqkeydesc[i].irqnum = gpio_to_irq(irqDev.irqkeydesc[i].gpio);
#endif
        printk("key%d:gpio=%d, irqnum=%d\r\n", i, irqDev.irqkeydesc[i].gpio,
                                                  irqDev.irqkeydesc[i].irqnum);
    }

    /* 2. 按键中断初始化 */
    /* 设置中断处理函数和按键初始值 */
    /* 因为只有一个key0.,所以这里也没用循环 */
    irqDev.irqkeydesc[0].handler = key0_handler;
    irqDev.irqkeydesc[0].value = KEY0VALUE;
     /* 申请中断 */
    for (i = 0; i < KEY_NUM; i++) {
        /* request_irq参数
         * 中断号,中断函数,中断触发类型,中断名字,传递给中断处理函数的参数(第二个),这里传的结构体
         * */
        ret = request_irq(irqDev.irqkeydesc[i].irqnum, irqDev.irqkeydesc[i].handler,
                        IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
                        irqDev.irqkeydesc[i].name, &irqDev);
        if(ret < 0){
            printk("irq %d request failed!\r\n", irqDev.irqkeydesc[i].irqnum);
            return -EFAULT;
        }
    }

    /* 3. 创建定时器 */
    init_timer(&irqDev.timer);
    irqDev.timer.function = timer_function;
    /* 注意下面不能让定时器运行,因为要按下按键之后再运行 */
    /* 启动定时器通过mod_timer启动,通常在初始化阶段的定时器用的是add_timer */

    /* 初始化等待队列头 */
    init_waitqueue_head(&irqDev.r_wait);
    return 0;
}


static int imx6uirq_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &irqDev;
    return 0;
}

static int imx6uirq_release(struct inode *inode, struct file *filp)
{
    //struct imx6uirq_dev  *dev = (struct imx6uirq_dev  *)filp->private_data;
    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;

/* 注意使用wait_event可以有同样的效果,但是不可以被信号打断了 */
#if 0
    /* 加入等待队列,等待被唤醒,也就是有按键按下的时候 */
    /* 此时应用函数里面虽然有while循环,但是CPU使用率会降很低 */
    /* 注意如果没有唤醒,还是一直在这待着,可以(也可以不用)在中断中唤醒 */
    ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));
    if(ret < 0){
        /* 如果失败了,其实还是在挂起状态,需要重新切换到运行态 */
        goto wait_error;
    }
#endif

    /* wait就是队列项,current就是当前进程 */
    DECLARE_WAITQUEUE(wait, current);               /* 定义一个等待队列 */

    /* 如果按键没按下,就进入休眠状态 */
    if(atomic_read(&dev->releasekey) == 0) {        /* 没有按键按下 */
        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; /* 因为中断中或了一个0x80,这里面去掉0x80 */
            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 const struct file_operations imx6uirq_fops = {
    .owner = THIS_MODULE,
    .open = imx6uirq_open,
    .release = imx6uirq_release,
    .read = imx6uirq_read
};

/* 模块入口函数 */
static int __init imx6uirq_init(void)
{
    /* 定义一些所需变量 */
    int ret = 0;

    /* 1. 注册字符设备驱动 */
    irqDev.major = 0;
    if(irqDev.major) {
        irqDev.devid = MKDEV(irqDev.major, 0);
        ret = register_chrdev_region(irqDev.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME );  
    } else {
        alloc_chrdev_region(&irqDev.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME );
        irqDev.major = MAJOR(irqDev.devid);
        irqDev.minor = MINOR(irqDev.devid);
    }
    if(ret < 0){
        goto fail_devid;
    }
    printk("Make devid success! \r\n");
    printk("major = %d, minor = %d \r\n", irqDev.major, irqDev.minor);

    /* 2. 初始化cdev */
    irqDev.cdev.owner = THIS_MODULE;
    cdev_init(&irqDev.cdev, &imx6uirq_fops);
    ret = cdev_add(&irqDev.cdev, irqDev.devid, IMX6UIRQ_CNT);
    if (ret < 0){
        goto fail_cdev;
    } else {
        printk("Cdev add sucess! \r\n");
    }

    /* 3. 自动创建设备节点 */
    irqDev.class = class_create(THIS_MODULE, IMX6UIRQ_NAME );
    if(IS_ERR(irqDev.class)) {
        ret = PTR_ERR(irqDev.class);
        goto fail_class;
    } else {
        printk("Class create sucess! \r\n");
    }

    irqDev.device = device_create(irqDev.class, NULL, irqDev.devid, NULL, IMX6UIRQ_NAME );
    if(IS_ERR(irqDev.device)) {
        ret = PTR_ERR(irqDev.device);
        goto fail_device;
    } else {
        printk("Device create sucess! \r\n");
    }

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

    printk("irqDev init! \r\n");
    return 0;

/* 错误处理 */
fail_device:
    class_destroy(irqDev.class);
fail_class:
    cdev_del(&irqDev.cdev);
fail_cdev:
    unregister_chrdev_region(irqDev.devid, IMX6UIRQ_CNT);
fail_devid:
    return ret;
}

/* 模块出口函数 */
static void __exit imx6uirq_exit(void)
{
    unsigned int i = 0;
    /* 删除定时器 */
    del_timer_sync(&irqDev.timer); 

    /* 释放中断 */
    for (i = 0; i < KEY_NUM; i++) {
        free_irq(irqDev.irqkeydesc[i].irqnum, &irqDev);
    }

    /* 1. 释放设备号 */
    cdev_del(&irqDev.cdev);
    /* 2. 注销设备号 */
    unregister_chrdev_region(irqDev.devid, IMX6UIRQ_CNT);
    /* 3. 摧毁设备 */
    device_destroy(irqDev.class, irqDev.devid);
    /* 4.摧毁类 */
    class_destroy(irqDev.class);


    printk("irqDev exit! \r\n");
}

/* 模块入口和出口注册 */
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Shao Zheming");

2.2 测试代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "linux/ioctl.h"

/* 
 * argc: 应用程序参数个数
 * argv[]: 参数是什么,具体的参数,说明参数是字符串的形式
 * .chrdevbaseApp  <0:1> 0表示关灯,1表示开灯
 * .chrdevbaseApp /dev/led 0 关灯
 * .chrdevbaseApp /dev/led 1 开灯
 * */


int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        printf("Error Usage!\r\n");
        return -1;
    }
    
    int fd, ret;
    char *filename;
    unsigned char data;

    filename = argv[1];
    fd = open(filename, O_RDWR);
    if(fd < 0)
    {
        printf("file %s open failed! \r\n", filename);
        return -1;
    }

    while (1) {
        ret = read(fd, &data, sizeof(data));
        if (ret < 0) { /* 数据读取错误或者无效 */
        
        } else { /* 数据读取正确 */
            if (data) /* 读取到数据 */
                printf("key value = %#X\r\n", data);
        }
    }

    close(fd);
    return 0;
}

2.3 效果

可以看到,CPU使用率降低了好多

杀死进程后

3.非阻塞IO实验

3.1 驱动代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define IMX6UIRQ_CNT            1               /* 设备号个数 */
#define IMX6UIRQ_NAME           "noblockio"        /* 名字 */
#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 *);    /* 中断服务函数 */
};

/* irq设备结构体 */
struct imx6uirq_dev {
    dev_t               devid;          /* 设备号 */
    struct 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;          /* 定义一个(读)等待队列头 */
};

struct imx6uirq_dev  irqDev; /* 定义LED结构体 */

/* @description : 中断服务函数,开启定时器,延时 10ms,
* 定时器用于按键消抖。
* 两个参数是中断处理函数的必须写法
* @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;

    /* 采用定时器削抖,如果再定时器时间内还是这个值,说明是真的按下了,在定时器中断中处理 */
    /* 这里设置为0是简易处理,因为只有一个按键 */
    /* 有其他按键要再建一个中断处理函数,并把curkeynum改成相应的按键值 */
    /* 注意不能所有按键用一个中断函数,第一是一起按的时候会出错 */
    /* 第二,无法用curkeynum判断使用的是第几个按键 */
    dev->curkeynum = 0;
    /* 传递给定时器的参数,注意要强转,在中断处理函数里面再转回来 */
    dev->timer.data = (volatile long)dev_id;
    /* mod_timer会启动定时器,第二个参数是要修改的超时时间 */
    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; 

    /* 因为只有一个按键,这里是0 */
    num = dev->curkeynum;
    keydesc = &dev->irqkeydesc[num]; 
    value = gpio_get_value(keydesc->gpio); /* 读取 IO 值 */

    if(value == 0){ /* 按下按键 */
        atomic_set(&dev->keyvalue, keydesc->value);
    }
    else{ /* 按键松开 */
        /* 这种情况是按下再松开的松开,使用keyValue加上releaseKey */
        /* 没按下的话, releasekey一直为0*/
        atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
        atomic_set(&dev->releasekey, 1); /* 标记松开按键 */ 
    }

    /* 中断处理之后可以唤醒进程 */
    if(atomic_read(&dev->releasekey)) { /* 完成一次按键过程,没按下还是唤不醒 */
    /* wake_up(&dev->r_wait); */
        wake_up_interruptible(&dev->r_wait);
    } 
}

/*
* @description : 按键 IO 初始化
* @param : 无
* @return : 无
*/
static int keyio_init(void)
{
    unsigned char i = 0;

    int ret = 0;

    /* 1.获取key节点 */
    irqDev.nd = of_find_node_by_path("/key");
    if (irqDev.nd== NULL){
        printk("key node not find!\r\n");
        return -EINVAL;
    }
    /* 对每个按键都提取 GPIO */
    for (i = 0; i < KEY_NUM; i++) {
        irqDev.irqkeydesc[i].gpio = of_get_named_gpio(irqDev.nd, "key-gpios", i);
        if (irqDev.irqkeydesc[i].gpio < 0) {
            printk("can't get key%d\r\n", i);
        }
    }

    /* 初始化 key 所使用的 IO,并且设置成中断模式 */
    for (i = 0; i < KEY_NUM; i++) {
        /* 先对每一个IO命名 */
        /* 先对命名清0 */
        memset(irqDev.irqkeydesc[i].name, 0, sizeof(irqDev.irqkeydesc[i].name)); 
        /* 给IO命名 */
        sprintf(irqDev.irqkeydesc[i].name, "KEY%d", i); 
        /* 请求GPIO */
        gpio_request(irqDev.irqkeydesc[i].gpio, irqDev.irqkeydesc[i].name);
        
        /* 设置GPIO为输入 */
        gpio_direction_input(irqDev.irqkeydesc[i].gpio); 

        /* 获取中断号,以下为两个方法,都可以获取到 */
        /* 从interrupts属性里面获取 */
        /* 注意i是根据设备树里面设置了多少个就是多少个,都会获取到 */
        /* 下面的方法是通用的获取中断号的函数 */
        irqDev.irqkeydesc[i].irqnum = irq_of_parse_and_map(irqDev.nd, i);
#if 0
        /* 此方法是gpio获取中断号的方法 */
        irqDev.irqkeydesc[i].irqnum = gpio_to_irq(irqDev.irqkeydesc[i].gpio);
#endif
        printk("key%d:gpio=%d, irqnum=%d\r\n", i, irqDev.irqkeydesc[i].gpio,
                                                  irqDev.irqkeydesc[i].irqnum);
    }

    /* 2. 按键中断初始化 */
    /* 设置中断处理函数和按键初始值 */
    /* 因为只有一个key0.,所以这里也没用循环 */
    irqDev.irqkeydesc[0].handler = key0_handler;
    irqDev.irqkeydesc[0].value = KEY0VALUE;
     /* 申请中断 */
    for (i = 0; i < KEY_NUM; i++) {
        /* request_irq参数
         * 中断号,中断函数,中断触发类型,中断名字,传递给中断处理函数的参数(第二个),这里传的结构体
         * */
        ret = request_irq(irqDev.irqkeydesc[i].irqnum, irqDev.irqkeydesc[i].handler,
                        IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
                        irqDev.irqkeydesc[i].name, &irqDev);
        if(ret < 0){
            printk("irq %d request failed!\r\n", irqDev.irqkeydesc[i].irqnum);
            return -EFAULT;
        }
    }

    /* 3. 创建定时器 */
    init_timer(&irqDev.timer);
    irqDev.timer.function = timer_function;
    /* 注意下面不能让定时器运行,因为要按下按键之后再运行 */
    /* 启动定时器通过mod_timer启动,通常在初始化阶段的定时器用的是add_timer */

    /* 初始化等待队列头 */
    init_waitqueue_head(&irqDev.r_wait);
    return 0;
}


static int imx6uirq_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &irqDev;
    return 0;
}

static int imx6uirq_release(struct inode *inode, struct file *filp)
{
    //struct imx6uirq_dev  *dev = (struct imx6uirq_dev  *)filp->private_data;
    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;

/* 注意使用wait_event可以有同样的效果,但是不可以被信号打断了 */
#if 0
    /* 加入等待队列,等待被唤醒,也就是有按键按下的时候 */
    /* 此时应用函数里面虽然有while循环,但是CPU使用率会降很低 */
    /* 注意如果没有唤醒,还是一直在这待着,可以(也可以不用)在中断中唤醒 */
    ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));
    if(ret < 0){
        /* 如果失败了,其实还是在挂起状态,需要重新切换到运行态 */
        goto wait_error;
    }
#endif
    if(filp->f_flags & O_NONBLOCK){ /* 如果是非阻塞访问 */
        if(atomic_read(&dev->releasekey) == 0) /* 没有按键按下 */
            return -EAGAIN;

    }else{ /* 阻塞访问 */
        /* wait就是队列项,current就是当前进程 */
        DECLARE_WAITQUEUE(wait, current);               /* 定义一个等待队列 */

        /* 如果按键没按下,就进入休眠状态 */
        if(atomic_read(&dev->releasekey) == 0) {        /* 没有按键按下 */
            add_wait_queue(&dev->r_wait, &wait);        /* 添加到等待队列头 */
            __set_current_state(TASK_INTERRUPTIBLE);    /* 设置任务状态为可被打断 */
            /* 切换其他任务,切换之后任务休眠 */
            schedule();                                 /* 进行一次任务切换 */

            /* 没人搭理这个进程,就一直停留再上面了,之后唤醒才会往下走 */
            /* 唤醒之后进入这里,说明唤醒之后任务回来了 */
            if(signal_pending(current)) {               /* 判断是否为信号引起的唤醒 */
                ret = -ERESTARTSYS;
                remove_wait_queue(&dev->r_wait, &wait); /* 将等待队列移除 */
                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; /* 因为中断中或了一个0x80,这里面去掉0x80 */
            ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
        } else {
            goto data_error;
        }
        /* 读完之后标志变成0 */
        atomic_set(&dev->releasekey, 0); /* 按下标志清零 */
    } else { /* 没有按下 */
        goto data_error;
    }
    return 0;


wait_error:
    set_current_state(TASK_RUNNING); /* 设置任务为运行态 */
    return ret;

data_error:
    return -EINVAL;
}

/*
* @description : poll 函数,用于处理非阻塞访问
* @param - filp : 要打开的设备文件(文件描述符)
* @param - wait : 等待列表(poll_table)
* @return : 设备或者资源状态,
*/
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_wait(filp, &dev->r_wait, wait); 

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

/* 字符设备操作集 */
static const struct file_operations imx6uirq_fops = {
    .owner = THIS_MODULE,
    .open = imx6uirq_open,
    .release = imx6uirq_release,
    .read = imx6uirq_read,
    .poll = imx6uirq_poll,
};

/* 模块入口函数 */
static int __init imx6uirq_init(void)
{
    /* 定义一些所需变量 */
    int ret = 0;

    /* 1. 注册字符设备驱动 */
    irqDev.major = 0;
    if(irqDev.major) {
        irqDev.devid = MKDEV(irqDev.major, 0);
        ret = register_chrdev_region(irqDev.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME );  
    } else {
        alloc_chrdev_region(&irqDev.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME );
        irqDev.major = MAJOR(irqDev.devid);
        irqDev.minor = MINOR(irqDev.devid);
    }
    if(ret < 0){
        goto fail_devid;
    }
    printk("Make devid success! \r\n");
    printk("major = %d, minor = %d \r\n", irqDev.major, irqDev.minor);

    /* 2. 初始化cdev */
    irqDev.cdev.owner = THIS_MODULE;
    cdev_init(&irqDev.cdev, &imx6uirq_fops);
    ret = cdev_add(&irqDev.cdev, irqDev.devid, IMX6UIRQ_CNT);
    if (ret < 0){
        goto fail_cdev;
    } else {
        printk("Cdev add sucess! \r\n");
    }

    /* 3. 自动创建设备节点 */
    irqDev.class = class_create(THIS_MODULE, IMX6UIRQ_NAME );
    if(IS_ERR(irqDev.class)) {
        ret = PTR_ERR(irqDev.class);
        goto fail_class;
    } else {
        printk("Class create sucess! \r\n");
    }

    irqDev.device = device_create(irqDev.class, NULL, irqDev.devid, NULL, IMX6UIRQ_NAME );
    if(IS_ERR(irqDev.device)) {
        ret = PTR_ERR(irqDev.device);
        goto fail_device;
    } else {
        printk("Device create sucess! \r\n");
    }

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

    printk("irqDev init! \r\n");
    return 0;

/* 错误处理 */
fail_device:
    class_destroy(irqDev.class);
fail_class:
    cdev_del(&irqDev.cdev);
fail_cdev:
    unregister_chrdev_region(irqDev.devid, IMX6UIRQ_CNT);
fail_devid:
    return ret;
}

/* 模块出口函数 */
static void __exit imx6uirq_exit(void)
{
    unsigned int i = 0;
    /* 删除定时器 */
    del_timer_sync(&irqDev.timer); 

    /* 释放中断 */
    for (i = 0; i < KEY_NUM; i++) {
        free_irq(irqDev.irqkeydesc[i].irqnum, &irqDev);
    }

    /* 1. 释放设备号 */
    cdev_del(&irqDev.cdev);
    /* 2. 注销设备号 */
    unregister_chrdev_region(irqDev.devid, IMX6UIRQ_CNT);
    /* 3. 摧毁设备 */
    device_destroy(irqDev.class, irqDev.devid);
    /* 4.摧毁类 */
    class_destroy(irqDev.class);


    printk("irqDev exit! \r\n");
}

/* 模块入口和出口注册 */
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Shao Zheming");

3.2 测试代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "linux/ioctl.h"
#include "poll.h"
#include "sys/select.h"
#include "sys/time.h"


/*
* @description : main 主程序
* @param - argc : argv 数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        printf("Error Usage!\r\n");
        return -1;
    }
    
    int fd, ret;
    char *filename;
    struct pollfd fds;
    fd_set readfds;
    struct timeval timeout;
    unsigned char data;

    filename = argv[1];
    fd = open(filename, O_RDWR | O_NONBLOCK); /* 设置成非阻塞打开 */
    if(fd < 0)
    {
        printf("file %s open failed! \r\n", filename);
        return -1;
    }
#if 0
    /* poll */
    fds.fd = fd;            /* 文件描述符设置 */
    fds.events = POLLIN;    /* 设置监听事件为 有数据可以读取 */
    while (1) {
        ret = poll(&fds, 1, 500); /* poll结构体,最大文件描述符, 延时 */
        if (ret) { /* 数据有效 */
            ret = read(fd, &data, sizeof(data));
            if(ret < 0) {
                /* 读取错误 */
            } else {
                if(data)
                printf("key value = %d \r\n", data);
            } 
        } else if (ret == 0) { /* 超时 */
            /* 用户自定义超时处理 */
            //printf("Time out! \r\n");
        } else if (ret < 0) { /* 错误 */
            /* 用户自定义错误处理 */
            printf("Poll Error! \r\n");
        }
    }
#endif

    /* select */
    while(1){
        FD_ZERO(&readfds);      /* 读信号置零 */
        FD_SET(fd, &readfds);   /* 监控读绑定fd */
        /* 构造超时时间 */
        timeout.tv_sec = 0;
        timeout.tv_usec = 500000; /* 500ms */


        ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
        switch (ret) {
            case 0: /* 超时 */
                /* 用户自定义超时处理 */
                break;
            case -1: /* 错误 */
                /* 用户自定义错误处理 */
                break;
            default: /* 可以读取数据 */
                if(FD_ISSET(fd, &readfds)) {
                    ret = read(fd, &data, sizeof(data));
                    if (ret < 0) {
                        /* 读取错误 */
                    } else {
                        if (data)
                            printf("key value=%d\r\n", data);
                    }
                }
                break;
        }
    }

    close(fd);
    return 0;
}

相关