所谓休眠就是把自己的状态改为非 RUNNING,这样内核的调度器就不会让它运行。当按下按键,驱动程序中的中断服务程序被调用,它会记录数据,并唤醒 APP1。所以唤醒就是把程序的状态改为 RUNNING,这样内核的调度器有合适的时间就会让它运行。
- APP调用read等函数;
- APP进入内核态read函数,等待按键事件发生,开始休眠;
- 当按键事件发生时,驱动程序的中断服务程序被调用,它会读取按键值并唤醒APP;
- 驱动中read被唤醒,将按键值返回给应用层;
- 应用层read函数返回


如上图所示是休眠唤醒的驱动框架,根据该框架,我们需要做这几件事情:
- 初始化wq队列,使用DECLARE_WAIT_QUEUE_HEAD()宏函数实现。
- 在驱动层的drv_read中,调用wait_event_interruptible()函数:
- 它本身会判断event参数是否为FALSE,如果为FASLE表示无数据,会将自己放入wq等待队列并休眠。
- 当从wait_event_interruptible()函数返回后,会继续把数据复制回用户空间。
- 在中断服务程序中,设置event为TRUE,并且调用wake_up_interruptible唤醒正在队列中休眠的线程/进程
参考内核源码: include\linux\wait.h
休眠函数:
wq:waitqueue,等待队列。
- 休眠时除了把程序状态改为非
RUNNING之外,还要把进程/进程放入wq中,以后中断服务程序要从wq中把它取出来唤醒。condition:可以是变量,也可以是任何表达式,表示一直等待,直到condition为真。
唤醒函数:

APP不知道驱动程序中是否有数据,可以先调用poll函数查询一下,poll函数可以传入超时时间。
- APP进入内核态,调用到驱动程序的drv_poll函数,如果有数据的话立刻返回。
- 如果发现没有数据时就休眠一段时间。
- 当按下按键时,驱动程序的中断服务程序被调用,它会记录数据并且唤醒APP。
- 当超时时间到了之后,内核也会唤醒APP。
- APP根据poll函数的返回值就可以知道是否有数据,如果有数据就调用read得到数据。

如上图所示poll机制执行的流程,分为9步:
1. APP使用open系统调用打开按键设备。
2. 通过内核文件系统中sys_open函数调用file_operation结构体中的drv_open驱动函数来打开设备。
每一个设备,内核都会将其看成是一个文件,都会在内核中创建一个struct file结构体来描述这个设备,该结构体就位于内核的文件系统中。
3. APP调用poll系统调用后进入内核态。
4. 内核文件系统中的sys_poll,会在死循环for中,先调用驱动程序的drv_poll来获取状态event。
在驱动程序drv_poll中,要把当前线程挂入到等待队列wq中,否则在唤醒的时候就找不到该线程了。
但是驱动程序drv_poll并不会让当前线程休眠。
5. 返回的状态表示当前没有数据,那么内核文件系统就让该线程休眠一会儿。6. 线程休眠过程中,按下了按键,产生了按键中断,在中断服务函数中记录按键值,并且从wq等待队列中将线程唤醒。
7. 线程被唤醒后处于内核文件系统中的for死循环中,所以还要再执行一次drv_poll驱动程序,获取按键数据的状态。
8. 此时获取到的数据状态表示有按键数据,就会从返回到用户态,APP可以继续执行不再阻塞。
9. APP根据poll的返回值发现有按键数据,则调用read函数读取按键数据。
如果一直没有按键数据,也就是线程在休眠后一直没有被唤醒,此时的流程也是类似的,从第三步开始看:
3. APP调用poll系统调用后进入内核态。
4. 导致驱动程序的drv_poll被调用。
5. 假设当前没有数据,则休眠一会。
6. 在休眠过程中,一直没有按下了按键,超时时间到了,内核把这个线程唤醒。
7. 线程从休眠中被唤醒,继续执行for循环,再次调用drv_poll驱动程序获取数据状态。
8. 此时获取到的数据状态仍然表示没有数据,但是超时时间已经到了,也只能从内核态返回用户态了。
9. APP根据poll的返回值发现没有按键数据,则不能调用read函数读取按键数据。这个过程中有几点需要注意:
- drv_poll要把线程挂入队列wq,但是并不是在drv_poll中进入休眠,而 是在调用drv_poll之后休眠。
drv_poll驱动程序只做两件事情:
- 把线程放入到等待队列wq中,但是不休眠。
- 返回event 事件状态,而不是返回事件值。
APP调用一次poll,有可能会导致drv_poll被调用两次:
- 进入内核文件系统的for循环中先调用一次获取状态。
- 被唤醒后(中断或者超时)执行for循环再调用一次,判断是数据到来还是超时,然后返回用户态。
APP要判断poll返回的原因:
- 有数据到来,还是超时。有数据到来才能调用read函数读取按键值
使用 poll 机制时,驱动程序的核心就是提供对应的 drv_poll 函数。在drv_poll 函数中要做 2 件事:
1. 把当前线程挂入队列 wq: poll_wait
a) APP 调用一次 poll,可能导致 drv_poll 被调用 2 次,但是我们并不需要把当前线程挂入队列 2 次。
b) 可以使用内核的函数 poll_wait 把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。
2. 返回设备状态:
APP 调用 poll 函数时,有可能是查询“有没有数据可以读”: POLLIN, 也有可能是查询“你有没有空间给我写数据”: POLLOUT。所以 drv_poll 要返回自己的当前状态: (POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。
a) POLLRDNORM 等同于 POLLIN,为了兼容某些 APP 把它们一起返回。
b) POLLWRNORM 等同于 POLLOUT ,为了兼容某些 APP 把它们一起返回。
- static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
- {
- if (p && p->_qproc && wait_address)
- p->_qproc(filp, wait_address, p);
- }
- 使用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读取按键数据,此时一定是有数据的,并不会休眠。
在整个流程中可以看到,FASYNC的变化是为了启动drv_fasync,而真正做事情的是fasync_helper函数。
使用异步通知时,驱动程序的核心有 2:
① 提供对应的 drv_fasync 函数;
② 并在合适的时机发信号
- /*
- * fasync_helper() is used by almost all character device drivers
- * to set up the fasync queue, and for regular files by the file
- * lease code. It returns negative on error, 0 if it did no changes
- * and positive if it added/deleted the entry.
- */
- int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
- /*
- #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
- */
-
- void kill_fasync(struct fasync_struct **fp, int sig, int band)
所谓定时器,就是闹钟,时间到后你就要做某些事。有 2 个要素:时间、做事,换成程序员的话就是:超时时间、函数
| 函数 | 说明 |
| 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) | 删除定时器 |
- struct timer_list {
- /*
- * All fields that change during normal runtime grouped to the
- * same cacheline
- */
- struct list_head entry;
- unsigned long expires; /* 定时器超时时间 */
- struct tvec_base *base;
-
- void (*function)(unsigned long); /* 超时函数 */
- unsigned long data; /* 超时函数的参数 */
-
- int slack;
-
- #ifdef CONFIG_TIMER_STATS
- int start_pid;
- void *start_site;
- char start_comm[16];
- #endif
- #ifdef CONFIG_LOCKDEP
- struct lockdep_map lockdep_map;
- #endif
- };
编译内核时,可以在内核源码根目录下用ls -a看到一个隐藏文件,它就 是内核配置文件。打开后可以看到CONFIG_HZ=100,它表示内核每秒中会发生 100 次系统滴答中断(tick)。
每发生一次 tick 中断,全局变量jiffies就会累加1,定时器的时间就是基于 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 秒
tasklet结构体:
- struct tasklet_struct
- {
- struct tasklet_struct *next;
- unsigned long state;
- atomic_t count;
- void (*func)(unsigned long);
- unsigned long data;
- };
⚫ 其中的 state 有 2 位:
◼ bit0 表示 TASKLET_STATE_SCHED等于 1 时表示已经执行了 tasklet_schedule 把该 tasklet 放入队列了;tasklet_schedule 会判断该位,如果已经等于 1 那么它就不会再次把tasklet 放入队列。
◼ bit1 表示 TASKLET_STATE_RUN等于 1 时,表示正在运行 tasklet 中的 func 函数;函数执行完后内核会把该位清 0。
⚫ 其中的 count 表示该 tasklet 是否使能:等于 0 表示使能了,非 0 表示被禁止了。对于 count 非 0 的 tasklet,里面的 func 函数不会被执行。
1. 初始化tasklet结构体
- #define DECLARE_TASKLET(name, func, data) \
- struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
-
- #define DECLARE_TASKLET_DISABLED(name, func, data) \
- struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
-
- void tasklet_init(struct tasklet_struct *t,
- void (*func)(unsigned long), unsigned long data);
◼ 使用 DECLARE_TASKLET 定义的 tasklet 结构体,它是使能的;
◼ 使用 DECLARE_TASKLET_DISABLED 定义的 tasklet 结构体,它是禁止的;使用之前要先调用 tasklet_enable 使能它
2. 使能/禁止 tasklet
- static inline void tasklet_enable(struct tasklet_struct *t);
- static inline void tasklet_disable(struct tasklet_struct *t);
◼ tasklet_enable 把 count 增加 1;
◼ tasklet_disable 把 count 减 1。
3. 调度 tasklet
static inline void tasklet_schedule(struct tasklet_struct *t);
◼ 把 tasklet 放入链表,并且设置它的 TASKLET_STATE_SCHED 状态为1
4. kill tasklet
extern void tasklet_kill(struct tasklet_struct *t);
◼ 如 果 一 个 tasklet 未 被 调 度 , tasklet_kill 会 把 它 的TASKLET_STATE_SCHED 状态清 0;
◼ 如果一个 tasklet 已被调度, tasklet_kill 会等待它执行完华,再把它的 TASKLET_STATE_SCHED 状态清 0。
内核线程、工作队列(workqueue)都由内核创建了,我们只是使用。使用的核心是一个 work_struct 结构体,定义如下:

使用工作队列时,步骤如下:
第1步 构造一个 work_struct 结构体,里面有函数;
第2步 把这个 work_struct 结构体放入工作队列,内核线程就会运行 work 中的函数
1. 定义 work
- #define DECLARE_WORK(n, f) \
- struct work_struct n = __WORK_INITIALIZER(n, f)
-
- #define DECLARE_DELAYED_WORK(n, f) \
- struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)
-
- #define INIT_WORK(_work, _func)
⚫ 第 1 个宏是用来定义一个 work_struct 结构体,要指定它的函数。
⚫ 第 2 个宏用来定义一个 delayed_work 结构体,也要指定它的函数。所以“ delayed”,意思就是说要让它运行时,可以指定:某段时间之后你再执行。
⚫如果要在代码中初始化 work_struct 结构体,可以使用 INIT_WORK
2. 使用 work
- /**
- * schedule_work - put work task in global workqueue
- * @work: job to be done
- *
- * Returns %false if @work was already on the kernel-global workqueue and
- * %true otherwise.
- *
- * This puts a job in the kernel-global workqueue if it was not already
- * queued and leaves it in the same position on the kernel-global
- * workqueue otherwise.
- */
- static inline bool schedule_work(struct work_struct *work)
- {
- return queue_work(system_wq, work);
- }
3. 其他函数
