<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知

<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知

交叉编译环境搭建:
<Linux开发> linux开发工具-之-交叉编译环境搭建

uboot移植可参考以下:
<Linux开发> -之-系统移植 uboot移植过程详细记录(第一部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第三部分)(uboot移植完结)

Linux内核及设备树移植可参考以下:
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第一部分)
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第二部分完结)

Linux文件系统构建移植参考以下:
<Linux开发>系统移植 -之- linux构建BusyBox根文件系统及移植过程详细记录
<Linux开发>系统移植 -之-使用buildroot构建BusyBox根文件系统

Linux驱动开发参考以下:
<Linux开发>驱动开发 -之-pinctrl子系统
<Linux开发>驱动开发 -之-gpio子系统
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的LED驱动
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的beep驱动
<Linux开发>驱动开发 -之-资源的并发与竞争处理
<Linux开发>驱动开发 -之-内核定时器与中断

一 前言

在<Linux开发>驱动开发 -之-内核定时器与中断的末尾我们看到,当应用程序运行时cpu占用率时很高的,这是因为一直开循环读取设备导致的,那么有什么方法可以有效处理这种情况呢?在kernel中提供了 阻塞IO、非阻塞IO以及异步通知等方法。为有效提升资源利用和降低cpu占用率,从而提升效率,我们一一分析这三种方法。

二 阻塞与非阻塞IO

2.1 简介

这里的 IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
阻塞IO的流程视图如下:
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第1张图片
上图中应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。

非阻塞IO的流程视图如下:
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第2张图片
从上图可以看出,应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。

在之前的文章中所用的方式都是阻塞的,示例如下:

int devfd;
int readdata=0;

devfd = open("/dev/xxx",O_RDWR);			/* 默认阻塞方式打开 */
read(devfd; &readdata, sizeof(readdata));	/* 读取数据 */

在kernel中设备驱动文件是默认以阻塞式读取的。

非阻塞示例如下:

int devfd;
int readdata=0;

devfd = open("/dev/xxx",O_RDWR | O_NONBLOCK);			/* 以非阻塞方式打开 */
read(devfd; &readdata, sizeof(readdata));	/* 读取数据 */

添加了参数“O_NONBLOCK”,表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式。

2.2 等待队列

2.2.1 等待队列头

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

路径:include\linux\wait.h
struct __wait_queue_head {
	spinlock_t		lock;
	struct list_head	task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第3张图片
定义好等待队列头以后需要初始化,使用 init_waitqueue_head 函数初始化等待队列头,函数原型如下:
路径:include\linux\wait.h

void init_waitqueue_head(wait_queue_head_t *q)

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

也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化。
路径:include\linux\wait.h

#define DECLARE_WAIT_QUEUE_HEAD(name) \
	wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

2.2.3 等待队列项

等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等待队列项,结构体内容如下:
路径:include\linux\wait.h

...
typedef struct __wait_queue wait_queue_t;
.....
struct __wait_queue {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	task_list;
};

使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
路径:

#define DECLARE_WAITQUEUE(name, tsk)					\
	wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

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

2.2.3 添加/移除等待队列项

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

路径:kernel\sched\wait.c

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;

	wait->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&q->lock, flags);
	__add_wait_queue(q, wait);
	spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(add_wait_queue);

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

等待队列项移除 API 函数如下:

路径:kernel\sched\wait.c

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;

	spin_lock_irqsave(&q->lock, flags);
	__remove_wait_queue(q, wait);
	spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(remove_wait_queue);

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

2.2.4 等待唤醒

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

void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
路径:sound\oss\dmasound\dmasound.h

#define WAKE_UP(queue)		(wake_up_interruptible(&queue))
路径:include\linux\wait.h
#define wake_up_interruptible(x)	__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。

wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。

2.2.5 等待事件

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

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

2.3 轮询

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

2.3.1 select 函数

select 函数原型如下:

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

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

比如我们现在要从一个设备文件中读取数据,那么就可以定义一个 fd_set 变量,这个变量要传递给参数 readfds。当我们定义好一个 fd_set 变量以后可以使用如下所示几个宏进行操作:

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_ZERO 用于将 fd_set 变量的所有位都清零;
FD_SET 用于将 fd_set 变量的某个位置 1,也就是向 fd_set 添加一个文件描述符,参数 fd 就是要加入的文件描述符;
FD_CLR 用于将 fd_set变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述符;
FD_ISSET 用于测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符。

timeout:超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时间使用结构体 timeval 表示,结构体定义如下所示:
路径:include\uapi\linux\time.h

struct timeval {
	long tv_sec; /* 秒 */
	long tv_usec; /* 微妙 */
};

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

返回值:
0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作;
-1,发生错误;
其他值,可以进行操作的文件描述符个数。

select 函数使用示例如下:

int main(int argc, char *argv[])
{
	int fd, ret;			/* 要监视的文件描述符 */
	fd_set readfds;	/* 读操作文件描述符集 */
	struct timeval timeout; 	/* 超时结构体 */
	unsigned char data;
	
	fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK);	/* 非阻塞访问 */
	if (fd < 0) {
		printf("Can't open file %s\r\n", “/dev/xxx”);
		return -1;
	}
	while (1) {	
		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: 	/* 超时 */
				/* 用户自定义超时处理 */
				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 ret;
}

2.3.2 poll函数

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

#include 

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

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

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

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

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

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

nfds:poll 函数要监视的文件描述符数量。
timeout:超时时间,单位为 ms。
返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述符数量;0,超时;-1,发生错误,并且设置 errno 为错误类型。

使用 poll 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:

int main(int argc, char *argv[])
{
	int fd, ret;			/* 要监视的文件描述符 */
	fd_set readfds;	/* 读操作文件描述符集 */
	struct timeval timeout; 	/* 超时结构体 */
	unsigned char data;
	struct pollfd fds;
	
	fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK);	/* 非阻塞访问 */
	if (fd < 0) {
		printf("Can't open file %s\r\n", “/dev/xxx”);
		return -1;
	}
	/* 构造结构体 */
	fds.fd = fd;
	fds.events = POLLIN;
		
	while (1) {
		ret = poll(&fds, 1, 500);   /* 轮询文件是否可操作,超时 500ms */
		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) { 	/* 超时 */
			/* 用户自定义超时处理 */
		} else if (ret < 0) {	/* 错误 */
			/* 用户自定义错误处理 */
		}
	}

	close(fd);
	return ret;
}

2.3.3 epoll函数

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

#include 
int epoll_create(int size)

函数参数和返回值含义如下:
size:从 Linux2.6.8 开始此参数已经没有意义了,随便填写一个大于 0 的值就可以。
返回值:epoll 句柄,如果为-1 的话表示创建失败。

epoll 句柄创建成功以后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件,epoll_ctl 函数原型如下所示:

#include 

int epoll_ctl(int epfd,  int op,  int fd, struct epoll_event *event)

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

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

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

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

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

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

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

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

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

#include 

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,本文我们就使用 sellect 和 poll 这两个函数。

2.4 Linux 驱动下的 poll 操作函数

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

unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)

函数参数和返回值含义如下:
filp:要打开的设备文件(文件描述符)。
wait:结构体 poll_table_struct 类型指针,由应用程序传递进来的。一般将此参数传递给poll_wait 函数。
返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:

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

我们需要在驱动程序的 poll 函数中调用 poll_wait 函数,poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中,poll_wait 函数原型如下:
路径:include\linux\poll.h

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 参数。

三 阻塞IO测试

在<Linux开发>驱动开发 -之-内核定时器与中断文中末尾,有一个CPU占用很高的现象,本文就基于该文例程代码加以修改验证。
本测试以编译KO文件方式进行手动挂载验证。

3.1 修改驱动代码

........
#include 
#include  
#include 
..........
#define	KEYIRQ_NAME	"KEYIRQ_BLOCKIO"				/*设备名*/
............
/* keyirq设备结构体 */
struct keyirq_dev{

	struct irq_keydesc irqkeydesc[KEY_NUM]; 	/* 按键描述数组 */
	unsigned char curkeynum;		 /* 当前的按键号 */
	
	wait_queue_head_t r_wait; /*  //新增 读等待队列头 */
};
.....

void timer_function(unsigned long arg)
{
......
	if(value == 0){ 						/* 按下按键 */
		atomic_set(&dev->keyvalue, keydesc->value);
	}
	else{ 									/* 按键松开 */
		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);
	 }
 }
......
static int keyio_init(void)
{
	char name[10];
.......
	/* 初始化key所使用的IO,并且设置成中断模式 */
	for (i = 0; i < KEY_NUM; i++) {
		memset(keyirq.irqkeydesc[i].name, 0, sizeof(name));	/* 缓冲区清零 */
		sprintf(keyirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
		gpio_request(keyirq.irqkeydesc[i].gpio, keyirq.irqkeydesc[i].name);
		gpio_direction_input(keyirq.irqkeydesc[i].gpio);	
		keyirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(keyirq.nd, i);
	.............
	/* 创建定时器 */
	init_timer(&keyirq.timer);
	keyirq.timer.function = timer_function;

	/* 新增 初始化等待队列头 */
	init_waitqueue_head(&keyirq.r_wait);
	return 0;
}
....................
static ssize_t keyirq_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 keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;
	
	/* 新增 */
	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;
			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;
}
..................

3.2 完整驱动代码

注意:如果<Linux开发>驱动开发 -之-内核定时器与中断中使用的是编译进内核的形式,需要先去掉,“make menuconfig”进入配置界面去掉即可,如下图:
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第4张图片
去掉后重新保存到配置文件;
完整驱动如下:

/***************************************************************
Copyright © OneFu Co., Ltd. 2018-2023. All rights reserved.
文件名 : key.c
作者 : water
版本 : V1.0
描述 : Linux blockio中断驱动实验
其他 : 无
日志 : 初版 V1.0 2023/06/12 water创建
***************************************************************/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include 

#define	KEYIRQ_CNT	1						/*	设备号个数	*/
#define	KEYIRQ_NAME	"KEYIRQ_BLOCKIO"		/*设备名*/
#define	KEY0VALUE		0X01				/*	key0按键值	*/
#define	INVAKEY				0XFF			/*	无效按键值	*/
#define	KEY_NUM			1					/*	按键数量	*/

/*	IO中断描述结构体	*/
struct irq_keydesc	{
	int gpio;						/* 使用的GPIO */
	int irqnum;						/* GPIO中断号 */
	unsigned char value;			/* 按键键值 */
	char name[10];					/* 设备名 */
	irqreturn_t (*handler)(int, void *);	/* 中断处理函数 */
};

/* keyirq设备结构体 */
struct keyirq_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; /*  //新增 读等待队列头 */
};

struct keyirq_dev  keyirq; /* irq 设备 */

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

	dev->curkeynum = 0;
	dev->timer.data = (volatile long)dev_id;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定时 */
	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 keyirq_dev *dev = (struct keyirq_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];

	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
	if(value == 0){ 						/* 按下按键 */
		atomic_set(&dev->keyvalue, keydesc->value);
	}
	else{ 									/* 按键松开 */
		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;
    char name[10];
	
	keyirq.nd = of_find_node_by_path("/key");
	if (keyirq.nd== NULL){
		printk("key node not find!\r\n");
		return -EINVAL;
	} else {
        printk("key node has been found!\r\n");
    }

	/* 提取GPIO */
	for (i = 0; i < KEY_NUM; i++) {
		keyirq.irqkeydesc[i].gpio = of_get_named_gpio(keyirq.nd ,"key-gpio", i);
		if (keyirq.irqkeydesc[i].gpio < 0) {
			printk("can't get key%d\r\n", i);
		}
	}
	
	/* 初始化key所使用的IO,并且设置成中断模式 */
	for (i = 0; i < KEY_NUM; i++) {
		memset(keyirq.irqkeydesc[i].name, 0, sizeof(name));	/* 缓冲区清零 */
		sprintf(keyirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
		gpio_request(keyirq.irqkeydesc[i].gpio, keyirq.irqkeydesc[i].name);
		gpio_direction_input(keyirq.irqkeydesc[i].gpio);	
		keyirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(keyirq.nd, i);
#if 0
		keyirq.irqkeydesc[i].irqnum = gpio_to_irq(keyirq.irqkeydesc[i].gpio);
#endif
		printk("key%d:gpio=%d, irqnum=%d\r\n",i, keyirq.irqkeydesc[i].gpio, 
                                         keyirq.irqkeydesc[i].irqnum);
	}
	/* 申请中断 */
	keyirq.irqkeydesc[0].handler = key0_handler;
	keyirq.irqkeydesc[0].value = KEY0VALUE;
	
	for (i = 0; i < KEY_NUM; i++) {
		ret = request_irq(keyirq.irqkeydesc[i].irqnum, keyirq.irqkeydesc[i].handler, 
		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, keyirq.irqkeydesc[i].name, &keyirq);
		if(ret < 0){
			printk("irq %d request failed!\r\n", keyirq.irqkeydesc[i].irqnum);
			return -EFAULT;
		}
	}

	/* 创建定时器 */
	init_timer(&keyirq.timer);
	keyirq.timer.function = timer_function;

	/* 新增 初始化等待队列头 */
	init_waitqueue_head(&keyirq.r_wait);

	return 0;
}

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

 /*
  * @description     : 从设备读取数据 
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - buf     : 返回给用户空间的数据缓冲区
  * @param - cnt     : 要读取的数据长度
  * @param - offt    : 相对于文件首地址的偏移
  * @return          : 读取的字节数,如果为负值,表示读取失败
  */
static ssize_t keyirq_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 keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;
	
	/* 新增 */
	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;
			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 keyirq_fops = {
	.owner = THIS_MODULE,
	.open = keyirq_open,
	.read = keyirq_read,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init keyirq_init(void)
{
	/* 1、构建设备号 */
	if (keyirq.major) {
		keyirq.devid = MKDEV(keyirq.major, 0);
		register_chrdev_region(keyirq.devid, KEYIRQ_CNT, KEYIRQ_NAME);
	} else {
		alloc_chrdev_region(&keyirq.devid, 0, KEYIRQ_CNT, KEYIRQ_NAME);
		keyirq.major = MAJOR(keyirq.devid);
		keyirq.minor = MINOR(keyirq.devid);
	}

	/* 2、注册字符设备 */
	cdev_init(&keyirq.cdev, &keyirq_fops);
	cdev_add(&keyirq.cdev, keyirq.devid, KEYIRQ_CNT);

	/* 3、创建类 */
	keyirq.class = class_create(THIS_MODULE, KEYIRQ_NAME);
	if (IS_ERR(keyirq.class)) {
		return PTR_ERR(keyirq.class);
	}

	/* 4、创建设备 */
	keyirq.device = device_create(keyirq.class, NULL, keyirq.devid, NULL, KEYIRQ_NAME);
	if (IS_ERR(keyirq.device)) {
		return PTR_ERR(keyirq.device);
	}
	
	/* 5、初始化按键 */
	atomic_set(&keyirq.keyvalue, INVAKEY);
	atomic_set(&keyirq.releasekey, 0);
	keyio_init();
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit keyirq_exit(void)
{
	unsigned int i = 0;
	/* 删除定时器 */
	del_timer_sync(&keyirq.timer);	/* 删除定时器 */
		
	/* 释放中断 */
	for (i = 0; i < KEY_NUM; i++) {
		free_irq(keyirq.irqkeydesc[i].irqnum, &keyirq);
		gpio_free(keyirq.irqkeydesc[i].gpio);
	}
	cdev_del(&keyirq.cdev);
	unregister_chrdev_region(keyirq.devid, KEYIRQ_CNT);
	device_destroy(keyirq.class, keyirq.devid);
	class_destroy(keyirq.class);
}

module_init(keyirq_init);
module_exit(keyirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("water");

3.3 编写测试app

测试app完全复用<Linux开发>驱动开发 -之-内核定时器与中断中的测试app即可。

3.4 编译验证

编写Makefile:

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

KERNELDIR := /home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/
CURRENT_PATH := $(shell pwd)
obj-m := blockio.o

KBUILD_CFLAGS += -fno-pie

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译驱动:

make

<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第5张图片

编译app:

arm-linux-gnueabihf-gcc blockio_app.c -o blockio_app

在这里插入图片描述
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第6张图片

拷贝到设备上:

cp app/blockio_app  ../../nfs/buildrootfs/root/water_soft/blockio/
cp blockio.ko  ../../nfs/buildrootfs/lib/modules/4.1.15+/

在这里插入图片描述

挂载驱动:

depmod //第一次加载驱动的时候需要运行此命令
modprobe blockio.ko //加载驱动

<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第7张图片

运行app:

./blockio_app  /dev/KEYIRQ_BLOCKIO &

<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第8张图片

通过top命令查看CPU占用率。
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第9张图片

实际现象符合预期设计,且cpu占用率很低。
关闭进程命令: kill -9 120
其中120为进程号:
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第10张图片
卸载驱动名:rmmod noblockio.ko
在这里插入图片描述

四 非阻塞IO测试

同样非阻塞IO可以解决<Linux开发>驱动开发 -之-内核定时器与中断文中末尾提到的CPU占用很高的现象,本文就基于该文例程代码加以修改验证。
本测试以编译KO文件方式进行手动挂载验证。

4.1 修改驱动代码

.........
#include 
#include 
#include 
#include 
#include 
..........
#define	KEYIRQ_NAME	"KEYIRQ_NOBLOCKIO"				/*设备名*/
/* keyirq设备结构体 */
struct keyirq_dev{
.....
	struct irq_keydesc irqkeydesc[KEY_NUM]; 	/* 按键描述数组 */
	unsigned char curkeynum;		 /* 当前的按键号 */
	
	wait_queue_head_t r_wait; /*  //新增 读等待队列头 */
};
.....

void timer_function(unsigned long arg)
{
......
	if(value == 0){ 						/* 按下按键 */
		atomic_set(&dev->keyvalue, keydesc->value);
	}
	else{ 									/* 按键松开 */
		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);
	 }
 }
....................
static ssize_t keyirq_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 keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;
	
	/* 新增 */
	if (filp->f_flags & O_NONBLOCK)	{ /* 非阻塞访问 */
		if(atomic_read(&dev->releasekey) == 0)	/* 没有按键按下,返回-EAGAIN */
			return -EAGAIN;
	} else {							/* 阻塞访问 */
		/* 加入等待队列,等待被唤醒,也就是有按键按下 */
 		ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); 
		if (ret) {
			goto wait_error;
		}
	}
	
	keyvalue = atomic_read(&dev->keyvalue);
	releasekey = atomic_read(&dev->releasekey);

	if (releasekey) { /* 有按键按下 */	
		if (keyvalue & 0x80) {
			keyvalue &= ~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:
	return ret;
	
data_error:
	return -EINVAL;
}
..................
/*
  * @description     : poll函数,用于处理非阻塞访问
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - wait    : 等待列表(poll_table)
  * @return          : 设备或者资源状态,
  */
unsigned int keyirq_poll(struct file *filp, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	struct keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;

	poll_wait(filp, &dev->r_wait, wait);	/* 将等待队列头添加到poll_table中 */
	
	if(atomic_read(&dev->releasekey)) {		/* 按键按下 */
		mask = POLLIN | POLLRDNORM;			/* 返回PLLIN */
	}
	return mask;
}
...............
static struct file_operations keyirq_fops = {
	.owner = THIS_MODULE,
	.open = keyirq_open,
	.read = keyirq_read,
	.poll = keyirq_poll,
};
...........

4.2 完整驱动代码

/***************************************************************
Copyright © OneFu Co., Ltd. 2018-2023. All rights reserved.
文件名 : noblockio.c
作者 : water
版本 : V1.0
描述 : Linux noblockio 中断驱动实验
其他 : 无
日志 : 初版 V1.0 2023/06/12 water创建
***************************************************************/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define	KEYIRQ_CNT	1						/*	设备号个数	*/
#define	KEYIRQ_NAME	"KEYIRQ_NOBLOCKIO"		/*设备名*/
#define	KEY0VALUE		0X01				/*	key0按键值	*/
#define	INVAKEY				0XFF			/*	无效按键值	*/
#define	KEY_NUM			1					/*	按键数量	*/

/*	IO中断描述结构体	*/
struct irq_keydesc	{
	int gpio;						/* 使用的GPIO */
	int irqnum;						/* GPIO中断号 */
	unsigned char value;			/* 按键键值 */
	char name[10];					/* 设备名 */
	irqreturn_t (*handler)(int, void *);	/* 中断处理函数 */
};

/* keyirq设备结构体 */
struct keyirq_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; /*  //新增 读等待队列头 */
};

struct keyirq_dev  keyirq; /* irq 设备 */

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

	dev->curkeynum = 0;
	dev->timer.data = (volatile long)dev_id;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定时 */
	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 keyirq_dev *dev = (struct keyirq_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];

	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
	if(value == 0){ 						/* 按下按键 */
		atomic_set(&dev->keyvalue, keydesc->value);
	}
	else{ 									/* 按键松开 */
		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;
    char name[10];
	
	keyirq.nd = of_find_node_by_path("/key");
	if (keyirq.nd== NULL){
		printk("key node not find!\r\n");
		return -EINVAL;
	} else {
        printk("key node has been found!\r\n");
    }

	/* 提取GPIO */
	for (i = 0; i < KEY_NUM; i++) {
		keyirq.irqkeydesc[i].gpio = of_get_named_gpio(keyirq.nd ,"key-gpio", i);
		if (keyirq.irqkeydesc[i].gpio < 0) {
			printk("can't get key%d\r\n", i);
		}
	}
	
	/* 初始化key所使用的IO,并且设置成中断模式 */
	for (i = 0; i < KEY_NUM; i++) {
		memset(keyirq.irqkeydesc[i].name, 0, sizeof(name));	/* 缓冲区清零 */
		sprintf(keyirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
		gpio_request(keyirq.irqkeydesc[i].gpio, keyirq.irqkeydesc[i].name);
		gpio_direction_input(keyirq.irqkeydesc[i].gpio);	
		keyirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(keyirq.nd, i);
#if 0
		keyirq.irqkeydesc[i].irqnum = gpio_to_irq(keyirq.irqkeydesc[i].gpio);
#endif
		printk("key%d:gpio=%d, irqnum=%d\r\n",i, keyirq.irqkeydesc[i].gpio, 
                                         keyirq.irqkeydesc[i].irqnum);
	}
	/* 申请中断 */
	keyirq.irqkeydesc[0].handler = key0_handler;
	keyirq.irqkeydesc[0].value = KEY0VALUE;
	
	for (i = 0; i < KEY_NUM; i++) {
		ret = request_irq(keyirq.irqkeydesc[i].irqnum, keyirq.irqkeydesc[i].handler, 
		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, keyirq.irqkeydesc[i].name, &keyirq);
		if(ret < 0){
			printk("irq %d request failed!\r\n", keyirq.irqkeydesc[i].irqnum);
			return -EFAULT;
		}
	}

	/* 创建定时器 */
	init_timer(&keyirq.timer);
	keyirq.timer.function = timer_function;

	/* 新增 初始化等待队列头 */
	init_waitqueue_head(&keyirq.r_wait);

	return 0;
}

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

 /*
  * @description     : 从设备读取数据 
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - buf     : 返回给用户空间的数据缓冲区
  * @param - cnt     : 要读取的数据长度
  * @param - offt    : 相对于文件首地址的偏移
  * @return          : 读取的字节数,如果为负值,表示读取失败
  */
static ssize_t keyirq_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 keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;
	
	/* 新增 */
	if (filp->f_flags & O_NONBLOCK)	{ /* 非阻塞访问 */
		if(atomic_read(&dev->releasekey) == 0)	/* 没有按键按下,返回-EAGAIN */
			return -EAGAIN;
	} else {							/* 阻塞访问 */
		/* 加入等待队列,等待被唤醒,也就是有按键按下 */
 		ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); 
		if (ret) {
			goto wait_error;
		}
	}


	keyvalue = atomic_read(&dev->keyvalue);
	releasekey = atomic_read(&dev->releasekey);

	if (releasekey) { /* 有按键按下 */	
		if (keyvalue & 0x80) {
			keyvalue &= ~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:
	return ret;

data_error:
	return -EINVAL;
}

/*
  * @description     : poll函数,用于处理非阻塞访问
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - wait    : 等待列表(poll_table)
  * @return          : 设备或者资源状态,
  */
unsigned int keyirq_poll(struct file *filp, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	struct keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;

	poll_wait(filp, &dev->r_wait, wait);	/* 将等待队列头添加到poll_table中 */
	
	if(atomic_read(&dev->releasekey)) {		/* 按键按下 */
		mask = POLLIN | POLLRDNORM;			/* 返回PLLIN */
	}
	return mask;
}

/* 设备操作函数 */
static struct file_operations keyirq_fops = {
	.owner = THIS_MODULE,
	.open = keyirq_open,
	.read = keyirq_read,
	.poll = keyirq_poll,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init keyirq_init(void)
{
	/* 1、构建设备号 */
	if (keyirq.major) {
		keyirq.devid = MKDEV(keyirq.major, 0);
		register_chrdev_region(keyirq.devid, KEYIRQ_CNT, KEYIRQ_NAME);
	} else {
		alloc_chrdev_region(&keyirq.devid, 0, KEYIRQ_CNT, KEYIRQ_NAME);
		keyirq.major = MAJOR(keyirq.devid);
		keyirq.minor = MINOR(keyirq.devid);
	}

	/* 2、注册字符设备 */
	cdev_init(&keyirq.cdev, &keyirq_fops);
	cdev_add(&keyirq.cdev, keyirq.devid, KEYIRQ_CNT);

	/* 3、创建类 */
	keyirq.class = class_create(THIS_MODULE, KEYIRQ_NAME);
	if (IS_ERR(keyirq.class)) {
		return PTR_ERR(keyirq.class);
	}

	/* 4、创建设备 */
	keyirq.device = device_create(keyirq.class, NULL, keyirq.devid, NULL, KEYIRQ_NAME);
	if (IS_ERR(keyirq.device)) {
		return PTR_ERR(keyirq.device);
	}
	
	/* 5、初始化按键 */
	atomic_set(&keyirq.keyvalue, INVAKEY);
	atomic_set(&keyirq.releasekey, 0);
	keyio_init();
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit keyirq_exit(void)
{
	unsigned int i = 0;
	/* 删除定时器 */
	del_timer_sync(&keyirq.timer);	/* 删除定时器 */
		
	/* 释放中断 */
	for (i = 0; i < KEY_NUM; i++) {
		free_irq(keyirq.irqkeydesc[i].irqnum, &keyirq);
		gpio_free(keyirq.irqkeydesc[i].gpio);
	}
	cdev_del(&keyirq.cdev);
	unregister_chrdev_region(keyirq.devid, KEYIRQ_CNT);
	device_destroy(keyirq.class, keyirq.devid);
	class_destroy(keyirq.class);
}

module_init(keyirq_init);
module_exit(keyirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("water");

4.3 编写测试app

测试app如下:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"
#include "poll.h"
#include "sys/select.h"
#include "sys/time.h"
/***************************************************************
Copyright © OneFu Co., Ltd. 2018-2023. All rights reserved.
文件名		: noblockio_app.c
作者	  	: water
版本	   	: V1.0
描述	   	: 非阻塞IO中断测试应用程序
其他	   	: 无
使用方法	:./noblockio_app  /dev/KEYIRQ_NOBLOCKIO 打开测试App
日志	   	: 初版V1.0 2023/06/12 water创建
***************************************************************/

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int keyfd;
	int ret = 0;
	char *filename;
	unsigned char data;
	struct pollfd fds;
	fd_set readfds;
	struct timeval timeout;

	int ledstatu = 0;
	char ledfilename[]="/dev/gpioled";
	int ledfd,retvalue;
	unsigned char databuf[1];       //定义的buf,用来读写数据用

	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	keyfd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞访问 打开驱动文件*/
	if (keyfd < 0) {
		printf("Can't open file %s\r\n", filename);
		return -1;
	}
	ledfd = open(ledfilename,O_RDWR);      /* 打开驱动文件*/
    if(ledfd < 0){
        printf("Can't open file %s\r\n",ledfilename);      /*打开失败,输出提示*/
        return -1;
    }
#if 0
	/* 构造结构体 */
	fds.fd = keyfd;
	fds.events = POLLIN;
		
	while (1) {
		ret = poll(&fds, 1, 500);
		if (ret) {	/* 数据有效 */
			ret = read(keyfd, &data, sizeof(data));
			if(ret < 0) {
				/* 读取错误 */
			} else {
				if (data)	/* 读取到数据 */
						{
							printf("key value = %#X\r\n", data);
							ledstatu = !ledstatu;
							databuf[0] = ledstatu;
							retvalue = write(ledfd, databuf, sizeof(databuf));     /*向设备驱动写入数据*/
							if(retvalue < 0){
								printf("LED Control Failed!\r\n",ledfilename);     /*写入错误输出提示*/
							}
						}
			} 	
		} else if (ret == 0) { 	/* 超时 */
			/* 用户自定义超时处理 */
		} else if (ret < 0) {	/* 错误 */
			/* 用户自定义错误处理 */
		}
	}
#endif

	while (1) {
		FD_ZERO(&readfds);
		FD_SET(keyfd, &readfds);
		/* 构造超时时间 */
		timeout.tv_sec = 0;
		timeout.tv_usec = 500000; /* 500ms */
		ret = select(keyfd + 1, &readfds, NULL, NULL, &timeout);
		switch (ret) {
			case 0: 	/* 超时 */
				/* 用户自定义超时处理 */
				break;
			case -1:	/* 错误 */
				/* 用户自定义错误处理 */
				break;
			default:  /* 可以读取数据 */
				if(FD_ISSET(keyfd, &readfds)) {
					ret = read(keyfd, &data, sizeof(data));
					if (ret < 0) {
						/* 读取错误 */
					} else {
						if (data)	/* 读取到数据 */
						{
							printf("key value = %#X\r\n", data);
							ledstatu = !ledstatu;
							databuf[0] = ledstatu;
							retvalue = write(ledfd, databuf, sizeof(databuf));     /*向设备驱动写入数据*/
							if(retvalue < 0){
								printf("LED Control Failed!\r\n",ledfilename);     /*写入错误输出提示*/
							}
						}
					}
				}
				break;
		}
	}
	retvalue = close(keyfd);
	if(retvalue < 0){
        printf("Can't close file %s\r\n",filename);     /*关闭错误输出提示*/
        ret = -1;
		goto OUT1;
    }
OUT1:
	retvalue = close(ledfd);
	if(retvalue < 0){
        printf("Can't close file %s\r\n",ledfilename);     /*关闭错误输出提示*/
        ret = -1;
		goto OUT2;
    }
OUT2:
	return ret;
}

//编译:arm-linux-gnueabihf-gcc noblockio_app.c -o noblockio_app

第66行:使用 poll 函数来实现非阻塞访问。
第97行:使用 select 函数来实现非阻塞访问。

4.4 编译验证

编写Makefile:

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

KERNELDIR := /home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/
CURRENT_PATH := $(shell pwd)
obj-m := noblockio.o

KBUILD_CFLAGS += -fno-pie

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译驱动:

make

<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第11张图片
编译app:

arm-linux-gnueabihf-gcc noblockio_app.c -o noblockio_app

在这里插入图片描述
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第12张图片

拷贝到设备上:

cp noblockio.ko  ../../nfs/buildrootfs/lib/modules/4.1.15+/
cp app/noblockio_app  ../../nfs/buildrootfs/root/water_soft/noblockio/

在这里插入图片描述

挂载驱动:

depmod //第一次加载驱动的时候需要运行此命令
modprobe noblockio.ko //加载驱动

<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第13张图片

运行app:

./noblockio_app  /dev/KEYIRQ_NOBLOCKIO &

<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第14张图片
使用top命令查看cpu占用率:
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第15张图片

实际现象符合预期设计,且cpu占用率很低。

关闭进程命令: kill -9 127
其中127为进程号:
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第16张图片

卸载驱动:rmmod noblockio.ko
在这里插入图片描述

五 异步通知

在前面了解的阻塞或者非阻塞的方式来读取驱动中按键值都是应用程序主动读取的,对于非阻塞方式来说还需要应用程序通过 poll 函数不断的轮询。最好的方式就是驱动程序能主动向应用程序发出通知,报告自己可以访问,然后应用程序在从驱动程序中读取或写入数据,类似于中断效果,但不是中断。Linux 提供了异步通知这个机制来完成此功能,接下来我们就来看看。

5.1 异步通知简介

我们可以回想<Linux开发>驱动开发 -之-内核定时器与中断,中断是处理器提供的一种异步机制,我们配置好中断以后就可以让处理器去处理其他的事情了,当中断发生以后会触发我们事先设置好的中断服务函数,在中断服务函数中做具体的处理。比如在<Linux开发>驱动开发 -之-内核定时器与中断的测试,我们通过按键去开关LED灯,采用中断以后处理器就不需要时刻的去查看按键有没有被按下,因为按键按下以后会自动触发中断。同样的,Linux 应用程序可以通过阻塞或者非阻塞这两种方式来访问驱动设备,通过阻塞方式访问的话应用程序会处于休眠态,等待驱动设备可以使用,非阻塞方式的话会通过 poll 函数来不断的轮询,查看驱动设备文件是否可以使用。这两种方式都需要应用程序主动的去查询设备的使用情况,如果能提供一种类似中断的机制,当驱动程序可以访问的时候主动告诉应用程序那就最好了。

“信号”为此应运而生,信号类似于我们硬件上使用的“中断”,只不过信号是软件层次上的。算是在软件层次上对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据了。整个过程就相当于应用程序收到了驱动发送过来了的一个中断,然后应用程序去响应这个中断,在整个处理过程中应用程序并没有去查询驱动设备是否可以访问,一切都是由驱动设备自己告诉给应用程序的。
阻塞、非阻塞、异步通知,这三种是针对不同的场合提出来的不同的解决方法,没有优劣之分,在实际的工作和学习中,根据自己的实际需求选择合适的处理方法即可。

5.2 异步通知信号

异步通知的核心就是信号,在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所支持的所有信号,这些信号如下所示:

路径:arch\xtensa\include\uapi\asm\signal.h
#define SIGHUP		 1
#define SIGINT		 2
#define SIGQUIT		 3
#define SIGILL		 4
#define SIGTRAP		 5
#define SIGABRT		 6
#define SIGIOT		 6
#define SIGBUS		 7
#define SIGFPE		 8
#define SIGKILL		 9
#define SIGUSR1		10
#define SIGSEGV		11
#define SIGUSR2		12
#define SIGPIPE		13
#define SIGALRM		14
#define SIGTERM		15
#define SIGSTKFLT	16
#define SIGCHLD		17
#define SIGCONT		18
#define SIGSTOP		19
#define SIGTSTP		20
#define SIGTTIN		21
#define SIGTTOU		22
#define SIGURG		23
#define SIGXCPU		24
#define SIGXFSZ		25
#define SIGVTALRM	26
#define SIGPROF		27
#define SIGWINCH	28
#define SIGIO		29
#define SIGPOLL		SIGIO
/* #define SIGLOST		29 */
#define SIGPWR		30
#define SIGSYS		31
#define	SIGUNUSED	31

/* These should not be considered constants from userland.  */
#define SIGRTMIN	32
#define SIGRTMAX	(_NSIG-1)

这些信号中,除了 SIGKILL(9)和 SIGSTOP(19)这两个信号不能被忽略外,其他的信号都可以忽略。这些信号就相当于中断号,不同的中断号代表了不同的中断,不同的中断所做的处理不同,因此,驱动程序可以通过向应用程序发送不同的信号来实现不同的功能。

5.3 应用程序注册信号处理函数

我们使用中断的时候需要设置中断处理函数,同样的,如果要在应用程序中使用信号,那么就必须设置信号所使用的信号处理函数,在应用程序中使用 signal 函数来设置指定信号的处理函数,signal 函数原型如下所示:

sighandler_t signal(int signum, sighandler_t handler)

函数参数和返回值含义如下:
signum:要设置处理函数的信号。
handler:信号的处理函数。
返回值:设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR。

信号处理函数原型如下所示:

typedef void (*sighandler_t)(int)

熟悉shell命令的,应该知道 使用“kill -9 PID”杀死指定进程的方法就是向指定的进程(PID)发送SIGKILL 这个信号。当按下键盘上的 CTRL+C 组合键以后会向当前正在占用终端的应用程序发出 SIGINT 信号,SIGINT 信号默认的动作是关闭当前应用程序。这里我们修改一下 SIGINT 信号的默认处理函数,当按下 CTRL+C 组合键以后先在终端上打印出“…test SIGINT signal…”这行字符串,然后再关闭当前应用程序。新建 signal_test.c 文件,然后输入如下所示内容:

#include 
#include 
#include 

void signal_handler(int num)
{
    printf("\r\n......test SIGINT signal.......\r\n");
    exit(0);
}

void main(void)
{
    signal(SIGINT, signal_handler);
    while (1)
    {
        /* code */
    }
    return;
}

编译:

gcc signal_test.c  -o signal_test

运行:

./signal_test 

在这里插入图片描述
在上述代码中我们设置 SIGINT 信号的处理函数为 sigint_handler,当按下 CTRL+C向 signal_test 发送 SIGINT 信号以后 sigint_handler 函数就会执行,此函数先输出一行“…test SIGINT signal…”字符串,然后调用 exit 函数关闭 signal_test 应用程序。

5.4 驱动中的信号处理

前面介绍中说过,异步信号类似中断效果,设备驱动会主动通知应用程序;那么驱动部分需要如何实现呢?

首先驱动中需要维护一个结构体fasync_struct;在驱动程序中定义一个 fasync_struct 结构体指针变量,fasync_struct 结构体内容如下:

路径:include\linux\fs.h
struct fasync_struct {
	spinlock_t		fa_lock;
	int			magic;
	int			fa_fd;
	struct fasync_struct	*fa_next; /* singly linked list */
	struct file		*fa_file;
	struct rcu_head		fa_rcu;
};

一般我们定义fasync_struct结构体变量时,并不是单独定义一个全局变量,而是将其定义在设备的结构体中,我们仍然以<Linux开发>驱动开发 -之-内核定时器与中断中的代码修改来验证;
如下:

struct keyirq_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;		 /* 当前的按键号 */
	struct fasync_struct *async_queue; /* 异步相关结构体 */
};

结构体最后一个成员变量,就是在该设备结构体中定义一个fasync_struct结构体变量。

如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,此函数格式如下所示:

int (*fasync) (int fd, struct file *filp, int on)

fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针,fasync_helper 函数原型如下:

int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

fasync_helper 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数就是要初始化的 fasync_struct 结构体指针变量。

当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。

驱动代码中的实现示例如下:

struct xxx_dev {
	......
	struct fasync_struct *async_queue; /* 异步相关结构体 */
};

static int xxx_fasync(int fd, struct file *filp, int on)
{
	struct xxx_dev *dev = (xxx_dev)filp->private_data;
	
	if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)
		return -EIO;
	return 0;
}

static int xxx_release(struct inode *inode, struct file *filp)
{
	return xxx_fasync(-1, filp, 0); /* 删除异步通知 */
}

static struct file_operations xxx_ops = {
	......
	.fasync = xxx_fasync,
	.release = xxx_release,
	......
};

上述讲了 驱动代码 需要定义一个 fasync_struct 结构体指针变量,还有需要实现file_operations操作集中的fasync函数,接着就需要讲一下驱动如何发送消息给应用程序了。

驱动程序通过kill_fasync 函数将信号发送给应用程序;当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。kill_fasync函数负责发送指定的信号,kill_fasync 函数原型如下所示:

void kill_fasync(struct fasync_struct **fp, int sig, int band)

函数参数和返回值含义如下:
fp:要操作的 fasync_struct。
sig:要发送的信号。
band:可读时设置为 POLL_IN,可写时设置为 POLL_OUT。
返回值:无。

还是以<Linux开发>驱动开发 -之-内核定时器与中断修改,我们知道有按键按下就会产生中断,最后会在内核定时器的中断函数里判断按键是否按下,那么我们只需要在定时器处理函数判断按键按下之后调用kill_fasync函数将信号通知给应用程序就好了。
例如:

void timer_function(unsigned long arg)
{
    .........................
	if(atomic_read(&dev->releasekey)) {		/* 一次完整的按键过程 */
		if(dev->async_queue)
			kill_fasync(&dev->async_queue, SIGIO, POLL_IN);	/* 释放SIGIO信号 */
	}

}

5.5 应用程序开启信号通知

前面5.3小节,我们只讲了定义一个信号处理函数 和 设置信号处理函数,但是我们如果是自己写的驱动代码的话,是需要应用程序主动开启驱动程序中的FASYNC(异步通知)的,正如5.4小节说明的,我们自己写的驱动定义实现了异步通知功能,但是是否启动这个异步通知,还是需要去开启它。
应用程序开启异步通知函数如下:

flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */

通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。

5.6 应用程序异步通知整体流程

第一步:定义信号处理函数,并注册信号处理函数
对应5.3小节;
第二步:将当前应用程序的进程号告诉给内核
使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核。
第三步:开启异步通知
对应5.5小节;

通过上述三步之后,驱动程序就会通过kill_fasync函数通知到应用程序,并执行应用程序中的信号处理函数。

六 异步通知测试

6.1 硬件部分

本次测试是基于[<Linux开发>驱动开发 -之-内核定时器与中断](https://blog.csdn.net/qq_39257814/article/details/130952736)修改,所以设备树等完全以其相同即可;

6.2 编写驱动程序

在<Linux开发>驱动开发 -之-内核定时器与中断中的驱动代码基础上 并 加上 非阻塞IO 修改如下:

......
#include 
#include 
#include 
.......
#define	KEYIRQ_NAME	"KEYIRQ_FASYNC"				/*设备名*/
.......
struct keyirq_dev{
	.................
	wait_queue_head_t r_wait; /*  //新增 读等待队列头 */
	struct fasync_struct *async_queue;		/* 异步相关结构体 */
};
......
void timer_function(unsigned long arg)
{
 ..............
	if(atomic_read(&dev->releasekey)) {		/* 一次完整的按键过程 */
		if(dev->async_queue)
			kill_fasync(&dev->async_queue, SIGIO, POLL_IN);	/* 释放SIGIO信号 */
	}

#if 0
	/* 唤醒进程 */
	if(atomic_read(&dev->releasekey)) {	/* 完成一次按键过程 */
		/* wake_up(&dev->r_wait); */
		wake_up_interruptible(&dev->r_wait);
	}
#endif
}
.............
static int keyio_init(void)
{
	.......
	/* 初始化等待队列头 */
	init_waitqueue_head(&imx6uirq.r_wait);
}
............
static ssize_t keyirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	.........
	if (filp->f_flags & O_NONBLOCK)	{ /* 非阻塞访问 */
		if(atomic_read(&dev->releasekey) == 0)	/* 没有按键按下,返回-EAGAIN */
			return -EAGAIN;
	} else {							/* 阻塞访问 */
		/* 加入等待队列,等待被唤醒,也就是有按键按下 */
 		ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); 
		if (ret) {
			goto wait_error;
		}
	}
	...........
wait_error:
	return ret;
......
}
.......
/*
  * @description     : poll函数,用于处理非阻塞访问
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - wait    : 等待列表(poll_table)
  * @return          : 设备或者资源状态,
  */
unsigned int keyirq_poll(struct file *filp, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	struct keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;

	poll_wait(filp, &dev->r_wait, wait);	/* 将等待队列头添加到poll_table中 */
	
	if(atomic_read(&dev->releasekey)) {		/* 按键按下 */
		mask = POLLIN | POLLRDNORM;			/* 返回PLLIN */
	}
	return mask;
}

/*
 * @description     : fasync函数,用于处理异步通知
 * @param - fd		: 文件描述符
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @param - on      : 模式
 * @return          : 负数表示函数执行失败
 */
static int keyirq_fasync(int fd, struct file *filp, int on)
{
	struct keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;
	return fasync_helper(fd, filp, on, &dev->async_queue);
}

/*
 * @description     : release函数,应用程序调用close关闭驱动文件的时候会执行
 * @param - inode	: inode节点
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @return          : 负数表示函数执行失败
 */
static int keyirq_release(struct inode *inode, struct file *filp)
{
	return keyirq_fasync(-1, filp, 0);
}

/* 设备操作函数 */
static struct file_operations keyirq_fops = {
	.owner = THIS_MODULE,
	.open = keyirq_open,
	.read = keyirq_read,
	.poll = keyirq_poll,
	.fasync = keyirq_fasync,
	.release = keyirq_release,
};
.......

6.3 完整驱动程序

完成的驱动程序如下:

/***************************************************************
Copyright © OneFu Co., Ltd. 2018-2023. All rights reserved.
文件名 : noblockio.c
作者 : water
版本 : V1.0
描述 : Linux noblockio 中断驱动实验
其他 : 无
日志 : 初版 V1.0 2023/06/12 water创建
***************************************************************/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define	KEYIRQ_CNT	1						/*	设备号个数	*/
#define	KEYIRQ_NAME	"KEYIRQ_FASYNC"		/*设备名*/
#define	KEY0VALUE		0X01				/*	key0按键值	*/
#define	INVAKEY				0XFF			/*	无效按键值	*/
#define	KEY_NUM			1					/*	按键数量	*/

/*	IO中断描述结构体	*/
struct irq_keydesc	{
	int gpio;						/* 使用的GPIO */
	int irqnum;						/* GPIO中断号 */
	unsigned char value;			/* 按键键值 */
	char name[10];					/* 设备名 */
	irqreturn_t (*handler)(int, void *);	/* 中断处理函数 */
};

/* keyirq设备结构体 */
struct keyirq_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; /*  //新增 读等待队列头 */
	struct fasync_struct *async_queue;		/* 异步相关结构体 */
};

struct keyirq_dev  keyirq; /* irq 设备 */

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

	dev->curkeynum = 0;
	dev->timer.data = (volatile long)dev_id;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定时 */
	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 keyirq_dev *dev = (struct keyirq_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];

	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
	if(value == 0){ 						/* 按下按键 */
		atomic_set(&dev->keyvalue, keydesc->value);
	}
	else{ 									/* 按键松开 */
		atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
		atomic_set(&dev->releasekey, 1);	/* 标记松开按键,即完成一次完整的按键过程 */			
	}	

	if(atomic_read(&dev->releasekey)) {		/* 一次完整的按键过程 */
		if(dev->async_queue)
			kill_fasync(&dev->async_queue, SIGIO, POLL_IN);	/* 释放SIGIO信号 */
	}

#if 0
	/*新增 唤醒进程 */
	 if(atomic_read(&dev->releasekey)) { /* 完成一次按键过程 */
	 /* wake_up(&dev->r_wait); */
	 wake_up_interruptible(&dev->r_wait);
	 }
#endif
}

/*
 * @description	: 按键IO初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keyio_init(void)
{
	unsigned char i = 0;
	int ret = 0;
    char name[10];
	
	keyirq.nd = of_find_node_by_path("/key");
	if (keyirq.nd== NULL){
		printk("key node not find!\r\n");
		return -EINVAL;
	} else {
        printk("key node has been found!\r\n");
    }

	/* 提取GPIO */
	for (i = 0; i < KEY_NUM; i++) {
		keyirq.irqkeydesc[i].gpio = of_get_named_gpio(keyirq.nd ,"key-gpio", i);
		if (keyirq.irqkeydesc[i].gpio < 0) {
			printk("can't get key%d\r\n", i);
		}
	}
	
	/* 初始化key所使用的IO,并且设置成中断模式 */
	for (i = 0; i < KEY_NUM; i++) {
		memset(keyirq.irqkeydesc[i].name, 0, sizeof(name));	/* 缓冲区清零 */
		sprintf(keyirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
		gpio_request(keyirq.irqkeydesc[i].gpio, keyirq.irqkeydesc[i].name);
		gpio_direction_input(keyirq.irqkeydesc[i].gpio);	
		keyirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(keyirq.nd, i);
#if 0
		keyirq.irqkeydesc[i].irqnum = gpio_to_irq(keyirq.irqkeydesc[i].gpio);
#endif
		printk("key%d:gpio=%d, irqnum=%d\r\n",i, keyirq.irqkeydesc[i].gpio, 
                                         keyirq.irqkeydesc[i].irqnum);
	}
	/* 申请中断 */
	keyirq.irqkeydesc[0].handler = key0_handler;
	keyirq.irqkeydesc[0].value = KEY0VALUE;
	
	for (i = 0; i < KEY_NUM; i++) {
		ret = request_irq(keyirq.irqkeydesc[i].irqnum, keyirq.irqkeydesc[i].handler, 
		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, keyirq.irqkeydesc[i].name, &keyirq);
		if(ret < 0){
			printk("irq %d request failed!\r\n", keyirq.irqkeydesc[i].irqnum);
			return -EFAULT;
		}
	}

	/* 创建定时器 */
	init_timer(&keyirq.timer);
	keyirq.timer.function = timer_function;

	/* 新增 初始化等待队列头 */
	init_waitqueue_head(&keyirq.r_wait);

	return 0;
}

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

 /*
  * @description     : 从设备读取数据 
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - buf     : 返回给用户空间的数据缓冲区
  * @param - cnt     : 要读取的数据长度
  * @param - offt    : 相对于文件首地址的偏移
  * @return          : 读取的字节数,如果为负值,表示读取失败
  */
static ssize_t keyirq_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 keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;
	
	/* 新增 */
	if (filp->f_flags & O_NONBLOCK)	{ /* 非阻塞访问 */
		if(atomic_read(&dev->releasekey) == 0)	/* 没有按键按下,返回-EAGAIN */
			return -EAGAIN;
	} else {							/* 阻塞访问 */
		/* 加入等待队列,等待被唤醒,也就是有按键按下 */
 		ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); 
		if (ret) {
			goto wait_error;
		}
	}


	keyvalue = atomic_read(&dev->keyvalue);
	releasekey = atomic_read(&dev->releasekey);

	if (releasekey) { /* 有按键按下 */	
		if (keyvalue & 0x80) {
			keyvalue &= ~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:
	return ret;

data_error:
	return -EINVAL;
}

/*
  * @description     : poll函数,用于处理非阻塞访问
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - wait    : 等待列表(poll_table)
  * @return          : 设备或者资源状态,
  */
unsigned int keyirq_poll(struct file *filp, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	struct keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;

	poll_wait(filp, &dev->r_wait, wait);	/* 将等待队列头添加到poll_table中 */
	
	if(atomic_read(&dev->releasekey)) {		/* 按键按下 */
		mask = POLLIN | POLLRDNORM;			/* 返回PLLIN */
	}
	return mask;
}

/*
 * @description     : fasync函数,用于处理异步通知
 * @param - fd		: 文件描述符
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @param - on      : 模式
 * @return          : 负数表示函数执行失败
 */
static int keyirq_fasync(int fd, struct file *filp, int on)
{
	struct keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;
	return fasync_helper(fd, filp, on, &dev->async_queue);
}

/*
 * @description     : release函数,应用程序调用close关闭驱动文件的时候会执行
 * @param - inode	: inode节点
 * @param - filp    : 要打开的设备文件(文件描述符)
 * @return          : 负数表示函数执行失败
 */
static int keyirq_release(struct inode *inode, struct file *filp)
{
	return keyirq_fasync(-1, filp, 0);
}

/* 设备操作函数 */
static struct file_operations keyirq_fops = {
	.owner = THIS_MODULE,
	.open = keyirq_open,
	.read = keyirq_read,
	.poll = keyirq_poll,
	.fasync = keyirq_fasync,
	.release = keyirq_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init keyirq_init(void)
{
	/* 1、构建设备号 */
	if (keyirq.major) {
		keyirq.devid = MKDEV(keyirq.major, 0);
		register_chrdev_region(keyirq.devid, KEYIRQ_CNT, KEYIRQ_NAME);
	} else {
		alloc_chrdev_region(&keyirq.devid, 0, KEYIRQ_CNT, KEYIRQ_NAME);
		keyirq.major = MAJOR(keyirq.devid);
		keyirq.minor = MINOR(keyirq.devid);
	}

	/* 2、注册字符设备 */
	cdev_init(&keyirq.cdev, &keyirq_fops);
	cdev_add(&keyirq.cdev, keyirq.devid, KEYIRQ_CNT);

	/* 3、创建类 */
	keyirq.class = class_create(THIS_MODULE, KEYIRQ_NAME);
	if (IS_ERR(keyirq.class)) {
		return PTR_ERR(keyirq.class);
	}

	/* 4、创建设备 */
	keyirq.device = device_create(keyirq.class, NULL, keyirq.devid, NULL, KEYIRQ_NAME);
	if (IS_ERR(keyirq.device)) {
		return PTR_ERR(keyirq.device);
	}
	
	/* 5、初始化按键 */
	atomic_set(&keyirq.keyvalue, INVAKEY);
	atomic_set(&keyirq.releasekey, 0);
	keyio_init();
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit keyirq_exit(void)
{
	unsigned int i = 0;
	/* 删除定时器 */
	del_timer_sync(&keyirq.timer);	/* 删除定时器 */
		
	/* 释放中断 */
	for (i = 0; i < KEY_NUM; i++) {
		free_irq(keyirq.irqkeydesc[i].irqnum, &keyirq);
		gpio_free(keyirq.irqkeydesc[i].gpio);
	}
	cdev_del(&keyirq.cdev);
	unregister_chrdev_region(keyirq.devid, KEYIRQ_CNT);
	device_destroy(keyirq.class, keyirq.devid);
	class_destroy(keyirq.class);
}

module_init(keyirq_init);
module_exit(keyirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("water");

6.4 编写测试应用程序

新建fasync_app.c文件,输入以下内容:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "poll.h"
#include "sys/select.h"
#include "sys/time.h"
#include "linux/ioctl.h"
#include "signal.h"
/***************************************************************
Copyright © OneFu Co., Ltd. 2018-2023. All rights reserved.
文件名		: fasync_app.c
作者	  	: water
版本	   	: V1.0
描述	   	: 异步通知测试应用程序
其他	   	: 无
使用方法	:./fasync_app /dev/KEYIRQ_FASYNC 打开测试App
日志	   	: 初版V1.0 2023/06/15 water创建
***************************************************************/

static int fd = 0;	/* 文件描述符 */
static int ledfd = 0;	/* 文件描述符 */
static char ledfilename[]="/dev/gpioled";
static int ledstatu = 0;
static unsigned char databuf[1];       //定义的buf,用来读写数据用
static int retvalue;
/*
 * SIGIO信号处理函数
 * @param - signum 	: 信号值
 * @return 			: 无
 */
static void sigio_signal_func(int signum)
{
	int err = 0;
	unsigned int keyvalue = 0;

	err = read(fd, &keyvalue, sizeof(keyvalue));
	if(err < 0) {
		/* 读取错误 */
	} else {
		printf("sigio signal! key value=%d\r\n", keyvalue);
		if (keyvalue)	/* 读取到数据 */
			{
				printf("key value = %#X\r\n", keyvalue);
				ledstatu = !ledstatu;
				databuf[0] = ledstatu;
				retvalue = write(ledfd, databuf, sizeof(databuf));     /*向设备驱动写入数据*/
				if(retvalue < 0){
					printf("LED Control Failed!\r\n",ledfilename);     /*写入错误输出提示*/
				}
			}
	}
}

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

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

	ledfd = open(ledfilename,O_RDWR);                         /*打开驱动文件*/
    if(ledfd < 0){
        printf("Can't open file %s\r\n",ledfilename);      /*打开失败,输出提示*/
        return -1;
	}

	/* 设置信号SIGIO的处理函数 */
	signal(SIGIO, sigio_signal_func);
	
	fcntl(fd, F_SETOWN, getpid());		/* 设置当前进程接收SIGIO信号 	*/
	flags = fcntl(fd, F_GETFL);			/* 获取当前的进程状态 			*/
	fcntl(fd, F_SETFL, flags | FASYNC);	/* 设置进程启用异步通知功能 	*/	

	while(1) {
		sleep(2);
	}

	close(fd);
	close(ledfd);
	return 0;
}

//编译:arm-linux-gnueabihf-gcc fasync_app.c -o fasync_app

6.5 编译

编写 Makefile 文件,内容如下:

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

KERNELDIR := /home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/
CURRENT_PATH := $(shell pwd)
obj-m := fasync.o

KBUILD_CFLAGS += -fno-pie

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean片

驱动编译:make
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第17张图片

测试应用程序编译:

arm-linux-gnueabihf-gcc fasync_app.c -o fasync_app

在这里插入图片描述

6.6 运行验证

拷贝到设备上:

cp app/fasync_app  ../../nfs/buildrootfs/root/water_soft/fasync/
cp fasync.ko  ../../nfs/buildrootfs/lib/modules/4.1.15+/

在这里插入图片描述

将ko驱动文件放到设备的“/lib/modules/4.1.15+”目录下,并执行:

depmod //第一次加载驱动的时候需要运行此命令
modprobe fasync.ko //加载驱动

<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第18张图片

将测试应用程序放到设备“/root/water_soft/fasync/”目录下,并执行:

./fasync_app  /dev/KEYIRQ_FASYNC &

按下开发板上的 KEY0 键,终端就会输出按键值如下
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第19张图片

运行“top”命令,查看CPU占用率,如下:
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第20张图片
实际现象符合预期设计,且cpu占用率很低。

关闭进程命令: kill -9 132
其中132为进程号:
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知_第21张图片
卸载驱动:rmmod fasync.ko

在这里插入图片描述

6.7 结束测试

停止后台运行的应用程序:
得先查看进程的PID(进程号)

kill -9 PID

卸载驱动命令:

rmmod xxx.ko

七 结论

上述讲解的三种方法,阻塞IO、非阻塞IO以及异步通知,是驱动程序中比较常用的,特别是对硬件设备访问时,如果硬件设备读写不可使用时,就需要通过此类方法处理,有效降低CPU占有率等。方法不分差异,凭适用情况选用合适的方法即可。

你可能感兴趣的:(Linux驱动开发,驱动开发,linux,arm开发)