多路 I/O復用(Multiplexed I/O):
1.定義:系統提供的I/O事件通知機制
2.應用:是一種 I/O 編程模型,用于在單線程中同時處理多個(阻塞)?I/O 操作,避免因等待某個 I/O 操作完成而阻塞整個程序的執行,及時處理。
3.I/O模型:①阻塞I/O(默認),閑等待()沒有就等待
? ? ? ? ? ? ? ? ? ②非阻塞(fcntl,NONBLOCK)I/O,忙等待(不停詢問)。cpu使用率高
? ? ? ? ? ? ? ? ? ③信號驅動I/O(不要求)SIGIO:使用少
? ? ? ? ? ? ? ? ? ④并發I/O:進程線程(開銷大,浪費內存)
? ? ? ? ? ? ? ? ?⑤多路I/O:select,epoll,poll
一,基于 有名管道(FIFO)?的簡單 IO 交互程序,核心功能是同時監聽 “有名管道” 和 “終端輸入”,并分別打印接收到的數據。下面從?功能邏輯、關鍵技術、代碼細節?三方面逐步解析
(1)核心功能總覽
程序的本質是一個 “雙源數據監聽器”:
- 先創建一個名為?
myfifo
?的命名管道(用于進程間通信); - 以 “只讀 + 非阻塞” 模式打開管道,同時監聽終端輸入(標準輸入?
stdin
); - 進入死循環,不斷嘗試:
- 從管道讀取數據(非阻塞,沒數據也不卡),讀到就打印;
- 從終端讀取用戶輸入(默認阻塞),讀到就打印;
- 實現 “管道數據” 和 “終端輸入” 的并行處理(本質是輪詢非阻塞 IO)。
讀:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{//創建有名管道int ret=mkfifo("myfifo",0666);{if(EEXIST==errno)//管道已存在不報錯(正常情況){}else//其他錯誤,報錯退出{perror("mkfifo errro\n");return 1;}}//打開管道并設置“非阻塞模式”//一只讀模式打開管道int fd=open("myfifo",O_RDONLY);if(-1==fd){perror("open error\n");return 1;}//將管道設置為“非阻塞IO”模式int flag=fcntl(fd,F_GETFL,0);//獲取當前fd的狀態(阻塞,只讀)fcntl(fd,F_SETFL,flag|O_NONBLOCK);// F_SETFL:設置標志,在原有的基礎上添加 O_NONBLOCK(非阻塞)
//fileno():將流指針轉換成整型flag=fcntl(fileno(stdin),F_GETFL,0);//獲取到了輸入端的狀態值//死循環監聽數據while(1){char buf[512]={0};if(read(fd,buf,sizeof(buf)>0))//將fd中的數據讀到buf里面{printf("fifo:%s\n",buf);}//清空緩沖區buf,準備接收終端輸入bzero(buf,sizeof(buf));// ② 從終端讀取用戶輸入(默認阻塞)if (fgets(buf, sizeof(buf), stdin)){// fgets返回非NULL:讀到了輸入printf("terminal:%s", buf); // 打印終端輸入fflush(stdout); // 強制刷新 stdout(避免輸出緩存)}}close(fd);//system("pause");return 0;
}
fcntl
?作用:修改文件描述符的屬性(File Control):F_GETFL
:獲取當前?fd
?的狀態(比如是否是阻塞、只讀 / 寫等);flag | O_NONBLOCK
:在原有標志基礎上,添加 “非阻塞” 標志(O_NONBLOCK
)—— 設置后,read(fd, ...)
?若沒數據,不會阻塞等待,而是立即返回?-1
?并設置?errno=EAGAIN
。
寫:
#include <errno.h> // 提供錯誤碼定義,用于錯誤處理
#include <fcntl.h> // 提供文件控制操作函數(如open)的聲明
#include <stdio.h> // 標準輸入輸出函數
#include <stdlib.h> // 標準庫函數(如exit)
#include <string.h> // 字符串處理函數(如strlen)
#include <sys/stat.h> // 提供文件狀態相關定義(如mkfifo所需的權限位)
#include <sys/types.h> // 提供基本系統數據類型定義
#include <unistd.h> // 提供POSIX操作系統API(如write、sleep、close)int main(int argc, char *argv[])
{// 創建名為"myfifo"的命名管道,權限為0666(所有用戶可讀寫)int ret = mkfifo("myfifo", 0666);// 檢查mkfifo調用是否失敗if (-1 == ret){// 如果錯誤碼是EEXIST,表示FIFO已存在,屬于正常情況,不做處理if (EEXIST == errno){// 空語句塊:已存在則無需重新創建,繼續執行后續代碼}// 其他錯誤情況(如權限不足),打印錯誤信息并退出else{perror("mkfifo error\n"); // 打印具體錯誤原因return 1; // 非0返回值表示程序異常退出}}// 以只寫模式(O_WRONLY)打開FIFO,獲取文件描述符int fd = open("myfifo", O_WRONLY);// 檢查open調用是否失敗if (-1 == fd){perror("open error\n"); // 打印打開失敗的原因return 1; // 異常退出}// 無限循環:持續向FIFO寫入數據while (1){// 定義緩沖區并初始化要發送的字符串char buf[512] = "hello,this is fifo tested...\n";// 向FIFO寫入數據:參數為文件描述符、緩沖區、數據長度(包含結束符)write(fd, buf, strlen(buf) + 1);//strlen(buf) + 1確保包含字符串結束符\0// 暫停3秒,控制發送頻率,使程序每 3 秒寫入一次數據sleep(3);}// 關閉文件描述符(注:由于上面是無限循環,此句實際永遠不會執行)close(fd);return 0; // 程序正常退出(實際不會執行到此處)
}
二,信號驅動I/O:
使用命名管道(FIFO)進行異步讀取的程序,它通過信號機制實現當 FIFO 中有數據到來時進行處理
寫:
#include <errno.h> // 錯誤處理相關定義
#include <fcntl.h> // 文件控制操作(fcntl等)
#include <signal.h> // 信號處理相關函數和定義
#include <stdio.h> // 標準輸入輸出
#include <stdlib.h> // 標準庫函數
#include <string.h> // 字符串處理函數
#include <sys/stat.h> // 文件狀態相關定義
#include <sys/types.h> // 基本系統數據類型
#include <unistd.h> // POSIX系統APIint fd; // 全局文件描述符,供信號處理函數使用// 信號處理函數:當接收到SIGIO信號時被調用
void myhandle(int num)
{char buf[512] = {0};// 從FIFO讀取數據read(fd, buf, sizeof(buf));// 打印從FIFO接收到的數據printf("fifo :%s\n", buf);
}int main(int argc, char *argv[])
{// 注冊SIGIO信號的處理函數為myhandlesignal(SIGIO, myhandle);// 創建命名管道"myfifo",權限0666int ret = mkfifo("myfifo", 0666);if (-1 == ret){// 如果管道已存在,不做處理if (EEXIST == errno){}// 其他錯誤則打印信息并退出else{perror("mkfifo error\n");return 1;}}// 以只讀模式打開FIFOfd = open("myfifo", O_RDONLY);if (-1 == fd){perror("open error\n");return 1;}// 獲取文件描述符當前的標志int flag = fcntl(fd, F_GETFL);// 設置文件描述符為異步模式(O_ASYNC)fcntl(fd, F_SETFL, flag | O_ASYNC);// 設置異步I/O的所有者為當前進程,當有數據時內核會向該進程發送SIGIO信號fcntl(fd, F_SETOWN, getpid());// 主循環:持續從終端讀取輸入并打印while (1){char buf[512] = {0};bzero(buf, sizeof(buf)); // 清空緩沖區// 從標準輸入讀取數據fgets(buf, sizeof(buf), stdin);// 打印終端輸入的數據printf("terminal:%s", buf);fflush(stdout); // 刷新輸出緩沖區}// 關閉文件描述符(實際不會執行,因為上面是無限循環)close(fd);// remove("myfifo"); // 注釋掉了刪除FIFO的操作return 0;
}
程序核心功能說明:
異步 I/O 處理機制:
- 使用
fcntl
設置 FIFO 為異步模式(O_ASYNC
) - 當 FIFO 中有數據到達時,內核會自動向進程發送
SIGIO
信號 - 注冊了
SIGIO
信號的處理函數myhandle
,在信號到來時讀取并處理數據
- 使用
程序工作流程:
- 創建并打開 FIFO(只讀模式)
- 配置 FIFO 為異步通知模式,并設置當前進程為接收通知的進程
- 主循環負責從終端讀取用戶輸入并打印
- 當有數據寫入 FIFO 時,觸發
SIGIO
信號,調用myhandle
讀取并打印 FIFO 中的數據
特點:
- 實現了非阻塞式的 FIFO 讀取,主程序可以同時處理其他任務(這里是終端輸入)
- 信號驅動的 I/O 模型提高了效率,不需要主動輪詢 FIFO 狀態
讀:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{int ret = mkfifo("myfifo", 0666);if (-1 == ret){if (EEXIST == errno){}else{perror("mkfifo error\n");return 1;}}int fd = open("myfifo", O_WRONLY);if (-1 == fd){perror("open error\n");return 1;}while (1){char buf[512] = "hello,this is fifo tested...\n";write(fd, buf, strlen(buf) + 1);sleep(3);}close(fd);return 0;
}
三,I/O多路復用:select,ep
(1) select函數:
????????通過監聽一組文件描述符(
fd_set
),來判斷其中是否有描述符就緒(可讀、可寫或異常)。程序會阻塞在?select
?調用上,直到有描述符就緒或者超時。核心功能:幫你同時盯著多個 “消息入口”(文件描述符:比如套接字、鍵盤輸入),對應的信息放入對應的集合里,等某個 / 某些入口有消息了,再通知你處理 —— 避免你自己挨個蹲守,讓程序更高效、不卡頓。
select
?是最早的多路 I/O 實現方式,在 POSIX 標準中定義,具有較好的跨平臺性(在 Linux、Windows 等系統中都有支持)。
1.使用步驟:①創建集合
? ? ? ? ? ? ? ? ? ②加入fd;
? ? ? ? ? ? ? ? ? ③select輪詢
? ? ? ? ? ? ? ? ? ④找到對應的fd? r/w
2. 函數原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
返回值:就緒的文件描述符數量;-1 表示出錯;0 表示超時
3. 核心參數
nfds
:需監聽的最大文件描述符 + 1(因 FD 從 0 開始編號)。readfds
:監聽 “可讀事件” 的 FD 集合(輸入參數,內核修改后返回就緒 FD)。writefds
:監聽 “可寫事件” 的 FD 集合(同上)。exceptfds
:監聽 “異常事件” 的 FD 集合(同上)。timeout
:超時時間(NULL
?表示永久阻塞;struct timeval{0,0}
?表示非阻塞;其他值表示等待指定時間)。
select
?阻塞等待,程序暫停,不占用 CPU
4. 輔助宏(操作 FD 集合)
FD_ZERO(fd_set *set); // 清空 FD 集合
FD_SET(int fd, fd_set *set); // 將 FD 添加到集合
FD_CLR(int fd, fd_set *set); // 從集合中移除 FD
FD_ISSET(int fd, fd_set *set); // 檢查 FD 是否在就緒集合中(返回非 0 表示就緒)
基本原理:
select
?函數通過監聽一組文件描述符(fd_set
),來判斷其中是否有描述符就緒(可讀、可寫或異常)。程序會阻塞在?select
?調用上,直到有描述符就緒或者超時。
5. 特點與局限
- 優點:跨平臺性好,接口簡單,適合監聽少量 FD(如 < 1000)。
- 缺點:
- FD 數量限制(默認最大 1024,由?
FD_SETSIZE
?定義)。 - 每次調用需將 FD 集合從用戶空間拷貝到內核空間,效率低。
- 需遍歷所有 FD 才能判斷就緒狀態(時間復雜度 O (n))。
- FD 數量限制(默認最大 1024,由?
使用?
select
?多路復用機制同時監聽管道(fifo)和標準輸入(終端)的程序,功能與之前的?epoll
?版本類似,但使用了不同的 I/O 多路復用技術。
讀端:重點處理
#include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h>/* 包含select相關頭文件 */ /* 根據POSIX.1-2001, POSIX.1-2008標準 */ #include <sys/select.h>/* 根據早期標準 */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h>int main(int argc, char *argv[]) {// 創建命名管道"myfifo",權限為0666(所有用戶可讀寫)int ret = mkfifo("myfifo", 0666);if (-1 == ret) // 創建失敗{if (EEXIST == errno) // 錯誤為"文件已存在",屬于正常情況,不處理{}else // 其他錯誤(如權限不足),輸出錯誤信息并退出{perror("mkfifo error\n");return 1;}}// 以只讀方式打開管道文件int fd = open("myfifo", O_RDONLY);if (-1 == fd) // 打開失敗{perror("open error\n");return 1;}// 定義select所需的文件描述符集合// rd_set:用于select調用的臨時集合(會被select修改)// tmp_set:保存初始集合(用于每次循環恢復rd_set)fd_set rd_set, tmp_set;// 初始化集合,清空所有位FD_ZERO(&rd_set);FD_ZERO(&tmp_set);// 向集合中添加需要監聽的文件描述符(對應集合類型)FD_SET(0, &tmp_set); // 添加標準輸入(終端,文件描述符為0)FD_SET(fd, &tmp_set); // 添加管道文件描述符// 事件循環:持續監聽輸入事件while (1){// 每次循環前,從備份集合恢復rd_set// 因為select會修改集合,只保留就緒的文件描述符rd_set = tmp_set;// 調用select等待事件發生// 參數1:最大文件描述符值+1(fd是管道描述符,比0大)// 參數2:監聽讀事件的集合// 參數3、4:NULL,表示不監聽寫事件和異常事件// 參數5:NULL,表示無限期等待,直到有事件發生select(fd + 1, &rd_set, NULL, NULL, NULL);// 緩沖區,用于存儲讀取的數據char buf[512] = {0};// FD_ISSET函數:檢查管道是否有數據可讀(文件描述符在就緒集合中)if (FD_ISSET(fd, &rd_set))//如果fd文件準備就緒,就執行他{// 從管道讀取數據read(fd, buf, sizeof(buf));// 打印管道中的數據printf("fifo :%s\n", buf);}// 檢查終端(標準輸入)是否有輸入if (FD_ISSET(0, &rd_set))//如果終端有輸入,就執行他{// 清空緩沖區bzero(buf, sizeof(buf));// 從終端讀取一行輸入fgets(buf, sizeof(buf), stdin);// 打印終端輸入的數據printf("terminal:%s", buf);// 刷新標準輸出緩沖區,確保內容立即顯示fflush(stdout);}}// 關閉管道文件描述符(實際運行中循環不會退出,此句很少執行)close(fd);// 注釋:刪除管道文件(此處注釋掉,保留文件供后續測試)// remove("myfifo");return 0; }
寫端:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{int ret = mkfifo("myfifo", 0666);if (-1 == ret){if (EEXIST == errno){}else{perror("mkfifo error\n");return 1;}}int fd = open("myfifo", O_WRONLY);if (-1 == fd){perror("open error\n");return 1;}while (1){char buf[512] = "hello,this is fifo tested...\n";write(fd, buf, strlen(buf) + 1);sleep(3);}close(fd);return 0;
}
(2)
epoll
?函數(Linux 特有):精準告訴你 “哪些設備有數據要處理了”,讓程序不用瞎等、不用瞎查,直接處理有動靜的設備就行。(加強的select)
epoll
?是 Linux 專為高并發設計的多路 I/O 復用機制,性能遠超?select
/poll
,支持海量 FD 且時間復雜度為 O (1)。
(1)epoll 工作流程總結
- 創建實例:通過?
epoll_create
?創建 epoll 實例(epfd
),默認創建兩個集合(二叉樹); - 注冊事件:通過?
epoll_ctl
?向?epfd
?中添加需要監聽的 FD 及事件(如?EPOLLIN
)結構體; - 等待就緒:通過?
epoll_wait
?阻塞等待,內核自動將就緒事件寫入?events
?數組; - 處理事件:遍歷?
events
?數組,根據就緒的 FD 和事件類型(如可讀)進行處理; - 循環監聽:重復步驟 3~4,持續處理新的就緒事件。
(2)核心函數詳解
1.?epoll_create
:創建 epoll 實例
功能:在內核中創建一個 epoll 實例(事件表),用于管理后續需要監聽的文件描述符(FD)和事件。
原型:
#include <sys/epoll.h>
int epoll_create(int size);
參數:
size
:早期版本用于指定監聽的 FD 數量上限,現在已廢棄(內核不再使用此值),傳入任意正整數即可(通常傳 2)。
返回值:
- 成功:返回一個 epoll 實例的文件描述符(
epfd
),后續操作通過該描述符進行; - 失敗:返回?
-1
,并設置?errno
(如?ENFILE
?表示系統文件描述符耗盡)。
int epfd = epoll_create(1); // 創建 epoll 實例
if (epfd == -1) {perror("epoll_create failed");exit(EXIT_FAILURE);
}
2.?epoll_ctl
:管理 epoll 實例中的事件
功能:向 epoll 實例中添加、修改或刪除需要監聽的文件描述符及其事件(如 “可讀”“可寫”)。
原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數:
epfd
:epoll_create
?返回的 epoll 實例描述符;op
:操作類型,可選值:EPOLL_CTL_ADD
:向 epoll 實例添加一個新的 FD 及事件;EPOLL_CTL_MOD
:修改已添加的 FD 的監聽事件;EPOLL_CTL_DEL
:從 epoll 實例中刪除一個 FD(此時?event
?可設為?NULL
);
fd
:需要監聽的文件描述符(如 socket、管道 FD 等);event
:指向?struct epoll_event
?的指針,用于描述監聽的事件及用戶數據:struct epoll_event {uint32_t events; // 監聽的事件類型(如 EPOLLIN 表示可讀)epoll_data_t data; // 用戶數據(通常存儲 FD 或自定義指針) };// 用戶數據聯合體(可存儲多種類型) typedef union epoll_data {void *ptr; // 自定義指針(如指向業務數據結構)int fd; // 最常用:存儲當前監聽的 FDuint32_t u32;uint64_t u64; } epoll_data_t;
常見事件類型(events
?字段):
EPOLLIN
:FD 可讀(如 socket 有數據、管道有輸入);EPOLLOUT
:FD 可寫(如 socket 發送緩沖區空閑);EPOLLERR
:FD 發生錯誤(無需手動設置,內核自動觸發);EPOLLET
:邊緣觸發模式(ET,高效模式,需配合非阻塞 IO);EPOLLONESHOT
:只監聽一次事件,事件觸發后需重新添加才會再次監聽。
返回值:
- 成功:返回?
0
; - 失敗:返回?
-1
,并設置?errno
(如?EEXIST
?表示添加已存在的 FD)。
struct epoll_event ev;
// 監聽 FD=5 的“可讀事件”,并設為邊緣觸發(ET)
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = 5; // 存儲 FD 到用戶數據中// 向 epoll 實例添加該 FD 及事件
if (epoll_ctl(epfd, EPOLL_CTL_ADD, 5, &ev) == -1) {perror("epoll_ctl add failed");exit(EXIT_FAILURE);
}
3.?epoll_wait
:等待并獲取就緒事件
功能:阻塞等待 epoll 實例中監聽的 FD 發生就緒事件(如可讀、可寫),返回就緒的事件列表。
原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
參數:
epfd
:epoll 實例描述符;events
:用戶空間數組,內核會將所有就緒的事件寫入該數組(輸出參數);maxevents
:events
?數組的最大長度(必須 ≥ 1,且不能超過?epoll_create
?時的?size
?早期限制);timeout
:超時時間(毫秒):-1
:永久阻塞,直到有事件就緒;0
:立即返回,無論是否有事件就緒;- 正數:等待?
timeout
?毫秒后返回(若期間有事件就緒則提前返回)
返回值:
- 成功:返回就緒事件的數量(
>0
); - 超時:返回?
0
(timeout
?非?-1
?時); - 失敗:返回?
-1
,并設置?errno
(如?EINTR
?表示被信號中斷)。
struct epoll_event events[10]; // 最多存儲 10 個就緒事件
int nfds;// 永久阻塞等待事件(-1)
nfds = epoll_wait(epfd, events, 10, -1);
if (nfds == -1) {perror("epoll_wait failed");exit(EXIT_FAILURE);
}// 遍歷所有就緒事件并處理
for (int i = 0; i < nfds; i++) {if (events[i].events & EPOLLIN) {// FD 可讀,處理數據(events[i].data.fd 為就緒的 FD)handle_read(events[i].data.fd);}
}
2. 事件類型與觸發模式
- 常見事件:
EPOLLIN
(可讀)、EPOLLOUT
(可寫)、EPOLLERR
(錯誤)。 - 觸發模式:
- 水平觸發(LT,默認):只要 FD 就緒,每次?
epoll_wait
?都會返回。 - 邊緣觸發(ET):僅在 FD 從 “未就緒” 變為 “就緒” 時觸發一次(需配合非阻塞 IO 使用)。
- 水平觸發(LT,默認):只要 FD 就緒,每次?
3. 特點
- 優點:
- 無 FD 數量限制(支持上萬甚至百萬級 FD)。
- FD 僅在添加時拷貝到內核,后續無需重復拷貝。
- 內核直接返回就緒 FD 列表,無需遍歷(O (1) 復雜度)。
- 缺點:僅支持 Linux 系統,不跨平臺。
poll
?多路復用機制,實現了同時監聽管道(fifo)和標準輸入(終端)的功能
讀端:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h> // epoll相關函數頭文件
#include <sys/stat.h> // 文件狀態相關函數
#include <sys/types.h> // 基本數據類型定義
#include <unistd.h> // POSIX系統調用/*** 向epoll實例添加需要監聽的文件描述符* @param epfd epoll實例的文件描述符* @param fd 要添加的文件描述符* @return 0表示成功,1表示失敗*/
int add_fd(int epfd, int fd)
{struct epoll_event ev; // epoll事件結構體,用于描述監聽的事件類型和關聯數據ev.events = EPOLLIN; // 監聽讀事件(當有數據可讀時觸發)ev.data.fd = fd; // 將文件描述符與事件綁定,方便后續識別// 調用epoll_ctl添加文件描述符到epoll實例// 參數:epoll實例、操作類型(添加)、目標文件描述符、事件結構體int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);if (-1 == ret){perror("add fd"); // 添加失敗時輸出錯誤信息return 1;}return 0; // 成功添加
}int main(int argc, char *argv[])
{// 創建命名管道"myfifo",權限為0666(所有用戶可讀寫)int ret = mkfifo("myfifo", 0666);if (-1 == ret) // 創建失敗{if (EEXIST == errno) // 錯誤碼為EEXIST,表示管道已存在,屬于正常情況{// 管道已存在,無需處理,繼續執行}else // 其他錯誤(如權限不足){perror("mkfifo error\n"); // 輸出錯誤信息return 1; // 異常退出}}// 以只讀方式打開管道文件int fd = open("myfifo", O_RDONLY);if (-1 == fd) // 打開失敗{perror("open error\n"); // 輸出錯誤信息return 1; // 異常退出}// 定義epoll事件數組,用于存儲epoll_wait返回的就緒事件// 大小為2,因為我們最多監聽2個文件描述符(終端和管道)struct epoll_event rev[2];// 1. 創建epoll實例// 參數2表示期望處理的文件描述符數量(僅供內核參考,非嚴格限制)int epfd = epoll_create(2);if (-1 == epfd) // 創建失敗{perror("epoll_create"); // 輸出錯誤信息return 1; // 異常退出}// 2. 向epoll實例添加需要監聽的文件描述符add_fd(epfd, 0); // 添加標準輸入(終端,文件描述符固定為0)add_fd(epfd, fd); // 添加管道文件描述符// 事件循環:持續監聽并處理事件while (1){char buf[512] = {0}; // 數據緩沖區,用于存儲讀取到的數據// 等待事件發生,epoll_wait會阻塞直到有事件觸發// 參數:epoll實例、存儲就緒事件的數組、數組大小、超時時間(-1表示無限等待)int ep_ret = epoll_wait(epfd, rev, 2, -1);// 遍歷所有就緒的事件for (int i = 0; i < ep_ret; i++){// 判斷就緒的是管道文件描述符if (rev[i].data.fd == fd){read(fd, buf, sizeof(buf)); // 從管道讀取數據printf("fifo :%s\n", buf); // 打印管道中的數據}// 判斷就緒的是標準輸入(終端)else if (0 == rev[i].data.fd){bzero(buf, sizeof(buf)); // 清空緩沖區fgets(buf, sizeof(buf), stdin); // 從終端讀取輸入printf("terminal:%s", buf); // 打印終端輸入的數據fflush(stdout); // 刷新標準輸出,確保內容立即顯示}}}// 關閉管道文件描述符(實際運行中因無限循環,此句很少執行)close(fd);// 注釋:刪除管道文件(此處保留注釋,實際運行時不刪除,方便后續測試)// remove("myfifo");return 0;
}
寫端:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{int ret = mkfifo("myfifo", 0666);if (-1 == ret){if (EEXIST == errno){}else{perror("mkfifo error\n");return 1;}}int fd = open("myfifo", O_WRONLY);if (-1 == fd){perror("open error\n");return 1;}while (1){char buf[512] = "hello,this is fifo tested...\n";write(fd, buf, strlen(buf) + 1);sleep(3);}close(fd);return 0;
}
對比維度 | select | epoll |
---|---|---|
底層機制 | 輪詢(遍歷所有監聽 fd) | 主動上報(有設備的中斷觸發) |
文件描述符限制 | 有上限(默認 1024,由?FD_SETSIZE ?定義),修改需重新編譯內核 | 無上限(僅受系統內存和進程 fd 限制) |
性能隨 fd 增長趨勢 | 性能急劇下降(O (n),n 為監聽 fd 數量) | 性能穩定(O (1),與監聽 fd 數量無關) |
用戶態 / 內核態交互 | 每次調用需拷貝整個 fd_set 到內核(高開銷) | 僅注冊 / 修改時拷貝 fd 到內核,后續無拷貝 |
就緒 fd 通知方式 | 僅告知 “有就緒”,需用戶二次遍歷檢查 | 直接返回就緒 fd 列表,無需二次檢查 |
支持的事件類型 | 僅支持水平觸發(LT) | 支持水平觸發(LT)和邊緣觸發(ET) |
接口復雜度 | 簡單(3 個函數:select /FD_SET /FD_ISSET ) | 稍復雜(3 個函數:epoll_create /epoll_ctl /epoll_wait ) |
可移植性 | 高(POSIX 標準,支持 Linux/Windows/macOS) | 低(僅 Linux 特有,非 POSIX 標準) |
適用場景 | 監聽 fd 數量少(如 < 1000)、簡單場景 | 監聽 fd 數量多(如萬級 / 十萬級)、高性能場景(如服務器) |