如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。
- poll、epoll 和 select 可以用于处理轮询
- 应用程序通过 select、epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。
- 当应用程序调用 select、epoll 或 poll 函数的时候设备驱动程序中的
poll
函数就会执行,因此需要在设备驱动程序中编写 poll 函数
。
先来看一下应用程序中使用的 select 这个函数,poll 和 epoll 的使用待补充
函数原型及参数解析
函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解析
nfds
- nfds 表示需要监控的文件描述符的最大值加1。这个值通常设为所有文件描述符中的最大值加1,以确保select能够正确地监控所有需要的文件描述符。
readfds/writefds/exceptfds
fd_set
结构体原型
// 定义类型别名 __fd_mask,本质是 long int
typedef long int __fd_mask;
•/* fd_set 记录要监听的fd集合,及其对应状态 */
typedef struct {
// fds_bits是long类型数组,长度为 1024/32 = 32
// 共1024个bit位,每个bit位代表一个fd,0代表未就绪,1代表就绪
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
// ...
} fd_set;
所以 select
函数能够监视的文件描述符数量有最大的限制,一般是 1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率。
- readfds 表示要监控的读的文件描述符集合,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取,那么 select 就会返回一个大于 0 的值表示文件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时,可以将 readfds 设置为 NULL,表示不关心任何文件的读变化
- writefds 表示要监控的写的文件描述符集合,和 readfds 类似,用于监视这些文件是否可以进行写操作。
- exceptfds 表示要监控的异常条件的文件描述符集合,和 readfds 类似,用于监视这些文件的异常。
它们都是由 fd_set 类型表示的位图结构。可以使用以下四个宏来操作这些集合:
- FD_SET(fd, &set): 将文件描述符 fd 添加到 se t集合中,用于将 fd_set 变量的某个 bit 置 1。
- FD_CLR(fd, &set): 从 set 集合中删除文件描述符 fd,用于将 fd_set 变量的某个 bit 清零。
- FD_ISSET(fd, &set): 检查 fd 是否在 set 集合中。
- FD_ZERO(&set): 清空 set 集合,用于将 fd_set 变量的所有 bit 都清零。
timeout
- timeout 参数是一个 timeval 结构指针,用于设置 select 函数的超时时间。
- 当 timeout 为 NULL 时,select 将无限期地等待,直到有文件描述符准备好。
- 当 timeout 设置为 0 时,select 将立即返回。
- 当 timeout 设置为非零值时,select 将等待指定的时间,直到有文件描述符准备好或超时。
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值
select函数的返回值表示以下三种情况:
- 返回值大于0:表示有准备好的文件描述符,即已经发生的I/O事件数量。
- 返回值等于0:表示超时,即在指定的时间内没有任何I/O事件发生。
- 返回值小于0:表示发生错误。在这种情况下,可以使用perror或strerror函数来获取错误信息。
在调用 select 函数后,可以通过检查 readfds,writefds 和 exceptfds 集合的状态,以确定哪些文件描述符准备好进行 I/O 操作。然后,程序可以根据文件描述符的状态来执行相应的读、写或异常处理操作。
示例程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int main(void)
{
fd_set readfds;
struct timeval timeout;
int ret, fd_max;
/* 创建两个管道 */
int pipefds1[2];
int pipefds2[2];
pipe(pipefds1);
pipe(pipefds2); /* 向管道写入数据 */
write(pipefds1[1], "Hello", 5);
write(pipefds2[1], "World", 5);
while (1)
{
FD_ZERO(&readfds); /* 清除 readfds */
FD_SET(pipefds1[0], &readfds); /* 将 pipe0 读 fd 添加到 readfds 集合中 */
FD_SET(pipefds2[0], &readfds); /* 将 pipe1 读 fd 添加到 readfds 集合中 */
/* 设置最大文件描述符 */
fd_max = (pipefds1[0] > pipefds2[0]) ? pipefds1[0] : pipefds2[0];
/* 设置超时时间 */
timeout.tv_sec = 5;
timeout.tv_usec = 0;
ret = select(fd_max + 1, &readfds, NULL, NULL, &timeout);
if (ret == -1)
{
perror("select"); /* 错误 */
exit(EXIT_FAILURE);
}
else if (ret == 0)
{
printf("Timeout!\n"); /* 超时 */
break;
}
else /* 可以读取数据 */
{
if (FD_ISSET(pipefds1[0], &readfds)) /* 判断是否为 pipe0 读 fd */
{
char buf[6];
read(pipefds1[0], buf, 5);
buf[5] = '\0';
printf("Data from pipe1: %s\n", buf);
}
if (FD_ISSET(pipefds2[0], &readfds)) /* 判断是否为 pipe1 读 fd */
{
char buf[6];
read(pipefds2[0], buf, 5);
buf[5] = '\0';
printf("Data from pipe2: %s\n", buf);
}
break;
}
}
close(pipefds1[0]);
close(pipefds1[1]);
close(pipefds2[0]);
close(pipefds2[1]);
return 0;
}
程序运行结果
Data from pipe1: Hello
Data from pipe2: World
缺点
- 一开始需要将整个
fd_set
从用户空间拷贝到内核空间,在select
结束之后还要再次拷贝回用户空间 - 文件描述符数量限制:select函数所能处理的文件描述符数量受限于FD_SETSIZE,这可能导致无法处理大量连接的问题。
- 效率问题:当文件描述符数量增加时,select函数的效率会降低,因为它需要遍历所有文件描述符以检查状态。
参考
- https://www.xjx100.cn/news/212725.html?action=onClick
- https://blog.csdn.net/weixin_44471948/article/details/120846877
- https://juejin.cn/post/7129070726249709599#heading-8