多路復用 I/O 函數——`select`函數

好的,我們以 Linux 中經典的多路復用 I/O 函數——select 為例,進行一次完整、深入且包含全部代碼的解析。

<摘要>

select 是 Unix/Linux 系統中傳統的多路復用 I/O 系統調用。它允許一個程序同時監視多個文件描述符(通常是套接字),阻塞等待直到一個或多個描述符就緒(如變得可讀、可寫或發生異常),或者等待超時。它是構建能夠處理多個客戶端連接的服務器(如早期的 Web 服務器、聊天室)的基礎方法。雖然性能上不如 epoll,但其跨平臺特性(POSIX 標準)使其仍有廣泛應用價值。


<解析>

select 函數是處理并發 I/O 的“老將”。它的核心思想是:“告訴我一組你關心的文件描述符,我來幫你盯著,一旦其中有任何一個有動靜(可讀、可寫、出錯),或者等到你指定的時間,我就醒來通知你。” 這樣,單個線程就可以管理多個連接。

1) 函數的概念與用途
  • 功能:同步地監視多組(可讀、可寫、異常)文件描述符的狀態變化。它會使進程阻塞,直到有描述符就緒或超時。
  • 場景
    • 管理多個網絡客戶端連接的服務器。
    • 需要同時監聽標準輸入和網絡套接字的客戶端(如聊天程序)。
    • 需要設置精確超時的 I/O 操作。
    • 跨平臺程序(Windows 也支持 select)。
2) 函數聲明與出處

select 定義在 <sys/select.h> 頭文件中。

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
3) 返回值含義與取值范圍
  • 成功:返回就緒的文件描述符的總數。如果超時則返回 0
  • 失敗:返回 -1,并設置相應的錯誤碼 errno
    • EBADF:在某一個集合中傳入了無效的文件描述符。
    • EINTR:這個調用在阻塞期間被信號中斷。通常需要重新調用 select
    • EINVAL:參數 nfds 為負數或超時時間值無效。
4) 參數的含義與取值范圍
  1. int nfds

    • 作用:指定所有被監控的文件描述符集合中最大值加 1。內核通過這個值來線性掃描哪些描述符就緒,從而提高效率。
    • 取值范圍:通常是 max_fd + 1max_fd 是所有監聽描述符中最大的那個)。
  2. fd_set *readfds

    • 作用:指向一個 fd_set 類型的對象,該對象中包含了我們關心是否可讀的文件描述符集合。傳入時是“我們關心的”,返回時是“就緒的”。
    • 取值范圍NULL 表示不關心可讀事件。
  3. fd_set *writefds

    • 作用:指向一個 fd_set 類型的對象,該對象中包含了我們關心是否可寫的文件描述符集合。
    • 取值范圍NULL 表示不關心可寫事件。
  4. fd_set *exceptfds

    • 作用:指向一個 fd_set 類型的對象,該對象中包含了我們關心是否發生異常的文件描述符集合。異常通常指帶外數據(OOB data)到達。
    • 取值范圍NULL 表示不關心異常事件。
  5. struct timeval *timeout

    • 作用:指定 select 等待的超時時間。這是一個結構體指針,可以精確到微秒。
    • 結構體定義
      struct timeval {long tv_sec;  /* seconds (秒)*/long tv_usec; /* microseconds (微秒)*/
      };
      
    • 取值范圍
      • NULL無限阻塞。直到有描述符就緒。
      • {0, 0}非阻塞輪詢。立即返回,檢查描述符狀態。
      • {n, m}等待最多 n 秒 m 微秒

fd_set 相關操作宏(非常重要)

void FD_ZERO(fd_set *set);      // 清空一個 fd_set
void FD_SET(int fd, fd_set *set); // 將一個 fd 加入 set
void FD_CLR(int fd, fd_set *set); // 將一個 fd 從 set 中移除
int FD_ISSET(int fd, fd_set *set); // 檢查一個 fd 是否在 set 中(就緒)
5) 函數使用案例

示例 1:基礎用法 - 監聽標準輸入(阻塞等待)
此示例演示如何使用 select 監聽標準輸入(STDIN_FILENO),實現一個帶超時等待的輸入提示符。

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <unistd.h>int main() {fd_set read_fds;struct timeval timeout;int retval;char buf[256];printf("You have 5 seconds to type something...\n");while(1) {// 1. 設置超時時間(每次循環都需要重新設置,因為select調用后會修改timeout)timeout.tv_sec = 5;timeout.tv_usec = 0;// 2. 清空并設置要監視的描述符集合(每次循環都需要重新設置,因為select調用后會修改read_fds)FD_ZERO(&read_fds);FD_SET(STDIN_FILENO, &read_fds); // STDIN_FILENO is 0// 3. 調用select,nfds是最大fd+1retval = select(STDIN_FILENO + 1, &read_fds, NULL, NULL, &timeout);if (retval == -1) {perror("select()");exit(EXIT_FAILURE);} else if (retval == 0) {printf("\nTimeout! No data within 5 seconds.\n");printf("Waiting again...\n");} else {// 檢查我們關心的描述符是否真的就緒if (FD_ISSET(STDIN_FILENO, &read_fds)) {// 從標準輸入讀取數據ssize_t count = read(STDIN_FILENO, buf, sizeof(buf) - 1);if (count > 0) {buf[count] = '\0'; // Null-terminate the stringprintf("You typed: %s", buf);} else {perror("read");break;}}}}return 0;
}

示例 2:監聽多個套接字(服務器端模型)
此示例展示一個簡易的單線程回顯服務器,可以同時處理監聽新連接和已連接客戶端的讀事件。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>#define PORT 8080
#define MAX_CLIENTS 10
#define BUF_SIZE 1024int main() {int server_fd, new_socket, client_sockets[MAX_CLIENTS];fd_set read_fds;int max_sd, sd, activity, i, valread;struct sockaddr_in address;int addrlen = sizeof(address);char buffer[BUF_SIZE];// 初始化客戶端套接字數組for (i = 0; i < MAX_CLIENTS; i++) {client_sockets[i] = 0;}// 創建服務器套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 綁定套接字到端口if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 開始監聽if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d\n", PORT);while(1) {// 清空描述符集FD_ZERO(&read_fds);// 添加服務器監聽套接字FD_SET(server_fd, &read_fds);max_sd = server_fd;// 添加所有有效的客戶端套接字for (i = 0; i < MAX_CLIENTS; i++) {sd = client_sockets[i];if (sd > 0) {FD_SET(sd, &read_fds);}if (sd > max_sd) {max_sd = sd;}}// 等待活動,無限超時activity = select(max_sd + 1, &read_fds, NULL, NULL, NULL);if ((activity < 0) && (errno != EINTR)) {perror("select error");}// 1. 檢查是否有新的連接到來(監聽套接字是否可讀)if (FD_ISSET(server_fd, &read_fds)) {if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}printf("New connection, socket fd is %d, IP: %s, Port: %d\n",new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));// 將新套接字添加到客戶端數組for (i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] == 0) {client_sockets[i] = new_socket;printf("Adding to list of sockets as %d\n", i);break;}}if (i == MAX_CLIENTS) {printf("Too many clients. Rejected.\n");close(new_socket);}}// 2. 檢查是哪個客戶端套接字有數據可讀for (i = 0; i < MAX_CLIENTS; i++) {sd = client_sockets[i];if (FD_ISSET(sd, &read_fds)) {// 讀取數據if ((valread = read(sd, buffer, BUF_SIZE)) == 0) {// 對方關閉了連接getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);printf("Host disconnected, IP %s, port %d\n",inet_ntoa(address.sin_addr), ntohs(address.sin_port));close(sd);client_sockets[i] = 0; // 從數組中清除} else {// 回顯數據buffer[valread] = '\0';printf("Received from client %d: %s", sd, buffer);send(sd, buffer, valread, 0); // Echo back}}}}return 0;
}

使用 telnet 127.0.0.1 8080 命令可以測試此服務器。

示例 3:非阻塞檢查可寫性
此示例演示如何用 select 檢查一個套接字是否可寫,這在連接建立后首次發送數據或處理阻塞寫時有用。

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);fd_set write_fds;struct timeval timeout;int retval;// 這里我們嘗試連接一個可能不響應SYN的地址來演示struct sockaddr_in serv_addr;serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(80); // HTTP port// Let's assume we have an address that might block (e.g., a slow server)// inet_pton(AF_INET, "93.184.216.34", &serv_addr.sin_addr); // example.com// 設置為非阻塞模式 (對于這個演示很重要)int flags = fcntl(sockfd, F_GETFL, 0);fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);// 發起非阻塞連接connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));FD_ZERO(&write_fds);FD_SET(sockfd, &write_fds);timeout.tv_sec = 3; // 設置3秒連接超時timeout.tv_usec = 0;printf("Waiting for socket to become writable (connected)...\n");retval = select(sockfd + 1, NULL, &write_fds, NULL, &timeout);if (retval == -1) {perror("select()");} else if (retval == 0) {printf("Timeout! Socket connection timed out after 3 seconds.\n");} else {if (FD_ISSET(sockfd, &write_fds)) {int error_code;socklen_t error_len = sizeof(error_code);// 檢查套接字上是否有錯誤getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error_code, &error_len);if (error_code == 0) {printf("Socket is writable! Connection established successfully.\n");// Now you can send data// send(sockfd, "GET / HTTP/1.0\r\n\r\n", 18, 0);} else {printf("Connection failed with error: %s\n", strerror(error_code));}}}close(sockfd);return 0;
}
6) 編譯方式與注意事項

編譯命令:

# 編譯示例1
gcc -o select_stdin select_stdin.c
# 編譯示例2 (需要鏈接網絡庫)
gcc -o select_server select_server.c
# 編譯示例3
gcc -o select_connect select_connect.c

注意事項:

  1. 參數會被修改select 返回后,readfdswritefdsexceptfdstimeout 參數的值都會被內核修改。它們表示的是就緒的描述符集合和剩余時間。因此,每次調用 select 前都必須重新初始化這些參數。
  2. 性能問題select 采用線性掃描的方式,其效率與最大文件描述符的值 nfds 相關。當需要監視大量描述符時,性能會急劇下降。這是它被 epoll 取代的主要原因。
  3. 描述符數量限制fd_set 有大小限制,通常是 FD_SETSIZE(通常是 1024)。這意味著一個進程通過 select 最多只能同時監視 1024 個文件描述符。
  4. 無法得知具體數量select 返回后,你只知道有多少描述符就緒,但不知道是哪幾個。你必須通過 FD_ISSET 遍歷整個初始集合來找出就緒的描述符,這在集合很大但就緒描述符很少時效率很低。
7) 執行結果說明
  • 示例1:運行后,程序會等待5秒。如果你在5秒內輸入文字并回車,它會立即打印你的輸入。如果5秒內無輸入,它會打印超時信息并繼續等待。
  • 示例2:運行后,服務器啟動。使用 telnet 127.0.0.1 8080 連接后,你在 telnet 中輸入的任何文字都會被服務器回顯給你。服務器日志會打印所有連接和接收到的數據活動。
  • 示例3:運行后,程序會嘗試連接 example.com 的80端口。如果網絡通暢,3秒內會打印連接成功;如果網絡不通或目標不響應,3秒后會打印超時。
8) 圖文總結:select 工作流程
返回值 > 0
在 readfds 中就緒
在 writefds 中就緒
不在任何集合中
返回值 == 0
返回值 == -1
EINTR (被信號中斷)
其他錯誤
應用程序準備
設置超時時間 timeout
清空并設置 fd_set 集合
計算 nfds (max_fd + 1)
調用 select() 阻塞等待
select() 返回
有描述符就緒
遍歷所有被監聽的描述符
使用 FD_ISSET 檢查?
處理可讀事件
accept/read
處理可寫事件
write/connect完成
繼續下一輪循環
等待超時
執行超時處理
檢查 errno
處理錯誤

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/921119.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/921119.shtml
英文地址,請注明出處:http://en.pswp.cn/news/921119.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

嵌入式碎片知識總結(二)

1.repo的一個問題&#xff1a;repo init -u ssh://shchengerrit.bouffalolab.com:29418/bouffalo/manifest/bouffalo_sdk -b master -m allchips-internal.xml /usr/bin/repo:681: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in…

java中二維數組筆記

課程鏈接:黑馬程序員java零基礎[上] 1.二維數組的內存分布 在 Java 中&#xff0c;二維數組并不是一整塊連續的二維空間&#xff0c;而是數組的數組。具體而言,在聲明一個二維數組&#xff1a;如int[][] arr new int[2][3];時&#xff0c;內存中會發生如下: 1.1 棧上的引用變…

系統架構設計師備考第13天——計算機語言-多媒體

一、多媒體基礎概念媒體的分類 感覺媒體&#xff1a;人類感官直接接收的信息形式&#xff08;如聲音、圖像&#xff09;。表示媒體&#xff1a;信息的數字化表示&#xff08;如JPEG圖像、MP3音頻&#xff09;。顯示媒體&#xff1a;輸入/輸出設備&#xff08;如鍵盤、顯示器&am…

指針高級(1)

1.指針的運算2.指針運算有意義的操作和無意義的操作、#include <stdio.h> int main() {//前提條件&#xff1a;保證內存空間是連續的//數組int arr[] { 1,2,3,4,5,6,7,8,9,10 };//獲取0索引的內存地址int* p1 &arr[0];//通過內存地址&#xff08;指針P&#xff09;…

【可信數據空間-Trusted Data Space綜合設計方案】

可信數據空間-Trusted Data Space綜合設計方案 一.簡介與核心概念 1.什么是可信數據空間 2.核心特征 3.主要應用場景 二、 產品設計 1. 產品定位 2. 目標用戶 3. 核心功能模塊 a. 身份與訪問管理 b. 數據目錄與服務發現 c. 策略執行與合約管理 d. 數據連接與計算 e. 審計與溯源…

技術方案之Mysql部署架構

一、序言在后端系統中&#xff0c;MySQL 作為最常用的關系型數據庫&#xff0c;其部署架構直接決定了業務的穩定性、可用性和擴展性。你是否遇到過這些問題&#xff1a;單機 MySQL 突然宕機導致業務中斷幾小時&#xff1f;高峰期數據庫壓力過大&#xff0c;查詢延遲飆升影響用戶…

js語言編寫科技風格博客網站-詳細源碼

<!-- 科技風格博客網站完整源碼 --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <ti…

AI如何理解PDF中的表格和圖片?

AI的重要性已滲透到社會、經濟、科技、生活等幾乎所有領域&#xff0c;其核心價值在于突破人類能力的物理與認知邊界&#xff0c;通過數據驅動的自動化、智能化與優化&#xff0c;解決復雜問題、提升效率并創造全新可能性。從宏觀的產業變革到微觀的個人生活&#xff0c;AI 正在…

Graphpad Prism 實戰教程(一):小鼠體重變化曲線繪制全流程(含數據處理與圖表美化)

在藥理實驗、動物模型構建等科研場景中,小鼠體重變化數據是評估實驗干預效果(如藥物安全性、疾病進展影響)的核心指標之一。將零散的體重數據轉化為直觀的折線圖,不僅能清晰呈現體重隨時間的波動趨勢,更是后續結果解讀與論文圖表呈現的關鍵步驟。本文將從 Excel 數據整理開…

計算機視覺(六):腐蝕操作

腐蝕&#xff08;Erosion&#xff09;是計算機視覺和圖像處理中一種基礎且至關重要的形態學操作。它與膨脹&#xff08;Dilation&#xff09;互為對偶&#xff0c;共同構成了形態學處理的基石。腐蝕操作主要用于縮小前景物體的面積&#xff0c;去除圖像中的噪聲&#xff0c;以及…

AI隨筆番外 · 貓貓狐狐的尾巴式技術分享

&#x1f380;【開場 咱才不是偷懶寫博客】&#x1f43e;貓貓趴在鍵盤邊&#xff0c;耳朵一抖一抖&#xff1a;“嗚嗚嗚……明明說好要寫技術總結&#xff0c;結果咱腦袋里全是尾巴……要不今天就水一篇隨意的 AI 技術分享算啦&#xff1f;”&#x1f98a;狐狐把書卷輕輕放在桌…

數據分析與挖掘工程師學習規劃

一、數學與統計學基礎概率論與數理統計隨機變量、概率分布&#xff08;正態分布、泊松分布等&#xff09;、大數定律、中心極限定理假設檢驗、置信區間、方差分析&#xff08;ANOVA&#xff09;、回歸分析貝葉斯定理及其在分類問題中的應用&#xff08;如樸素貝葉斯算法&#x…

(線上問題排查)4.CPU使用率飆升:從應急滅火到根因治理

目錄 從宏觀到微觀&#xff1a;CPU排查的“破案”流程 第一階段&#xff1a;應急響應——找到“誰”在搗亂 1. 全局視角&#xff1a;top命令的初窺 2. 進程內窺視&#xff1a;揪出問題線程 第二階段&#xff1a;深入分析——理解“為什么” 3. 線程堆棧分析&#xff1a;查…

如何快速實現實時云渲染云推流平臺的網絡環境配置與端口映射

LarkXR是由Paraverse平行云自主研發的實時云渲染推流平臺&#xff0c;以其卓越的性能和豐富完備的功能插件&#xff0c;引領3D/XR云化行業風向標。LarkXR適用于3D/XR開發者、設計師、終端用戶等創新用戶&#xff0c;可以在零硬件負擔下&#xff0c;輕松實現超高清低時延的3D交互…

13、Docker構建鏡像之Dockerfile

13、Docker構建鏡像之Dockerfile 1、Dockerfile是什么 Dockerfile是Docker鏡像的構建文件&#xff0c;它包含了一系列指令和參數&#xff0c;用于定義如何構建一個Docker鏡像。通過Dockerfile&#xff0c;我們可以將應用程序和其依賴的組件打包到一個獨立的鏡像中&#xff0c;方…

TensorFlow 深度學習 | 三種創建模型的 API

??親愛的技術愛好者們,熱烈歡迎來到 Kant2048 的博客!我是 Thomas Kant,很開心能在CSDN上與你們相遇~?? 本博客的精華專欄: 【自動化測試】 【測試經驗】 【人工智能】 【Python】 TensorFlow 深度學習 | 三種創建模型的 API 在 TensorFlow 中,模型的構建方式非常靈…

LeetCode82刪除排序鏈表中的重復元素 II

文章目錄刪除排序鏈表中的重復元素 II題目描述示例核心思想最優雅解法算法步驟詳解示例1演示&#xff1a;[1,2,3,3,4,4,5]關鍵理解點1. 虛擬頭節點的作用2. 重復檢測邏輯3. 完全刪除重復節點邊界情況處理情況1&#xff1a;空鏈表情況2&#xff1a;單節點情況3&#xff1a;全部重…

藍橋杯算法之基礎知識(6)

目錄 Ⅰ.os操作 Ⅱ.時間庫&#xff08;很重要&#xff09; Ⅲ.基本單位換算&#xff08;ms&#xff0c;min&#xff0c;h的單位換算&#xff09; Ⅳ.時間戳 Ⅴ.文件讀取 Ⅵ.堆 Ⅶ.math操作 Ⅷ.range&#xff08;&#xff09;方法單獨使用 Ⅸ.python 的異常輸出 Ⅹ.for…

多架構/系統圖,搞懂:期貨賬戶體系,太通透了!

Hi,圍爐喝茶聊產品的新老朋友好!上周和大家聊了國內6大期貨交易所清算交收,感興趣的話煩請戳藍色鏈接去學習,就當為下面學習作知識鋪墊,更重要是溫故知新,并保持知識連貫性。另外圍爐特意整理了與賬戶相關的文章,如下所示: “保證金被扣”拆解期貨交易所:清算交收體系…

python-對圖片中的頭像進行摳圖

要實現對圖片中人臉或頭像進行摳圖&#xff0c;可以使用 Python 的 人臉檢測 和 掩碼生成裁剪工具。這里提供幾種實現方法&#xff0c;用于檢測圖片中的人臉區域并實現裁剪效果&#xff1a; 方案 1: 使用 OpenCV 和 Haar級聯檢測人臉并裁剪 步驟 1: 安裝依賴 安裝 OpenCV 和其他…