字符设备驱动-6-poll底层驱动机制

1 前言引入#

前面字符设备驱动-3-GPIO驱动KEY示例 | Hexo (fuzidage.github.io)

字符设备驱动-3.gpio驱动(按键) - fuzidage - 博客园 (cnblogs.com)

就引入了poll机制,那么底层驱动的poll机制实现原理到底是什么呢?

1.1 阻塞与非阻塞IO#

APP 调用 open 函数时,不要传入“ O_NONBLOCK”。APP 调用 read 函数读取数据时,为阻塞io
APP 调用 open 函数时,传入“ O_NONBLOCK”表示“非阻塞”。APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read函数会返回数据,否则也会立刻返回错误。这种需要APP反复主动去”轮询”设备,否则无法及时响应。
注意:对于普通文件、块设备文件,O_NONBLOCK不起作用。
注意:对于字符设备文件,O_NONBLOCK 起作用的前提是驱动程序针对O_NONBLOCK做了处理
只能在 open 时表明 O_NONBLOCK 吗?
在 open 之后,也可以通过 fcntl 修改为阻塞或非阻塞:
⚫ open 时设置:

1
2
int fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK); /* 非阻塞方式 */
int fd = open(“/dev/xxx”, O_RDWR ); /* 阻塞方式 */

⚫ open 之后设置:

1
2
3
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* 非阻塞方式 */
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); /* 阻塞方式 */

驱动O_NONBLOCK flag的话,如果没有数据read函数立即返回。
image

1.2 带超时的阻塞IO(poll/select)#

POLL 机制、SELECT机制是完全一样的,只是 APP 接口函数不一样。简单地说,它们就是 “定个闹钟” :在调用 poll、 select 函数时可以传入“超时时间”。在这段时间内,条件合适时(比如有数据可读、有空间可写)就会立刻返回,否则等到“超时时间”结束时返回错误。
使用 poll 时,如果传入的超时时间不为 0,这种访问方法也是阻塞的。
使用 poll 时,可以设置超时时间为 0,这样即使没有数据它也会立刻返回,这就是非阻塞方式。

1.2.1 select#

select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。

那么select会有2个结果:

1
2
3
4
1, 查询到资源,返回查询到的fd总数。
2,没查到,则睡眠
①带timeout参数,timeout后,唤醒退出,此时fd总数为0
②不带timeout, 阻塞且睡眠中,直到有资源可用才唤醒

fd_set结构体就是一个可用资源文件描述符的集合。

1.2.1.1 select使用示例#

1
2
3
4
FD_SET(int fd, fd_set *fdset);       //将fd加入set集合
FD_CLR(int fd, fd_set *fdset); //将fd从set集合中清除
FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0
FD_ZERO(fd_set *fdset); //将set清零使集合中不含任何fd

image

该示例假如传入200us的timeout,表示200us内有被驱动唤醒就可以检测到fd在set集合中,从而调用read读取数据。假如不传入timeout,那么select查询会立即返回。

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FD_ZERO(&wfds);
FD_SET(fd, &wfds);
tv.tv_sec = 0;
tv.tv_usec = 100 * 1000;

struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);

ret = select(fd + 1, NULL, &wfds, NULL, &tv)) == -1);
if (ret == -1) {
printf("select error(%s)\n", strerror(errno));
return ret;
}

if (ret == 0) {
printf("select timeout\n");
ret = -1;
return ret;
}

if (FD_ISSET(fd, &wfds)) {
//start write data to drv
}

该示例传入100ms的timeout,表示100ms内有被驱动唤醒就可以检测到fd在set集合中,从而start write data to drv, 否则select timeout.

1.2.2 poll#

使用休眠唤醒机制,实现简单。比如前面所讲的一个按键字符设备驱动中,read函数中进行等待队列wait_event, 然后当按键按下,中断服务程序进行唤醒等待队列wake_up,read函数将会从休眠中唤醒返回数据给用户。

但是这种有一个缺点,如果要等很久,那么这种方式明显不好。 如果按键一直不去按下,read函数将会一直休眠,应用程序用户线程被一直阻塞。

1. 那么poll机制就是给它加一个超时机制,防止一直休眠和用户线程被阻塞。
2. APP 不知道驱动程序中是否有数据,可以先调用 poll 函数查询一下, poll 函数可以传入超时时间;
3. APP 进入内核态,调用到驱动程序的 poll 函数
    3.1 如果发现没有数据时就休眠一段时间;当超时时间到了之后,内核也会唤醒 APP;
    3.2 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒 APP;
4. APP 根据 poll 函数的返回值就可以知道是否有数据,如果有数据就调用read 得到数据

1.2.2.1 polls使用示例#

poll/select 监测的事件
image

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct pollfd fds[1];
nfds_t nfds = 1;
while (1) {
fds[0].fd = fd;
fds[0].events = POLLIN;
fds[0].revents = 0;
ret = poll(fds, nfds, 5000);
if (ret > 0) {
if (fds[0].revents == POLLIN) {
while (read(fd, &event, sizeof(event)) == sizeof(event)) {
printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value);
}
}
} else if (ret == 0) {
printf("time out\n");
} else {
printf("poll err\n");
}
}
  1. 打开设备文件。
  2. 设置 pollfd 结构体:
    想查询哪个文件(fd)?
    想查询什么事件(POLLIN)?
    先清除 “返回的事件” (revents)。
    使用 poll 函数查询事件,指定超时时间为 5000(ms)。

1.3 休眠唤醒#

这里由于poll底层机制提前用到了一个休眠唤醒机制,也就是等待队列wait_queue。先提前引入概念,

字符设备驱动-6-pre-休眠唤醒机制 | Hexo (fuzidage.github.io)

字符设备驱动-8.休眠唤醒机制 - fuzidage - 博客园 (cnblogs.com) 有展开细讲。

1.3.1 等待队列#

wait_event_interruptible_timeout:
image

image

1
2
3
4
5
6
7
void init_waitqueue_head(wait_queue_head_t *q);
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
wait_event(wq, condition);
wait_event_timeout(wq, condition, timeout);
wait_event_interruptible(wq, condition);
wait_event_interruptible_timeout(wq, condition, timeout);

1.3.2 等待队列项#

利用等待队列项来实现read函数的阻塞式访问,底层驱动去进行状态切换。下图把wait_event的方式换成等待队列项。
image

3 poll机制驱动底层原理#

3.1 我们期望的poll流程#

我们期望的大致流程如下:
image

1. app进行open, drv进行drv_open,注册好中断服务
2. app进行poll , drv进行drv_poll
3. 第一次如果没有数据到来,那么会执行else进行休眠,加入等待队列。
    要么被中断服务程序唤醒,进入for循环此时有数据到来返回;
    要么超时,也会从等待队列唤醒回来,进入for循环此时返回超时

可以看到会查询判断2次,但实际上内核做的更好,我们drv_poll中只需要(这些流程内核帮我们已经做好了):

1.把线程放入wq等待队列,并不会调用休眠
2.返回event状态

3.2 Linux内核实际的poll机制#

实际内核中poll函数流程如下:内核把poll抽出去了,调用sys_poll
image

1. app进行open, drv进行drv_open,注册好中断服务
2. app进行poll , 内核文件系统进行sys_poll
3. 调用驱动开发者实现的drv_poll
    调用poll_wait,把线程加入wq,但是不会进入休眠
    而是直接返回event状态
4. drv_poll返回后,sys_poll中进行数据判断(如果第一次进入没有数据到来,执行else, 将线程休眠(可以看到休眠是drv_poll上层sys_poll已经帮我们做好了),如果有数据则直接返回,那么就只会进入一次drv_poll)
   sys_poll函数执行else休眠的过程中,会被event唤醒or被超时唤醒
   第2进入for循环执行drv_poll,如果被event唤醒了,则返回数据,否则说明是超时唤醒,返回超时
5.最终内核文件系统sys_poll返回,唤醒userspace线程

可以看到当用户调用poll函数,在底层drv下可能会调用2次drv_poll。用户线程不会被一直阻塞休眠,要么有数据时中断的event唤醒,要么超时唤醒。我们只要实现drv_poll的部分,也就是紫色绿色的提示部分。

4 poll驱动编程实例(gpio key为例)#

使用 poll 机制时,驱动程序的核心就是提供对应的 drv_poll 函数。在drv_poll 函数中要做 2 件事:

1
2
3
4
5
6
7
8
9
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 把它们一起返回。

APP 调用 poll 后,很有可能会休眠。对应的,在中断服务程序中,也要有唤醒操作。
完整驱动代码如下:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>

struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major = 0;
static struct class *gpio_key_class;

/* 环形缓冲区 */
#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 = NEXT_POS(w);
}
}

static int get_key(void){
int key = 0;
if (!is_key_buf_empty()) {
key = g_keys[r];
r = NEXT_POS(r);
}
return key;
}

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
int err;
int key;

if (file->f_flags & O_NONBLOCK) { /* 非阻塞访问 */
if(atomic_read(&dev->releasekey) == 0) /* 没有按键按下 */
return -EAGAIN;
} else {
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
}
return 4;
}

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;
}

static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
};

static irqreturn_t gpio_key_isr(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);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED;
}

/* 1. 从platform_device获得GPIO
* 2. gpio=>irq
* 3. request_irq
*/
static int gpio_key_probe(struct platform_device *pdev){
int err;
struct device_node *node = pdev->dev.of_node;
int count;
int i;
enum of_gpio_flags flag;

count = of_gpio_count(node);
if (!count){
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++){
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0){
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
}

for (i = 0; i < count; i++){
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
}

major = register_chrdev(0, "100ask_gpio_key", &gpio_key_drv);
gpio_key_class = class_create(THIS_MODULE, "100ask_gpio_key_class");/* /sys/class/100ask_gpio_key_class */
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "100ask_gpio_key");
return PTR_ERR(gpio_key_class);
}
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "100ask_gpio_key"); /* /dev/100ask_gpio_key */
return 0;
}

static int gpio_key_remove(struct platform_device *pdev){
struct device_node *node = pdev->dev.of_node;
int count;
int i;

device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "100ask_gpio_key");

count = of_gpio_count(node);
for (i = 0; i < count; i++){
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
}
kfree(gpio_keys_100ask);
return 0;
}

static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "100ask_gpio_key",
.of_match_table = ask100_keys,
},
};

static int __init gpio_key_init(void){
int err;
err = platform_driver_register(&gpio_keys_driver);
return err;
}
static void __exit gpio_key_exit(void){
platform_driver_unregister(&gpio_keys_driver);
}
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");

4.1 probe函数分析#

image

定义gpio_key:
image
先确保dts中含有gpio_key设备树节点,才能通过.compatible = "100ask,gpio_key"匹配plateform_deviceplatform_driver, 当insmod ko时probe函数被调用。

1
struct device_node *node = pdev->dev.of_node;//可以从platform_device获取到device_node

image
of_gpio_count可以根据设备树节点获取到gpio的数量。

image
of_get_gpio_flags可以根据设备树节点获取到gpio编号和gpio flags

image
获取gpio描述子和gpio中断号

image
注册中断服务程序gpio_key_isr,当按键按下会触发gpio中断,执行gpio_key_isr.

4.2 drv_poll函数分析#

1
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

这里先定义并初始化一个wait_queue gpio_key_wait,也就是等待队列。(等待队列使用详见kernel下include\linux\wait.h)
image

  1. drv_poll函数中call poll_wait将线程加入等待队列,并返回event

    1. 可以看到如果有数据,返回POLLIN | POLLRDNORM(那么drv_poll只会被调用1次); 回到sys_poll
    2. 如果没有数据则返回0, 也回到sys_poll
  2. 回到sys_poll后发现:

  3. 如果有数据,直接返回

  4. 如果没有数据则进入休眠,当超时或者被event唤醒后,sys_poll又会再次进入drv_poll,此时判断is_key_buf_empty,如果是按键按下触发中断响应,那么就有数据,返回POLLIN | POLLRDNORM,否则无数据表示是被超时唤醒,event为0。这种就是sys_poll调用2次。

3.`sys_poll`最终返回,回到`userspace`线程

4.3 drv_read函数分析#

image
当userspace得知驱动有数据时,调用read函数,进入drv_read中调用wait_event_interruptible
image
这里是利用等待队列,等待队列gpio_key_waitevent为true后,wait_event_interruptible该函数会返回。那么在对应的中断处理函数中,需要call wake_up函数来唤醒等待队列,并且把event设置成true,也就是把wait_event_interruptiblecondition设置成true.

4.4 按键中断服务函数分析#

image
put_key是将event设置成true,这样表示有数据了,is_key_buf_empty非空了。调用wake_up_interrptible函数唤醒等待队列gpio_key_wait,因此drv_read函数就能立马返回数据。

4.5 测试#

测试demo用户态程序如下:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
/*
* ./button_test /dev/100ask_gpio_key
*
*/
int main(int argc, char **argv){
int fd;
int val;
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;

if (argc != 2) {
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}

fd = open(argv[1], O_RDWR);
if (fd == -1){
printf("can not open file %s\n", argv[1]);
return -1;
}
fds[0].fd = fd;
fds[0].events = POLLIN;

while (1){
ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN)){
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}else{
printf("timeout\n");
}
}
close(fd);
return 0;
}