LV10-03-IO模型-02-IO模型在驱动层的实现-02-复用IO

本文主要是IO模型在驱动层的实现中的复用IO的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
Linux开发板 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2
u-boot 2013.01
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
文件下载链接
------

一、复用IO

1. 应用层

1.1 三套接口

应用层层主要是等待有可以操作的文件描述符,有三种接口:select、poll和epoll。这三种我们在学习网络IO的时候都有学习过,用于应用层程序编写的话,其实是和网络IO一样的。

  • select :位运算实现 监控的描述符数量有限(32位机1024,64位机2048) 效率差。

  • poll :链表实现,监控的描述符数量不限 效率差。

  • epoll :效率最高,监控的描述符数量不限。

下边以select为例,记录一个测试用的模板。

1.2 select()

1.2.1 函数说明

在 linux 下可以使用 man 2 select 命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

【函数说明】该函数用于执行 I/O 多路复用操作,能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

【函数参数】

  • nfds : int 类型,通常表示需要监视的最大文件描述符编号值加 1 。

  • readfds : fd_set 类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符读是否就绪(是否可读)。 select() 返回后, readfds 将清除除准备读取的文件描述符外的所有文件描述符。

  • writefds : fd_set 类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符写是否就绪(是否可写)。 select() 返回后, writefds 将清除除准备写入的文件描述符外的所有文件描述符。

  • exceptfds : fd_set 类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符是否出现异常,需要注意异常情况并不是在文件描述符上出现了一些错误。 select() 返回后, exceptfds 将清除除了发生异常条件的文件描述符以外的所有文件描述符。

  • timeout : struct timeval 类型结构体指针变量,用于设定 select() 阻塞的时间上限,控制 select 的阻塞行为。

点击查看 timeout 参数说明

在使用 man 手册的时候,会有该结构体的说明,该结构体的成员如下:

1
2
3
4
5
struct timeval
{
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};

(1)我们可将 timeout 参数设置为 NULL ,表示 select() 将会一直阻塞、直到某一个或多个文件描述符成为就绪态;

(2)如果参数 timeout 指向一个 struct timeval 结构体对象,并且这个结构体对象中的两个成员变量都为 0 ,那么此时 select() 函数不会阻塞,它只是简单地轮训指定的文件描述符集合,看看其中是否有就绪的文件描述符并立刻返回。

(3)若参数 timeout 将为 select() 指定一个等待(阻塞)时间的上限值,如果在阻塞期间内,文件描述符集合中的某一个或多个文件描述符成为就绪态,将会结束阻塞并返回;如果超过了阻塞时间的上限值, select() 函数将会返回。

【返回值】 int 类型,返回值可能会有以下三种情况:

  • 返回 -1 :表示有错误发生,并且会设置 errno 。可能的错误码包括 EBADF 、 EINTR 、 EINVAL 、 EINVAL 以及 ENOMEM ,其中 EBADF 表示 readfds 、 writefds 或 exceptfds 中有一个文件描述符是非法的; EINTR 表示该函数被信号处理函数中断了,其他更详细的我们可以使用 man 命令查看帮助手册去详细了解。
  • 返回 0 :表示在任何文件描述符成为就绪态之前 select() 调用已经超时,在这种情况下, readfds , writefds 以及 exceptfds 所指向的文件描述符集合都会被清空。
  • 返回正整数:表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过 FD_ISSET() 宏进行检查,以此查找发生的 I/O 事件是什么。如果同一个文件描述符在 readfds , writefds 以及 exceptfds 中同时被指定,且它有多个 I/O 事件都处于就绪态的话,那么这个文件描述符就会被统计多次,换句话说,也就是说 select() 返回三个集合中被标记为就绪态的文件描述符的总数。

1.2.2 使用实例

暂无

1.3 文件描述符集操作函数

在使用 select() 函数之前,需要先了解一下文件描述符的相关操作,这里一共有四个函数:

1
2
3
4
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

1.3.1 FD_CLR()

1.3.1.1 函数说明

在 linux 下可以使用 man 2 FD_CLR 命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
void FD_CLR(int fd, fd_set *set);

【函数说明】该函数用于从文件描述符集中移除一个文件描述符。

【函数参数】

  • fd : int 类型,表示要移除的文件描述符。
  • set : fd_set 类型,表示已经定义好的文件描述符集。

【返回值】 none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <sys/select.h>

/* 至少应该有的语句 */
fd_set fset
FD_CLR(fd, &fset);

【注意事项】 none

1.3.1.2 使用实例

暂无

1.3.2 FD_ISSET()

1.3.2.1 函数说明

在 linux 下可以使用 man 2 FD_ISSET 命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
int FD_ISSET(int fd, fd_set *set);

【函数说明】该函数用于测试指定的文件描述符是否还存在于指定的文件描述符集。

【函数参数】

  • fd : int 类型,表示要测试的文件描述符。
  • set : fd_set 类型,表示已经定义好的文件描述符集。

【返回值】 int 类型,文件描述符 fd 在 set 中存在, FD_ISSET() 返回非 0 值,如果不存在则返回 0 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
/* 需要包含的头文件 */
#include <sys/select.h>

/* 至少应该有的语句 */
fd_set fset
if(FD_ISSET(0, &rset))
{
...
}

【注意事项】 none

1.3.2.2 使用实例

暂无

1.3.3 FD_SET()

1.3.3.1 函数说明

在 linux 下可以使用 man 2 FD_SET 命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
void FD_SET(int fd, fd_set *set);

【函数说明】该函数用于向文件描述符集中添加一个我们关心的文件描述符。

【函数参数】

  • fd : int 类型,表示要添加的文件描述符。
  • set : fd_set 类型,表示已经定义好的文件描述符集。

【返回值】 none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <sys/select.h>

/* 至少应该有的语句 */
fd_set fset
FD_SET(fd, &fset);

【注意事项】 none

1.3.3.2 使用实例

暂无

1.3.4 FD_ZERO()

1.3.4.1 函数说明

在 linux 下可以使用 man 2 FD_ZERO 命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
void FD_ZERO(fd_set *set);

【函数说明】该函数用于将一个文件描述符集合初始化为空。

【函数参数】

  • set : fd_set 类型,表示需要初始化为空的文件描述符集。

【返回值】 none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <sys/select.h>

/* 至少应该有的语句 */
fd_set fset
FD_ZERO(&fset);

【注意事项】 none

1.3.4.2 使用实例

暂无

1.4 使用实例

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
fd_set rfds;       /* 文件描述符集 */
fd_set wfds; /* 文件描述符集 */

while (1)
{
/* 3.1清空文件描述符集 */
FD_ZERO(&rfds);
FD_ZERO(&wfds);
/* 3.2添加我们关心的文件描述符 */
FD_SET(fd1, &rfds);
FD_SET(fd2, &rfds);
FD_SET(fd1, &wfds);
/* 3.3监控文件描述符集 */
if(select(fd + 1, &rfds, NULL, NULL, NULL) < 0)
{
if (errno == EINTR)
{
continue;
}
else
{
printf("select error\n");
break;
}
}
/* 3.4判断是否有数据可读 */
if(FD_ISSET(fd1,&rfds))
{
//....
}
if(FD_ISSET(fd2,&rfds))
{
//读数据
//....
}
if(FD_ISSET(fd1,&wfds))
{
//写数据
//....
}
}

2.驱动层

在驱动层,我们只需要实现驱动自己的 poll 函数即可,但是需要借助等待队列,关于等待队列的定义和初始化可以看阻塞IO的笔记。

2.1 相关函数

2.1.1 poll_wait()

2.1.1.1 函数说明

我们使用以下命令查询一下函数所在头文件:

1
grep poll_wait -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
/* 需包含的头文件 */
#include <linux/poll.h>

/* 函数定义 */
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);
}

【函数说明】该函数用于将等待队列头添加至 poll_table 表中。

【函数参数】

  • filp :struct file * 类型,表示要打开的设备文件。
  • wait : wait_queue_head_t * 类型, 要添加到 poll_table 中的等待队列头 。
  • p :poll_table * 类型,file_operations(设备操作函数集) 中 poll 函数的 wait 参数。

【返回值】none

【使用格式】

1
2
3
4
5
6
/* 需包含的头文件 */
#include <linux/wait.h>

/* 至少要有的语句 */
wait_queue_head_t rq; /* 读等待队列头数据类型 */
poll_wait(pfile, &rq, wait);

【注意事项】 none

2.1.1.2 使用实例

暂无

2.2 驱动poll函数

这里是一个驱动层我们自己实现的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
/**
* @Function : mydev_poll
* @brief : 该函数与select、poll、epoll_wait函数相对应,协助这些多路监控函数判断本设备是否有数据可读写
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次ioctl对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param wait :poll_table * 类型。
* @return : 返回可读写的文件描述符标记
* @Description : 函数名初始化给 struct file_operations的成员.poll
*/
unsigned int mydev_poll(struct file *pfile, poll_table *wait)
{
unsigned int mask = 0;
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data); /* 通过私有数据方式获取 g_my_dev设备属性 */
/* 1.将所有等待队列头加入poll_table表中 */
poll_wait(pfile, &p_my_dev->rq, wait);
poll_wait(pfile, &p_my_dev->wq, wait);
/* 2.判断是否可读,如可读则mask |= POLLIN | POLLRDNORM; */
if(p_my_dev->curlen > 0 )
{
mask |= POLLIN | POLLRDNORM;
}
/* 3.判断是否可写,如可写则mask |= POLLOUT | POLLWRNORM; */
if(p_my_dev->curlen < BUF_LEN )
{
mask |= POLLOUT | POLLWRNORM;
}
return mask;
}

3. 使用步骤

  • (1)用wait_queue_head_t数据类型定义等待队列头,注意读写使用不同的等待队列头,也就是需要定义两个等待队列头;
  • (2)用 init_waitqueue_head() 函数初始化等待队列头;
  • (3)在驱动中实现 xxx_poll(struct file *pfile, poll_table *wait)函数