Linux驱动开发( 阻塞和非阻塞 IO)

阻塞和非阻塞 IO

阻塞和非阻塞 IO 是 Linux 驱动开发里面很常见的两种设备访问模式,在编写驱动的时候一定要考虑到阻塞和非阻塞

阻塞和非阻塞简介

这里的"IO"并不是我们学习单片机的时候所说的"GPIO" (也就是引脚)。这里的 IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作

当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃

阻塞式访问应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序

应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功

应用程序可以使用如下所示示例代码来实现阻塞访问和非阻塞式访问

c 复制代码
int fd;
int data = 0;

fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
......................................................................
int fd;
int data = 0;

fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */

等待队列

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

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

c 复制代码
struct wait_queue_head {
	spinlock_t		lock;
	struct list_head	head;
};
typedef struct wait_queue_head wait_queue_head_t;

定义好等待队列头以后需要初始化,使用 init_waitqueue_head 函数初始化等待队列头,函数原型如下

c 复制代码
void init_waitqueue_head(struct wait_queue_head *wq_head)

参数 wq_head 就是要初始化的等待队列头

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

等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的队列项添加到等待队列里面

在 5.4 版本的 linux 内核中使用wait_queue_entry 结构体表示等待队列项

wait_queue_entry 结构体内容如下

c 复制代码
struct wait_queue_entry {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	entry;
};

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

c 复制代码
DECLARE_WAITQUEUE(name, tsk)

name 就是等待队列项的名字, tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current , 在 Linux 内 核 中 current 相当于一个全局变量,表示当前进程

因 此 宏DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项

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

等待队列项添加 API 函数

c 复制代码
void add_wait_queue(struct wait_queue_head *wq_head,
					struct wait_queue_entry *wq_entry)

wq_head: 等待队列项要加入的等待队列头

wq_entry:要加入的等待队列项

返回值:无

等待队列项移除 API 函数

c 复制代码
void remove_wait_queue(struct wait_queue_head *wq_head,
					   struct wait_queue_entry *wq_entry)

wq_head: 要删除的等待队列项所处的等待队列头

wq_entry:要删除的等待队列项

返回值:无

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

c 复制代码
void wake_up(struct wait_queue_head *wq_head)
void wake_up_interruptible(struct wait_queue_head *wq_head)

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

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

函数 描述
wait_event(wq_head, condition) 等待以 wq_head 为等待队列头的等待队列被唤醒,前提是 condition 条件必须满足(为真),否则一直阻塞。此函数会将进程设置为 TASK_UNINTERRUPTIBLE 状态
wait_event_timeout(wq_head, condition, timeout) 功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位。此函数有返回值,如果返回 0 的话表示超时时间到,而且 condition 为假。为 1 的话表示condition 为真,也就是条件满足了
wait_event_interruptible(wq_head, condition) 与 wait_event 函数类似,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断
wait_event_interruptible_timeout(wq_head,condition, timeout) 与 wait_event_timeout 函数类似,此函数也将进程设置为TASK_INTERRUPTIBLE,可以被信号打断

轮询

如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询

poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据

当应用程序调用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数

下面是应用程序中使用的 select、 poll 和 epoll

select函数

c 复制代码
int select(int nfds,
		   fd_set *readfds,
	       fd_set *writefds,
		   fd_set *exceptfds,
		   struct timeval *timeout)

nfds: 需要检查的文件描述符(FD)的范围,通常设置为 所有监控的 FD 中的最大值加 1

readfds、 writefds 和 exceptfds:这三个指针指向描述符集合,三个参数都是 fd_set 类型的, fd_set 类型变量的每一个位都代表了一个文件描述符

readfds 用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时,可以将 readfs设置为 NULL,表示不关心任何文件的读变化。 writefds 和 readfs 类似

简单来说

readfds是用于检查可读事件的文件描述符集合

writefds用于检查可写事件的文件描述符集合

exceptfds用于检查异常事件的文件描述符集合

比如我们现在要从一个设备文件中读取数据,那么就可以定义一个 fd_set 变量,这个变量可以传递给select函数的参数 readfds

c 复制代码
void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)

fd是文件描述符,*set是他的集合即fd_set类型

FD_ZERO 用于将 fd_set 变量的所有位都清零

FD_SET 用于将 fd_set 变量的某个位置 1,也就是向 fd_set 添加一个文件描述符,即添加fd这个文件描述符到set这个文件描述符集合中

FD_CLR 用户将 fd_set变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述符

FD_ISSET 用于测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符

timeout:超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时间使用结构体 timeval 表示

c 复制代码
struct timeval {
	long tv_sec; /* 秒 */
	long tv_usec; /* 微妙 */
}

当 timeout 为 NULL 的时候就表示无限期的等待

select函数的返回值

发生超时,但是没有任何文件描述符可以进行操作返回0

发生错误,返回-1

其他值,返回可以进行操作的文件描述符个数

select示例

c 复制代码
int 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;
    }
    
    return 0;
}

poll 函数

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

c 复制代码
int poll(struct pollfd *fds,
		 nfds_t nfds,
		 int timeout)

fds: 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd类型的

c 复制代码
struct pollfd {
	int fd; /* 文件描述符 */
	short events; /* 请求的事件 */
	short revents; /* 返回的事件 */
}

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

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

revents 是返回参数,也就是返回的事件,由 Linux 内核设置具体的返回事件

nfds: poll 函数要监视的文件描述符数量

timeout: 超时时间,单位为 ms

返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述符数量

超时返回0,发生错误返回-1,并且设置 errno 为错误类型

poll示例

c 复制代码
int main(void) {
    int ret, 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) {       /* 错误 */
        /* 错误处理 */
    }
    
    return 0;
}

epoll 函数

传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间

epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。应用程序需要先使用 epoll_create 函数创建一个 epoll 句柄

c 复制代码
int epoll_create(int size)

size: 从 Linux2.6.8 开始此参数已经没有意义了,随便填写一个大于 0 的值就可以

返回值: epoll 句柄,如果为-1 的话表示创建失败

epoll 句柄创建成功以后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件

c 复制代码
int epoll_ctl(int epfd,
		   	  int op,
			  int fd,
			  struct epoll_event *event)

epfd: 要操作的 epoll 句柄,也就是使用 epoll_create 函数创建的 epoll 句柄

op: 表示要对 epfd(epoll 句柄)进行的操作,可以设置为

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

fd:要监视的文件描述符

event: 要监视的事件类型,为 epoll_event 结构体类型指针

c 复制代码
struct epoll_event {
	uint32_t events; /* epoll 事件 */
	epoll_data_t data; /* 用户数据 */
}

结构体 epoll_event 的 events 成员变量表示要监视的事件,可选的事件如下所示

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

上面这些事件可以进行"或"操作,也就是说可以设置监视多个事件

返回值: 0,成功; -1,失败,并且设置 errno 的值为相应的错误码

一切都设置好以后应用程序就可以通过 epoll_wait 函数来等待事件的发生,类似 select 函数

c 复制代码
int epoll_wait(int epfd,
			   struct epoll_event *events,
			   int maxevents,
			   int timeout)

epfd: 要等待的 epoll

events: 指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events,调用者可以根据 events 判断发生了哪些事件

maxevents: events 数组大小,必须大于 0

timeout: 超时时间,单位为 ms

返回值: 0,超时; -1,错误;其他值,准备就绪的文件描述符数量

epoll 更多的是用在大规模的并发服务器上,因为在这种场合下 select 和 poll 并不适合。当设计到的文件描述符(fd)比较少的时候就适合用 selcet 和 poll

epoll示例

以下示例为ai编写仅供参考

c 复制代码
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024

int main(void) {
    int epfd, fd, ret;
    struct epoll_event ev, events[MAX_EVENTS];
    char buffer[BUFFER_SIZE];

    /* 打开设备文件(非阻塞模式) */
    fd = open("dev_xxx", O_RDWR | O_NONBLOCK);
    if (fd == -1) {
        perror("无法打开设备文件");
        return -1;
    }

    /* 创建 epoll 实例 */
    epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create1");
        close(fd);
        return -1;
    }

    /* 设置并注册事件 */
    ev.events = EPOLLIN;      /* 监听可读事件 */
    ev.data.fd = fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
        perror("epoll_ctl");
        close(fd);
        close(epfd);
        return -1;
    }

    /* 等待事件发生(超时时间 500ms) */
    ret = epoll_wait(epfd, events, MAX_EVENTS, 500);
    if (ret == -1) {
        perror("epoll_wait");
    } else if (ret == 0) {
        printf("超时!\n");
    } else {
        /* 处理就绪事件 */
        for (int i = 0; i < ret; i++) {
            if (events[i].data.fd == fd && (events[i].events & EPOLLIN)) {
                /* 读取数据(边缘触发模式需要循环读取直到返回 EAGAIN) */
                int bytes;
                while ((bytes = read(fd, buffer, BUFFER_SIZE)) > 0) {
                    buffer[bytes] = '\0';
                    printf("读取到数据: %s\n", buffer);
                }
                
                if (bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
                    /* 已读取完所有数据 */
                    continue;
                } else if (bytes == -1) {
                    perror("read");
                } else if (bytes == 0) {
                    printf("设备关闭\n");
                }
            }
        }
    }

    /* 关闭文件描述符 */
    close(fd);
    close(epfd);
    return 0;
}

Linux 驱动下的 poll 操作函数

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

c 复制代码
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)

filp: 要打开的设备文件(文件描述符)

wait: 结构体 poll_table_struct 类型指针,由应用程序传递进来的。一般将此参数传递给poll_wait 函数

返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下

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

我们需要在驱动程序的 poll 函数中调用 poll_wait 函数, poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中

c 复制代码
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)

参数 wait_address 是要添加到 poll_table 中的等待队列头,参数 p 就是 poll_table,就是file_operations 中 poll 函数的 wait 参数

案例

皆为正点原子例程

驱动程序

c 复制代码
#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/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define KEY_CNT			1		/* 设备号个数 	*/
#define KEY_NAME		"key"	/* 名字 		*/

/* 定义按键状态 */
enum key_status {
    KEY_PRESS = 0,      // 按键按下
    KEY_RELEASE,        // 按键松开
    KEY_KEEP,           // 按键状态保持
};

/* key设备结构体 */
struct key_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	struct device_node	*nd; /* 设备节点 */
	int key_gpio;			/* key所使用的GPIO编号		*/
	struct timer_list timer;			/* 按键值 		*/
	int irq_num;			/* 中断号 		*/
	
	atomic_t status;   		/* 按键状态 */
	wait_queue_head_t r_wait;	/* 读等待队列头 */
};

static struct key_dev key;          /* 按键设备 */

static irqreturn_t key_interrupt(int irq, void *dev_id)
{
	/* 按键防抖处理,开启定时器延时15ms */
	mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));
    return IRQ_HANDLED;
}

/*
 * @description	: 初始化按键IO,open函数打开驱动的时候
 * 				  初始化按键所使用的GPIO引脚。
 * @param 		: 无
 * @return 		: 无
 */
static int key_parse_dt(void)
{
	int ret;
	const char *str;
	
	/* 设置LED所使用的GPIO */
	/* 1、获取设备节点:key */
	key.nd = of_find_node_by_path("/key");
	if(key.nd == NULL) {
		printk("key node not find!\r\n");
		return -EINVAL;
	}

	/* 2.读取status属性 */
	ret = of_property_read_string(key.nd, "status", &str);
	if(ret < 0) 
	    return -EINVAL;

	if (strcmp(str, "okay"))
        return -EINVAL;
    
	/* 3、获取compatible属性值并进行匹配 */
	ret = of_property_read_string(key.nd, "compatible", &str);
	if(ret < 0) {
		printk("key: Failed to get compatible property\n");
		return -EINVAL;
	}

    if (strcmp(str, "alientek,key")) {
        printk("key: Compatible match failed\n");
        return -EINVAL;
    }

	/* 4、 获取设备树中的gpio属性,得到KEY0所使用的KYE编号 */
	key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);
	if(key.key_gpio < 0) {
		printk("can't get key-gpio");
		return -EINVAL;
	}

    /* 5 、获取GPIO对应的中断号 */
    key.irq_num = irq_of_parse_and_map(key.nd, 0);
    if(!key.irq_num){
        return -EINVAL;
    }

	printk("key-gpio num = %d\r\n", key.key_gpio);
	return 0;
}

static int key_gpio_init(void)
{
	int ret;
    unsigned long irq_flags;
	
	ret = gpio_request(key.key_gpio, "KEY0");
    if (ret) {
        printk(KERN_ERR "key: Failed to request key-gpio\n");
        return ret;
	}	
	
	/* 将GPIO设置为输入模式 */
    gpio_direction_input(key.key_gpio);

   /* 获取设备树中指定的中断触发类型 */
	irq_flags = irq_get_trigger_type(key.irq_num);
	if (IRQF_TRIGGER_NONE == irq_flags)
		irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
		
	/* 申请中断 */
	ret = request_irq(key.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL);
	if (ret) {
        gpio_free(key.key_gpio);
        return ret;
    }

	return 0;
}

static void key_timer_function(struct timer_list *arg)
{
    static int last_val = 1;
    int current_val;

    /* 读取按键值并判断按键当前状态 */
    current_val = gpio_get_value(key.key_gpio);
    if (0 == current_val && last_val){
        atomic_set(&key.status, KEY_PRESS);	// 按下
		wake_up_interruptible(&key.r_wait);	// 唤醒r_wait队列头中的所有队列
	}
    else if (1 == current_val && !last_val) {
        atomic_set(&key.status, KEY_RELEASE);   // 松开
		wake_up_interruptible(&key.r_wait);	// 唤醒r_wait队列头中的所有队列
	}
    else
        atomic_set(&key.status, KEY_KEEP);              // 状态保持

    last_val = current_val;
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int key_open(struct inode *inode, struct file *filp)
{
	return 0;
}


/*
 * @description     : 从设备读取数据 
 * @param -- filp        : 要打开的设备文件(文件描述符)
 * @param -- buf     : 返回给用户空间的数据缓冲区
 * @param -- cnt     : 要读取的数据长度
 * @param -- offt        : 相对于文件首地址的偏移
 * @return          : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t key_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
    int ret;

    /* 加入等待队列,当有按键按下或松开动作发生时,才会被唤醒 */
    ret = wait_event_interruptible(key.r_wait, KEY_KEEP != atomic_read(&key.status));
    if(ret)
    return ret;

    /* 将按键状态信息发送给应用程序 */
    ret = copy_to_user(buf, &key.status, sizeof(int));

    /* 状态重置 */
    atomic_set(&key.status, KEY_KEEP);

    return ret;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int key_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations key_fops = {
	.owner = THIS_MODULE,
	.open = key_open,
	.read = key_read,
	.write = key_write,
	.release = 	key_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init mykey_init(void)
{
	int ret;
	
	/* 初始化等待队列头 */
	init_waitqueue_head(&key.r_wait);
	
	/* 初始化按键状态 */
	atomic_set(&key.status, KEY_KEEP);

	/* 设备树解析 */
	ret = key_parse_dt();
	if(ret)
		return ret;
		
	/* GPIO 中断初始化 */
	ret = key_gpio_init();
	if(ret)
		return ret;
		
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);	/* 申请设备号 */
	if(ret < 0) {
		pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", KEY_NAME, ret);
		goto free_gpio;
	}
	
	/* 2、初始化cdev */
	key.cdev.owner = THIS_MODULE;
	cdev_init(&key.cdev, &key_fops);
	
	/* 3、添加一个cdev */
	ret = cdev_add(&key.cdev, key.devid, KEY_CNT);
	if(ret < 0)
		goto del_unregister;
		
	/* 4、创建类 */
	key.class = class_create(THIS_MODULE, KEY_NAME);
	if (IS_ERR(key.class)) {
		goto del_cdev;
	}

	/* 5、创建设备 */
	key.device = device_create(key.class, NULL, key.devid, NULL, KEY_NAME);
	if (IS_ERR(key.device)) {
		goto destroy_class;
	}
	
	/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
	timer_setup(&key.timer, key_timer_function, 0);
	
	return 0;

destroy_class:
	device_destroy(key.class, key.devid);
del_cdev:
	cdev_del(&key.cdev);
del_unregister:
	unregister_chrdev_region(key.devid, KEY_CNT);
free_gpio:
	free_irq(key.irq_num, NULL);
	gpio_free(key.key_gpio);
	return -EIO;
}


/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit mykey_exit(void)
{
	/* 注销字符设备驱动 */
	cdev_del(&key.cdev);/*  删除cdev */
	unregister_chrdev_region(key.devid, KEY_CNT); /* 注销设备号 */
	del_timer_sync(&key.timer);		/* 删除timer */
	device_destroy(key.class, key.devid);/*注销设备 */
	class_destroy(key.class); 		/* 注销类 */
	free_irq(key.irq_num, NULL);	/* 释放中断 */
	gpio_free(key.key_gpio);		/* 释放IO */
}

module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

应用程序

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

/*
 * @description		: main主程序
 * @param -- argc		: argv数组元素个数
 * @param -- argv		: 具体参数
 * @return			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
    int fd, ret;
    int key_val;

    /* 判断传参个数是否正确 */
    if(2 != argc) {
        printf("Usage:\n"
             "\t./keyApp /dev/key\n"
            );
        return -1;
    }

    /* 打开设备 */
    fd = open(argv[1], O_RDONLY);
    if(0 > fd) {
        printf("ERROR: %s file open failed!\n", argv[1]);
        return -1;
    }

    /* 循环读取按键数据 */
    for ( ; ; ) {

        read(fd, &key_val, sizeof(int));
        if (0 == key_val)
            printf("Key Press\n");
        else if (1 == key_val)
            printf("Key Release\n");
    }

    /* 关闭设备 */
    close(fd);
    return 0;
}