SylixOS 中 select 原理及使用分析

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);
......
}
相关推荐
ssr——ssss1 小时前
网络华为HCIA+HCIP 策略路由,双点双向
网络·华为
Yang三少喜欢撸铁2 小时前
【部署k8s集群时,彻底删除calico、flannel网络插件】
网络·容器·kubernetes
网安墨雨3 小时前
网络安全(一):常见的网络威胁及防范
网络·安全·web安全
会挖坑的石头3 小时前
C语言术语
c语言
拖孩3 小时前
[特殊字符]我在 Chatterbox(话匣子)中 Websocket 的使用-上篇(基本介绍)
网络·websocket·网络协议
不羁。。4 小时前
【网络通信安全】深入解析 OSPF 协议:从概念到 eNSP 实战配置(附完整代码与排错指南)
网络·智能路由器
编程就是如此4 小时前
Ubuntu桌面环境下网络设置选项缺失问题解决
网络·ubuntu
陈璆鸣4 小时前
交换综合实验
网络·智能路由器
每天敲200行代码5 小时前
Linux UDP网络编程套接字sockets
linux·网络·udp
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.5 小时前
第三次作业
linux·服务器·网络