目次
1. 前言
C語言在開發系統程式或網路應用程式時,會遇到「想同時監控多個輸入與輸出」或「想在帶有逾時的情況下等待使用者輸入或 socket 通訊」等需求。這種情況下,強而有力的幫手不是 C 語言標準函式庫,而是 UNIX 系統提供的select 函式。 select 函式是能同時監控多個檔案描述符(檔案、socket、標準輸入等)是否處於「可讀」「可寫」「例外發生」等狀態的 I/O 多工基本功能。特別是在需要伺服器應用程式或非同步處理的情境下,它以簡單卻高度通用的方式被長期使用。 此外,select 函式可以彈性設定等待時間(逾時),因此能輕鬆實現「只等待一定時間的輸入,若無則繼續處理」等控制。這不僅是網路程式設計新手,對於中高階工程師也是應該了解的基礎知識。 本文將從 select 函式的基本語法與用法、常見的應用範例,乃至進階的使用模式與注意事項,系統性地說明在實務上立即可用的技巧。2. select的基本語法與參數說明
select 函式是 UNIX 系統中用於有效監控檔案描述符(FD)的標準 API。此處將詳細說明 select 的基本語法以及各參數的作用。#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
各參數的意義
- nfds 指定要監視的檔案描述符中,最大值加 1 的數字。例如,若要監視的 FD 有 3 個(3、5、7),則指定「8」。此數值用於核心內部有效管理 FD。
- readfds 設定想要監視是否「可讀」的檔案描述符集合。例如,用於判斷標準輸入或 socket 是否有資料可接收,即「資料是否已到達」的情況。
- writefds 設定想要監視是否「可寫」的檔案描述符集合。當傳送緩衝區有空間,想判斷「是否能立即寫入」時使用。
- exceptfds 設定想要監視例外狀態(錯誤或特殊狀態)的檔案描述符集合。主要用於帶外資料或錯誤發生時的通知,但在一般用途上較少使用。
- timeout 設定 select 的等待時間(逾時)。
- 若指定為 NULL,則會無限期等待,直到任一檔案描述符變為監視目標的狀態。
- 若以 struct timeval 型設定具體時間(秒與微秒),則「只等待該時間」,逾時時返回 0。
- 若指定 0 秒,則作為「非阻塞」(立即返回)使用。
select 的返回值
- 0:在逾時前未發生監視目標的狀態變化時
- 正值:發生狀態變化的 FD 數量
- -1:發生錯誤(參數錯誤或訊號中斷等)
3. 基本步驟:select的使用方式(7 步)
select函式要正確運用,必須深入了解檔案描述符集合(fd_set)的操作與監控流程。以下將以實際程式碼範例,說明利用 select 進行 I/O 多工的基本步驟,分為 7 個階段。步驟1:fd_set 的初始化
FD_ZERO(&readfds);
步驟2:設定監控目標的 FD
FD_SET(0, &readfds);
步驟3:設定逾時值
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
步驟4:執行 select
int ret = select(nfds, &readfds, NULL, NULL, &tv);
步驟5:判斷返回值
- 返回值 > 0:監控的 FD 中有任一發生狀態變化
- 返回值 = 0:逾時
- 返回值 < 0:發生錯誤
步驟6:判斷哪個 FD 發生狀態變化
if (FD_ISSET(0, &readfds)) {
// 標準輸入有資料
}
步驟7:執行必要的處理
對於偵測到狀態變化的每個 FD,實作必要的讀寫處理。例如,從標準輸入讀取資料時,可使用fgets
或 read
等函式。
結合這 7 個步驟,即可使用 select 函式實現高效的 I/O 多工。4. 使用範例 ①:帶逾時的標準輸入(stdin)讀取
select 函式在想要只等待使用者輸入一定時間的情境下非常有用。例如,在測驗應用程式中出現「請在 10 秒內作答」的情況時,使用帶逾時的方式監控標準輸入是 select 的典型用法之一。範例:只等待 10 秒的標準輸入的 C 語言程式
以下是判斷使用者是否在 10 秒內輸入了內容的範例程式碼。#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
int main(void) {
fd_set readfds;
struct timeval tv;
int ret;
FD_ZERO(&readfds); // 初始化檔案描述符集合
FD_SET(0, &readfds); // 將標準輸入 (fd=0) 加入監視對象
tv.tv_sec = 10; // 超時 10 秒
tv.tv_usec = 0;
printf("請在 10 秒內輸入任何內容: ");
fflush(stdout);
ret = select(1, &readfds, NULL, NULL, &tv);
if (ret == -1) {
perror("select");
return 1;
} else if (ret == 0) {
printf("n逾時了。n");
} else {
char buf[256];
if (FD_ISSET(0, &readfds)) {
fgets(buf, sizeof(buf), stdin);
printf("已輸入: %s", buf);
}
}
return 0;
}
說明:此範例的重點
- 使用
FD_ZERO
與FD_SET
,將 fd_set 設定為僅監視標準輸入。 - 使用
struct timeval
設定 10 秒的逾時。 - select 會持續等待,直到指定的檔案描述符有資料到達或 10 秒過去。
- 若有輸入,會使用
FD_ISSET
判斷並接收其內容。逾時時會回傳 0,並顯示「逾時了」。
scanf
或 fgets
難以處理的逾時需求,也能透過活用 select 智慧地解決。5. 使用範例 ②:多重 Socket 監控(UDP / TCP)
select 函式的真正價值在於需要同時處理多個 socket 通訊的情況。特別是在伺服器端,像「同時等待多個客戶端的連線」或「監控多個 UDP socket 的資料接收」等情境,select 非常有效。範例:多重 UDP socket 的同時監控
例如,示範同時監控兩個 UDP 埠口,當任一埠收到資料時即進行接收處理的簡單範例。#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#define PORT1 5000
#define PORT2 5001
int main(void) {
int sock1, sock2, maxfd;
struct sockaddr_in addr1, addr2, client_addr;
fd_set readfds;
char buf[256];
// 建立 UDP socket 1
sock1 = socket(AF_INET, SOCK_DGRAM, 0);
memset(&addr1, 0, sizeof(addr1));
addr1.sin_family = AF_INET;
addr1.sin_addr.s_addr = INADDR_ANY;
addr1.sin_port = htons(PORT1);
bind(sock1, (struct sockaddr *)&addr1, sizeof(addr1));
// 建立 UDP socket 2
sock2 = socket(AF_INET, SOCK_DGRAM, 0);
memset(&addr2, 0, sizeof(addr2));
addr2.sin_family = AF_INET;
addr2.sin_addr.s_addr = INADDR_ANY;
addr2.sin_port = htons(PORT2);
bind(sock2, (struct sockaddr *)&addr2, sizeof(addr2));
maxfd = (sock1 > sock2 ? sock1 : sock2) + 1;
while (1) {
FD_ZERO(&readfds);
FD_SET(sock1, &readfds);
FD_SET(sock2, &readfds);
if (select(maxfd, &readfds, NULL, NULL, NULL) > 0) {
socklen_t addrlen = sizeof(client_addr);
if (FD_ISSET(sock1, &readfds)) {
recvfrom(sock1, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &addrlen);
printf("在 PORT1 接收: %sn", buf);
}
if (FD_ISSET(sock2, &readfds)) {
recvfrom(sock2, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &addrlen);
printf("在 PORT2 接收: %sn", buf);
}
}
}
close(sock1);
close(sock2);
return 0;
}
TCP 伺服器的 select 應用範例(簡易說明)
在 TCP 的情況下,可以同時監控多個已連線的 socket 與監聽 socket。新的連線請求由監聽 socket 接收,客戶端資料的到達則在各通訊 socket 上以 FD_SET 註冊,並透過 FD_ISSET 判斷。說明:本節重點
- 使用 FD_SET 可以同時監控多個 socket(檔案描述符)。
- 當任一 socket 接收到資料時,透過 FD_ISSET 判斷並執行相應的處理。
- 在伺服器程式或支援多客戶端的程式中,select 非常有用。
6. Windows(Winsock2)的 select
select 函式原本在 UNIX 系統上被廣泛使用,但在 Windows 環境中透過 Winsock2(Windows Sockets API)也能實現相同的 I/O 多工。此處將說明 Windows 上 select 的基本用法,以及與 UNIX 系統的差異與注意事項。Windows 上的 select 基本
Windows 中,select 函式定義於<winsock2.h>
標頭。與 UNIX 系統的介面幾乎相同,但有幾個要點。- 需要進行 Winsock 的初始化與清除(
WSAStartup
與WSACleanup
)。 - 檔案描述符僅限於「socket」使用。無法像 UNIX 那樣監控標準輸入(fd=0)或檔案。

基本語法(Windows 的情況)
#include <winsock2.h>
#include <stdio.h>
int main(void) {
WSADATA wsaData;
SOCKET sock;
fd_set readfds;
timeval tv;
int ret;
// Winsock 初始化
WSAStartup(MAKEWORD(2,2), &wsaData);
// socket 生成與綁定處理等(省略)
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
tv.tv_sec = 5;
tv.tv_usec = 0;
ret = select(0, &readfds, NULL, NULL, &tv);
if (ret == SOCKET_ERROR) {
printf("select 錯誤n");
} else if (ret == 0) {
printf("逾時n");
} else {
if (FD_ISSET(sock, &readfds)) {
printf("資料可接收n");
}
}
// 清除
WSACleanup();
return 0;
}
UNIX 系 select 的主要差異
- nfds(第1 引數)被忽略 在 UNIX 中會指定「最大 FD + 1」,但在 Windows 版此參數會被忽略,始終使用 0 即可。
- 返回值的處理 錯誤時返回
SOCKET_ERROR
,成功時返回狀態變化的 FD 數量,逾時時返回 0。基本處理相同,但取得錯誤代碼需使用WSAGetLastError()
。 - 僅支援 socket Windows 的 select 不支援監控標準輸入或一般檔案。請記住它僅用於「socket 的輸入輸出管理」。
注意事項與補充
- 即使處理多個 socket,也可使用 FD_SET 一次性監控。
- 為提升非同步 I/O 與大規模連線的效能,可考慮使用 Windows 獨有的「IOCP(I/O 完成埠)」或 WSAPoll 等較新的 API。
7. 帶超時的 I/O 實作(應用)
select 函式的最大優勢是能夠以「帶超時」的方式監控 I/O。這在網路通信或使用者輸入等需要「控制等待時間」的情況下特別有用。在此將說明帶超時的 I/O 實作模式、使用 select 的好處以及實際的使用範例。應用範例:TCP Socket 的帶超時接收
在網路程式中,常常會有「只等待一定時間接收資料,若未收到則視為超時」的需求。使用 select 可以安全地將 recv 或 read 包裝成帶超時的操作。#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
int recv_with_timeout(int sockfd, void *buf, size_t len, int timeout_sec) {
fd_set readfds;
struct timeval tv;
int ret;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
tv.tv_sec = timeout_sec;
tv.tv_usec = 0;
ret = select(sockfd + 1, &readfds, NULL, NULL, &tv);
if (ret > 0) {
// 可接收
return recv(sockfd, buf, len, 0);
} else if (ret == 0) {
// 超時
return 0;
} else {
// 錯誤
return -1;
}
}
要點說明
- 使用 select 可以「最多等待 timeout 秒,直到 socket 可讀」
- 若發生超時,會回傳「0」;若有接收資料則照常呼叫 recv。
- 透過製作此類封裝函式,可避免 I/O 阻塞導致的無限等待,提升整體程式的回應性。
與其他超時控制手法的比較
使用 select 進行超時控制的優點是可以針對每個 I/O 靈活調整等待時間。 另一方面,使用 socket 選項(SO_RCVTIMEO
或 SO_SNDTIMEO
)也可以設定「整體的超時」。但因為不同作業系統的行為略有差異,若需要細緻的控制或同時管理多個 I/O,select 更為方便。 如此一來,使用 select 的帶超時 I/O 可廣泛應用於穩定的網路程式、互動式 CLI 工具等。8. 優點、限制與替代方法
select 函式長期以來一直是以 UNIX/Linux 系統為中心使用的 I/O 多工典範。然而,除了優點之外,現代的大規模系統開發也指出了若干限制與瓶頸。在此整理了 select 的優點與缺點,以及近年受到關注的替代技術。select 的主要優點
- 通用性高 檔案、socket、管道等,各種檔案描述符皆可一次性監控。
- 實作簡單 作為 C 語言標準 API,有大量文件與範例,初學者也容易理解。
- 可在廣泛環境使用 支援 Linux、BSD、macOS、Windows(Winsock2)等多種平台。
- 逾時功能 可為 I/O 等待指定彈性的逾時時間。
select 的主要限制與注意事項
- 可監控的 FD 數量有限制 FD_SET 可處理的檔案描述符數量受系統常數
FD_SETSIZE
(多數環境為 1024)限制。超過此上限的大量連線同時監控並不適合。 - 可擴展性問題 監控目標增多時,select 的內部處理(線性檢查所有 FD)會變得沉重,導致大規模系統效能下降(俗稱「C10K 問題」)。
- 需要重新初始化 FD 集合 每次呼叫 select 時都必須重新構建 fd_set,程序容易變得冗長。
- 事件通知粒度粗糙 必須自行每次檢查哪個 FD 發生了什麼事件。
select 的主要替代方法
- poll 與 select 同樣實現 I/O 多工的 API。沒有監控 FD 數量上限,使用陣列管理,彈性更高。
- epoll(Linux 專屬) 能有效處理大量 FD 的事件驅動模型。具高可擴展性,最適合伺服器用途。
- kqueue(BSD 系) 在 BSD 與 macOS 可使用的高效能 I/O 事件通知系統。
- IOCP(Windows 專屬) 在 Windows 上能有效實作大規模非同步 I/O 的機制。
- libuv、libevent 等函式庫 支援多個作業系統,並封裝 epoll、poll、kqueue、IOCP 等的高階函式庫。
總結
select 仍然被廣泛視為「簡單且可靠的 I/O 多工 API」而使用,但在 FD 數量眾多的應用程式或要求高效能的環境中,考慮遷移至更先進的 API 也是值得的。請依據目的與規模選擇最適合的 I/O 模型。9. 完全總結(總結章節)
在本文中,我們對 C 語言的 select 函式從基本原理、使用方法、應用範例,甚至優點與限制、替代技術等廣泛說明。最後,對本文內容作總結,整理出進一步加深對 select 理解的要點。select 的本質與應用場景
select 是一個便利的函式,能以簡單的方式解決「同時監控多個檔案描述子」的 I/O 多工基本課題。它在標準輸入的逾時監控、同時接收多個 socket 等日常系統程式開發情境中有許多實用的應用場景。本文學到的主要重點
- 語法與參數的意義正確理解後,任何 I/O 都能以 select 靈活監控。
- fd_set 的操作步驟(FD_ZERO、FD_SET、FD_ISSET 等)掌握後,可撰寫錯誤少且穩健的程式碼。
- 帶逾時的 I/O 與多 socket 監控等,可立即套用於實務情境。
- 關注 Windows 與 UNIX 的差異、以及 select 的限制,即可向更高階的網路開發延伸。
熟練使用 select 的技巧
- 使用 select 時,務必徹底執行「每次初始化 fd_set」與「別忘了指定最大 FD+1」等基本步驟。
- 若受到 FD 數量限制或效能問題,請考慮導入 poll、epoll 等更具擴展性的 I/O 模型。
- 積極執行範例程式碼,培養對 select「運作感」的體驗,是快速理解的捷徑。