第19章 驱动程序基石
19.1 休眠与唤醒
19.1.1 适用场景
在前面引入中断时,我们曾经举过一个例子:

妈妈怎么知道卧室里小孩醒了?
- 休眠-唤醒:进去房间陪小孩一起睡觉,小孩醒了会吵醒她
不累,但是妈妈干不了活了
当应用程序必须等待某个事件发生,比如必须等待按键被按下时,可以使用“休眠-唤醒”机制:
- APP调用read等函数试图读取数据,比如读取按键;
- APP进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;
- 如果APP在内核态,也就是在驱动程序中发现没有数据,则APP休眠;
- 当有数据时,比如当按下按键 时,驱动程序的中断服务程序被调用,它会记录数据、唤醒APP;
- APP继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回。
驱动中有数据时,图 19.1中红线就是APP1的执行过程,涉及用户态、内核态:

驱动中没有数据时,APP1在内核态执行到drv_read时会休眠。所谓休眠就是把自己的状态改为非RUNNING,这样内核的调度器就不会让它运行。当按下按键,驱动程序中的中断服务程序被调用,它会记录数据,并唤醒APP1。所以唤醒就是把程序的状态改为RUNNING,这样内核的调度器有合适的时间就会让它运行。当APP1再次运行时,就会继续执行drv_read中剩下的代码,把数据复制回用户空间,返回用户空间。APP1的执行过程如下图的红色实线所示,它被分成了2段:

图 19.2 APP1的执行过程
值得注意的是,上面2个图中红线部分都属于APP1的“上下文”,或者这样说:红线所涉及的代码,都是APP1调用的。但是按键的中断服务程序,不属于APP1的“上下文”,这是突如其来的,当中断发生时,APP1正在休眠呢。
在APP1的“上下文”,也就是在APP1的执行过程中,它是可以休眠的。
在中断的处理过程中,也就是gpio_key_irq的执行过程中,它不能休眠:“中断”怎么能休眠?“中断”休眠了,谁来调度其他APP啊?
所以,请记住:在中断处理函数中,不能休眠,也就不能调用会导致休眠的函数。
19.1.2 内核函数
-
-
-
-
- 休眠函数
-
-
-
参考内核源码:include\linux\wait.h。
表 19‑1 wait内核函数
| 函数 | 说明 |
|---|---|
| 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为真,信号也不好使 |
比较重要的参数就是:
- 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”的线程,唤醒其中的所有线程 |
19.1.3 驱动框架
驱动框架如下:

图 19.3 驱动框架
要休眠的线程,放在wq队列里,中断处理函数从wq队列里把它取出来唤醒。
所以,我们要做这几件事:
- 初始化wq队列
- 在驱动的read函数中,调用wait_event_interruptible:
它本身会判断event是否为FALSE,如果为FASLE表示无数据,则休眠。
当从wait_event_interruptible返回后,把数据复制回用户空间。
- 在中断服务程序里:
设置event为TRUE,并调用wake_up_interruptible唤醒线程。
19.1.4 编程
使用GIT命令载后,源码位于这个目录下:
01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\
source\06_gpio_irq\02_read_key_irq\ 和 03_read_key_irq_circle_buffer
03_read_key_irq_circle_buffer使用了环型缓冲区,可以避免按键丢失。
-
-
-
-
- 驱动程序关键代码
-
-
-
- 02_read_key_irq\gpio_key_drv.c中,要先定义“wait queue”:
41 static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
- 在驱动的读函数里调用wait_event_interruptible:
44 static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
45 {
46 //printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
47 int err;
48
49 wait_event_interruptible(gpio_key_wait, g_key);
50 err = copy_to_user(buf, &g_key, 4);
51 g_key = 0;
52
53 return 4;
54 }
第49行并不一定会进入休眠,它会先判断g_key是否为TRUE。
执行到第50行时,表示要么有了数据(g_key为TRUE),要么有信号等待处理(本节课程不涉及信号)。
假设g_key等于0, 那么APP会执行到上述代码第49行时进入休眠状态。它被谁唤醒?被控制的中断服务程序:
64 static irqreturn_t gpio_key_isr(int irq, void *dev_id)
65 {
66 struct gpio_key *gpio_key = dev_id;
67 int val;
68 val = gpiod_get_value(gpio_key->gpiod);
69
70
71 printk("key %d %d\n", gpio_key->gpio, val);
72 g_key = (gpio_key->gpio << 8) | val;
73 wake_up_interruptible(&gpio_key_wait);
74
75 return IRQ_HANDLED;
76 }
上述代码中,第72行确定按键值g_key,g_key也就变为TRUE了。
然后在第73行唤醒gpio_key_wait中的第1个线程。
注意这2个函数,一个没有使用“&”,另一个使用了“&”:
wait_event_interruptible(gpio_key_wait, g_key);
wake_up_interruptible(&gpio_key_wait);
-
-
-
-
- 应 用程序
-
-
-
应用程序并不复杂,调用open、read即可,代码在button_test.c中:
25 /* 2. 打开文件 */
26 fd = open(argv[1], O_RDWR);
27 if (fd == -1)
28 {
29 printf("can not open file %s\n", argv[1]);
30 return -1;
31 }
32
33 while (1)
34 {
35 /* 3. 读文件 */
36 read(fd, &val, 4);
37 printf("get button : 0x%x\n", val);
38 }
在33行~38行的循环中,APP基本上都是休眠状态。你可以执行top命令查看CPU占用率。
19.1.5 上机实验
跟上一节视频类似,需要先修改设备树,请使用上一节视频的设备树文件。然后安装驱动程序,运行测试程序。
[root@100ask:~]# insmod -f gpio_key_drv.ko
[root@100ask:~]# ls /dev/100ask_gpio_key
/dev/100ask_gpio_key
[root@100ask:~]# ./button_test /dev/100ask_gpio_key &
[root@100ask:~]# top
19.1.6 使用环形缓冲区改进驱动程序
使用GIT命令载后,源码位于这个目录下:
01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\
source\06_gpio_irq\03_read_key_irq_circle_buffer
使用环形缓冲区,可以在一定程序上避免按键数据丢失,关键代码如下:

使用环形缓冲区之后,休眠函数可以这样写:
86 wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
87 key = get_key();
88 err = copy_to_user(buf, &key, 4);
唤醒函数可以这样写:
111 key = (gpio_key->gpio << 8) | val;
112 put_key(key);
113 wake_up_interruptible(&gpio_key_wait);
19.2 POLL机制
使用GIT命令载后,本节源码位于这个目录下:
01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\06_gpio_irq\04_read_key_irq_poll
19.2.1 适用场景
在前面引入中断时,我们曾经举过一个例子:妈妈怎么知道卧室里小孩醒了?
- poll方式:妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟
要浪费点时间,但是可以继续干活。妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
使用休眠-唤醒的方式等待某个事件发生时,有一个缺点:等待的时间可能很久。我们可以加上一个超时时间,这时就可以使用poll机制。
- APP不知道驱动程序中是否有数据,可以先调用poll函数查询一下,poll函数可以传入超时时间;
- APP进入内核态,调用到驱动程序的poll函数,如果有数据的话立刻返回;
- 如果发现没有数据时就休眠一段时间;
- 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒APP;
- 当超时时间到了之后,内核也会唤醒APP;
- APP根据poll函数的返回值就可以知道是否有数据,如果有数据就调用read得到数据。
19.2.2 使用流程
妈妈进入房间时,会先看小孩醒没醒,闹钟响之后走出房间之前又会再看小孩醒没醒。
注意:看了2次小孩!
POLL机制也是类似的,流程如下:

图 19.4 poll机制
函数执行流程如上图①~⑧所示,重点从③开始看。假设一开始无按键数据:
③PP调用poll之后,进入内核态;
④致驱动程序的drv_poll被调用:
注意,drv_poll要把自己这个线程挂入等待队列wq中;假设不放入队列里,那以后发生中断时,中断服务程序去哪里找到你嘛?
drv_poll还会判断一下:有没有数据啊?返回这个状态。
⑤当前没有数据,则休眠一会;
⑥过程中,按下了按键,发生了中断:
-
- 在中断服务程序里记录了按键值,并且从wq中把线程唤醒了。
⑦从休眠中被唤醒,继续执行for循环,再次调用drv_poll:
-
- drv_poll返回数据状态
⑧哦,你有数据,那从内核态返回到应用态吧
⑨APP调用read函数读数据
如果一直没有数据,调用流程也是类似的,重点从③开始看,如下:
③ APP调用poll之后,进入内核态;
④ 导致驱动程序的drv_poll被调用:
注意,drv_poll要把自己这个线程挂入等待队列wq中;假设不放入队列里,那以后发生中断时,中断服务程序去哪里找到你嘛?
drv_poll还会判断一下:有没有数据啊?返回这个状态。
⑤ 假设当前没有数据,则休眠一会;
⑥ 在休眠过程中,一直没有按下了按键,超时时间到:内核把这个线程唤醒;
⑦ 线程从休眠中被唤醒,继续执行for循环,再次调用drv_poll:drv_poll返回数据状态
⑧ 哦,你还是没有数据,但是超时时间到了,那从内核态返回到应用态吧
⑨ APP不能调用read函数读数据
注意几点:
- drv_poll要把线程挂入队列wq,但是并不是在drv_poll中进入休眠,而是在调用drv_poll之后休眠
- drv_poll要返回数据状态
- APP调用一次poll,有可能会导致drv_poll被调用2次
- 线程被唤醒的原因有2:中断发生了去队列wq中把它唤醒,超时时间到了内核把它唤醒
- APP要判断poll返回的原因:有数据,还是超时。有数据时再去调用read函数。
19.2.3 驱动编程
使用poll机制时,驱动程序的核心就是提供对应的drv_poll函数。在drv_poll函数中要做2件事:
- 把当前线程挂入队列wq:poll_wait
- APP调用一次poll,可能导致drv_poll被调用2次,但是我们并不需要把当前线程挂入队列2次。
- 可以使用内核的函数poll_wait把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。
- 返回设备状态:
APP调用poll函数时,有可能是查询“有没有数据可以读”:POLLIN,也有可能是查询“你有没有空间给我写数据”:POLLOUT。所以drv_poll要返回自己的当前状态:(POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。
-
- POLLRDNORM等同于POLLIN,为了兼容某些APP把它们一起返回。
- POLLWRNORM 等同于POLLOUT ,为了兼容某些APP把它们一起返回。
APP调用poll后,很有可能会休眠。对应的,在按键驱动的中断服务程序中,也要有唤醒操作。
驱动程序中poll的代码如下:
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
19.2.4 应用编程
注意:APP可以调用poll或select函数,这2个函数的作用是一样的。
poll/select函数可以监测多个文件,可以监测多种事件:
| 事件类型 | 说明 |
|---|---|
| POLLIN | 有数据可读 |
| POLLRDNORM | 等同于POLLIN |
| POLLRDBAND | Priority band data can be read,有优先级较较高的“band data”可读 Linux系统中很少使用这个事件 |
| POLLPRI | 高优先级数据可读 |
| POLLOUT | 可以写数据 |
| POLLWRNORM | 等同于POLLOUT |
| POLLWRBAND | Priority data may be written |
| POLLERR | 发生了错误 |
| POLLHUP | 挂起 |
| POLLNVAL | 无效的请求,一般是fd未open |
在调用poll函数时,要指明:
- 你要监测哪一个文件:哪一个fd
- 你想监测这个文件的哪种事件:是POLLIN、还是POLLOUT
最后,在poll函数返回时,要判断状态。
应用程序代码如下:
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
fds[0].fd = fd;
fds[0].events = POLLIN;
ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN))
{
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
19.2.5 现场编程
驱动程序、应用程序的核心都在上面的章节列了出来,怎么写程序?观看视频体验更佳,更能抓住重点。
19.2.6 上机实验
使用GIT命令载后,本节源码位于这个目录下:
01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\06_gpio_irq\04_read_key_irq_poll
首先,把“04_read_key_irq_poll”整个目录上传单Ubuntu中,修改里面的Makefile指定内核路径,如下修改:
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
然后执行“make”命令,生成驱动程序“gpio_key_drv.ko”、应用程序“button_test”。
接着,还需要修改设备树,参照下图修改Ubuntu中的设备树文件“Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dts”:

图 19.5 POLL机制实验的设备树
修改完设备树后,在内核目录下载执行“make dtbs”生产新的设备树文件“arch/arm/boot/dts/100ask_imx6ull-14x14.dtb”,把它放到开发板的“/boot”目录下,重启开发板。
最后,把Ubuntu上的驱动程序、测试程序下载到开发板/root目录,执行如下命令测试:

图 19.6 POLL机制上机实验
19.2.7 POLL机制的内核代码详解
Linux APP系统调用,基本都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数。比如系统调用open、read、write、poll,与之对应的内核函数为:sys_open、sys_read、sys_write、sys_poll。
对于系统调用poll或select,它们对应的内核函数都是sys_poll。分析sys_poll,即可理解poll机制。
-
-
-
-
- sys_poll函数
-
-
-
sys_poll位于fs/select.c文件中,代码如下:
SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,
int, timeout_msecs)
{
struct timespec64 end_time, *to = NULL;
int ret;
if (timeout_msecs >= 0) {
to = &end_time;
poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC,
NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
}
ret = do_sys_poll(ufds, nfds, to);
……
- SYSCALL_DEFINE3是一个宏,它定义于include/linux/syscalls.h,展开后就有sys_poll函数。
- sys_poll对超时参数稍作处理后,直接调用do_sys_poll。
-
-
-
- do_sys_poll函数
-
-
-
do_sys_poll位于fs/select.c文件中,我们忽略其他代码,只看关键部分:
int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
struct timespec64 *end_time)
{
……
poll_initwait(&table);
fdcount = do_poll(head, &table, end_time);
poll_freewait(&table);
……
}
poll_initwait函数非常简单,它初始化一个poll_wqueues变量table:
poll_initwait
init_poll_funcptr(&pwq->pt, __pollwait);
pt->qproc = qproc;
即table->pt->qproc = __pollwait,__pollwait将在驱动的poll函数里用到。do_poll函数才是核心,继续看代码。
-
-
-
-
- do_poll函数
-
-
-
do_poll函数位于fs/select.c文件中,这是POLL机制中最核心的代码,贴图如下:

图 19.7 do_poll机制
① 从这里开始,将会导致驱动程序的poll函数被第一次调用。
沿着②③④⑤,你可以看到:驱动程序里的poll_wait会调用__pollwait函数把线程放入某个队列。
当执行完①之后,在⑥或⑦处,pt->_qproc被设 置为NULL,所以第二次调用驱动程序的poll时,不会再次把线程放入某个队列里。
⑧ 如果驱动程序的poll返回有效值,则count非0,跳出循环;
⑨ 否则休眠一段时间;当休眠时间到,或是被中断唤醒时,会再次循环、再次调用驱动程序的poll。
回顾APP的代码,APP可以指定“想等待某些事件”,poll函数返回后,可以知道“发生了哪些事件”:

图 19.8 POLL机制的APP核心代码
驱动程序里怎么体现呢?在上上一个图中,看②位置处,细说如下:

图 19.9 POLL机制的内核核心代码
19.3 异步通知
使用GIT命令载后,本节源码位于这个目录下:
01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\
source\06_gpio_irq\05_read_key_irq_poll_fasync
19.3.1 适用场景
在前面引入中断时,我们曾经举过一个例子:

妈妈怎么知道卧室里小孩醒了?
- 异步通知:妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈
- 妈妈、小孩互不耽误。
使用休眠-唤醒、POLL机制时,都需要休眠等待某个事件发生时,它们的差别在于后者可以指定休眠的时长。
在现实生活中:妈妈可以不陪小孩睡觉,小孩醒了之后可以主动通知妈妈。
如果APP不想休眠怎么办?也有类似的方法:驱动程序有数据时主动通知APP,APP收到信号后执行信息处理函数。
什么叫“异步通知”?
- 你去买奶茶:
- 你在旁边等着,眼睛盯着店员,生怕别人插队,他一做好你就知道:你是主动等待他做好,这叫“同步”。
- 你付钱后就去玩手机了,店员做好后他会打电话告诉你:你是被动获得结果,这叫“异步”。
19.3.2 使用流程
驱动程序怎么通知APP:发信号,这只有3个字,却可以引发很多问题:
- 谁发:驱动程序发
- 发什么:信号
- 发什么信号:SIGIO
- 怎么发:内核里提供有函数
- 发给谁:APP,APP要把自己告诉驱动
- APP收到后做什么:执行信号处理函数
- 信号处理函数和信号,之间怎么挂钩:APP注册信号处理函数
小孩通知妈妈的事情有很多:饿了、渴了、想找人玩。
Linux系统中也有很多信号,在Linux内核源文件include\uapi\asm-generic\signal.h中,有很多信号的宏定义:

就APP而言,你想处理SIGIO信息,那么需要提供信号处理函数,并且要跟SIGIO挂钩。这可以通过一个signal函数来“给某个信号注册处理函数”, 用法如下:

APP还要做什么事?想想这几个问题:
-
- 内核里有那么多驱动,你想让哪一个驱动给你发SIGIO信号?
APP要打开驱动程序的设备节点。
-
- 驱动程序怎么知道要发信号给你而不是别人?
APP要把自己的进程ID告诉驱动程序。
-
- APP有时候想收到信号,有时候又不想收到信号:
应该可以把APP的意愿告诉驱动。
驱动程序要做什么?发信号。
- APP设置进程ID时,驱动程序要记录下进程ID;
- APP还要使能驱动程序的异步通知功能,驱动中有对应的函数:
APP打开驱动程序时,内核会创建对应的file结构体,file中有f_flags;
f_flags中有一个FASYNC位,它被设置为1时表示使能异步通知功能。
当f_flags中的FASYNC位发生变化时,驱动程序的fasync函数被调用。
-
- 发生中断时,有数据时,驱动程序调用内核辅助函数发信号。
这个辅助函数名为kill_fasync。完美!
APP收到信号后,是怎么执行信号处理函数的?这个,很难,有兴趣的话就看本节最后的文档。初学者没必要看。
综上所述,使用异步通知,也就是使用信号的流程如下图所示:

图 19.10 异步通知的信号流程
重点从②开始:
② APP给SIGIO这个信号注册信号处理函数func,以后APP收到SIGIO信号时,这个函数会被自动调用;
③ 把APP的PID(进程ID)告诉驱动程序,这个调用不涉及驱动程 序,在内核的文件系统层次记录PID;
④ 读取驱动程序文件Flag;
⑤ 设置Flag里面的FASYNC位为1:当FASYNC位发生变化时,会导致驱动程序的fasync被调用;
⑥⑦ 调用faync_helper,它会根据FAYSNC的值决定是否设置button_async->fa_file=驱动文件filp:
驱动文件filp结构体里面含有之前设置的PID。
⑧ APP可以做其他事;
⑨⑩ 按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用kill_fasync发信号;
⑪⑫⑬ APP收到信号后,它的信号处理函数被自动调用,可以在里面调用read函数读取按键。
19.3.3 驱动编程
使用异步通知时,驱动程序的核心有2:
① 提供对应的drv_fasync函数;
② 并在合适的时机发信号。
drv_fasync函数很简单,调用fasync_helper函数就可以,如下:
static struct fasync_struct *button_async;
static int drv_fasync (int fd, struct file *filp, int on)
{
return fasync_helper (fd, filp, on, &button_async);
}
fasync_helper函数会分配、构造一个fasync_struct结构体button_async:
- 驱动文件的flag被设置为FAYNC时:
button_async->fa_file = filp; // filp表示驱动程序文件,里面含有之前设置的PID
- 驱动文件被设置为非FASYNC时:
button_async->fa_file = NULL;
以后想发送信号时,使用button_async作为参数就可以,它里面“可能”含有PID。
什么时候发信号呢?在本例中,在GPIO中断服务程序中发信号。
怎么发信号呢?代码如下:
kill_fasync (&button_async, SIGIO, POLL_IN);
- 第1个参数:button_async->fa_file非空时,可以从中得到PID,表示发给哪一个APP;
- 第2个参数表示发什么信号:SIGIO;
- 第3个参数表示为什么发信号:POLL_IN,有数据可以读了。(APP用不到这个参数)
19.3.4 应用编程
应用程序要做的事情有这几件:
- 编写信号处理函数:
static void sig_func(int sig)
{
int val;
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
- 注册信号处理函数:
signal(SIGIO, sig_func);
- 打开驱动:
fd = open(argv[1], O_RDWR);
- 把进程ID告诉驱动:
fcntl(fd, F_SETOWN, getpid());
- 使能驱动的FASYNC功能:
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
19.3.5 现场编程
驱动程序、应用程序的核心都在上面的章节列了出来,怎么写程序?观看视频体验更佳,更能抓住重点。
19.3.6 上机实验
使用GIT命令载后,本节源码位于这个目录下:
01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\
source\06_gpio_irq\05_read_key_irq_poll_fasync
首先,把“05_read_key_irq_poll_fasync”整个目录上传单Ubuntu中,修改里面的Makefile指定内核路径,如下修改:
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
然后执行“make”命令,生成驱动程序“gpio_key_drv.ko”、应用程序“button_test”。
接着,还需要修改设备树,参照下图修改Ubuntu中的设备树文件“Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dts”:

图 19.11 异步通知机制实验的设备树
修改完设备树后,在内核目录下载执行“make dtbs”生产新的设备树文件“arch/arm/boot/dts/100ask_imx6ull-14x14.dtb”,把它放到开发板的“/boot”目录下,重启开发板。
最后,把Ubuntu上的驱动程序、测试程序下载到开发板/root目录,,执行如下命令测试:

图 19.12 异步通知机制上机实验
19.3.7 异步通知机制内核代码详解
异步通知的本质是“发信号”,涉及2个对象:发送者、接收者。发送者可以是驱动程序,可以是进程;接收者必定是进程。驱动程序要想给进程发送信号,有2个问题需要解决:
① 使能驱动程序的“异步”功能,即:允许它发出信号
② 告诉驱动程序,发信号时,发给“谁”
应用编程时,需要执行如下操作:
- 打开驱动:
fd = open(“/dev/xxx”, O_RDWR);
- 把进程ID告诉驱动:
fcntl(fd, F_SETOWN, getpid());
- 使能驱动的FASYNC功能:
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
对于F_SETOWN、F_GETFL、F_SETFL,内核或驱动程序如何处理?
APP执行fcntl系统调用时,会导致内核“fs/fcntl.c”的如下函数被调用:

图 19.13 异步通知机制系统调用接口
在“do_fcntl”函数中,对于“F_SETOWN”,一路查看代码,发现最终如下设置:

图 19.14 异步通知机制F_SETOWN内部实现
在“do_fcntl”函数中,对于“F_GETFL”,仅仅是返回“filp->f_flags”;对于“F_SETFL”,会调用“setfl”函数进一步处理。代码如下:

图 19.15 异步通知机制F_GETFL/F_SETFL内部实现
“setfl”函数会比较“filp->f_flags”中的“FASYNC”位,发现它发生了变化时,就会调用驱动程序的faysnc函数:

图 19.16 异步通知机制F_SETFL内部实现
驱动程序的faysnc函数代码如下:

图 19.17 驱动程序中的异步通知代码
它使用fasync_helper函数来设置指针button_fasync,简化后的示例代码如下:
if (on)
{
struct fasync_struct *new;
new = fasync_alloc();
new->magic = FASYNC_MAGIC;
new->fa_file = filp;
new->fa_fd = fd;
button_fasync = new;
}
else
{
kfree(button_fasync);
button_fasync = NULL;
}
所以,启动了FASYNC功能的话,驱动程序的button_fasync就被设置了,它指向的fasync_struct结构体里含有filp,filp里含有PID(接收方的PID)。
在驱动程序的中断函数里,使用如下代码发出中断:
kill_fasync(&button_fasync, SIGIO, POLL_IN);
它的核心就是从button_fasync指针中,取出fasync_struct结构体,从这个结构体的fa_file中得到接收方的PID,然后使用“send_sigio”函数发送信号。

图 19.18 驱动程序中的发信号的过程
“send_sigio”函数的实质是:根据PID找到进程在内核的task_struct结构体,修改里面的某些成员表示收到了信号。
APP收到信号后,它的信号处理函数时如何被调用的呢?信号相当于APP的中断,处理过程也跟中断的处理过程类似:保存现场、处理信号,恢复现场。
APP进入内核态时,内核在APP的栈里保存“APP的运行环境”:APP在用户态进入内核态瞬间各个寄存器的值,包括“运行地址”(即 恢复运行时从哪里继续运行)。
APP退出内核态时,内核会从APP的栈里恢复“APP的运行环境”,比如APP将从之前保存的“运行地址”继续运行。
APP收到信号瞬间,APP必定处于内核态,因为信号的发送函数“send_sigio”要么由驱动程序调用,要么由APP通过系统调用来间接调用,函数“send_sigio”处于内核态。APP从内核态返回到用户态前,内核发现APP有信号在等待处理时,会修改APP的栈,增加一个新的“运行环境”:新环境里“运行地址”是信号处理函数的地址。这样,APP从内核态返回用户态时,运行的是信号处理函数。信号处理函数执行完后,会再次返回到内核态,在内核态里再使用旧的“运行环境”恢复APP的运行。
19.4 阻塞与非阻塞
所谓阻塞,就是等待某件事情发生。比如调用read读取按键时,如果没有按键数据则read函数不会返回,它会让线程休眠等待。
使用poll时,如果传入的超时时间不为0,这种访问方法也是阻塞的。
使用poll时,可以设置超时时间为0,这样即使没有数据它也会立刻返回,这就是非阻塞方式。能不能让read函数既能工作于阻塞方式,也可以工作于非阻塞方式?可以!
APP调用open函数时,传入O_NONBLOCK,就表示要使用非阻塞方式;默认是阻塞方式。
注意:对于普通文件、块设备文件,O_NONBLOCK不起作用。
注意:对于字符设备文件,O_NONBLOCK起作用的前提是驱动程序针对O_NONBLOCK做了处理。
只能在open时表明O_NONBLOCK吗?在open之后,也可以 通过fcntl修改为阻塞或非阻塞。
使用GIT命令载后,本节源码位于这个目录下:
01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\
source\06_gpio_irq\06_read_key_irq_poll_fasync_block
19.4.1 应用编程
- open时设置:
int fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK); /* 非阻塞方式 */
int fd = open(“/dev/xxx”, O_RDWR ); /* 阻塞方式 */
- open之后设置:
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* 非阻塞方式 */
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); /* 阻塞方式 */
19.4.2 驱动编程
- 以drv_read为例:
static ssize_t drv_read(struct file *fp, char __user *buf, size_t count, loff_t *ppos)
{
if (queue_empty(&as->queue) && fp->f_flags & O_NONBLOCK)
return -EAGAIN;
wait_event_interruptible(apm_waitqueue, !queue_empty(&as->queue));
……
}
从驱动代码也可以看出来,当APP打开某个驱动时,在内核中会有一个struct file结构体对应这个驱动,这个结构体中有f_flags,就是打开文件时的标记位;可以设置f_flasgs的O_NONBLOCK位,表示非阻塞;也可以清除这个位表示阻塞。
驱动程序要根据这个标记位决定事件未就绪时是休眠和还是立刻返回。
19.4.3 驱动开发原则
驱动程序程序“只提供功能,不提供策略”。就是说驱动程序可以提供休眠唤醒、查询等等各种方式,,驱动程序只提供这些能力,怎么用由APP决定。
19.5 定时器
使用GIT命令载后,本节源码位于这个目录下:
01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\
source\06_gpio_irq\07_read_key_irq_poll_fasync_block_timer
19.5.1 内核函数
所谓定时器,就是闹钟,时间到后你就要做某些事。有2个要素:时间、做事,换成程序员的话就是:超时时间、函数。
在内核中使用定时器很简单,涉及这些函数(参考内核源码include\linux\timer.h):
- setup_timer(timer, fn, data):
- 设置定时器,主要是初始化timer_list结构体,设置其中的函数、参数。
- void add_timer(struct timer_list *timer):
- 向内核添加定时器。timer->expires表示超时时间。
- 当超时时间到达,内核就会调用这个函数:timer->function(timer->data)。
- int mod_timer(struct timer_list *timer, unsigned long expires):
- 修改定时器的超时时间,
- 它等同于:del_timer(timer); timer->expires = expires; add_timer(timer);
- 但是更加高效。
- int del_timer(struct timer_list *timer):
- 删除定时器。
19.5.2 定时器时间单位
编译内核时,可以在内核源码根目录下用“ls -a”看到一个隐藏文件,它就是内核配置文件。打开后可以看到如下这项:
CONFIG_HZ=100
这表示内核每秒中会发生100次系统滴答中断(tick),这就像人类的心跳一样,这是Linux系统的心跳。每发生一次tick中断,全局变量jiffies就会累加1。
CONFIG_HZ=100表示每个滴答是10ms。
定时器的时间就是基于jiffies的,我们修改超时时间时,一般使用这2种方法:
- 在add_timer之前,直接修改:
timer.expires = jiffies + xxx; // xxx表示多少个滴 答后超时,也就是xxx*10ms
timer.expires = jiffies + 2*HZ; // HZ等于CONFIG_HZ,2*HZ就相当于2秒
- 在add_timer之后,使用mod_timer修改:
mod_timer(&timer, jiffies + xxx); // xxx表示多少个滴答后超时,也就是xxx*10ms
mod_timer(&timer, jiffies + 2*HZ); // HZ等于CONFIG_HZ,2*HZ就相当于2秒
19.5.3 使用定时器处理按键抖动
在实际的按键操作中,可能会有机械抖动:

图 19.19 按键震动
按下或松开一个按键,它的GPIO电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。
如果不处理抖动的话,用户只操作一次按键,中断程序可能会上报多个数据。怎么处理?
-
- 在按键中断程序中,可以循环判断几十亳秒,发现电平稳定之后再上报
- 使用定时器
显然第1种方法太耗时,违背“中断要尽快处理”的原则,你的系统会很卡。怎么使用定时器?看下图:

图 19.20 震动触发定时器
核心在于:在GPIO中断中并不立刻记录按键值,而是修改定时器超时时间,10ms后再处理。
-
- 如果10ms内又发生了GPIO中断,那就认为是抖动,这时再次修改超时时间为10ms。
- 只有10ms之内再无GPIO中断发生,那么定时器的函数才会被调用。在定时器函数中记录按键值。