Linux其他備選高級IO模型用法詳解
其他高級 I/O 模型
以上基本介紹的都是同步IO相關(guān)知識點(diǎn),即在同步I/O模型中,程序發(fā)起I/O操作后會(huì)等待I/O操作完成,即程序會(huì)被阻塞,直到I/O完成。
整個(gè)I/O過程在同一個(gè)線程中進(jìn)行,程序在等待期間不能執(zhí)行其他任務(wù)。下面將會(huì)介紹除同步IO之外的其他常見IO模型。
什么是IO?什么是高效的IO?
**I/O(輸入/輸出)**是計(jì)算機(jī)與外部世界進(jìn)行數(shù)據(jù)交換的過程。在Linux系統(tǒng)中,I/O操作通常指的是程序與硬盤、網(wǎng)絡(luò)設(shè)備、終端等進(jìn)行數(shù)據(jù)交換的操作。I/O性能直接影響到系統(tǒng)的響應(yīng)速度和吞吐量,是很多應(yīng)用系統(tǒng)優(yōu)化的關(guān)鍵目標(biāo)。
高效的IO 涉及優(yōu)化I/O操作的延遲、吞吐量和資源消耗。在一個(gè)復(fù)雜的系統(tǒng)中,I/O效率通常通過以下方式得到提升:
- 減少I/O等待時(shí)間(例如,通過非阻塞I/O或異步I/O)
- 最大化吞吐量(例如,通過數(shù)據(jù)預(yù)讀或緩存機(jī)制)
- 優(yōu)化系統(tǒng)資源的利用(例如,通過多路復(fù)用和線程池等技術(shù))
高效的IO不僅可以提升程序的響應(yīng)速度,還能減少系統(tǒng)的負(fù)載,提高并發(fā)處理能力。
IO模型分析方法
出處:Linux五種IO模型
分析IO模型需要了解2個(gè)問題:
問題1:發(fā)送IO請求,IO請求可以理解為用戶空間和內(nèi)核空間數(shù)據(jù)同步,根據(jù)發(fā)起者不同分為以下兩種情況:
- 由用戶程序發(fā)起(同步IO)。
- 由內(nèi)核發(fā)起(異步IO)。
問題2:等待數(shù)據(jù)到來,等待數(shù)據(jù)到來的方式有以下幾種:
- 阻塞(阻塞IO)。
- 輪詢(非阻塞IO)。
- 信號通知(信號驅(qū)動(dòng)IO)。
內(nèi)核空間和用戶空間數(shù)據(jù)同步由誰發(fā)起是分析Linux IO模型最核心的問題
1.阻塞式I/O(Blocking I/O)
- 阻塞式I/O是最常見的I/O模型,在這種模型下,當(dāng)程序請求I/O操作時(shí),會(huì)阻塞當(dāng)前線程直到I/O操作完成(如讀/寫數(shù)據(jù))。在等待期間,線程無法進(jìn)行其他操作。
- 優(yōu)點(diǎn):簡單,易于理解和實(shí)現(xiàn)。
- 缺點(diǎn):性能瓶頸,當(dāng)進(jìn)行大量I/O操作時(shí),阻塞會(huì)導(dǎo)致線程無法有效利用,浪費(fèi)CPU資源。
代碼示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 創(chuàng)建一個(gè)TCP套接字
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("套接字創(chuàng)建失敗");
return -1;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 綁定套接字
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("綁定失敗");
close(sockfd);
return -1;
}
// 監(jiān)聽端口
if (listen(sockfd, 3) < 0) {
perror("監(jiān)聽失敗");
close(sockfd);
return -1;
}
printf("等待客戶端連接...\n");
int new_sock;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
// 接受客戶端連接
if ((new_sock = accept(sockfd, (struct sockaddr*)&client_addr, &client_len)) < 0) {
perror("接受連接失敗");
close(sockfd);
return -1;
}
printf("客戶端已連接。等待數(shù)據(jù)...\n");
// 阻塞式recv:等待客戶端發(fā)送數(shù)據(jù)
while (1) {
printf("發(fā)起 I/O 請求:調(diào)用 recv() 等待數(shù)據(jù)...\n");
ssize_t bytes_received = recv(new_sock, buffer, BUFFER_SIZE - 1, 0); // BUFFER_SIZE -1 保證留出 '\0'
if (bytes_received < 0) {
perror("recv 失敗");
break;
}
if (bytes_received == 0) {
printf("客戶端已斷開連接。\n");
break;
}
buffer[bytes_received] = '\0'; // 確保字符串結(jié)束
printf("收到數(shù)據(jù):%s\n", buffer); // 數(shù)據(jù)到達(dá)并喚醒進(jìn)程
}
close(new_sock);
close(sockfd);
return 0;
}
原理分析:

階段 1: 用戶程序調(diào)用 recv 發(fā)起 I/O 請求(同步 I/O)
背景: 在阻塞 I/O 模式下,用戶程序發(fā)起 I/O 操作時(shí)(比如通過 recv 函數(shù)從套接字讀取數(shù)據(jù)),如果內(nèi)核空間的套接字緩沖區(qū)沒有數(shù)據(jù)準(zhǔn)備好,用戶進(jìn)程會(huì)被阻塞,直到數(shù)據(jù)可用。
步驟 1: 發(fā)起 I/O 請求
ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE, 0);
- 當(dāng)用戶程序調(diào)用
recv函數(shù)時(shí),內(nèi)核會(huì)檢查指定的套接字是否有足夠的數(shù)據(jù)可以讀取。如果套接字的緩沖區(qū)為空,內(nèi)核會(huì)將調(diào)用進(jìn)程從TASK_RUNNING狀態(tài)切換到TASK_INTERRUPTIBLE狀態(tài)。
步驟 2: 進(jìn)程切換狀態(tài)并阻塞
- 進(jìn)程狀態(tài)的切換意味著 CPU 將會(huì)把當(dāng)前進(jìn)程放入 進(jìn)程等待隊(duì)列,并且該進(jìn)程不再占用 CPU 資源。系統(tǒng)調(diào)度程序會(huì)選擇其他可以運(yùn)行的進(jìn)程來執(zhí)行,這就是“進(jìn)程阻塞”的表現(xiàn)。
- 被阻塞的進(jìn)程會(huì)被加入到該套接字的等待隊(duì)列中,等待數(shù)據(jù)的到來。一旦數(shù)據(jù)可用,進(jìn)程就會(huì)被喚醒并繼續(xù)執(zhí)行。
重要的概念:
- 阻塞 I/O 并不意味著阻塞 CPU,它只會(huì)使進(jìn)程切換到阻塞狀態(tài),讓出 CPU 的控制權(quán),其他進(jìn)程可以繼續(xù)執(zhí)行。
- 進(jìn)程的阻塞狀態(tài)是由內(nèi)核進(jìn)行管理的,通常是
TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,這取決于 I/O 請求的性質(zhì)。TASK_INTERRUPTIBLE狀態(tài)表示進(jìn)程可以響應(yīng)信號中斷,而TASK_UNINTERRUPTIBLE狀態(tài)下進(jìn)程不會(huì)響應(yīng)信號。
階段 2: 網(wǎng)卡收到數(shù)據(jù)包并喚醒進(jìn)程
背景: 數(shù)據(jù)包通過網(wǎng)絡(luò)接口卡(NIC)到達(dá)時(shí),內(nèi)核通過硬件中斷機(jī)制將數(shù)據(jù)拷貝到內(nèi)核空間。內(nèi)核會(huì)將這些數(shù)據(jù)存入相應(yīng)的套接字緩沖區(qū),進(jìn)而喚醒之前等待的進(jìn)程。
步驟 1: 網(wǎng)卡接收數(shù)據(jù)包
- 網(wǎng)絡(luò)接口卡(NIC)通過 DMA(直接內(nèi)存訪問)機(jī)制將收到的網(wǎng)絡(luò)數(shù)據(jù)包直接拷貝到內(nèi)核空間的環(huán)形緩沖區(qū)(RingBuffer)中,這一過程在硬件和內(nèi)核中自動(dòng)完成。
- 一旦數(shù)據(jù)包被成功接收,NIC 會(huì)觸發(fā)硬件中斷,通知操作系統(tǒng)數(shù)據(jù)包已到達(dá)。
步驟 2: 數(shù)據(jù)包復(fù)制到套接字接收緩沖區(qū)
- 中斷處理程序會(huì)將數(shù)據(jù)包從環(huán)形緩沖區(qū)復(fù)制到對應(yīng)套接字的接收緩沖區(qū)。
- 在這時(shí),內(nèi)核會(huì)檢查是否有任何進(jìn)程(如在第一階段被阻塞的進(jìn)程)正在等待該套接字的接收數(shù)據(jù)。如果有,內(nèi)核會(huì)喚醒等待隊(duì)列中的進(jìn)程,使其恢復(fù)執(zhí)行。
步驟 3: 喚醒等待的進(jìn)程
- 被喚醒的進(jìn)程會(huì)重新進(jìn)入
TASK_RUNNING狀態(tài),系統(tǒng)調(diào)度器會(huì)將該進(jìn)程從等待隊(duì)列中移除,并將其放回可運(yùn)行隊(duì)列。 - 用戶進(jìn)程被恢復(fù)執(zhí)行后,
recv函數(shù)會(huì)從套接字接收緩沖區(qū)中讀取數(shù)據(jù),并將數(shù)據(jù)返回到用戶空間的緩沖區(qū)(例如buffer)。
步驟 4: 用戶程序完成 I/O 操作
- 一旦數(shù)據(jù)被成功讀取,
recv函數(shù)會(huì)返回讀取的字節(jié)數(shù),用戶程序可以繼續(xù)處理接收到的數(shù)據(jù)。
2.非阻塞IO(Non-blocking I/O)
首先我們先來認(rèn)識一個(gè)新的函數(shù)fcntl():
2.1fcntl函數(shù)的基本用法
fcntl 函數(shù)主要用于對已打開的文件描述符進(jìn)行控制操作,以改變文件描述符的屬性或獲取其狀態(tài)。
fcntl函數(shù)通常在處理低級別的文件I/O時(shí)使用,例如復(fù)制文件描述符、獲取和設(shè)置文件描述符標(biāo)志、獲取和設(shè)置文件狀態(tài)標(biāo)志、文件鎖定(共享鎖和排他鎖)、設(shè)置文件所有者。
fcntl函數(shù)的基本原型如下:
#include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ );
參數(shù)說明:
fd是文件描述符。cmd是要執(zhí)行的命令。arg可選參數(shù),可能還需要一個(gè)或多個(gè)附加參數(shù),具體取決于cmd的值。
返回值:
- 成功時(shí),根據(jù)
cmd的不同,可能需要一個(gè)或多個(gè)附加參數(shù)。 - 失敗時(shí),返回
-1,并設(shè)置errno以只是錯(cuò)誤原因。
fcntl的常見命令
復(fù)制文件描述符
示例代碼:
F_DUPFD:返回一個(gè)大于或等于指定值的最小可用文件描述符,該描述符與原來的描述符指向同一個(gè)文件。F_DUPFD_CLOEXEC:與F_DUPFD類似,但新描述符設(shè)置FD_CLOEXEC標(biāo)志。
?
#include <fcntl.h> // 包含 fcntl 相關(guān)的頭文件
#include <unistd.h> // 包含 close 和 open 函數(shù)的頭文件
#include <iostream> // 包含輸入輸出流的頭文件
int main() {
int old_fd = open("example.txt", O_RDONLY); // 打開文件用于只讀
if (old_fd == -1) {
std::perror("打開文件失?。?); // 輸出錯(cuò)誤信息
return -1;
}
int new_fd = fcntl(old_fd, F_DUPFD, 3); // 復(fù)制文件描述符,要求新的描述符至少為 3
if (new_fd == -1) {
std::perror("復(fù)制文件描述符失敗:");
close(old_fd);
return -1;
}
std::cout << "新的文件描述符:" << new_fd << std::endl; // 輸出新的文件描述符
close(old_fd); // 關(guān)閉舊的文件描述符
close(new_fd); // 關(guān)閉新的文件描述符
return 0;
}
?獲取/設(shè)置文件描述符標(biāo)志
標(biāo)志:
- 示例代碼:
?
#include <fcntl.h> // 包含 fcntl 相關(guān)的頭文件
#include <unistd.h> // 包含 close 和 open 函數(shù)的頭文件
#include <iostream> // 包含輸入輸出流的頭文件
int main() {
int fd = open("example.txt", O_RDONLY); // 打開文件用于只讀
if (fd == -1) {
std::perror("打開文件失?。?); // 輸出錯(cuò)誤信息
return -1;
}
int flags = fcntl(fd, F_GETFD); // 獲取文件描述符的標(biāo)志
if (flags == -1) {
std::perror("獲取文件描述符標(biāo)志失?。?);
close(fd);
return -1;
}
// 設(shè)置 FD_CLOEXEC 標(biāo)志,使文件描述符在執(zhí)行 exec 時(shí)關(guān)閉
if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
std::perror("獲取文件描述符標(biāo)志失敗:");
close(fd);
return -1;
}
std::cout << "設(shè)置 FD_CLOEXEC 前的標(biāo)志:" << flags << std::endl; // 輸出設(shè)置前的標(biāo)志
std::cout << "設(shè)置 FD_CLOEXEC 后的標(biāo)志:" << new_flags << std::endl; // 輸出設(shè)置后的標(biāo)志
close(fd); // 關(guān)閉文件描述符
return 0;
}
?F_GETFD:獲取文件描述符的標(biāo)志。F_SETFD:設(shè)置文件描述符的標(biāo)志。FD_CLOEXEC:設(shè)置或取消close-on-exec標(biāo)志,該標(biāo)志表示在執(zhí)行exec系列函數(shù)時(shí)關(guān)閉該描述符。O_NONBLOCK:非阻塞模式。O_APPEND:追加模式。O_ASYNC:啟用信號驅(qū)動(dòng) I/O。O_DIRECT:直接 I/O(盡量繞過緩沖區(qū)緩存)。O_NOATIME:不更新文件的訪問時(shí)間。
獲取/設(shè)置文件狀態(tài)標(biāo)志
- 示例代碼:
?
#include <fcntl.h> // 包含 fcntl 相關(guān)的頭文件
#include <unistd.h> // 包含 close 和 open 函數(shù)的頭文件
#include <iostream> // 包含輸入輸出流的頭文件
int main() {
int fd = open("example.txt", O_RDONLY); // 打開文件用于只讀
if (fd == -1) {
std::perror("打開文件失敗:"); // 輸出錯(cuò)誤信息
return -1;
}
int flags = fcntl(fd, F_GETFL); // 獲取文件狀態(tài)標(biāo)志
if (flags == -1) {
std::perror("獲取文件狀態(tài)標(biāo)志失?。?);
close(fd);
return -1;
}
// 設(shè)置 O_NONBLOCK 標(biāo)志,使文件描述符處于非阻塞模式
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
int new_flags = fcntl(fd, F_GETFL); // 再次獲取文件狀態(tài)標(biāo)志
if (new_flags == -1) {
std::perror("獲取文件狀態(tài)標(biāo)志失?。?);
close(fd);
return -1;
}
std::cout << "設(shè)置 O_NONBLOCK 前的標(biāo)志:" << flags << std::endl; // 輸出設(shè)置前的標(biāo)志
std::cout << "設(shè)置 O_NONBLOCK 后的標(biāo)志:" << new_flags << std::endl; // 輸出設(shè)置后的標(biāo)志
close(fd); // 關(guān)閉文件描述符
return 0;
}
?F_GETFL:獲取文件狀態(tài)標(biāo)志。F_SETFL:設(shè)置文件狀態(tài)標(biāo)志。
2.2 阻塞模式 vs 非阻塞模式
阻塞模式:(上面已有介紹)
- 默認(rèn)情況下,文件描述符處于阻塞模式。
- 當(dāng)你嘗試從一個(gè)沒有數(shù)據(jù)可讀的文件描述符中讀取數(shù)據(jù),或者嘗試向一個(gè)寫緩沖區(qū)已滿的文件描述符寫入數(shù)據(jù)時(shí),進(jìn)程會(huì)被阻塞,直到操作可以完成。
非阻塞模式:
非阻塞I/O模型下,I/O請求立即返回,不會(huì)阻塞程序。如果數(shù)據(jù)沒有準(zhǔn)備好,程序會(huì)收到錯(cuò)誤或“沒有數(shù)據(jù)”的通知,之后可以重新嘗試。
- 如果設(shè)置了
O_NONBLOCK標(biāo)志,那么當(dāng)嘗試讀取沒有數(shù)據(jù)可讀或?qū)懢彌_區(qū)已滿的情況下,不會(huì)阻塞進(jìn)程,而是立即返回一個(gè)錯(cuò)誤。 - 典型的錯(cuò)誤碼是
EAGAIN或EWOULDBLOCK,這表明操作不能立即完成。 - 優(yōu)點(diǎn):線程可以進(jìn)行其他任務(wù),而不是等待I/O操作完成。
- 缺點(diǎn):需要不斷輪詢(polling)I/O操作,增加了額外的CPU開銷。
代碼示例:
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <cstring>
using namespace std;
#define PORT 8080
#define BUFFER_SIZE 1024
// 設(shè)置套接字為非阻塞模式
int setNonBlocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
cerr << "獲取文件狀態(tài)標(biāo)志失敗: " << strerror(errno) << endl;
return -1;
}
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
cerr << "設(shè)置非阻塞模式失敗: " << strerror(errno) << endl;
return -1;
}
return 0;
}
int main() {
// 創(chuàng)建套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
cerr << "創(chuàng)建套接字失敗: " << strerror(errno) << endl;
return -1;
}
// 設(shè)置套接字為非阻塞
if (setNonBlocking(server_fd) == -1) {
close(server_fd);
return -1;
}
// 綁定地址和端口
struct sockaddr_in address;
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) {
cerr << "綁定地址和端口失敗: " << strerror(errno) << endl;
close(server_fd);
return -1;
}
// 監(jiān)聽連接
if (listen(server_fd, 3) < 0) {
cerr << "監(jiān)聽連接失敗: " << strerror(errno) << endl;
close(server_fd);
return -1;
}
cout << "服務(wù)器正在監(jiān)聽端口 " << PORT << endl;
// 接受客戶端連接
struct sockaddr_in client_address;
socklen_t addr_len = sizeof(client_address);
int client_fd = -1;
while (client_fd == -1) {
client_fd = accept(server_fd, (struct sockaddr *)&client_address, &addr_len);
if (client_fd == -1) {
// 如果沒有連接,errno 會(huì)被設(shè)置為 EWOULDBLOCK 或 EAGAIN
if (errno == EWOULDBLOCK || errno == EAGAIN) {
cout << "沒有可用連接,繼續(xù)等待..." << endl;
sleep(1); // 延時(shí) 1s 后重試
} else {
cerr << "接受客戶端連接失敗: " << strerror(errno) << endl;
close(server_fd);
return -1;
}
}
}
cout << "接受到客戶端連接" << endl;
// 設(shè)置客戶端套接字為非阻塞
if (setNonBlocking(client_fd) == -1) {
close(server_fd);
close(client_fd);
return -1;
}
char buffer[BUFFER_SIZE];
int bytes_read;
while (true) {
// 嘗試讀取客戶端數(shù)據(jù)
bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
if (bytes_read == -1) {
// 如果沒有數(shù)據(jù),errno 會(huì)被設(shè)置為 EWOULDBLOCK 或 EAGAIN
if (errno == EWOULDBLOCK || errno == EAGAIN) {
cout << "沒有可用數(shù)據(jù),執(zhí)行其他任務(wù)..." << endl;
usleep(100000); // 假設(shè)其他任務(wù)的延時(shí)(100ms)
} else {
cerr << "接收數(shù)據(jù)失敗: " << strerror(errno) << endl;
break;
}
} else if (bytes_read == 0) {
// 客戶端關(guān)閉連接
cout << "客戶端已斷開連接" << endl;
break;
} else {
// 成功接收到數(shù)據(jù)
buffer[bytes_read] = '\0'; // 確保接收到的字符串以 '\0' 結(jié)尾
cout << "收到數(shù)據(jù): " << buffer << endl;
}
}
// 關(guān)閉連接
close(client_fd);
close(server_fd);
return 0;
}
原理分析:

非阻塞 I/O/階段1:用戶程序調(diào)用 recv 發(fā)起 I/O 請求
- 在循環(huán)中調(diào)用
recv,如果沒有數(shù)據(jù),recv會(huì)立即返回-1并設(shè)置errno為EWOULDBLOCK或EAGAIN。 - 程序檢測到
EWOULDBLOCK錯(cuò)誤后,執(zhí)行其他任務(wù)(如打印信息和休眠),然后繼續(xù)嘗試接收數(shù)據(jù)。 - 這對應(yīng)于用戶程序調(diào)用
recv發(fā)起 I/O 請求,讀取 socket 緩沖區(qū)數(shù)據(jù)。由于 socket 緩沖區(qū)沒有就緒數(shù)據(jù)包,非阻塞 I/Orecv直接返回EWOULDBLOCK錯(cuò)誤碼,用戶如果一直調(diào)用recv函數(shù)則一直返回EWOULDBLOCK錯(cuò)誤碼,直到數(shù)據(jù)準(zhǔn)備好。
非阻塞 I/O/階段2:數(shù)據(jù)到達(dá)并喚醒進(jìn)程
- 當(dāng)數(shù)據(jù)通過網(wǎng)絡(luò)接口卡(NIC)接收并被內(nèi)核處理后,數(shù)據(jù)被復(fù)制到套接字接收緩沖區(qū)。
- 隨后,
recv調(diào)用會(huì)成功讀取數(shù)據(jù),返回接收到的字節(jié)數(shù),程序繼續(xù)處理數(shù)據(jù)。 - 這與阻塞 I/O 的階段2 相同,即網(wǎng)卡收到數(shù)據(jù)包并喚醒進(jìn)程,包括數(shù)據(jù)復(fù)制到套接字接收緩沖區(qū)和喚醒等待的進(jìn)程。
拓展知識:Ctrl+C 和 Ctrl+D:
| 組合鍵 | 作用 | 退出狀態(tài)碼 | 典型場景 |
|---|---|---|---|
| Ctrl+C | 發(fā)送 SIGINT 信號給前臺進(jìn)程,請求程序終止 | 通常為 130(128 + 2) | 用于中斷正在運(yùn)行的程序 |
| Ctrl+D | 發(fā)送 EOF 信號,表示輸入結(jié)束 | 通常為 0 | 用于結(jié)束輸入或退出交互式 shell |
3.IO多路復(fù)用(Multiplexing)
IO多路復(fù)用是一種高效的IO處理方式,它可以讓一個(gè)進(jìn)程同時(shí)監(jiān)控多個(gè)文件描述符,當(dāng)其中任意一個(gè)文件描述符就緒時(shí),就可以進(jìn)行相應(yīng)的IO操作。
相比于傳統(tǒng)的阻塞IO和非阻塞IO,IO復(fù)用可以打打提高IO效率,減少CPU資源的浪費(fèi)。
在Linux中,常用的IO復(fù)用模型有select、poll、epoll等。
IO復(fù)用模型請求由用戶程序發(fā)起,所以IO復(fù)用模型為同步IO。
3.1 IO復(fù)用select模型
3.1.1 認(rèn)識select函數(shù)
系統(tǒng)調(diào)用select()會(huì)一直阻塞,直到一個(gè)或多個(gè)文件描述符集合成為就緒態(tài)。
它通過傳遞三個(gè)文件描述符集合(讀集合、寫集合和異常集合)給內(nèi)核,內(nèi)核會(huì)在這些集合中等待任意一個(gè)文件描述符就緒。
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
參數(shù):
nfds:設(shè)為比下面三個(gè)文件描述符集合中所包含的最大文件描述符號 +1。readfds:是用來檢測輸入是否就緒的文件描述符集合。writefds:是用來檢測輸出是否就緒的文件描述符集合。exceptfds:是用來檢測異常情況是否發(fā)生的文件描述符集合。timeout:超時(shí)時(shí)間,這個(gè)參數(shù)控制著select()的阻塞行為。
該參數(shù)可指定為NULL,此時(shí)select()會(huì)一直阻塞。又或者指向一個(gè)timeval結(jié)構(gòu)體。
如果結(jié)構(gòu)體timeval的兩個(gè)域都為0的話,此時(shí)select()不會(huì)阻塞。它只是簡單低輪詢指定的文件描述符集合,查看其中是否有就緒的文件描述符并立即返回。
否則,timeval的兩個(gè)域都不為0的話,timeval將為select()指定一個(gè)等待時(shí)間上限值。代表多久之后返回,即超時(shí)時(shí)間。
返回值:
- 如果有文件描述符變得可讀、可寫或發(fā)生異常,返回值為準(zhǔn)備就緒的文件描述符的數(shù)目。
- 如果
timeout到達(dá),返回值為 0。這種情況下每個(gè)返回的文件描述符集合將被清空。 - 如果發(fā)生錯(cuò)誤,返回值為 -1。
timeval結(jié)構(gòu)體原型:
struct timeval
{
__time_t tv_sec; /* 秒. */
__suseconds_t tv_usec; /* 微秒. */
};
當(dāng)timeout設(shè)為NULL,或其指向的結(jié)構(gòu)體字段非零時(shí),select()將阻塞直到下列事件發(fā)生:
readfds、writefds或exceptfds中指定的文件描述符中至少有一個(gè)成為就緒態(tài)。- 該調(diào)用被信號處理中斷。
timeout中指定的時(shí)間上限已超時(shí)。
關(guān)于 fd_set:fd_set 是一個(gè)位圖結(jié)構(gòu),用于標(biāo)識一組文件描述符(通常是網(wǎng)絡(luò)套接字)。fd_set 的定義通常是由底層實(shí)現(xiàn)細(xì)節(jié)決定的,但通常它是一個(gè)足夠大的位字段,能夠容納系統(tǒng)中可能的最大文件描述符值。fd_set 的主要用途包括:
- 輸入?yún)?shù):告訴
select哪些文件描述符應(yīng)該被監(jiān)控。你可以在調(diào)用select之前,通過調(diào)用FD_ZERO清空集合,然后用FD_SET向集合中添加感興趣的文件描述符。 - 輸出參數(shù):
select返回后,被監(jiān)控的文件描述符集合中,就緒的描述符對應(yīng)的比特位會(huì)被設(shè)置為1。你可以通過FD_ISSET宏來檢查特定的文件描述符是否已經(jīng)就緒。
3.1.2fd_set類型和宏
通常數(shù)據(jù)類型fd_set以位掩碼的形式來實(shí)現(xiàn)。但是,我們并不需要知道這些細(xì)節(jié),因?yàn)樗嘘P(guān)于文件描述符集合的操作都是通過四個(gè)宏完成的:
#include <sys/select.h> void FD_ZERO(fd_set *set); /* 將fdset所指向的集合初始化為空 */ void FD_SET(int fd, fd_set *set); /* 將文件描述符fd添加到由fdset所指向的集合中 */ void FD_CLR(int fd, fd_set *set); /* 將文件描述符fd從fdset所指向的集合中移除 */ int FD_ISSET(int fd, fd_set *set); /* 檢查已連接的套接字是否有數(shù)據(jù)可讀 */
參數(shù)readfds、writefds和exceptfds所指向的結(jié)構(gòu)體都是保存結(jié)果值的地方。
在調(diào)用select()之前,這些指向的結(jié)構(gòu)體必須初始化(通過FD_ZERO()和FD_SET()),以包含我們感興趣的文件描述符集合。
之后select()調(diào)用會(huì)修改這些結(jié)構(gòu)體,當(dāng)select()返回時(shí),它們包含的就是已處于就緒態(tài)的文件描述符集合了(由于這些結(jié)構(gòu)體會(huì)在調(diào)用中被修改,如果在循環(huán)中重復(fù)調(diào)用select(),我們必須保證每次都要重新初始化它們)。
之后這些結(jié)構(gòu)體可以通過FD_ISSET()來檢查是否有數(shù)據(jù)可讀。
3.1.3 select特點(diǎn)
文件描述符上限:
select函數(shù)能同時(shí)等待的文件描述符(fd)是有上限的。- 這個(gè)上限在很多操作系統(tǒng)中默認(rèn)為 1024,可以通過調(diào)整內(nèi)核參數(shù)來增加這個(gè)上限,但即使增加,仍然存在一個(gè)固定的上限。
- 這是因?yàn)?
select使用位圖來表示文件描述符集合,而位圖的大小是固定的。 - 如果需要更大的文件描述符數(shù)量,需要修改內(nèi)核或使用其他 I/O 多路復(fù)用技術(shù),如
epoll。
維護(hù)合法的文件描述符:
- 使用
select時(shí),需要維護(hù)一個(gè)第三方數(shù)組來保存合法的文件描述符。 - 這是因?yàn)樵趯?shí)際應(yīng)用中,文件描述符可能會(huì)動(dòng)態(tài)增加或減少,而
select本身并不提供任何機(jī)制來自動(dòng)跟蹤這些變化。因此,程序員需要自己維護(hù)這樣一個(gè)列表,確保每次調(diào)用select時(shí)提供的文件描述符集合是最新的。
輸入輸出型參數(shù):
select的參數(shù)是輸入輸出型的。這意味著在調(diào)用select之前,你需要設(shè)置好各個(gè)集合(readfds、writefds、exceptfds),告訴內(nèi)核你需要監(jiān)控哪些文件描述符的狀態(tài)。而在select返回后,你需要檢查這些集合,確定哪些文件描述符就緒。- 這種模式要求在每次調(diào)用
select之前重置集合,并在調(diào)用之后檢查集合,這增加了用戶的負(fù)擔(dān)。
第一個(gè)參數(shù)是最大 fd+1:
select的第一個(gè)參數(shù)nfds是最大的文件描述符值加 1。- 這是因?yàn)?
select在內(nèi)核中需要遍歷從 0 到最大文件描述符值的范圍,來檢查哪些文件描述符處于就緒狀態(tài)。 - 如果最大文件描述符是
n,那么實(shí)際上需要檢查的范圍是從0到n,共n+1個(gè)文件描述符。 - 例如,如果最大的文件描述符是 5,那么
select實(shí)際上需要檢查 0 到 5 的文件描述符,共 6 個(gè)。
位圖的使用:
select使用位圖(fd_set)來表示文件描述符集合。位圖是一種高效的數(shù)據(jù)結(jié)構(gòu),每個(gè)比特位代表一個(gè)文件描述符的狀態(tài)。- 當(dāng)用戶向內(nèi)核傳遞文件描述符集合時(shí),實(shí)際上是傳遞了一個(gè)位圖。內(nèi)核在監(jiān)控過程中會(huì)修改這個(gè)位圖,將就緒的文件描述符標(biāo)記出來。
- 當(dāng)
select返回后,用戶可以根據(jù)位圖來確定哪些文件描述符已經(jīng)準(zhǔn)備好。這種方式簡化了內(nèi)核與用戶空間之間的交互,但也帶來了額外的內(nèi)存拷貝成本。
代碼示例:
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
#include <fcntl.h>
#include <sys/select.h>
#define SERVER_PORT 8080
#define MAX_CLIENTS 10
#define BUF_SIZE 1024
void set_socket_non_blocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
int server_fd, client_fd, max_fd, new_socket;
struct sockaddr_in server_addr;
char buffer[BUF_SIZE];
fd_set read_fds, master_fds;
struct timeval timeout;
// 創(chuàng)建 TCP socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("創(chuàng)建套接字失敗");
return -1;
}
// 設(shè)置服務(wù)器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(SERVER_PORT);
// 綁定套接字
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("綁定失敗");
return -1;
}
// 開始監(jiān)聽客戶端連接
if (listen(server_fd, MAX_CLIENTS) == -1) {
perror("監(jiān)聽失敗");
return -1;
}
// 設(shè)置 server_fd 為非阻塞
set_socket_non_blocking(server_fd);
// 初始化 fd_set
FD_ZERO(&master_fds);
FD_SET(server_fd, &master_fds);
max_fd = server_fd;
while (true) {
// 將 master_fds 賦值給 read_fds,因?yàn)?select() 會(huì)修改 read_fds
read_fds = master_fds;
// 設(shè)置超時(shí),阻塞 5 秒
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// 調(diào)用 select() 進(jìn)行 I/O 多路復(fù)用
int activity = select(max_fd + 1, &read_fds, nullptr, nullptr, &timeout);
if (activity == -1) {
perror("select 錯(cuò)誤");
break;
} else if (activity == 0) {
std::cout << "沒有活動(dòng),繼續(xù)等待...\n";
continue;
}
// 遍歷所有文件描述符
for (int fd = 0; fd <= max_fd; ++fd) {
// 如果 fd 是活動(dòng)的文件描述符
if (FD_ISSET(fd, &read_fds)) {
if (fd == server_fd) {
// 處理新的客戶端連接
if ((new_socket = accept(server_fd, nullptr, nullptr)) == -1) {
perror("接受連接失敗,繼續(xù)...");
continue;
}
std::cout << "新連接,套接字 fd: " << new_socket << "\n";
// 設(shè)置新的套接字為非阻塞
set_socket_non_blocking(new_socket);
// 將新的客戶端套接字加入 fd_set
FD_SET(new_socket, &master_fds);
if (new_socket > max_fd) {
max_fd = new_socket;
}
} else {
// 處理已連接客戶端的 I/O 操作
int bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read == 0) {
// 客戶端關(guān)閉連接
std::cout << "客戶端斷開連接,套接字 fd: " << fd << "\n";
close(fd);
FD_CLR(fd, &master_fds);
} else if (bytes_read > 0) {
// 處理收到的數(shù)據(jù)
buffer[bytes_read] = '\0';
std::cout << "收到來自客戶端 " << fd << " 的數(shù)據(jù): " << buffer << "\n";
// 回送數(shù)據(jù)給客戶端
send(fd, buffer, bytes_read, 0);
} else {
perror("讀取錯(cuò)誤");
close(fd);
FD_CLR(fd, &master_fds);
}
}
}
}
}
close(server_fd);
return 0;
}
原理分析:

3.2 IO復(fù)用poll模型
poll是一種改進(jìn)的 IO 多路復(fù)用模型,它解決了select模型中的一些局限性,尤其是在處理大量文件描述符和提高性能方面。
poll與select類似,但它通過使用pollfd結(jié)構(gòu)體數(shù)組來管理文件描述符,而不是像select那樣依賴位圖(bitmask)。這使得poll在某些方面更加靈活和高效。
3.2.1poll的基本使用方法
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數(shù)說明:
fds:指向pollfd結(jié)構(gòu)體數(shù)組的指針,每個(gè)結(jié)構(gòu)體代表一個(gè)需要監(jiān)視的文件描述符及其事件。nfds:pollfd數(shù)組中元素的數(shù)量,即有多少個(gè)文件描述符需要被監(jiān)視。timeout:等待事件的超時(shí)時(shí)間,單位為毫秒。-1表示無限等待直到至少有一個(gè)文件描述符準(zhǔn)備好;0表示非阻塞;如果是正數(shù),則表示最大等待的時(shí)間長度。
pollfd 結(jié)構(gòu)體:
struct pollfd {
int fd; // 要監(jiān)視的文件描述符
short events; // 監(jiān)視的事件
short revents; // 實(shí)際發(fā)生的事件
};
事件類型常用值:
POLLIN:數(shù)據(jù)可讀。POLLOUT:可以寫數(shù)據(jù)。POLLERR:發(fā)生錯(cuò)誤。POLLHUP:掛斷。POLLPRI:高優(yōu)先級數(shù)據(jù)可讀。
3.2.2poll解決select存在的問題
poll相較于select在多個(gè)方面進(jìn)行了改進(jìn),解決了select模型中的一些關(guān)鍵限制。以下是poll解決select存在問題的主要方面:
- 無需重新設(shè)定參數(shù):與
select每次調(diào)用前都需要重新初始化監(jiān)視的文件描述符集合不同,poll使用一個(gè)獨(dú)立的結(jié)構(gòu)體數(shù)組(pollfd),該數(shù)組在函數(shù)調(diào)用后保持不變,只需更新事件結(jié)果。這使得代碼更簡潔、易于維護(hù)。 - 消除文件描述符數(shù)量的上限:
select受限于系統(tǒng)定義的最大文件描述符數(shù)(如FD_SETSIZE),而poll通過動(dòng)態(tài)管理文件描述符數(shù)組,僅受系統(tǒng)資源和內(nèi)核能力的限制,適合處理大量并發(fā)連接。 - 更高效的事件通知機(jī)制:盡管兩者都可能需要掃描所有文件描述符,
poll得益于其靈活的結(jié)構(gòu)和操作系統(tǒng)的優(yōu)化,在實(shí)際應(yīng)用中通常性能更優(yōu)。 - 支持更多事件類型:相較于
select僅支持基本事件類型,poll支持更多種類的事件,如高優(yōu)先級數(shù)據(jù)和掛斷事件,提供更細(xì)致的監(jiān)控能力。 - 更好的跨平臺兼容性:
poll在多種現(xiàn)代操作系統(tǒng)中的實(shí)現(xiàn)更為一致,簡化了跨平臺應(yīng)用的開發(fā)難度。
代碼示例:
#include <iostream>
#include <vector>
#include <poll.h>
#include <unistd.h>
#include <cstring>
#include <arpa/inet.h>
#include <fcntl.h>
#define PORT 8080
#define MAX_EVENTS 1024
#define BUFFER_SIZE 1024
int set_non_blocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
// 創(chuàng)建監(jiān)聽套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 設(shè)置套接字選項(xiàng)
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
close(server_fd);
exit(EXIT_FAILURE);
}
// 設(shè)置地址結(jié)構(gòu)
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");
close(server_fd);
exit(EXIT_FAILURE);
}
// 開始監(jiān)聽
if (listen(server_fd, 10) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
// 設(shè)置服務(wù)器套接字為非阻塞
if (set_non_blocking(server_fd) < 0) {
perror("set_non_blocking");
close(server_fd);
exit(EXIT_FAILURE);
}
// 初始化pollfd數(shù)組
std::vector<struct pollfd> fds;
struct pollfd server_pollfd;
server_pollfd.fd = server_fd;
server_pollfd.events = POLLIN;
server_pollfd.revents = 0;
fds.push_back(server_pollfd);
std::cout << "服務(wù)器正在端口 " << PORT << " 上監(jiān)聽" << std::endl;
while (true) {
int activity = poll(fds.data(), fds.size(), -1);
if (activity < 0) {
perror("poll error");
break;
}
for (size_t i = 0; i < fds.size(); ++i) {
// 檢測是否有事件發(fā)生
if (fds[i].revents & POLLIN) {
if (fds[i].fd == server_fd) {
// 有新的連接請求
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
continue;
}
// 設(shè)置新套接字為非阻塞
if (set_non_blocking(new_socket) < 0) {
perror("set_non_blocking");
close(new_socket);
continue;
}
// 添加新套接字到pollfd數(shù)組
struct pollfd client_pollfd;
client_pollfd.fd = new_socket;
client_pollfd.events = POLLIN;
client_pollfd.revents = 0;
fds.push_back(client_pollfd);
std::cout << "新連接,socket fd: " << new_socket << std::endl;
} else {
// 處理已連接的客戶端數(shù)據(jù)
char buffer[BUFFER_SIZE];
int valread = read(fds[i].fd, buffer, BUFFER_SIZE);
if (valread <= 0) {
// 客戶端關(guān)閉連接或發(fā)生錯(cuò)誤
close(fds[i].fd);
std::cout << "連接關(guān)閉,socket fd: " << fds[i].fd << std::endl;
fds.erase(fds.begin() + i);
--i;
continue;
}
buffer[valread] = '\0';
std::cout << "收到: " << buffer << " 來自socket fd: " << fds[i].fd << std::endl;
// 回顯數(shù)據(jù)給客戶端
send(fds[i].fd, buffer, valread, 0);
}
}
}
}
// 關(guān)閉所有套接字
for (auto &pfd : fds) {
close(pfd.fd);
}
return 0;
}
原理分析:
poll模型和select非常相似,主要區(qū)別為poll模型把位圖改為鏈表,poll通過鏈表實(shí)現(xiàn)IO復(fù)用,將 socket 注冊 poll_list 鏈表,通過poll系統(tǒng)調(diào)用輪詢鏈表,獲取 socket 事件。
成功獲取到 socket 事件后,poll成功返回,此時(shí)可以通過接收函數(shù)讀取 socket 緩沖區(qū)數(shù)據(jù)。

3.3 IO復(fù)用epoll模型
epoll是Linux下高效的IO多路復(fù)用機(jī)制,相較于select和poll,epoll在處理大量并發(fā)連接時(shí)具有更好的性能和擴(kuò)展性。
epoll的工作機(jī)制主要包括下面幾個(gè)步驟:
3.3.1 epoll的基本使用方法
- 創(chuàng)建 epoll 實(shí)例
使用 epoll_create 或 epoll_create1 系統(tǒng)調(diào)用創(chuàng)建一個(gè) epoll 實(shí)例,該調(diào)用返回一個(gè) epoll 文件描述符(epoll_fd)。這個(gè)文件描述符用于后續(xù)的事件注冊和事件等待。
#include <sys/epoll.h> int epoll_create1(int flags);
參數(shù):
flags: 可以是0或EPOLL_CLOEXEC。如果設(shè)置為EPOLL_CLOEXEC,則會(huì)在新創(chuàng)建的文件描述符上設(shè)置“執(zhí)行時(shí)關(guān)閉”標(biāo)志(close-on-exec),這意味著當(dāng)調(diào)用exec系列函數(shù)執(zhí)行新程序時(shí),這個(gè)文件描述符會(huì)自動(dòng)關(guān)閉,防止不必要的資源泄漏。- 返回值:成功時(shí)返回一個(gè)非負(fù)整數(shù)的文件描述符,失敗時(shí)返回
-1并設(shè)置相應(yīng)的errno錯(cuò)誤碼。
注冊感興趣的事件
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl 是 epoll API 的核心組成部分之一,它用于控制 epoll 實(shí)例中的文件描述符集合。通過這個(gè)函數(shù),可以向 epoll 實(shí)例中添加、修改或刪除感興趣的文件描述符及其對應(yīng)的事件類型。
使用 epoll_ctl 系統(tǒng)調(diào)用向 epoll 實(shí)例中注冊需要監(jiān)控的文件描述符及其感興趣的事件類型(如可讀、可寫等)
參數(shù):
epfd:epoll_create1或epoll_create返回的epoll實(shí)例的文件描述符。
op: 操作類型,可以是以下三種之一:
EPOLL_CTL_ADD: 向epoll實(shí)例中添加新的文件描述符,并注冊感興趣的事件。EPOLL_CTL_MOD: 修改已存在的文件描述符上的事件類型。EPOLL_CTL_DEL: 從epoll實(shí)例中移除指定的文件描述符,不再監(jiān)聽其上的任何事件。fd: 需要操作的目標(biāo)文件描述符。event: 如果op不是EPOLL_CTL_DEL,則需要提供一個(gè)指向epoll_event結(jié)構(gòu)體的指針,用于指定事件類型和用戶數(shù)據(jù)。
返回值:成功時(shí)返回 0;失敗時(shí)返回 -1 并設(shè)置相應(yīng)的 errno 錯(cuò)誤碼。
事件結(jié)構(gòu)體 epoll_event
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
在使用 epoll_ctl 之前,我們需要定義一個(gè) epoll_event 結(jié)構(gòu)體來描述要監(jiān)控的事件和關(guān)聯(lián)的數(shù)據(jù)。這個(gè)結(jié)構(gòu)體有兩個(gè)主要成員:
events:這是一個(gè)位掩碼,指定了我們對文件描述符感興趣的事件類型。
常見的事件類型包括:
EPOLLIN:文件描述符可讀(例如,socket接收緩沖區(qū)中有數(shù)據(jù))。EPOLLOUT:文件描述符可寫(例如,socket發(fā)送緩沖區(qū)有空間)。EPOLLET:邊緣觸發(fā)模式(Edge Triggered),意味著只有狀態(tài)變化時(shí)才會(huì)觸發(fā)事件。EPOLLRDHUP:遠(yuǎn)程端關(guān)閉連接或半關(guān)閉連接(TCP-specific)。
data:這是一個(gè)聯(lián)合體,通常用來存儲(chǔ)與該文件描述符相關(guān)的用戶數(shù)據(jù)。最常見的做法是使用 data.fd 來存儲(chǔ)文件描述符本身,以便在事件發(fā)生時(shí)能夠快速識別出哪個(gè)描述符觸發(fā)了事件。
示例代碼:
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 監(jiān)聽可讀事件,采用邊緣觸發(fā)
event.data.fd = sock_fd; // 將sock_fd綁定到event.data.fd
// 使用EPOLL_CTL_ADD操作將sock_fd加入epoll實(shí)例
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event) == -1) {
perror("epoll_ctl: add");
exit(EXIT_FAILURE);
}
等待事件的發(fā)生
使用 epoll_wait 系統(tǒng)調(diào)用阻塞等待注冊的事件發(fā)生。當(dāng)有事件就緒時(shí),epoll_wait 返回就緒事件的數(shù)量,并提供epoll_event數(shù)組中填充準(zhǔn)備好的事件信息。
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
參數(shù):
epfd: 指向epoll實(shí)例的文件描述符。events: 一個(gè)指向epoll_event結(jié)構(gòu)數(shù)組的指針,用于存儲(chǔ)已就緒的事件。maxevents: 表示events數(shù)組的最大大小,即一次最多能返回多少個(gè)事件。timeout: 超時(shí)時(shí)間(毫秒)。如果設(shè)置為-1,則無限期等待;如果設(shè)置為0,則立即返回,即使沒有事件發(fā)生;如果設(shè)置為正數(shù),則表示最長等待的時(shí)間。
返回值:成功時(shí)返回已就緒的文件描述符的數(shù)量;如果沒有事件發(fā)生且超時(shí)到期,則返回 0;出錯(cuò)時(shí)返回 -1 并設(shè)置 errno。
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (n == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < n; i++) {
if (events[i].events & EPOLLIN) {
// 處理可讀事件
}
// 處理其他事件類型
}
處理事件
根據(jù)就緒事件的類型,執(zhí)行相應(yīng)的 I/O 操作,如讀取數(shù)據(jù)、寫入數(shù)據(jù)或關(guān)閉連接等。在邊緣觸發(fā)模式下,必須一次性將所有可讀或可寫的數(shù)據(jù)處理完畢,否則可能會(huì)錯(cuò)過后續(xù)的事件通知。
關(guān)閉 epoll 實(shí)例
當(dāng)不再需要 epoll 實(shí)例時(shí),使用 close 關(guān)閉 epoll 文件描述符,釋放資源。
close(epoll_fd);
3.3.2 epoll原理
- 事件就緒的定義:底層的IO條件滿足了,可以進(jìn)行某種IO行為了,就叫做事件就緒。
select、poll和epoll的工作方式都是基于“等待”到“IO就緒”的事件通知機(jī)制。它們通過不同的數(shù)據(jù)結(jié)構(gòu)和算法實(shí)現(xiàn)對文件描述符的高效管理和事件通知。 - 紅黑樹:
epoll內(nèi)部使用紅黑樹來管理事件的注冊和監(jiān)控。紅黑樹作為一種自平衡的二叉搜索樹,能夠高效的執(zhí)行增、刪、改、查操作。紅黑樹的使用大大提高了epoll在大量文件描述符管理中的效率,尤其是在處理動(dòng)態(tài)注冊和插銷事件時(shí)。 - 回調(diào)通知機(jī)制:當(dāng)
epoll監(jiān)聽的套接字上有數(shù)據(jù)可讀或可寫時(shí),內(nèi)核會(huì)通過回調(diào)機(jī)制通知用戶進(jìn)程。這種機(jī)制能夠精確地通知程序哪些文件描述符上的事件發(fā)生了,而不需要每次都循環(huán)遍歷檢查數(shù)據(jù)是否到達(dá)以及數(shù)據(jù)該由哪個(gè)進(jìn)程處理。這樣,程序只需關(guān)注那些已經(jīng)就緒的文件描述符,避免了不必要的輪詢。
3.3.3 水平觸發(fā)(LT)模式 vs 邊緣觸發(fā)(ET)模式
epoll提供了兩種主要的事件觸發(fā)模式:水平觸發(fā)(LT) vs 邊緣觸發(fā)(ET)。它們決定了在文件描述符的狀態(tài)發(fā)生變化時(shí),內(nèi)核如何通知應(yīng)用程序。理解這兩種觸發(fā)的特點(diǎn)及其使用場景,對高效使用epoll至關(guān)重要。
工作原理
- 水平觸發(fā)(Level-triggered LT):當(dāng)一個(gè)文件描述符準(zhǔn)備好了,內(nèi)核會(huì)一直報(bào)告這個(gè)文件描述符,直到應(yīng)用程序處理這個(gè)事件。
- 邊緣觸發(fā)(Edge-triggered ET):當(dāng)一個(gè)文件描述符從無數(shù)據(jù)變?yōu)橛袛?shù)據(jù)時(shí),內(nèi)核只會(huì)報(bào)告一次,如果應(yīng)用程序沒有及時(shí)處理,那么需要等下一次數(shù)據(jù)變動(dòng)時(shí)才會(huì)再次報(bào)告。
水平觸發(fā)和邊緣觸發(fā)的比較
| 特性 | 水平觸發(fā)(LT) | 邊緣觸發(fā)(ET) |
|---|---|---|
| 通知次數(shù) | 只要事件未被處理完,內(nèi)核會(huì)持續(xù)通知。 | 只會(huì)在文件描述符的狀態(tài)發(fā)生變化時(shí)通知一次。 |
| 對阻塞 I/O 的要求 | 不要求,I/O 可以是阻塞的。 | 必須使用非阻塞 I/O,防止錯(cuò)過后續(xù)事件。 |
| 復(fù)雜度 | 實(shí)現(xiàn)簡單,事件處理較為直觀。 | 需要更復(fù)雜的事件處理邏輯,必須一次性處理所有數(shù)據(jù)。 |
| 資源消耗 | 可能會(huì)造成不必要的重復(fù)通知,增加 CPU 占用。 | 更加高效,減少了重復(fù)的事件通知。 |
| 適用場景 | 普通的 I/O 密集型應(yīng)用,如文件處理、輕量級的網(wǎng)絡(luò)服務(wù)等。 | 高并發(fā)、高性能的網(wǎng)絡(luò)應(yīng)用,如 Web 服務(wù)器、實(shí)時(shí)流處理等。 |
邊緣觸發(fā)(ET)模式的優(yōu)化技巧
由于 ET 模式要求事件處理更為高效,一些優(yōu)化策略可以確保高并發(fā)下的穩(wěn)定運(yùn)行:
非阻塞 I/O:
必須將所有受 epoll 監(jiān)控的文件描述符設(shè)置為非阻塞模式。否則,在數(shù)據(jù)未及時(shí)讀取或?qū)懭氲那闆r下,ET 模式可能錯(cuò)過后續(xù)事件通知,導(dǎo)致程序不響應(yīng)新事件。
int flags = fcntl(sock_fd, F_GETFL, 0); fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK);
一次性處理所有數(shù)據(jù):
在 ET 模式下,應(yīng)用程序必須確保在收到事件通知時(shí)一次性處理所有可讀或可寫的數(shù)據(jù)。如果不處理完所有數(shù)據(jù),下一次數(shù)據(jù)到達(dá)時(shí) epoll 可能不會(huì)再次通知,從而導(dǎo)致數(shù)據(jù)丟失。
例如,對于一個(gè)可讀事件的套接字,應(yīng)該不斷調(diào)用 read() 或 recv() 來讀取所有數(shù)據(jù),直到 read() 返回 EAGAIN 或 EWOULDBLOCK,表示數(shù)據(jù)已讀取完畢。
ssize_t bytes_read;
while ((bytes_read = read(sock_fd, buffer, sizeof(buffer))) > 0) {
// 處理數(shù)據(jù)
}
if (bytes_read < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
perror("read failed");
// 錯(cuò)誤處理
}
處理數(shù)據(jù)時(shí)避免阻塞:
ET 模式通常與非阻塞 I/O 一起使用,因此處理事件時(shí)要特別小心。確保每次操作不會(huì)阻塞整個(gè)進(jìn)程,避免應(yīng)用程序掛起。
合理選擇超時(shí)值:
如果你的應(yīng)用程序不需要實(shí)時(shí)響應(yīng),也可以考慮使用超時(shí)等待(epoll_wait 的第四個(gè)參數(shù)設(shè)置為適當(dāng)?shù)某瑫r(shí)值),這樣可以避免應(yīng)用程序因過于頻繁的調(diào)用 epoll_wait 而浪費(fèi) CPU 時(shí)間。
- 代碼示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAX_EVENTS 10
#define PORT 8080
#define BUFFER_SIZE 1024
// 設(shè)置文件描述符為非阻塞
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl(F_GETFL)");
return -1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl(F_SETFL)");
return -1;
}
return 0;
}
// 處理客戶端數(shù)據(jù)的函數(shù)
void handle_client_data(int client_fd) {
char buffer[BUFFER_SIZE];
ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
perror("read failed");
close(client_fd);
}
} else if (bytes_read == 0) {
// 客戶端關(guān)閉連接
printf("Client disconnected\n");
close(client_fd);
} else {
// 處理讀取的數(shù)據(jù)
buffer[bytes_read] = '\0';
printf("Received data: %s\n", buffer);
// 發(fā)送數(shù)據(jù)到客戶端
ssize_t bytes_written = write(client_fd, buffer, bytes_read);
if (bytes_written == -1) {
perror("write failed");
close(client_fd);
}
}
}
int main() {
int server_fd, epoll_fd;
struct sockaddr_in server_addr;
struct epoll_event ev, events[MAX_EVENTS];
// 創(chuàng)建監(jiān)聽套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 設(shè)置服務(wù)器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 綁定套接字
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 監(jiān)聽連接
if (listen(server_fd, 10) == -1) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 創(chuàng)建 epoll 實(shí)例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1 failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 將服務(wù)器套接字加入 epoll 事件監(jiān)聽
ev.events = EPOLLIN | EPOLLET; // 監(jiān)聽可讀事件,采用邊緣觸發(fā)模式
ev.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {
perror("epoll_ctl failed");
close(server_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
// 主事件循環(huán)
while (1) {
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (num_events == -1) {
perror("epoll_wait failed");
break;
}
// 處理就緒事件
for (int i = 0; i < num_events; i++) {
if (events[i].data.fd == server_fd) {
// 服務(wù)器套接字有連接請求
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd == -1) {
perror("accept failed");
continue;
}
// 設(shè)置客戶端套接字為非阻塞
if (set_nonblocking(client_fd) == -1) {
close(client_fd);
continue;
}
// 將客戶端套接字添加到 epoll 中
ev.events = EPOLLIN | EPOLLET; // 監(jiān)聽客戶端的可讀事件,采用邊緣觸發(fā)模式
ev.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
perror("epoll_ctl: add client failed");
close(client_fd);
continue;
}
printf("New client connected\n");
} else if (events[i].events & EPOLLIN) {
// 客戶端有數(shù)據(jù)可讀
handle_client_data(events[i].data.fd);
}
}
}
// 清理資源
close(server_fd);
close(epoll_fd);
return 0;
}
原理分析:
epoll 是一種高效的 I/O 事件通知機(jī)制,它通過以下方式提高效率:
- 紅黑樹管理文件描述符:使用
epoll_ctl系統(tǒng)調(diào)用將感興趣的 socket(或其他文件描述符)注冊到epoll實(shí)例中。這些描述符被存儲(chǔ)在一個(gè)內(nèi)部的紅黑樹結(jié)構(gòu)中,這使得添加、刪除和查找操作都非常高效。 - 就緒隊(duì)列記錄活動(dòng)事件:當(dāng)某個(gè) socket 上有數(shù)據(jù)到達(dá)時(shí),內(nèi)核會(huì)自動(dòng)將該 socket 標(biāo)記為就緒,并將其加入到一個(gè)就緒隊(duì)列中。這里并沒有使用回調(diào)函數(shù),而是依賴于內(nèi)核的通知機(jī)制。
- 事件通知而非輪詢:與
select和poll不同,epoll使用的是事件驅(qū)動(dòng)的通知機(jī)制。這意味著只有當(dāng)有實(shí)際事件發(fā)生時(shí)(如可讀或可寫),epoll_wait才會(huì)返回并告知應(yīng)用程序哪些 socket 已準(zhǔn)備好進(jìn)行 I/O 操作。這樣避免了對所有文件描述符的重復(fù)檢查,提高了效率。 epoll_wait獲取事件:應(yīng)用程序調(diào)用epoll_wait來等待事件的發(fā)生。一旦有事件發(fā)生,epoll_wait就會(huì)返回一個(gè)包含所有就緒事件的列表,供應(yīng)用程序處理。

4.信號驅(qū)動(dòng)式IO
信號驅(qū)動(dòng)式IO是Linux提供的一種IO模型,通過信號通知應(yīng)用程序某個(gè)文件描述符的狀態(tài)發(fā)生變化,適用于需要非阻塞IO操作的場景。它利用信號機(jī)制將內(nèi)核的事件通知傳遞給用戶空間,使得程序無需主動(dòng)倫旭即可對IO事件做出響應(yīng)。
4.1 工作原理
- 信號和信號處理:
- 在信號驅(qū)動(dòng)I/O中,當(dāng)某個(gè)I/O事件(如數(shù)據(jù)準(zhǔn)備好讀或?qū)懀┌l(fā)生時(shí),內(nèi)核會(huì)向進(jìn)程發(fā)送一個(gè)信號。進(jìn)程通過信號處理函數(shù)來處理該事件。
- 該信號通常是
SIGIO(I/O信號)或SIGURG(緊急數(shù)據(jù)的信號)等。
- 非阻塞模式:
- 為了使信號驅(qū)動(dòng)I/O工作,通常需要將相關(guān)的套接字設(shè)置為非阻塞模式。這樣,I/O操作(如
recv或send)不會(huì)因?yàn)闆]有數(shù)據(jù)而阻塞。 - 一旦數(shù)據(jù)準(zhǔn)備好,操作系統(tǒng)會(huì)發(fā)送
SIGIO信號,通知進(jìn)程可以進(jìn)行讀取或?qū)懭搿?/li>
- 信號處理函數(shù):
- 在信號觸發(fā)時(shí),操作系統(tǒng)會(huì)中斷進(jìn)程的正常執(zhí)行流程,并轉(zhuǎn)到預(yù)先注冊的信號處理函數(shù)中執(zhí)行。這使得信號驅(qū)動(dòng)I/O成為一種異步機(jī)制。
- 信號處理函數(shù)中通常會(huì)包含對
recv或send等函數(shù)的調(diào)用,以便處理就緒的I/O數(shù)據(jù)。
4.2 為什么使用信號驅(qū)動(dòng)IO
- **避免輪詢:**傳統(tǒng)的非阻塞I/O通常需要通過
select、poll或epoll等方式輪詢文件描述符,檢查是否有數(shù)據(jù)可以讀取。信號驅(qū)動(dòng)I/O消除了這種輪詢機(jī)制,避免了CPU時(shí)間的浪費(fèi)。 - **減少阻塞:**通過信號驅(qū)動(dòng)I/O,應(yīng)用程序不再需要阻塞等待I/O事件發(fā)生,而是通過信號觸發(fā)事件,從而使得程序可以在等待I/O的同時(shí)執(zhí)行其他任務(wù)。
- **資源高效:**信號驅(qū)動(dòng)I/O能夠?qū)崿F(xiàn)資源的高效利用,因?yàn)樗试S應(yīng)用程序在I/O事件到達(dá)時(shí)立即處理,而不需要檢查文件描述符狀態(tài)。
- **適用于實(shí)時(shí)應(yīng)用:**對于一些需要及時(shí)響應(yīng)的實(shí)時(shí)應(yīng)用,信號驅(qū)動(dòng)I/O提供了一種快速響應(yīng)數(shù)據(jù)的方式。
4.3 實(shí)現(xiàn)流程
信號驅(qū)動(dòng)式I/O的核心思想是預(yù)先告知內(nèi)核當(dāng)某個(gè)描述符準(zhǔn)備發(fā)生某件事情(如數(shù)據(jù)到達(dá))時(shí)發(fā)送一個(gè)信號(SIGIO)給進(jìn)程。這使得進(jìn)程可以在等待數(shù)據(jù)的過程中不被阻塞,只有在接收到SIGIO信號后才去處理I/O事件。程序需要按照如下步驟執(zhí)行:
- 為內(nèi)核發(fā)送的通知信號安裝一個(gè)信號處理例程。默認(rèn)情況下,這個(gè)通知信號為
SIGIO - 設(shè)定文件符的屬主,也就是當(dāng)文件描述符山可執(zhí)行IO時(shí)會(huì)接收通知信號的進(jìn)程或進(jìn)程組。通常我們讓調(diào)用進(jìn)程成為屬主。設(shè)定屬主可通過
fcntl()的F_SETOWN操作來完成
fcntl(fd, F_SETOWN , pid);
- 通過設(shè)定
O_NONBLOCK標(biāo)識使能非阻塞IO - 通過打開
O_ASYNC標(biāo)志使能信號驅(qū)動(dòng)IO。這可以和上一步合并為一個(gè)操作,因?yàn)樗鼈兌夹枰玫?code>fcntl()的F_SETFL操作
flags = fcntl(fd, F_GETFL); // get current flags fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
- 調(diào)用進(jìn)程線程可以執(zhí)行其他任務(wù)了。當(dāng)IO操作就緒時(shí),內(nèi)核為進(jìn)程發(fā)送一個(gè)信號,然后調(diào)用在第1步中安裝好的信號處理例程
- 信號驅(qū)動(dòng)IO提供的是邊緣觸發(fā)通知。這表示一旦進(jìn)程被通知IO就緒,它就應(yīng)該盡可能的能多地執(zhí)行 I/O(例如盡可能多地讀取字節(jié))。假設(shè)文件描述符時(shí)非阻塞的,這表示需要在循環(huán)中執(zhí)行IO系統(tǒng)調(diào)用直到失敗位置,此時(shí)的錯(cuò)誤碼為
EAGAIN(再來一次)或者EWOULDBLOCK(期望阻塞)。
示例代碼及圖解:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#define PORT 8080
#define MAXLINE 1024
static int sockfd; // 監(jiān)聽套接字
static struct sockaddr_in cli_addr;
static socklen_t clilen = sizeof(cli_addr);
// 信號處理函數(shù)
void do_sometime(int signal) {
char buffer[MAXLINE] = {0};
int len = recvfrom(sockfd, buffer, MAXLINE, 0, (struct sockaddr *)&cli_addr, (socklen_t*)&clilen);
if (len > 0) {
printf("收到客戶端消息: %s\n", buffer);
strcat(buffer, "→[Msg]");
sendto(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&cli_addr, clilen); // 回顯消息
} else {
printf("沒有收到數(shù)據(jù)或出現(xiàn)錯(cuò)誤\n");
}
}
int main(int argc, char const *argv[]) {
// 創(chuàng)建套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 注冊信號處理函數(shù)
struct sigaction act;
act.sa_handler = do_sometime;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART; // 重新啟動(dòng)被信號中斷的系統(tǒng)調(diào)用
sigaction(SIGIO, &act, NULL);
// 創(chuàng)建并初始化地址結(jié)構(gòu)
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = INADDR_ANY;
// 設(shè)置文件描述符的擁有者為當(dāng)前進(jìn)程
fcntl(sockfd, F_SETOWN, getpid());
int flags = fcntl(sockfd, F_GETFL, 0);
// 啟用信號驅(qū)動(dòng)模式 | 設(shè)置文件描述符為非阻塞模式
fcntl(sockfd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
// 綁定地址
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
while (1)
sleep(1); // 等待信號
close(sockfd);
return 0;
}

異步IO通知的工作流程:
- 注冊異步通知
在用戶空間,應(yīng)用程序通過 fcntl 系統(tǒng)調(diào)用來設(shè)置文件描述符的異步通知機(jī)制:
- 設(shè)置 O_ASYNC 標(biāo)志:使用
fcntl(fd, F_SETFL, flags | O_ASYNC)來啟用異步通知。 - 設(shè)置接收 SIGIO 信號的進(jìn)程 ID:使用
fcntl(fd, F_SETOWN, getpid())來指定哪個(gè)進(jìn)程應(yīng)該接收SIGIO信號,通知它可以進(jìn)行IO操作了。
- 內(nèi)核空間處理
在內(nèi)核空間,驅(qū)動(dòng)程序需要支持異步通知機(jī)制。具體步驟如下:
- 管理
fasync_struct隊(duì)列:使用fasync_helper函數(shù)來管理一個(gè)鏈表(隊(duì)列),該鏈表存儲(chǔ)了所有注冊了異步通知的進(jìn)程信息。 - 觸發(fā)事件:當(dāng)某個(gè)事件發(fā)生時(shí)(如數(shù)據(jù)到達(dá)),驅(qū)動(dòng)程序調(diào)用
kill_fasync函數(shù)來通知所有注冊了異步通知的進(jìn)程。
- 觸發(fā)事件
當(dāng)驅(qū)動(dòng)程序檢測到某個(gè)事件(如數(shù)據(jù)到達(dá))時(shí),它會(huì)調(diào)用 kill_fasync 函數(shù)來通知所有注冊了異步通知的進(jìn)程。kill_fasync 函數(shù)會(huì)遍歷 fasync_struct 隊(duì)列,并向每個(gè)進(jìn)程發(fā)送 SIGIO 信號。
- 處理信號
用戶空間的應(yīng)用程序收到 SIGIO 信號后,可以在其信號處理函數(shù)中執(zhí)行相應(yīng)的操作。例如,處理接收到的數(shù)據(jù)或進(jìn)行其他必要的操作。
5.異步IO
異步IO(Asynchronous IO,AIO)是一種處理輸入/輸出操作的方式,它允許程序在發(fā)起IO操作后立即返回,而不是等待操作完成。
這種方式可以顯著提高應(yīng)用程序的并發(fā)性和吞吐量,特別是在IO密集型的應(yīng)用場景中。下面將從多個(gè)角度深入探討異步IO的概念,實(shí)現(xiàn)機(jī)制及其應(yīng)用場景。
5.1 異步I/O的基本概念
5.1.1 同步 vs 異步
- 同步I/O:當(dāng)一個(gè)進(jìn)程發(fā)起I/O請求時(shí),它會(huì)被阻塞直到該請求完成。這意味著在此期間,進(jìn)程不能執(zhí)行其他任務(wù)。
- 異步I/O:當(dāng)一個(gè)進(jìn)程發(fā)起I/O請求時(shí),它可以立即繼續(xù)執(zhí)行其他任務(wù),而不需要等待I/O操作完成。一旦I/O操作完成,系統(tǒng)會(huì)通過某種方式通知進(jìn)程結(jié)果。
5.1.2 阻塞 vs 非阻塞
- 阻塞I/O:如果文件描述符未準(zhǔn)備好進(jìn)行讀寫操作,調(diào)用將被掛起,直到操作準(zhǔn)備好為止。
- 非阻塞I/O:如果文件描述符未準(zhǔn)備好進(jìn)行讀寫操作,調(diào)用會(huì)立即返回一個(gè)錯(cuò)誤碼,允許進(jìn)程嘗試其他操作或稍后再試。
雖然“異步”和“非阻塞”聽起來相似,但它們實(shí)際上是不同的概念。異步I/O指的是整個(gè)操作由內(nèi)核而非用戶進(jìn)程來完成,并且在完成后通知用戶進(jìn)程;而非阻塞I/O則是指用戶進(jìn)程可以在沒有數(shù)據(jù)可讀/寫時(shí)不被阻塞,但仍需主動(dòng)輪詢檢查狀態(tài)
5.1.3 異步IO的工作原理
異步 I/O 的實(shí)現(xiàn)通常依賴于以下幾個(gè)關(guān)鍵組件:
事件通知機(jī)制:內(nèi)核提供一種機(jī)制,能夠在 I/O 操作完成時(shí)通知應(yīng)用程序。常見的事件通知機(jī)制有:
- 信號通知:如
SIGIO。 - 回調(diào)通知:如
io_uring或AIO中的回調(diào)機(jī)制。 - 輪詢機(jī)制:應(yīng)用程序主動(dòng)檢查 I/O 狀態(tài),類似于
select()和poll()。
文件描述符和 I/O 操作:文件描述符是 I/O 操作的基礎(chǔ)。內(nèi)核會(huì)根據(jù)文件描述符的狀態(tài)來決定 I/O 操作是否可以完成,并在完成時(shí)通知程序。
非阻塞模式:異步 I/O 需要文件描述符處于非阻塞模式。通過設(shè)置 O_NONBLOCK 或其他標(biāo)志,程序可以立即返回,而不是等待 I/O 完成。
POSIX AIO 示例:
POSIX AIO 是 Linux 提供的一套接口,用于執(zhí)行異步 I/O 操作。
在異步 I/O 中,程序可以發(fā)起 I/O 操作(如 aio_read 或 aio_write),然后繼續(xù)執(zhí)行其他任務(wù),而不是等待 I/O 操作完成。
當(dāng) I/O 操作完成時(shí),程序可以通過輪詢、信號或回調(diào)來獲取結(jié)果。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <aio.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <errno.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
// 創(chuàng)建 TCP 套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 設(shè)置服務(wù)器地址結(jié)構(gòu)
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 綁定地址
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 監(jiān)聽連接
listen(server_fd, 5);
printf("Server listening on port %d...\n", PORT);
// 接受客戶端連接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
// 設(shè)置異步讀操作
char buffer[BUFFER_SIZE];
struct aiocb aio_read_cb;
memset(&aio_read_cb, 0, sizeof(struct aiocb));
aio_read_cb.aio_fildes = client_fd;
aio_read_cb.aio_buf = buffer;
aio_read_cb.aio_nbytes = sizeof(buffer);
aio_read_cb.aio_offset = 0;
// 發(fā)起異步讀操作
aio_read(&aio_read_cb);
// 等待讀操作完成
while (aio_error(&aio_read_cb) == EINPROGRESS) {
// 可以執(zhí)行其他操作
usleep(10000); // 等待10ms
}
// 讀取完成,獲取結(jié)果
int bytes_read = aio_return(&aio_read_cb);
printf("Received message: %s\n", buffer);
// 發(fā)送數(shù)據(jù)到客戶端
send(client_fd, buffer, bytes_read, 0);
close(client_fd);
close(server_fd);
return 0;
}

總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
阿里云基于CentOS用vsftpd搭建FTP服務(wù)器
這篇文章主要介紹了阿里云CentOS系統(tǒng)yum方式安裝vsftpd的相關(guān)資料,最近想要了解的朋友可以了解一下。2016-10-10
函數(shù)sync、fsync與fdatasync的總結(jié)整理(必看篇)
下面小編就為大家?guī)硪黄瘮?shù)sync、fsync與fdatasync的總結(jié)整理(必看篇)。小編覺得挺不錯(cuò)的?,F(xiàn)在就分享給大家。也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-12-12
Virtualbox安裝Lubuntu 18.04 64位的圖文教程
這篇文章主要為大家詳細(xì)介紹了Virtualbox安裝Lubuntu 18.04 64位的圖文教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
詳解虛擬機(jī)下CentOS 6.5配置IP地址的三種方法
本篇文章主要介紹了虛擬機(jī)下CentOS 6.5配置IP地址的三種方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-03-03
Apache中如何設(shè)置默認(rèn)首頁具體該怎么操作
這篇文章主要介紹了Apache中設(shè)置默認(rèn)首頁的方法,需要的朋友可以參考下2014-02-02
Centos 7之Firewalld相關(guān)命令詳細(xì)介紹
這篇文章主要介紹了Centos 7之Firewalld相關(guān)命令詳細(xì)介紹的相關(guān)資料,需要的朋友可以參考下2017-02-02

