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);
......
}
相关推荐
jz_ddk33 分钟前
[zynq] Zynq Linux 环境下 AXI BRAM 控制器驱动方法详解(代码示例)
linux·运维·c语言·网络·嵌入式硬件
Magnum Lehar42 分钟前
vulkan游戏引擎启动环境配置1
c语言
深思慎考1 小时前
Linux网络——socket网络通信udp
linux·网络·udp
孤寂大仙v1 小时前
【计算机网络】NAT、代理服务器、内网穿透、内网打洞、局域网中交换机
网络·计算机网络·智能路由器
LuckyRich11 小时前
【websocket】安装与使用
网络·websocket·网络协议
KIDAKN1 小时前
理解网络协议
网络·网络协议
待什么青丝2 小时前
【TMS570LC4357】之相关驱动开发学习记录1
c语言·arm开发·驱动开发·学习
C_Liu_2 小时前
C语言:数据在内存中的存储
c语言·开发语言
s_little_monster2 小时前
【Linux】网络--数据链路层--以太网
linux·运维·网络·经验分享·笔记·学习·计算机网络
ZZSCH7 小时前
哈工大计统大作业-程序人生
c语言