??作者:一只大喵咪1201
??专栏:《Linux驱动》
??格言:你只管努力,剩下的交给时间!
目录
- ??休眠与唤醒
-
- ??内核函数
- ??驱动框架及编程
- ??POLL机制
-
- ??驱动编程
- ??应用编程
- ??异步通知
-
- ??驱动编程
- ??应用编程
- ??阻塞与非阻塞
-
- ??应用编程
- ??驱动编程
- ??定时器
-
- ??定时器消抖
- ??总结
??休眠与唤醒
在前面讲解按键驱动时,本喵提到过休眠唤醒的按键驱动方式:应用程序必须等待按键按下后,才会继续执行,否则则处于阻塞状态。
如上图所示应用层APP获取按键值的流程:
- APP调用
read 系统调用读取按键数据。 - APP进入内核态,通过文件系统中的
sys_read 调用驱动层的drv_read 。 - 发现有数据则复制到用户空间并马上返回,如果没有数据则APP休眠,也就是阻塞不动。
驱动中没有数据时,APP在内核态执行到
如上图,当APP1因为没有按键数据而休眠时,内核会挑选出其他APP去运行,当按下按键后:
- 驱动程序中的中断服务程序被调用,它会记录数据,并唤醒APP1。
- 当APP1再次运行时,就会继续执行
drv_read 中剩下的代码,把数据复制回用户空间,然后返回用户空间。
所谓唤醒就是把APP的状态改为
APP1的执行过程如红色实线所示,它被分成了两段:
- 红线所涉及的代码,都是APP1调用的,它都属于APP1的上下文。
- 按键的中断服务程序,不属于APP1的上下文,它属于中断的上下文。
??内核函数
休眠函数:
当按键的驱动程序
函数 | 说明 |
---|---|
wait_event_interruptible(wq, condition) | 休眠,直到condition为真,休眠期间是可以被信号打断的 |
wait_event(wq, condition) | 休眠,直到condition为真,退出的唯一条件就是condition为真,信号也无法打断。 |
wait_event_interruptible_timeout(wq, condition, timeout) | 休眠,直到condition为真或者超时,休眠期间可以被信号打断 |
wait_event_timeout(wq, condition, timeout) | 休眠,直到condition为真或者超时,信号无法打断。 |
参数说明:
wq:waitqueue ,等待队列。- 休眠时除了把程序状态改为非
RUNNING 之外,还要把进程/进程放入wq 中,以后中断服务程序要从wq 中把它取出来唤醒。
- 休眠时除了把程序状态改为非
condition :可以是变量,也可以是任何表达式,表示一直等待,直到condition为真。
唤醒函数:
在按键的中断服务函数中,需要去唤醒处于休眠状态的进程/线程,同样也通过内核提供的唤醒函数:
函数 | 说明 |
---|---|
wake_up_interruptible(x) | 唤醒 x 队列中状态为TASK_INTERRUPTIBLE的线程,只唤 醒其中的一个线程 |
wake_up_interruptible_nr(x, nr) | 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程,只唤 醒其中的 nr 个线程 |
wake_up_interruptible_all(x) | 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”的线程,唤醒 其中的所有线程 |
wake_up(x) | 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE ”或 “TASK_UNINTERRUPTIBLE”的线程,只唤醒其中的一个线程 |
wake_up_nr(x, nr) | 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”或“TASK_UNINTERRUPTIBLE”的线程,只唤醒其中nr个线程 |
wake_up_all(x) | 唤醒 x 队列中状态为“TASK_INTERRUPTIBLE”或“TASK_UNINTERRUPTIBLE”的线程,唤醒其中的所有线程 |
参数说明:
x :就是前面在休眠时存放非RUNNING 状态的线程/进程的队列。TASK_INTERRUPTIBLE :该状态的休眠线程是可以被信号中断休眠的。TASK_UNINTERRUPTIBLE :该状态的休眠线程是不可以被信号中断休眠的。
??驱动框架及编程
如上图所示是休眠唤醒的驱动框架,根据该框架,我们需要做这几件事情:
- 初始化
wq 队列,使用DECLARE_WAIT_QUEUE_HEAD() 宏函数实现。 - 在驱动层的
drv_read 中,调用wait_event_interruptible() 函数:- 它本身会判断
event 参数是否为FALSE ,如果为FASLE 表示无数据,会将自己放入wq 等待队列并休眠。 - 当从
wait_event_interruptible() 函数返回后,会继续把数据复制回用户空间。
- 它本身会判断
- 在中断服务程序中,设置
event 为TRUE ,并且调用wake_up_interruptible 唤醒正在队列中休眠的线程/进程。
编程:
下面是在之前本喵实现的按键中断程序的基础上实现休眠唤醒,设备树文件本喵就不再贴图了,有兴趣的小伙伴移步这里。
注册
本喵这里为了防止按键数据丢失,使用环形缓冲区来记录按键数据:
如上图所示环形缓冲区以及操作的接口函数定义,包括判空,判满,写数据和读数据。
- gpio_key_drv_read函数
当应用层使用系统调用
如上图代码所示,先使用
当调用到驱动层的
- 如果环形缓冲区是空的,说明没有数据,则将当前线程放入到等待队列中休眠。
- 如果环形缓冲区不是空的,说明有数据,则不将当前线程放入等待队列,而是直接调用获取环形缓冲区中的数据并且拷贝到用户空间中。
如果因为没有数据而处于休眠状态,则该线程就会阻塞到这里,当该线程被唤醒后,说明环形缓冲区中有数据了,则读取数据并且拷贝到用户空间中。
- gpio_key_isr中断服务函数
如果线程因为没有按键数据而休眠了,那么只能在中断服务程序中将其唤醒:
如上图所示中断服务函数,当按键中断产生后,获取GPIO引脚的逻辑值,然后构造出按键值:
- 按键值
key :低八位是GPIO引脚的逻辑值,其余位是描述引脚信息的整数。
再调用
- 在中断服务函数中向环形缓冲区中写入了按键数据,也就是意味着驱动程序
drv_read 中的event:!is_key_buf_empty() 变成了TRUE ,所以线程就被唤醒了。- 由于
gpio_key_wait 是专门用来存放读取按键的线程的,所以目前该等待队列中只有一个线程,中断服务函数不会唤醒错误。
演示:
如上图应用层的
- 如果没有按键数据,该进程会阻塞在
read 调用处,不会执行下去。 - 当有按键数据时,进程就会继续执行。
将驱动程序和测试程序上传到服务器进行编译,将得到的
如上图所示,将按键驱动程序安装以后,再运行应用层的测试程序:
- 运行起来后,程序阻塞在
read 处,因为没有按键值。 - 按下开发板上的按键后,驱动程序打印出了按键的整数编号和逻辑值。
- 应用程序打印出了我们在驱动程序中构建后的按键值。
??POLL机制
休眠唤醒机制的致命缺陷在于,如果没有按键数据,进程/线程就会一直死等下去。而
- APP不知道驱动程序中是否有数据,可以先调用
poll 函数查询一下,poll 函数可以传入超时时间。 - APP进入内核态,调用到驱动程序的
drv_poll 函数,如果有数据的话立刻返回。 - 如果发现没有数据时就休眠一段时间。
- 当按下按键时,驱动程序的中断服务程序被调用,它会记录数据并且唤醒APP。
- 当超时时间到了之后,内核也会唤醒APP。
- APP根据
poll 函数的返回值就可以知道是否有数据,如果有数据就调用read 得到数据。
如上图所示
- APP使用
open 系统调用打开按键设备。 - 通过内核文件系统中
sys_open 函数调用file_operation 结构体中的drv_open 驱动函数来打开设备。
每一个设备,内核都会将其看成是一个文件,都会在内核中创建一个
-
APP调用
poll 系统调用后进入内核态。 -
内核文件系统中的
sys_poll ,会在死循环for 中,先调用驱动程序的drv_poll 来获取状态event 。- 在驱动程序
drv_poll 中,要把当前线程挂入到等待队列wq 中,否则在唤醒的时候就找不到该线程了。 - 但是驱动程序
drv_poll 并不会让当前线程休眠。
- 在驱动程序
-
返回的状态表示当前没有数据,那么内核文件系统就让该线程休眠一会儿。
-
线程休眠过程中,按下了按键,产生了按键中断,在中断服务函数中记录按键值,并且从
wq 等待队列中将线程唤醒。 -
线程被唤醒后处于内核文件系统中的
for 死循环中,所以还要再执行一次drv_poll 驱动程序,获取按键数据的状态。 -
此时获取到的数据状态表示有按键数据,就会从返回到用户态,APP可以继续执行不再阻塞。
-
APP根据
poll 的返回值发现有按键数据,则调用read 函数读取按键数据。
如果一直没有按键数据,也就是线程在休眠后一直没有被唤醒,此时的流程也是类似的,从第三步开始看:
- APP调用
poll 系统调用后进入内核态。 - 导致驱动程序的
drv_poll 被调用。 - 假设当前没有数据,则休眠一会。
- 在休眠过程中,一直没有按下了按键,超时时间到了,内核把这个线程唤醒。
- 线程从休眠中被唤醒,继续执行
for 循环,再次调用drv_poll 驱动程序获取数据状态。 - 此时获取到的数据状态仍然表示没有数据,但是超时时间已经到了,也只能从内核态返回用户态了。
- APP根据
poll 的返回值发现没有按键数据,则不能调用read 函数读取按键数据。
这个过程中有几点需要注意:
drv_poll 要把线程挂入队列wq ,但是并不是在drv_poll 中进入休眠,而 是在调用drv_poll 之后休眠。
drv_poll 驱动程序只做两件事情:
- 把线程放入到等待队列
wq 中,但是不休眠。- 返回
event 事件状态,而不是返回事件值。
- APP调用一次
poll ,有可能会导致drv_poll 被调用两次:- 进入内核文件系统的
for 循环中先调用一次获取状态。 - 被唤醒后(中断或者超时)执行
for 循环再调用一次,判断是数据到来还是超时,然后返回用户态。
- 进入内核文件系统的
- APP要判断
poll 返回的原因:- 有数据到来,还是超时。有数据到来才能调用
read 函数读取按键值。
- 有数据到来,还是超时。有数据到来才能调用
??驱动编程
使用
- 把当前线程挂入等待队列
wq ,使用内核提供的函数poll_wait 来实现:- APP调用一次
poll ,可能导致drv_poll 被调用2 次,但是我们并不需要把当前线程挂入队列2次。 - 使用内核的函数
poll_wait 把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。
- APP调用一次
- 返回设备状态,APP调用
poll 函数时,有可能是查询有没有数据可以读,有可能是查询有没有空间可以写数据。- 有数据可以读:
(POLLIN | POLLRDNORM) ,POLLRDNORM 等同于 POLLIN,为了兼容某些 APP 把它们一起返回。 - 有空间可以写:
(POLLOUT | POLLWRNORM) ,POLLWRNORM 等同于 POLLOUT ,为了兼容某些 APP 把它们一起返回。
- 有数据可以读:
APP 调用
所以中断服务函数不用做任何改动,主要是要实现
如上图所示代码,在
- 将当前线程放入等待队列
gpio_key_wait ,和前面休眠唤醒的是一个队列,但是不进行休眠。 - 返回按键数据状态,根据环形缓冲区是否为空,决定返回0,还是
(POLLOUT | POLLWRNORM) 。
可以看到,
??应用编程
应用层
如上图所示代码,由于我们只检测一个按键,所以
将驱动设备使用
全部配置好后,使用
- 没有按键数据,程序则阻塞在
poll 处休眠一会儿,等待被唤醒。 - 被唤醒后:
- 如果是因为超时唤醒,则
poll 得到的数据状态就是0,则打印超时。 - 如果是因为数据到来唤醒,则
poll 得到是数据状态就是(POLLOUT | POLLWRNORM) ,使用read 读取按键数据,此时read 必然不会阻塞。
- 如果是因为超时唤醒,则
演示:
最后将驱动程序和应用程序上传到服务器进行编译,并且将生成的
如上图所示,将驱动程序安装好以后,在开发板运行测试程序后:
drv_poll 驱动程序先运行一次查看按键数据状态,发现没有数据以后休眠一会儿。- 迟迟没有按键按下,被超时唤醒,打印
timeout 。 - 当按键数据按下后,被数据唤醒后,打印按键数据。
??异步通知
如果APP在读取按键数据的过程中不想休眠怎么办?使用异步通知,也就是当有了按键数据以后,驱动程序主动通知APP有数据了,此时APP去读取数据即可,平时APP可以干其他事情。
如上图所示使用异步通知的流程图。
- 使用
open 系统调用打开驱动,得到驱动的文件描述符fd 。 - 使用
signal 系统调用为SIGIO 信号注册信号处理函数func 。- 按键驱动程序发出的信号是
SIGIO 信号,表示有数据输入。 - APP收到
SIGIO 信号后,处理函数func 就会被自动调用。
- 按键驱动程序发出的信号是
- 使用
fcntl 将当前进程的PID设置到内核文件系统中的struct file 结构体中,方便后面驱动程序找到进程。 - 读取驱动程序文件的
Flag 。 - 设置
Flag 里面的FASYNC 位为 1:- 该
Flag 也是记录到内核文件系统的struct file 结构体中,驱动程序通过struct file* filp 指针可以获取该标志。 - 当
FASYNC 位发生变化时,内核文件系统就会调用驱动程序的drv_fasync 函数。
- 该
drv_fasync 是否调用是由FASYNC 标志位决定的,应用层并没有相应的fasync 函数。- 在
drv_fasync 函数中,调用内核提供的faync_helper 函数,它会根据FAYSNC 的值决定是否设置button_async->fa_file=驱动文件filp :- 驱动文件
filp 结构体里面含有之前设置的PID。 on 就代表是着FAYSNC 位,它为1则设置button_async ,它为0则不设置。
- 驱动文件
- APP可以做其他事情。
- 按键按下后,产生按键中断,调用中断服务函数。
- 在中断服务函数中调用内核提供的
kill_fasync 函数,向APP发送信号:- 如果
button_async->fa_file 非空,则从它指向的filp 的结构体中取出进程的PID,向该线程发送SIGIO 信号。 - 如果
button_async->fa_file 为空,则该函数什么都不做,不会发送任何信号。
- 如果
- 在按键中断服务程序中发送
SIGIO 信号后,信号处理函数func 被调用。 - 在
func 中使用read 系统调用读取按键数据。 - 最终会调用驱动层中的
drv_read 读取按键数据,此时一定是有数据的,并不会休眠。
在整个流程中可以看到,
??驱动编程
使用异步通知时,驱动程序的核心有两步:
- 提供对应的
drv_fasync 函数。 - 并在合适的时机发信号,这一点在按键中断服务程序中完成。
如上图所示,创建一个
在
如上图所示,在按键的中断服务函数中,通过内核提供的
??应用编程
异步通知比较复杂的部分是在应用层的测试函数中:
如上图所示代码,首先定义一个信号处理函数
在
但是驱动程序怎么知道要将
此时由于
此时异步通知就设置完毕了,APP就可以在
演示:
编译等过程本喵就不啰嗦了,直接演示:
如上图所示,运行测试程序后:
- 没有按键数据,APP在处理其他事情,打印
I am A Big Miaomi 字符串。 - 按键按下后,在信号处理函数中读取按键数据并且打印出来。
??阻塞与非阻塞
所谓阻塞,就是等待某件事情发生。比如调用
此时的休眠是我们在驱动程序中通过内核提供的
能不能让
- 在APP中使用
open 函数时,可以传入O_NONBLOCK ,就表示要使用非阻塞方式读取数据。默认情况是阻塞方式读取。 - 在APP中
open 一个文件之后,也可以通过fcntl 系统调用修改为阻塞或非阻塞读取数据。
- 对于字符设备文件,
O_NONBLOCK 起作用的前提是驱动程序针对O_NONBLOCK 做了处理。
??应用编程
这里本喵先来实现应用层编程:
如上图所示代码,它分为如下几步:
- 在使用
open 打开驱动设备时,传入O_NONBLOCK 标志,使用非阻塞访问的方式打开。 - 然后不管有没有按键数据,使用非阻塞方式读取十次按键数据:
- 有按键数据则返回按键数据,并且打印。
- 没有按键数据则直接返回,并且打印-1。
- 使用
fcntl 将驱动设备设置为阻塞访问方式。 - 在
while 死循环中,使用read 阻塞读取按键数据:- 有按键数据则返回按键数据,并且打印。
- 没有按键数据则阻塞不动,不会打印内容。
??驱动编程
如上图代码所示,需要在驱动层的
- 因为非阻塞方式在应用层设置的,并且
O_NONBLOCK 标志设置到内核文件系统的struct file 结构体中的。 - 所以在
drv_read 驱动程序中,如果是非阻塞方式,并且没有按键数据,则不阻塞直接返回-EAGAIN 。 - 如果是非阻塞方式,且有按键数据,则并不会将当前线程放入到等待队列中,而是返回按键数据。
此时驱动程序就支持阻塞和非阻塞两种访问方式了。
演示:
如上图所示,在开发板上运行起测试程序以后:
- 先非阻塞读取按键数据十次,由于此时没有按键数据,所以返回的都是-1。
- 然后开始阻塞读取按键数据,有按键数据时打印,没有按键数据时阻塞不动。
- 可以看到,我们在驱动程序中,这几种读取按键的方式都提供了。
- 应用层可以使用任意一种,我们不能指定应用层使用哪种方式。
- 驱动开发的原则只提供能力,不提供策略。
??定时器
所谓定时器,就是闹钟,时间到后就要做某些事。有两个要素:时间、做 事,换成程序员的话就是:超时时间、函数。
本喵这里讲解的定时器是软件定时器,它依赖系统的滴答定时器。在内核中使用定时器比较简单,涉及到一些内核提供的函数:
函数 | 说明 |
---|---|
setup_timer(timer, fn, data) | 设置定时器,主要是初始化 timer_list 结构体,设置其中的函数、超时事件等参数。 |
void add_timer(struct timer_list *timer) | 向内核添加定时器。 |
int mod_timer(struct timer_list *timer, unsigned long expires) | 修改定时器的超时时间 |
int del_timer(struct timer_list *timer) | 删除定时器。 |
当超时时间到达,内核就会调用这个超时函数:
编译内核时,可以在内核源码根目录下用
每发生一次 tick 中断,全局变量
- 在
add_timer 之前,直接修改:timer.expires = jiffies + xxx; // xxx 表示多少个滴答后超时,也就是 xxx*10mstimer.expires = jiffies + 2*HZ; // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒
- 在
add_timer 之后,使用mod_timer 修改:mod_timer(&timer, jiffies + xxx); // xxx 表示多少个滴答后超时,也就是 xxx*10msmod_timer(&timer, jiffies + 2*HZ); // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒
??定时器消抖
具体的原理本喵就不再讲解了,主要就是按下一次按键后会多次触发按键中断,由于本喵设置的是双边沿触发,所以触发的会更多,通过定时器来消除这个按键抖动。
如上图所示,在描述按键中断的结构体中,再增加一个
如上图所示,在
- 使用
setup_timer 初始化定时器,其中key_timer_expire 是超时函数,最后一个参数是超时函数的参数,把按键传进去,方便使用。 - 将超时时间设置为最大,如果是0的话,定时器一被添加到内核中就会执行超时函数。
- 使用
add_timer 将定时器添加到内核中。
如上图所示,在卸载驱动程序的时候,使用
如上图所示按键中断服务函数,此时读取按键数据就不在这里进行了,这里仅修改超时时间:
- 使用
mod_timer 修改超时时间,每发生一次抖动定时器超时向后推迟200ms。
如上图所示,在定时器的超时函数
- 构建按键数据,将其放入到环形缓冲区中。
- 从等待队列中唤醒休眠的线程。
- 向进程发送
SIGIO 信号。
还增加了一些打印语句,用于查看定时器超时函数和按键中断服务函数的执行次数。
演示:
只需要将驱动程序编译后重新安装到开发板上,运行之前的测试程序即可:
如上图所示,此时就达到了消抖的功能:
- 按键按下后,按键中断服务函数执行了两次,定时器两次推迟超时200ms。
- 按键稳定后,定时器超时函数执行一次,打印按键数据。
- 应用层也只读取了一次按键数据。
??总结
休眠唤醒,POLL机制,异步通知,阻塞与非阻塞都属于驱动程序的基石,非常的重要,后面的复杂的驱动程序也是依赖于这几个机制实现的。
不用应用层是否使用这几个机制,我们在驱动程序中需要全部提供,要遵守提供能力,不提供策略的原则。