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);
......
}