I/O 多路復用select,poll

目錄

I/O多路復用的介紹

多進程/多線程模型的弊端

網絡多路復用如何解決問題?

網絡多路復用的常見實現方式

常見的開源網絡庫

select詳細介紹

select函數介紹

套接字可讀事件,可寫事件,異常事件

fd_set類型介紹

select的兩次拷貝,兩次遍歷

select使用示例介紹

select服務器示例代碼

poll函數詳細介紹

poll函數介紹

pollfd類型介紹

poll的工作原理

poll的優缺點


I/O多路復用的介紹

多進程/多線程模型的弊端

在上一篇文章中我們詳細介紹了Linux中的網絡編程,使用相關API實現了多進程/多線程模型,即:

Linux網絡編程-CSDN博客

之前的客戶端—服務器端連接處理思路:每當有一個新的客戶端連接,服務器就創建一個新的進程或線程來處理它。我們之前的示例中在新創建的進程中還會使用fork來進行進一步創建一個進程,用來實現讀寫分離。

這樣就相當于每一個客戶端連接服務器,就需要多創建兩個進程來實現客戶端與服務器端的通信

弊端

  • 資源消耗大:每個進程或線程都需要獨立的內存空間(棧、堆等),并維護自己的上下文信息。大量的進程/線程會迅速耗盡系統內存。

  • 上下文切換開銷:操作系統在這么多進程/線程之間切換 CPU 時,會產生大量的上下文切換開銷,這會嚴重降低 CPU 的有效工作時間。

  • 文件描述符限制:每個進程/線程都會占用一個文件描述符。系統對單個進程或整個系統的文件描述符數量有上限,容易達到瓶頸。

網絡多路復用如何解決問題?

網絡多路復用允許單個進程/線程同時監控多個文件描述符(包括套接字)。當任何一個文件描述符準備好進行 I/O 操作(例如,有數據可讀或可以寫入數據)時,多路復用機制會通知應用程序。

這樣,你的服務器就不需要為每個客戶端都創建一個獨立的進程或線程。一個工作進程/線程就能高效地管理數百甚至上萬個并發連接。

網絡多路復用的常見實現方式

網絡多路復用的常見實現方式主要有三種:select、poll 和 epoll (Linux 特有)。它們都允許一個進程或線程同時監控多個文件描述符(包括網絡套接字),但具體機制和性能特點有所不同。

常見的開源網絡庫

在實際開發中,我們經常會選擇使用成熟的開源網絡庫或框架來構建高性能的并發服務器,而并不是會選擇使用select,poll,epoll這些來進行構建。

在 C++ 中,有很多優秀的開源網絡庫可以幫助你高效地開發網絡應用程序。這些庫封裝了底層操作系統的網絡 API(如 Linux 上的 epoll,macOS 上的 kqueue,Windows 上的 IOCP),提供了更高級、更易用的接口,并且通常具備高性能、跨平臺和豐富的功能。

下面介紹幾個 C++ 中常用的開源網絡庫:

Boost.Asio

Boost.Asio 是一個功能強大、設計精良的 C++ 異步 I/O 庫,是現代 C++ 網絡編程的首選。

它提供了一套統一的接口來處理各種異步 I/O 操作(包括網絡套接字、定時器、串口等)。其核心是 io_context (或 io_service),一個事件循環,用于分發 I/O 事件。

主要特點:

  • C++ 風格:與 C++ 標準庫和現代 C++ 特性(如模板、協程)高度融合,代碼更符合 C++ 習慣。

  • 功能全面:不僅處理網絡通信,還支持定時器、信號等多種 I/O。

  • 跨平臺:底層自動適配不同操作系統的高性能 I/O 多路復用機制(如 Linux 的 epoll)。

  • 靈活性高:支持同步和異步編程模型,以及多種并發模式。

?libevent 和?libev

libevent 和 libev 是輕量級、事件驅動的 C 語言網絡庫,專注于高性能的事件通知。 (libev 是 libevent 的一個更小、更快的替代品,設計理念類似)。

它們的核心是事件循環 (event loop),通過注冊回調函數來處理文件描述符上的 I/O 事件、定時器事件和信號事件。

  • 事件驅動:基于事件循環,當 I/O 事件發生時,通過回調函數通知應用程序,避免了阻塞。

  • 輕量和高效:庫本身的代碼量較小,運行效率高,資源占用低。

  • 跨平臺:支持 epollkqueueIOCPpollselect 等多種 I/O 復用機制。

  • 多種事件支持:不僅支持網絡 I/O 事件,還支持定時器、信號、文件 I/O 等事件。

適用場景: 適用于需要極致性能、資源受限或嵌入式環境下的網絡應用開發,如高性能代理服務器、聊天服務器、游戲服務器等。它們是構建自己的高性能網絡框架的理想基石。

使用開源網絡庫的好處?

  • 簡化開發:提供了抽象層,你不需要直接操作 epoll_create、epoll_ctl、epoll_wait 等底層函數。
  • 提高效率:這些庫通常由經驗豐富的開發者優化過,性能經過嚴格測試,并解決了許多難以發現的 bug 和邊界條件。
  • 跨平臺支持:許多流行的庫支持跨平臺,底層會自動根據操作系統選擇合適的 I/O 多路復用機制(epoll、kqueue、IOCP 等)。
  • 豐富的功能:除了基本的 I/O 封裝,它們往往還集成了定時器、線程池、內存管理、日志、協議編解碼等常用功能。

select詳細介紹

select函數介紹

select通過輪詢的方式檢查一組文件描述符的狀態,判斷它們是否準備好進行 I/O 操作。

函數原型

#include <sys/select.h>
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds,fd_set *restrict exceptfds,   struct timeval *restrict timeout
);

參數介紹

nfds:所有文件描述符集合中最大的文件描述符值加 1。select 內部會從 0 到 nfds-1 遍歷這些文件描述符。

readfds:指向一個 fd_set 結構體的指針,用于監聽可讀事件的文件描述符集合。如果不需要監聽可讀事件,可以設置為 NULL。

writefds:指向一個 fd_set 結構體的指針,用于監聽可寫事件的文件描述符集合。如果不需要監聽可寫事件,可以設置為 NULL。

exceptfds:指向一個 fd_set 結構體的指針,用于監聽異常事件的文件描述符集合。如果不需要監聽異常事件,可以設置為 NULL。

timeout:指向一個 struct timeval 結構體的指針,用于設置 select 的超時時間。

  • 如果為 NULL,select 將一直阻塞直到有文件描述符就緒。
  • 如果指向一個 struct timeval 結構體,且其成員 tv_sec 和 tv_usec 都為 0,select 將立即返回,不阻塞(非阻塞輪詢)。
  • 如果指向一個 struct timeval 結構體,且其成員 tv_sec 或 tv_usec 大于 0,select 將阻塞直到超時時間到達或有文件描述符就緒。

返回值

  • 成功時,返回就緒的文件描述符的數量。
  • 超時時,返回 0。
  • 失敗時,返回 -1,并設置 errno。

select返回值表示設置了多少位,這個位數是可讀事件+可寫事件+異常事件的總和

select 的返回值是 所有就緒的文件描述符的總數,無論是可讀、可寫還是異常事件就緒。它不會區分這些事件的類型,只是告訴你“有這么多 FD 就緒了”。

例如,如果 sock1 可讀,sock2 可寫,sock3 有異常,那么 select 將返回 3。

套接字可讀事件,可寫事件,異常事件

上面我們說到了select可以用來監控socket套接字集合的可讀事件,可寫事件,異常事件。

那么這三種事件究竟是什么呢?套接字在什么情況下會產生這些事件呢?

我們必須要搞清楚這三種事件,理解 select 何時會認為一個socket套接字的文件描述符是“可讀”或“可寫”是正確使用它的必要前提

select中的可讀事件(檢測可讀事件最常見)

  • 監聽套接字 (Listening Socket): 如果監聽套接字上發生了新的連接請求(即有客戶端嘗試連接服務器),它就會變得可讀。此時,你可以調用 accept() 來接受新的連接。

  • 連接套接字 (Connected Socket):

    • 接收緩沖區中有數據可讀。此時,你可以調用 read()recv() 來讀取數據,并且這些操作通常會立即返回而不會阻塞(除非緩沖區的數據量小于你請求讀取的量,在阻塞模式下仍可能阻塞,但通常會與非阻塞模式結合使用)。

    • 連接被對端關閉(發送了 FIN 包)。此時,read() 會返回 0,表示連接已正常關閉。

    • 連接發生錯誤,導致數據不再可讀。


?

select中的可寫事件

  • 發送緩沖區有空間: 套接字的發送緩沖區(send buffer)有足夠的空間可以容納你要發送的數據。此時,你可以調用 write()send() 來寫入數據,并且這些操作通常會立即返回而不會阻塞。

  • connect() 完成: 對于非阻塞的 connect() 調用,當連接建立成功或失敗時,套接字會變得可寫(或在 exceptfds 中報告錯誤)。你需要通過 getsockopt() 結合 SO_ERROR 選項來獲取連接的結果。


?

select 中的異常事件

exceptfds 用于監聽“異常事件”。在實際的網絡編程中,最常見(幾乎是唯一)的異常事件是:

TCP 帶外數據 (Out-of-Band Data, OOB):

當 TCP 套接字接收到帶外數據時,它會觸發一個異常事件。帶外數據是一種特殊的、優先級更高的數據流,它可以繞過正常的 TCP 緩沖區,用于傳輸緊急信息(例如,發送“緊急”信號來中斷遠程操作)。

接收帶外數據需要使用 recv() 并指定 MSG_OOB 標志。

fd_set類型介紹

fd_set 是一個位圖(bitmap),用來表示一組文件描述符(file descriptor,簡稱 FD)。每個位(bit)對應一個文件描述符,如果對應的位被設置(為 1),就表示這個文件描述符在這個集合中。


fd_set 集合的操作宏

為了方便用戶程序操作 fd_set 集合,標準庫提供了一組宏。這些宏實際上是對底層位圖操作的封裝:

  • FD_ZERO(fd_set *set):將 fd_set 集合中所有的位清零,即清空集合。
  • FD_SET(int fd, fd_set *set):將文件描述符 fd 加入到 fd_set 集合中。
  • FD_CLR(int fd, fd_set *set):將文件描述符 fd 從 fd_set 集合中移除。
  • FD_ISSET(int fd, fd_set *set):檢查文件描述符 fd 是否在 fd_set 集合中(即是否就緒)。

fd_set的限制

fd_set 最主要的限制是它能夠容納的文件描述符數量。這個上限由系統宏 FD_SETSIZE 定義,在大多數 Linux 系統上,其默認值通常是 1024。這意味著一個 fd_set 實例最多只能同時監聽 1024 個文件描述符。

這個限制對于處理高并發連接的服務器來說是一個嚴重的瓶頸。當需要處理超過 1024 個客戶端連接時,select 就不再適用,需要考慮使用 pollepoll 等其他 I/O 多路復用機制。

fd_set 的內存與性能開銷

  • 內存開銷: fd_set 的大小是固定的,通常是 FD_SETSIZE / 8 字節。例如,如果 FD_SETSIZE 是 1024,那么 fd_set 大約占用 128 字節 (1024 / 8 = 128)。這部分內存開銷通常不大。

  • 性能開銷(與 select 相關):

    • 用戶空間到內核空間的拷貝: 每次調用 select,都需要將完整的 fd_set 集合從用戶空間復制到內核空間。當 FD_SETSIZE 較大時,即使實際活躍的 FD 很少,也需要復制整個 fd_set,這會帶來不必要的開銷。

    • 內核遍歷: 內核需要遍歷 fd_set 中所有的 FD_SETSIZE 個位,以檢查哪些 FD 已經就緒。這個過程是 O(N) 的,其中 N 是 FD_SETSIZE 的值(或 nfds 的值)。

    • 內核空間到用戶空間的拷貝select 返回時,內核需要將包含就緒文件描述符的 fd_set 集合(經過內核修改后的)從內核空間復制回用戶空間。這同樣是一次完整的 fd_set 結構體的拷貝,帶來了額外的開銷。

    • 用戶空間遍歷: select 返回后,用戶程序也需要遍歷整個 fd_set 來找出是哪個 FD 就緒,這也是一個 O(N) 的操作。

select的兩次拷貝,兩次遍歷

從用戶程序調用一次 select 系統調用,通常涉及到兩次數據拷貝和兩次遍歷操作。我們來詳細分解一下:

1. 第一次拷貝:用戶空間到內核空間

當你調用 select(nfds, &readfds, &writefds, &exceptfds, &timeout) 時:

  • 拷貝內容: readfdswritefdsexceptfds 這三個 fd_set 結構體(以及 timeout 結構體)的完整內容會從用戶空間復制到內核空間,注意這里是整個位圖都會被拷貝過去,并不是根據nfds來選擇部分進行拷貝

  • 原因: 內核需要知道你對哪些文件描述符的哪些事件感興趣,以便進行監控。

2. 第一次遍歷:內核空間遍歷

在內核空間:

  • 遍歷過程: 內核會從 0nfds-1 遍歷每一個文件描述符。對于每個文件描述符,它會檢查其是否在你傳入的 readfdswritefdsexceptfds 的副本中被設置了位。

  • 檢查狀態: 如果被設置了位,內核就會去檢查這個文件描述符的實際狀態(例如,網絡緩沖區是否有數據,或者發送緩沖區是否有空間)。

  • 結果記錄: 如果文件描述符就緒,內核會在其內部的一個臨時就緒 fd_set 集合中標記對應的位。

3. 第二次拷貝:內核空間到用戶空間

select 返回時(有就緒 FD、超時或出錯):

  • 拷貝內容: 內核會將其內部維護的、只包含就緒文件描述符的臨時就緒 fd_set 集合復制回用戶空間,覆蓋掉你傳入的 readfdswritefdsexceptfds

  • 原因: 這是 select 返回就緒信息給用戶程序的方式。

4. 第二次遍歷:用戶空間遍歷

select 返回后,在用戶空間:

  • 遍歷過程: 用戶程序需要再次從 0nfds-1 (或你實際感興趣的 FD 范圍) 遍歷 readfdswritefdsexceptfds 這三個被修改過的 fd_set 集合。

  • 檢查狀態: 使用 FD_ISSET(fd, &set) 宏來逐個檢查是哪些文件描述符就緒了。

  • 執行操作: 根據 FD_ISSET 的結果,對就緒的文件描述符執行相應的 I/O 操作(read(), write(), accept() 等)。

????????大家應該會好奇一個問題,為什么不能讓程序將用戶空間中的套接字位圖fd_set的地址傳遞給內核空間呢?這樣不是可以避免拷貝嗎?為什么要拷貝一份數據過去,內核設置好了之后再將設置好的數據拷貝回用戶空間?

主要原因有如下兩點:
?

1.內存保護角度:隔離用戶空間和內核空間: 這是更重要的隔離。內核擁有最高的權限,負責管理所有硬件資源和系統核心功能。如果用戶程序能直接通過一個指針訪問內核內存,或者內核能隨意訪問用戶內存,那么:

  • 安全性風險: 惡意用戶程序可以修改內核數據結構,從而獲得特權,甚至破壞整個系統。
  • 穩定性風險: 用戶程序的錯誤(比如空指針解引用、越界訪問)可能會直接導致內核崩潰,從而引發整個系統宕機。
  • 一致性問題: 如果內核直接操作用戶數據,而用戶程序同時也在修改這些數據,會帶來復雜的數據同步和一致性問題。

2.虛擬內存差異角度

現代操作系統都采用虛擬內存技術。

  • 虛擬地址 vs. 物理地址: 用戶程序中使用的地址都是虛擬地址。這些虛擬地址需要通過內存管理單元(MMU)映射到實際的物理地址。每個進程都有自己的頁表,負責將本進程的虛擬地址映射到物理地址。

  • 不同的地址空間: 內核運行在它自己的虛擬地址空間中,用戶進程運行在它們各自的虛擬地址空間中。即使一個用戶進程傳遞給內核一個它自己虛擬地址空間中的指針,對于內核來說,這個指針指向的虛擬地址是無效的,因為它不屬于內核自己的地址空間。內核需要一套機制來“翻譯”或“安全地訪問”這些用戶空間的地址。

內核訪問用戶空間數據的正確方式

既然不能直接操作,那內核如何安全地訪問用戶空間數據呢?答案是通過特定的安全機制和系統調用

  • 拷貝(Copy_From_User / Copy_To_User): 這是最常見且最安全的方式。當用戶程序調用 selectpoll 這樣的系統調用并傳遞數據(如 fd_setpollfd 數組)時,內核會使用專門的函數(例如 Linux 內核中的 copy_from_user()copy_to_user())來:

    1. 驗證地址: 首先,內核會驗證用戶提供的地址是否合法,是否在用戶進程的有效虛擬地址范圍內,以及是否有足夠的權限訪問。

    2. 安全拷貝: 驗證通過后,內核會將用戶空間的數據完整地拷貝到內核空間的一塊臨時緩沖區中進行操作。操作完成后,再將結果拷貝回用戶空間。 這種拷貝雖然有性能開銷,但它確保了內核不會因為用戶空間的錯誤而崩潰,也避免了用戶程序的惡意篡改。

  • 內存映射(Memory Mapping): 對于一些需要高性能、大量數據傳輸的場景(例如文件I/O、共享內存),操作系統提供了內存映射機制(如 mmap())。這允許用戶空間和內核空間(或多個用戶進程)共享同一塊物理內存區域。但即使是 mmap,也需要通過系統調用來建立映射關系,并且內核會設置適當的權限和保護,確保安全。這種方式并非直接的指針傳遞,而是建立了一種受控的共享訪問機制。

select使用示例介紹

舉個例子:假設你正在編寫一個服務器程序,需要同時監聽客戶端連接請求(通過監聽套接字 listen_sock)以及已經建立的客戶端連接上的數據(通過連接套接字 client_sock1, client_sock2 等)。

這個示例將創建一個簡單的服務器,為了實現簡單,這個示例中select只檢測了socket套接字的可讀事件集合 (read_fds)

在這種情況下,你需要:

  1. 創建一個 fd_set? read_fds;
    ?
  2. 在每次循環開始時,調用 FD_ZERO(&read_fds);
    ?
  3. 將 listen_sock 和所有活動的 client_sock 使用 FD_SET 添加到 read_fds 中。
    ?
  4. 調用 select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
    ?
  5. select 返回后,首先檢查 FD_ISSET(listen_sock, &read_fds)。如果是,說明有新的連接請求,可以調用 accept()。
    ?
  6. 然后遍歷所有 client_sock,檢查 FD_ISSET(client_sock_i, &read_fds)。如果是,說明這個客戶端有數據可讀,可以調用 read()。
    ?
  7. 如果某個客戶端連接關閉了,就使用 FD_CLR 將其從 fd_set 中移除。

fd_set 是 select I/O 多路復用機制的核心數據結構,它以位圖的形式高效地管理文件描述符集合。盡管它使用簡單且具有良好的跨平臺性,但其固定的 FD_SETSIZE 限制和線性掃描的效率問題,使其在高并發場景下表現不佳。理解 fd_set 的工作原理對于掌握 select 的使用至關重要

select服務器示例代碼

#include <stdio.h>      // For printf, perror
#include <stdlib.h>     // For exit, EXIT_FAILURE
#include <string.h>     // For memset, strlen
#include <unistd.h>     // For close, read, write
#include <arpa/inet.h>  // For sockaddr_in, inet_ntop
#include <sys/socket.h> // For socket, bind, listen, accept
#include <sys/select.h> // For select, FD_ZERO, FD_SET, FD_CLR, FD_ISSET
#include <errno.h>      // For errno, EWOULDBLOCK#define PORT 8080        // 服務器監聽端口
#define MAX_CLIENTS 5    // 最大支持的客戶端連接數
#define BUFFER_SIZE 1024 // 數據緩沖區大小int main() {int listen_fd; // 監聽套接字文件描述符int client_fds[MAX_CLIENTS]; // 存儲已連接客戶端的套接字文件描述符int max_fd;      // select 監聽的最大文件描述符 + 1int i;           // 循環變量fd_set read_fds; // select 用來監聽可讀事件的文件描述符集合// 初始化客戶端文件描述符數組,設為 -1 表示空閑for (i = 0; i < MAX_CLIENTS; i++) {client_fds[i] = -1;}// --- 1. 創建監聽套接字 ---// AF_INET: IPv4協議族// SOCK_STREAM: TCP流式套接字// 0: 默認協議 (TCP)if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {perror("socket error");exit(EXIT_FAILURE);}printf("Listening socket created: %d\n", listen_fd);// 設置套接字選項:允許地址重用,防止 TIME_WAIT 狀態導致端口不能立即重用int opt = 1;if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {perror("setsockopt error");close(listen_fd);exit(EXIT_FAILURE);}// --- 2. 綁定地址和端口 ---struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr)); // 清零server_addr.sin_family = AF_INET;             // IPv4server_addr.sin_addr.s_addr = INADDR_ANY;     // 監聽所有可用網絡接口server_addr.sin_port = htons(PORT);           // 端口號,htons 將主機字節序轉為網絡字節序if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind error");close(listen_fd);exit(EXIT_FAILURE);}printf("Socket bound to port %d\n", PORT);// --- 3. 開啟監聽 ---// 10: 允許的最大等待連接隊列長度if (listen(listen_fd, 10) == -1) {perror("listen error");close(listen_fd);exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);// --- 4. select 循環處理事件 ---while (1) {FD_ZERO(&read_fds);       // 每次循環前清空文件描述符集合FD_SET(listen_fd, &read_fds); // 將監聽套接字加入可讀集合 (因為它可能接收新連接)// 確定當前需要監聽的最大文件描述符 + 1max_fd = listen_fd;for (i = 0; i < MAX_CLIENTS; i++) {if (client_fds[i] != -1) {FD_SET(client_fds[i], &read_fds); // 將每個活躍的客戶端套接字加入可讀集合if (client_fds[i] > max_fd) {max_fd = client_fds[i];}}}// 調用 select 進行 I/O 多路復用,阻塞等待事件// 第一個參數是所有要監聽的 FD 中的最大值加 1// 后三個參數分別代表監聽可讀、可寫、異常事件的 FD 集合// 最后一個參數是超時時間,NULL 表示永遠阻塞直到有事件發生printf("\nWaiting for events (max_fd = %d)...\n", max_fd);int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL);if ((activity < 0) && (errno != EINTR)) { // 檢查 select 返回值perror("select error");break; // 出現錯誤則退出循環}// --- 5. 處理就緒事件 ---// (1) 檢查監聽套接字是否可讀:表示有新的客戶端連接請求if (FD_ISSET(listen_fd, &read_fds)) {struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);// 接受新連接int new_socket = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (new_socket == -1) {perror("accept error");continue; // 繼續下一輪循環}char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);printf("New connection accepted. Socket FD: %d, IP: %s, Port: %d\n",new_socket, client_ip, ntohs(client_addr.sin_port));// 將新連接的套接字加入到 client_fds 數組中int found_slot = 0;for (i = 0; i < MAX_CLIENTS; i++) {if (client_fds[i] == -1) { // 找到一個空閑位置client_fds[i] = new_socket;found_slot = 1;printf("Adding client socket %d to array slot %d\n", new_socket, i);break;}}if (!found_slot) {printf("Max clients reached. Rejecting new connection %d\n", new_socket);close(new_socket); // 如果沒有空閑位置,關閉新連接}activity--; // 減少一個已處理的活動事件}// (2) 檢查已連接客戶端套接字是否可讀:表示有數據到來或連接關閉for (i = 0; i < MAX_CLIENTS; i++) {int client_fd = client_fds[i];if (client_fd != -1 && FD_ISSET(client_fd, &read_fds)) {char buffer[BUFFER_SIZE];memset(buffer, 0, BUFFER_SIZE); // 清空緩沖區// 從客戶端讀取數據ssize_t bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);if (bytes_read == 0) {// 對端關閉了連接printf("Client %d disconnected.\n", client_fd);close(client_fd);        // 關閉套接字client_fds[i] = -1;      // 將數組中的位置標記為空閑} else if (bytes_read == -1) {// 讀取錯誤perror("read error");close(client_fd);client_fds[i] = -1;} else {// 成功讀取到數據buffer[bytes_read] = '\0'; // 確保字符串以 null 結尾printf("Received from client %d: %s\n", client_fd, buffer);// 可選:將收到的數據回顯給客戶端if (write(client_fd, buffer, bytes_read) == -1) {perror("write error");}}activity--; // 減少一個已處理的活動事件}// 如果所有活動事件都已處理,可以提前退出循環if (activity == 0) {break;}}}// --- 6. 清理資源 (通常不會到達這里,除非發生嚴重錯誤) ---close(listen_fd);for (i = 0; i < MAX_CLIENTS; i++) {if (client_fds[i] != -1) {close(client_fds[i]);}}return 0;
}

poll函數詳細介紹

poll函數介紹

函數原型

#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);

參數介紹

fds:這是一個指向 struct pollfd 結構體數組的指針。每個 struct pollfd 結構體都代表一個我們希望監視的文件描述符及其感興趣的事件。

nfds:這是 fds 數組中元素的個數,即我們要監視的文件描述符的總數。

timeout:這是一個整數,指定 poll 函數的等待時間(毫秒)。

  • 大于0的整數: poll 將等待指定毫秒數,如果在此期間沒有事件發生,poll 將返回0。
  • 0: poll 不會等待,立即返回。它會檢查當前文件描述符的狀態,并返回已經準備好的文件描述符的數量。
  • -1: poll 將無限期等待,直到有事件發生或被信號中斷。

返回值

poll 函數的返回值表示就緒的文件描述符的數量,即 revents 字段非零的 struct pollfd 結構體的數量。

  • 大于0: 表示有指定數量的文件描述符就緒。

  • 0: 表示在 timeout 期間沒有文件描述符就緒。

  • -1: 表示 poll 函數調用失敗,此時可以通過 errno 變量獲取具體的錯誤信息。

pollfd類型介紹

struct pollfd {int fd;         /* 文件描述符 */short events;   /* 監視的事件 */short revents;  /* 實際發生的事件 */
};
  • fd:要監視的文件描述符。

  • events:這是一個位掩碼,表示我們感興趣的事件。可以是一個或多個事件的按位或組合。常用的事件標志包括:

    • POLLIN:文件描述符上有數據可讀

    • POLLOUT:文件描述符上可以寫入數據

    • POLLERR:文件描述符上發生錯誤

    • POLLHUP:對端掛斷連接(通常是EOF)。

    • POLLNVAL:無效的文件描述符請求。

  • revents:這是一個位掩碼,由 poll 函數返回,表示在文件描述符上實際發生的事件。它的取值與 events 類似,可以包含上述事件標志。

poll的工作原理

當調用 poll 函數時,內核會遍歷 fds 數組中的每個 struct pollfd 結構體,檢查其對應的文件描述符上是否發生了 events 中指定的事件

  • 如果事件發生,內核會在該 struct pollfd 的 revents 字段中設置相應的位,并將其標記為就緒。
  • 如果沒有事件發生,并且 timeout 尚未到期,poll 函數會進入睡眠狀態,直到事件發生或 timeout 到期。
  • 當 poll 返回時,程序可以遍歷 fds 數組,檢查每個 struct pollfd 的 revents 字段,以確定哪些文件描述符已經就緒,然后對這些文件描述符進行相應的I/O操作。

poll的優缺點

poll 的優點

  • 沒有文件描述符數量限制: 解決了 selectFD_SETSIZE 限制問題,可以監視任意數量的文件描述符,只受限于系統內存。

  • 更清晰的事件表示: struct pollfd 結構體使得事件的設置和檢查更加直觀。

  • 更好的性能: 尤其在文件描述符數量較多時,poll 的性能優于 select

  • 可重用性: fds 數組可以在多次 poll 調用中重用,而 selectfd_set 每次調用后都需要重新初始化。

  • 只拷貝實際監視的 nfdsstruct pollfd 結構體,數據更緊湊。

poll 的缺點

  • 仍然需要遍歷: 盡管 poll 沒有文件描述符數量限制,但在 poll 返回后,仍然需要遍歷整個 fds 數組來查找哪些文件描述符就緒,當文件描述符數量非常龐大時,這會成為一個性能瓶頸。

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

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

相關文章

最終分配算法【論文材料】

文章目錄一、最終分配算法1.1 平衡的情況1.2 不平衡的情況1.3 TDM 約束一、最終分配算法 上一步合法化后&#xff0c;group 的 TDM 情況大致分為兩類&#xff0c;一類是平衡的&#xff0c;最大的一些 group 的 TDM 比較接近。另外一種情況就是不平衡的&#xff0c;最大的 group…

《大數據技術原理與應用》實驗報告七 熟悉 Spark 初級編程實踐

目 錄 一、實驗目的 二、實驗環境 三、實驗內容與完成情況 3.1 Spark讀取文件系統的數據。 3.2 編寫獨立應用程序實現數據去重。 3.3 編寫獨立應用程序實現求平局值問題。 四、問題和解決方法 五、心得體會 一、實驗目的 1. 掌握使用 Spark 訪問本地文件和 HDFS 文件的…

機器學習漫畫小抄 - 彩圖版

斯坦福機器學習漫畫小抄&#xff0c;中文版來啦&#xff01; 下載地址&#xff1a; 通過網盤分享的文件&#xff1a;機器學習知識點彩圖版.pdf 鏈接: https://pan.baidu.com/s/1-fH9OpC_u_OrTqWy6gVUCA 提取碼: 246r

1.初始化

業務模塊核心技術棧業務&#xff08;亮點&#xff09;解決方案課程安排01 認識Vue3為什么需要學Vue3?Vue3組合式API體驗Vue3更多的優勢2 使用create-vue搭建Vue3項目認識 create-vue使用create-vue創建項目3 熟悉項目目錄和關鍵文件項目目錄和關鍵文件4 組合式API - setup選項…

Milvus分布式數據庫工作職責

主導騰訊云Milvus服務化項目&#xff0c;設計多租戶隔離方案&#xff0c;支撐日均10億向量請求&#xff0c;延遲降低40%。優化IVF_PQ索引構建流程&#xff0c;通過量化編碼壓縮使內存占用減少60%&#xff0c;QPS提升35%。開發基于Kubernetes的Milvus Operator&#xff0c;實現自…

FMEA-CP-PFD三位一體數字化閉環:汽車部件質量管控的速效引擎

FMEA-CP-PFD三位一體數字化閉環&#xff1a;汽車部件質量管控的速效引擎 全星FMEA軟件系統通過??FMEA&#xff08;失效模式分析&#xff09;、CP&#xff08;控制計劃&#xff09;、PFD&#xff08;過程流程圖&#xff09;三大工具的一體化協同管理??&#xff0c;為汽車部件…

VUE2 學習筆記1

目錄 VUE特點 文檔tips 開發者工具 從一個Hello world開始 hello world Demo 容器和實例的對應關系 差值語法{{}} VUE特點 構建用戶界面&#xff1a;可以用來把數據構建成用戶界面。 漸進式&#xff1a;自底向上&#xff0c;可以先從一個非常輕量級的框架開始&#xf…

嵌入式學習系統編程(四)進程

目錄 一、進程 1.程序和進程 2.進程的八種狀態 3. 幾個狀態 4.關于進程常用命令 二、關于進程的函數 1.fork 2.面問 3.孤兒進程 后臺進程 2. exec函數族 (只保留父子關系&#xff0c;做新的事情) strtok函數 三、進程的結束 1.分類 exit和_exit的區別 wait函數…

Linux中添加重定向(Redirection)功能到minishell

前言&#xff1a;在談論添加minishell之前&#xff0c;我再重談一下重定向的具體實現等大概思想&#xff01;&#xff01;&#xff01;方便自己回顧&#xff01;&#xff01;&#xff01; 目錄 一、重定向&#xff08;Redirection&#xff09;原理詳解 1、文件描述符基礎 2、…

Django由于數據庫版本原因導致數據庫遷移失敗解決辦法

在django開發中&#xff0c;一般我們初始化一個項目之后&#xff0c;創建應用一般就會生成如下的目錄&#xff1a;django-admin startproject myproject python manage.py startapp blogmyproject/ ├── manage.py └── myproject/ | ├── __init__.py | ├── se…

C++STL系列之vector

前言 vector是變長數組&#xff0c;有點像數據結構中的順序表&#xff0c;它和list也是經常被拿出作對比的&#xff0c; vector使用動態分配數組來存儲它的元素。當新元素插入時候&#xff0c;這個數組需要被重新分配大小&#xff0c;如果擴容&#xff0c;因為要開一個新數組把…

Functional C++ for Fun Profit

Lambda Conf上有人講C函數式編程。在Functional Conf 2019上&#xff0c;就有主題為“Lambdas: The Functional Programming Companion of Modern C”的演講。演講者介紹了現代C中函數式編程相關內容&#xff0c;講解了如何使用Lambda表達式編寫符合函數式編程原則的C代碼&…

Python基礎理論與實踐:從零到爬蟲實戰

引言Python如輕舟&#xff0c;載你探尋數據寶藏&#xff01;本文從基礎理論&#xff08;變量、循環、函數、模塊&#xff09;啟航&#xff0c;結合requests和BeautifulSoup實戰爬取Quotes to Scrape&#xff0c;適合零基礎到進階者。文章聚焦Python基礎&#xff08;變量、循環、…

ThingJS開發從入門到精通:構建三維物聯網可視化應用的完整指南

文章目錄第一部分&#xff1a;ThingJS基礎入門第一章 ThingJS概述與技術架構1.1 ThingJS平臺簡介1.2 技術架構解析1.3 開發環境配置第二章 基礎概念與核心API2.1 核心對象模型2.2 場景創建與管理2.3 對象操作基礎第三章 基礎開發實戰3.1 第一個ThingJS應用3.2 事件系統詳解3.3 …

關于list

1、什么是listlist是一個帶頭結點的雙向循環鏈表模版容器&#xff0c;可以存放任意類型&#xff0c;需要顯式定義2、list的使用有了前面學習string和vector的基礎&#xff0c;學習和使用list會方便很多&#xff0c;因為大部分的內容依然是高度重合的。與順序表不同&#xff0c;…

Mysql 查看當前事務鎖

在 MySQL 中查看事務鎖&#xff08;鎖等待、鎖持有等&#xff09;&#xff0c;可以使用以下方法&#xff1a; 一、查看當前鎖等待情況&#xff08;推薦&#xff09; SELECTr.trx_id AS waiting_trx_id,r.trx_mysql_thread_id AS waiting_thread,r.trx_query AS waiting_query,b…

【Keil5-map文件】

Keil5-map文件■ map文件■ map文件

k8s 基本架構

基于Kubernetes(K8s)的核心設計&#xff0c;以下是其關鍵基本概念的詳細解析。這些概念構成了K8s容器編排系統的基石&#xff0c;用于自動化部署、擴展和管理容器化應用。### 一、K8s核心概念概覽 K8s的核心對象圍繞容器生命周期管理、資源調度和服務發現展開&#xff0c;主要包…

Bell不等式賦能機器學習:微算法科技MLGO一種基于量子糾纏的監督量子分類器訓練算法技術

近年來&#xff0c;量子計算&#xff08;Quantum Computing&#xff09; 和 機器學習&#xff08;Machine Learning&#xff09; 的融合成為人工智能和計算科學領域的重要研究方向。隨著經典計算機在某些復雜任務上接近計算極限&#xff0c;研究人員開始探索量子計算的獨特優勢…

Edge瀏覽器設置網頁自動翻譯

一.瀏覽網頁自動翻譯設置->擴展->獲取Microsoft Edge擴展->搜索“沉浸式翻譯”->獲取 。提示&#xff1a;如果采用其他的翻譯擴展沒找自動翻譯功能&#xff0c;所以這里選擇“沉浸式翻譯”二.基于Java WebElement時自動翻譯Java關鍵代碼&#xff1a;提示&#xff1…