POLL底层驱动机制

发布时间 2023-05-17 17:23:02作者: fuzidage

1 前言

1.1 阻塞与非阻塞IO

APP 调用 open 函数时,不要传入“ O_NONBLOCK”。APP 调用 read 函数读取数据时,为阻塞io。
APP 调用 open 函数时,传入“ O_NONBLOCK”表示“非阻塞”。APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read函数会返回数据,否则也会立刻返回错误。这种需要APP反复主动去"轮询"设备,否则无法及时响应。

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

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

1.3 select

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

那么会select会有2个结果:

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

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

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

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

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 得到数据

2.1 poll应用编程示例

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

打开设备文件。
设置 pollfd 结构体。
想查询哪个文件(fd)?
想查询什么事件(POLLIN)?
先清除“返回的事件” (revents)。
使用 poll 函数查询事件,指定超时时间为 5000(ms)。

3 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状态

实际内核中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, 将线程休眠,如果有数据则直接返回)
   sys_poll函数执行else休眠的过程中,会被event唤醒or被超时唤醒
   第2进入for循环执行drv_poll,如果被event唤醒了,则返回数据,否则说明是超时唤醒,返回超时
5.最终内核文件系统sys_poll返回,唤醒userspace线程

可以看到当用户调用poll函数,在底层drv下会调用2次drv_poll。用户线程不会一直阻塞休眠,要么有数据时v下的event唤醒,要么超时唤醒。

3.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 后,很有可能会休眠。对应的,在中断服务程序中,也要有唤醒操作。