fuzidage
专注嵌入式、linux驱动 、arm裸机研究

导航

 

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 时设置:

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); /* 阻塞方式 */

驱动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, 查询到资源,返回查询到的fd总数。
2,没查到,则睡眠
	①带timeout参数,timeout后,唤醒退出,此时fd总数为0
	②不带timeout, 阻塞且睡眠中,直到有资源可用才唤醒

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

1.2.1.1 select使用示例

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查询会立即返回。

点击查看代码
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

点击查看代码
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。先提前引入概念,后面字符设备驱动-8.休眠唤醒机制 - fuzidage - 博客园 (cnblogs.com) 会展开细讲。

1.3.1 等待队列

wait_event_interruptible_timeout:
image

image

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. 把当前线程挂入队列 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 后,很有可能会休眠。对应的,在中断服务程序中,也要有唤醒操作。
完整驱动代码如下:

点击查看代码
#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函数被调用。

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函数分析

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后发现:

    1. 如果有数据,直接返回
    2. 如果没有数据则进入休眠,当超时或者被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用户态程序如下:

点击查看代码
#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;
}
posted on 2023-05-17 17:19  fuzidage  阅读(218)  评论(0编辑  收藏  举报