休眠-唤醒机制
- APP 调用
read
等函数时,试图读取数据,比如读取按键; - APP 进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;
- 如果APP 在内核态,也就是驱动程序中发现没有数据,则APP 休眠;
- 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,他会记录数据,唤醒APP;
- APP 继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回。
驱动中有数据时,图中红线就是APP1 的执行过程,设计用户态、内核态
由应用层发起,再回到应用层。处于进程/线程上下文可以调用复杂/ 耗时的函数。
在中断上下文则不一样,中断不能休眠
驱动中没有数据时,APP1在内核态执行到 drv_read
时会休眠。所谓休眠就是把自己的状态改为非 RUNNING
,这样内核的调度器就不会让它运行。当按下按键,驱动程序中的中断服务程序被调用,它会记录数据,并唤醒APP1。所以唤醒就是把程序的状态改为 RUNNING
,这样内核的调度器有合适的时间就会让它运行。当APP1 再次运行时,就会继续执行 drv_read
中剩下的代码,把数据复制回用户空间,返回用户空间。APP1的执行过程如下图红色实线所示,它被分为两段:
值得注意的是,上面 2个红线部分都属于 APP1 的“上下文”,或者这样说:红线所涉及的代码,都是 APP1调用的。但是按键的中断服务程序,不属于 APP1的“上下文”,这是突如其来的,当中断发生时, APP1正在休眠呢。
在 APP1的“上下文”,也就是在 APP1的执行过程中,它是可以休眠的。
在中断的处理过程中,也就是 gpio_key_irq
的执行过程中,它不能休眠:“中断”怎么能休眠?“中断”休眠了,谁来调度其他 APP 啊?
总结:在中断函数中,不能休眠,也就不能调用会导致休眠的函数
内核函数
休眠函数,参考内核源码: include\linux\wait.h
函数 | 说明 |
---|---|
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 为真;退出的唯一条件是 condition 为真,信号也不好使 |
#define wait_event_interruptible(wq_head, condition) \
({ \
int __ret = 0; \
might_sleep(); \
if (!(condition)) \
__ret = __wait_event_interruptible(wq_head, condition); \
__ret; \
})
#define wait_event(wq_head, condition) \
do { \
might_sleep(); \
if (condition) \
break; \
__wait_event(wq_head, condition); \
} while (0)
#define wait_event_interruptible_timeout(wq_head, condition, timeout) \
({ \
long __ret = timeout; \
might_sleep(); \
if (!___wait_cond_timeout(condition)) \
__ret = __wait_event_interruptible_timeout(wq_head, \
condition, timeout); \
__ret; \
})
#define wait_event_timeout(wq_head, condition, timeout) \
({ \
long __ret = timeout; \
might_sleep(); \
if (!___wait_cond_timeout(condition)) \
__ret = __wait_event_timeout(wq_head, condition, timeout); \
__ret; \
})
比较重要的参数就是
- wq:waitqueue,等待队列
- 休眠时除了把程序状态改为非 RUNNING 之外,还要把进程/进程放入wq 中,以后中断服务程序要从 wq 中把它取出来唤醒。
- 没有 wq 的话,茫茫人海中,中断服务程序去哪里找到你?
- condition
- 这可以是一个变量,也可以是任何表达式。表示“一直等待,直到condition 为真”。
唤醒函数
参考内核源码:include\linux\wait.h
函数 | 说明 |
---|---|
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”的线程,唤醒其中的所有线程 |
驱动框架
要休眠的线程,放在 wq
队列里,中断处理函数从 wq
队列里把它取出来唤醒,所以,需要做一下几件事
- 初始化
wq
队列 - 在驱动的
read
函数中,调用wait_event_interruptible
:- 它本身会判断
event
是否为FALSE
,如果为FASLE
表示无数据,则休眠。 - 当从
wait_event_interruptible
返回后,把数据复制回用户空间
- 它本身会判断
- 在中断服务程序里:
- 设置
event
为TRUE
,并调用wake_up_interruptible
唤醒线程。
- 设置
在驱动的读函数里调用 wait_event_interruptible:
// 实现自己的read函数,等待按键按下
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
// 当g_key为真时,休眠才被打断
wait_event_interruptible(gpio_key_wait, g_key);
err = copy_to_user(buf, &g_key, 4);
// 再次被置为0,进入休眠状态,等待下次唤醒
g_key = 0;
return 4;
}
假设 g_key 等于 0,那么 APP 会执行到上述代码进入休眠状态。它被谁唤醒?被控制的中断服务程序:
// 中断服务程序,唤醒线程
static irqreturn_t gpio_key_irq_winter(int irq, void* dev_id)
{
struct gpio_key *gpio_key = dev_id;
int val;
val = gpiod_get_value(gpio_key->gpiod);
printk("key %d %d\n", gpio_key->gpio, val);
// 哪一个按键放在高8位,按下/松开是val
g_key = (gpio_key->gpio << 8) | val;
// 唤醒g_key队列
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED;
}
注意这 2 个函数,一个没有使用“ &”,另一个使用了“ &”:
wait_event_interruptible(gpio_key_wait, g_key);
wake_up_interruptible(&gpio_key_wait);
应用程序并不复杂,调用 open、 read 即可,代码在 button_test.c 中
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./button_test /dev/100ask_button0
*
*/
int main(int argc, char **argv)
{
int fd;
int val;
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open(argv[1], O_RDWR);
if (fd == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
while (1)
{
/* 3. 读文件 */
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
close(fd);
return 0;
}
编译
测试
把编译出来的设备树文件拷贝到/boot目录下
reboot重启
安装驱动,强制安装
insmod -f gpio_key_drv.ko
按按键测试,按下是1,松开是0
利用top查看占资源情况
缺点是只能读最新的一个数据,因为按键数据放在全局变量g_key中了
使用环形缓冲区改进驱动程序
利用唤醒缓冲区存储数据
// 环形缓冲区
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
// 下一个位置
#define NEXT_POS(x) ((x+1) % BUF_LEN)
// 判断环形缓冲区是否为空
static int is_key_buf_empty(void)
{
// 相等时为空
return (r == w);
}
// 判断缓冲区是否满
static int is_key_buf_full(void)
{
return (r == NEXT_POS(w));
}
// 写数据进去
static void put_key(int key)
{
if (!is_key_buf_full())
{
g_keys[w] = key;
// w后移
w = NEXT_POS(w);
}
}
// 获得数据
static int get_key(void)
{
int key = 0;
if (!is_key_buf_empty())
{
key = g_keys[r];
// r后移
r = NEXT_POS(r);
}
return key;
}
使用环形缓冲区之后,休眠函数可以这样写
// 实现自己的read函数,等待按键按下
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
int key;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
// 当is_key_buf_empty为假时,也就是不空,休眠才被打断,才能get数据
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
return 4;
}
唤醒函数可以这样写
// 中断服务程序,唤醒线程
static irqreturn_t gpio_key_irq_winter(int irq, void* dev_id)
{
struct gpio_key *gpio_key = dev_id;
int val;
int key;
val = gpiod_get_value(gpio_key->gpiod);
printk("key %d %d\n", gpio_key->gpio, val);
// 哪一个按键放在高8位,按下/松开是val
key = (gpio_key->gpio << 8) | val;
// 加入数据
put_key(key);
// 唤醒g_key队列
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED;
}