1、select接口简介
1.1 select接口使用用例
select 是操作系统多路 I/O 复用技术实现的方式之一。
select 函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类 IO 操作了,包括可读,可写,发生异常三种。
select 在应用中使用的例子如下段代码所示。
#include <stdio.h>
#include <sys/select.h>int main (int argc, char *argv[])
{fd_set fdset;int ret;struct timeval timeout;char ch;timeout.tv_sec = 10;timeout.tv_usec = 0;for (;;) {FD_ZERO(&fdset);FD_SET(STDIN_FILENO, &fdset);ret = select(STDIN_FILENO + 1, &fdset, NULL, NULL, &timeout);if (ret <= 0) {break;} else if (FD_ISSET(STDIN_FILENO, &fdset)) {read(STDIN_FILENO, &ch, 1);if (ch == '\n') {continue;}fprintf(stdout, "input char: %c\n", ch);if (ch == 'q') {break;}}}return (0);
}
1.2 select函数原型分析
LW_API INT select(INT iWidth, fd_set *pfdsetRead,fd_set *pfdsetWrite,fd_set *pfdsetExcept,struct timeval *ptmvalTO);
- iWidth 为设置的文件集中,最大的文件号 + 1;
- pfdsetRead 为关心的可读文件集;
- pfdsetWrite 为关心的可写文件集;
- pfdsetExcept 为关心的异常文件集;
- ptmvalTO 为等待超时时间,LW_NULL 表示永远等待;
- 返回值:正常返回等待到的文件数量,错误返回 PX_ERROR。
2、select 实现
2.1 内核中 select 实现
select 函数具体实现如下,主体可以分为 3 个部分:
- 检查读文件集、写文件集、异常文件集,调用 ioctl 的
FIOSELECT
命令 - 调用
API_SemaphoreBPend
接口进行阻塞 - 被唤醒后,调用 ioctl 的
FIOUNSELECT
命令
LW_API
INT pselect (INT iWidth, fd_set *pfdsetRead,fd_set *pfdsetWrite,fd_set *pfdsetExcept,const struct timespec *ptmspecTO,const sigset_t *sigsetMask)
{......if (pfdsetRead) { /* 检查读文件集 */selwunNode.SELWUN_seltypType = SELREAD;if (__selDoIoctls(&pselctx->SELCTX_fdsetOrigReadFds, pfdsetRead, iWidth, FIOSELECT, &selwunNode, LW_TRUE)) { /* 遇到错误,立即退出 */iIsOk = PX_ERROR;}}....../* 开始等待,这里是 select 阻塞的根源。 一般会在驱动的中断处理函数中调用 wakeup_node ,去释放这个二进制信号量 */ulError = API_SemaphoreBPend(pselctx->SELCTX_hSembWakeup,ulWaitTime); /* 开始等待 */if (pfdsetRead) { /* 检查读文件集 */selwunNode.SELWUN_seltypType = SELREAD;if (__selDoIoctls(&pselctx->SELCTX_fdsetOrigReadFds, pfdsetRead, iWidth, FIOUNSELECT, &selwunNode, LW_FALSE)) { /* 如果存在节点,删除节点 */iIsOk = PX_ERROR;}}......
}
select 操作的一个重要数据结构,就是 “唤醒节点” ——LW_SEL_WAKEUPNODE
。
select 函数允许程序监视多个文件描述符,这里的每一个文件描述符,对应一个“唤醒节点”。唤醒节点中一个重要的变量就是 SELWUN_hThreadId
线程 ID,记录了创建该等待节点的线程句柄(其实就是调用 select 接口的线程)。该数据结构通过 ioctl 接口,传递到设备文件描述符对应的设备驱动中,由设备驱动去维护、管理该唤醒节点。
/*********************************************************************************************************等待节点类型
*********************************************************************************************************/typedef enum {SELREAD, /* 读阻塞 */SELWRITE, /* 写阻塞 */SELEXCEPT /* 异常阻塞 */
} LW_SEL_TYPE;/*********************************************************************************************************等待链表节点.
*********************************************************************************************************/typedef struct {LW_LIST_LINE SELWUN_lineManage; /* 管理链表 */UINT32 SELWUN_uiFlags;LW_OBJECT_HANDLE SELWUN_hThreadId; /* 创建节点的线程句柄 */INT SELWUN_iFd; /* 链接点的文件描述符 */LW_SEL_TYPE SELWUN_seltypType; /* 等待类型 */
} LW_SEL_WAKEUPNODE;
typedef LW_SEL_WAKEUPNODE *PLW_SEL_WAKEUPNODE;
2.2 设备驱动的 ioctl 实现
SylixOS 的 select 接口实现中,系统会调用到每一个 fd 对应的设备驱动的 ioctl 接口,并会调用到如下表所示的两个命令:
命令 | 说明 |
---|---|
FIOSELECT | 添加 SEL_WAKE_NODE 节点 |
FIOUNSELECT | 移除 SEL_WAKE_NODE 节点 |
驱动中 ioctl 的 FIOSELECT
实现,通常会调用 SEL_WAKE_NODE_ADD
接口,向设备驱动中添加一个“唤醒节点”(也可以把它理解成“等待节点”)。以 gpio 驱动为例:
static INT _gpiofdSelect (PLW_GPIOFD_FILE pgpiofdfil, PLW_SEL_WAKEUPNODE pselwunNode)
{......SEL_WAKE_NODE_ADD(&pgpiofdfil->GF_selwulist, pselwunNode);......
}static INT _gpiofdUnselect (PLW_GPIOFD_FILE pgpiofdfil, PLW_SEL_WAKEUPNODE pselwunNode)
{......SEL_WAKE_NODE_DELETE(&pgpiofdfil->GF_selwulist, pselwunNode);......
}static INT _gpiofdIoctl (PLW_GPIOFD_FILE pgpiofdfil, INT iRequest, LONG lArg)
{......switch (iRequest) {......case FIOSELECT:pselwunNode = (PLW_SEL_WAKEUPNODE)lArg;return (_gpiofdSelect(pgpiofdfil, pselwunNode));case FIOUNSELECT:pselwunNode = (PLW_SEL_WAKEUPNODE)lArg;return (_gpiofdUnselect(pgpiofdfil, pselwunNode));}......
}
节点的组织形式如下:
- 设备驱动相关结构体中,会维护一个指针,指向唤醒节点链表(这个链表由设备驱动去维护)
- 链表节点的添加,是调用
SEL_WAKE_NODE_ADD
函数完成的
2.3 阻塞与唤醒实现
阻塞
select 本身是一个阻塞函数。通过调用二进制信号量 API_SemaphoreBPend
,实现阻塞操作。
注意,这里的二进制信号量,实际上是一个同步信号量。在调用 pend 之前,pselect 会首先调用 ioctl,传递 FIOSELECT 参数。此接口中会判断 当前 是否满足 select 的唤醒条件,若满足则先调用 post,以使之后调用的 pend 不会被阻塞; 若 当前 不满足 select 的唤醒条件,则会进入阻塞状态,等待设备驱动主动去唤醒
唤醒
通常是由 select 所监听的文件描述符集对应的设备驱动去唤醒。还是以 gpio 驱动为例,当一个 gpio 中断(电平触发、边沿触发)产生时,就会告诉操作系统,该 gpio 的状态“可读”,通过调用 SEL_WAKE_UP_ALL
接口实现唤醒操作。该接口底层实现,实际上就是调用 API_SemaphoreBPost
LW_API
VOID API_SelWakeup (PLW_SEL_WAKEUPNODE pselwunNode)
{
....../* 根据唤醒节点中保存的线程 ID,获取线程 TCB 结构 */usIndex = _ObjectGetIndex(pselwunNode->SELWUN_hThreadId);ptcb = __GET_TCB_FROM_INDEX(usIndex);if (!ptcb || !ptcb->TCB_pselctxContext) { /* 线程不存在 */return;}/* 设置唤醒节点的 READY 属性 */LW_SELWUN_SET_READY(pselwunNode);/* 根据 TCB,找到需要唤醒的句柄 SELCTX_hSembWakeup */pselctxContext = ptcb->TCB_pselctxContext;API_SemaphoreBPost(pselctxContext->SELCTX_hSembWakeup); /* 提前激活即将等待线程 */
}static irqreturn_t _gpiofdIsr (PLW_GPIOFD_FILE pgpiofdfil)
{
......SEL_WAKE_UP_ALL(&pgpiofdfil->GF_selwulist, SELREAD);
......
}