1、select接口简介
1.1 select接口使用用例
select 是操作系统多路 I/O 复用技术实现的方式之一。
select 函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为"准备好"的状态。所谓的"准备好"状态是指:文件描述符不再是阻塞状态,可以用于某类 IO 操作了,包括可读,可写,发生异常三种。
select 在应用中使用的例子如下段代码所示。
c
#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函数原型分析
c
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
命令
c
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 接口,传递到设备文件描述符对应的设备驱动中,由设备驱动去维护、管理该唤醒节点。
c
/*********************************************************************************************************
等待节点类型
*********************************************************************************************************/
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 驱动为例:
c
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
c
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);
......
}