Linux其他備選高級IO模型用法詳解
其他高級 I/O 模型
以上基本介紹的都是同步IO相關(guān)知識點,即在同步I/O模型中,程序發(fā)起I/O操作后會等待I/O操作完成,即程序會被阻塞,直到I/O完成。
整個I/O過程在同一個線程中進行,程序在等待期間不能執(zhí)行其他任務(wù)。下面將會介紹除同步IO之外的其他常見IO模型。
什么是IO?什么是高效的IO?
**I/O(輸入/輸出)**是計算機與外部世界進行數(shù)據(jù)交換的過程。在Linux系統(tǒng)中,I/O操作通常指的是程序與硬盤、網(wǎng)絡(luò)設(shè)備、終端等進行數(shù)據(jù)交換的操作。I/O性能直接影響到系統(tǒng)的響應(yīng)速度和吞吐量,是很多應(yīng)用系統(tǒng)優(yōu)化的關(guān)鍵目標。
高效的IO 涉及優(yōu)化I/O操作的延遲、吞吐量和資源消耗。在一個復(fù)雜的系統(tǒng)中,I/O效率通常通過以下方式得到提升:
- 減少I/O等待時間(例如,通過非阻塞I/O或異步I/O)
- 最大化吞吐量(例如,通過數(shù)據(jù)預(yù)讀或緩存機制)
- 優(yōu)化系統(tǒng)資源的利用(例如,通過多路復(fù)用和線程池等技術(shù))
高效的IO不僅可以提升程序的響應(yīng)速度,還能減少系統(tǒng)的負載,提高并發(fā)處理能力。
IO模型分析方法
出處:Linux五種IO模型
分析IO模型需要了解2個問題:
問題1:發(fā)送IO請求,IO請求可以理解為用戶空間和內(nèi)核空間數(shù)據(jù)同步,根據(jù)發(fā)起者不同分為以下兩種情況:
- 由用戶程序發(fā)起(同步IO)。
- 由內(nèi)核發(fā)起(異步IO)。
問題2:等待數(shù)據(jù)到來,等待數(shù)據(jù)到來的方式有以下幾種:
- 阻塞(阻塞IO)。
- 輪詢(非阻塞IO)。
- 信號通知(信號驅(qū)動IO)。
內(nèi)核空間和用戶空間數(shù)據(jù)同步由誰發(fā)起是分析Linux IO模型最核心的問題
1.阻塞式I/O(Blocking I/O)
- 阻塞式I/O是最常見的I/O模型,在這種模型下,當程序請求I/O操作時,會阻塞當前線程直到I/O操作完成(如讀/寫數(shù)據(jù))。在等待期間,線程無法進行其他操作。
- 優(yōu)點:簡單,易于理解和實現(xiàn)。
- 缺點:性能瓶頸,當進行大量I/O操作時,阻塞會導致線程無法有效利用,浪費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)建一個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ù)到達并喚醒進程
}
close(new_sock);
close(sockfd);
return 0;
}
原理分析:

階段 1: 用戶程序調(diào)用 recv 發(fā)起 I/O 請求(同步 I/O)
背景: 在阻塞 I/O 模式下,用戶程序發(fā)起 I/O 操作時(比如通過 recv 函數(shù)從套接字讀取數(shù)據(jù)),如果內(nèi)核空間的套接字緩沖區(qū)沒有數(shù)據(jù)準備好,用戶進程會被阻塞,直到數(shù)據(jù)可用。
步驟 1: 發(fā)起 I/O 請求
ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE, 0);
- 當用戶程序調(diào)用
recv函數(shù)時,內(nèi)核會檢查指定的套接字是否有足夠的數(shù)據(jù)可以讀取。如果套接字的緩沖區(qū)為空,內(nèi)核會將調(diào)用進程從TASK_RUNNING狀態(tài)切換到TASK_INTERRUPTIBLE狀態(tài)。
步驟 2: 進程切換狀態(tài)并阻塞
- 進程狀態(tài)的切換意味著 CPU 將會把當前進程放入 進程等待隊列,并且該進程不再占用 CPU 資源。系統(tǒng)調(diào)度程序會選擇其他可以運行的進程來執(zhí)行,這就是“進程阻塞”的表現(xiàn)。
- 被阻塞的進程會被加入到該套接字的等待隊列中,等待數(shù)據(jù)的到來。一旦數(shù)據(jù)可用,進程就會被喚醒并繼續(xù)執(zhí)行。
重要的概念:
- 阻塞 I/O 并不意味著阻塞 CPU,它只會使進程切換到阻塞狀態(tài),讓出 CPU 的控制權(quán),其他進程可以繼續(xù)執(zhí)行。
- 進程的阻塞狀態(tài)是由內(nèi)核進行管理的,通常是
TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,這取決于 I/O 請求的性質(zhì)。TASK_INTERRUPTIBLE狀態(tài)表示進程可以響應(yīng)信號中斷,而TASK_UNINTERRUPTIBLE狀態(tài)下進程不會響應(yīng)信號。
階段 2: 網(wǎng)卡收到數(shù)據(jù)包并喚醒進程
背景: 數(shù)據(jù)包通過網(wǎng)絡(luò)接口卡(NIC)到達時,內(nèi)核通過硬件中斷機制將數(shù)據(jù)拷貝到內(nèi)核空間。內(nèi)核會將這些數(shù)據(jù)存入相應(yīng)的套接字緩沖區(qū),進而喚醒之前等待的進程。
步驟 1: 網(wǎng)卡接收數(shù)據(jù)包
- 網(wǎng)絡(luò)接口卡(NIC)通過 DMA(直接內(nèi)存訪問)機制將收到的網(wǎng)絡(luò)數(shù)據(jù)包直接拷貝到內(nèi)核空間的環(huán)形緩沖區(qū)(RingBuffer)中,這一過程在硬件和內(nèi)核中自動完成。
- 一旦數(shù)據(jù)包被成功接收,NIC 會觸發(fā)硬件中斷,通知操作系統(tǒng)數(shù)據(jù)包已到達。
步驟 2: 數(shù)據(jù)包復(fù)制到套接字接收緩沖區(qū)
- 中斷處理程序會將數(shù)據(jù)包從環(huán)形緩沖區(qū)復(fù)制到對應(yīng)套接字的接收緩沖區(qū)。
- 在這時,內(nèi)核會檢查是否有任何進程(如在第一階段被阻塞的進程)正在等待該套接字的接收數(shù)據(jù)。如果有,內(nèi)核會喚醒等待隊列中的進程,使其恢復(fù)執(zhí)行。
步驟 3: 喚醒等待的進程
- 被喚醒的進程會重新進入
TASK_RUNNING狀態(tài),系統(tǒng)調(diào)度器會將該進程從等待隊列中移除,并將其放回可運行隊列。 - 用戶進程被恢復(fù)執(zhí)行后,
recv函數(shù)會從套接字接收緩沖區(qū)中讀取數(shù)據(jù),并將數(shù)據(jù)返回到用戶空間的緩沖區(qū)(例如buffer)。
步驟 4: 用戶程序完成 I/O 操作
- 一旦數(shù)據(jù)被成功讀取,
recv函數(shù)會返回讀取的字節(jié)數(shù),用戶程序可以繼續(xù)處理接收到的數(shù)據(jù)。
2.非阻塞IO(Non-blocking I/O)
首先我們先來認識一個新的函數(shù)fcntl():
2.1fcntl函數(shù)的基本用法
fcntl 函數(shù)主要用于對已打開的文件描述符進行控制操作,以改變文件描述符的屬性或獲取其狀態(tài)。
fcntl函數(shù)通常在處理低級別的文件I/O時使用,例如復(fù)制文件描述符、獲取和設(shè)置文件描述符標志、獲取和設(shè)置文件狀態(tài)標志、文件鎖定(共享鎖和排他鎖)、設(shè)置文件所有者。
fcntl函數(shù)的基本原型如下:
#include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ );
參數(shù)說明:
fd是文件描述符。cmd是要執(zhí)行的命令。arg可選參數(shù),可能還需要一個或多個附加參數(shù),具體取決于cmd的值。
返回值:
- 成功時,根據(jù)
cmd的不同,可能需要一個或多個附加參數(shù)。 - 失敗時,返回
-1,并設(shè)置errno以只是錯誤原因。
fcntl的常見命令
復(fù)制文件描述符
示例代碼:
F_DUPFD:返回一個大于或等于指定值的最小可用文件描述符,該描述符與原來的描述符指向同一個文件。F_DUPFD_CLOEXEC:與F_DUPFD類似,但新描述符設(shè)置FD_CLOEXEC標志。
?
#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("打開文件失?。?); // 輸出錯誤信息
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è)置文件描述符標志
標志:
- 示例代碼:
?
#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("打開文件失敗:"); // 輸出錯誤信息
return -1;
}
int flags = fcntl(fd, F_GETFD); // 獲取文件描述符的標志
if (flags == -1) {
std::perror("獲取文件描述符標志失?。?);
close(fd);
return -1;
}
// 設(shè)置 FD_CLOEXEC 標志,使文件描述符在執(zhí)行 exec 時關(guān)閉
if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
std::perror("獲取文件描述符標志失?。?);
close(fd);
return -1;
}
std::cout << "設(shè)置 FD_CLOEXEC 前的標志:" << flags << std::endl; // 輸出設(shè)置前的標志
std::cout << "設(shè)置 FD_CLOEXEC 后的標志:" << new_flags << std::endl; // 輸出設(shè)置后的標志
close(fd); // 關(guān)閉文件描述符
return 0;
}
?F_GETFD:獲取文件描述符的標志。F_SETFD:設(shè)置文件描述符的標志。FD_CLOEXEC:設(shè)置或取消close-on-exec標志,該標志表示在執(zhí)行exec系列函數(shù)時關(guān)閉該描述符。O_NONBLOCK:非阻塞模式。O_APPEND:追加模式。O_ASYNC:啟用信號驅(qū)動 I/O。O_DIRECT:直接 I/O(盡量繞過緩沖區(qū)緩存)。O_NOATIME:不更新文件的訪問時間。
獲取/設(shè)置文件狀態(tài)標志
- 示例代碼:
?
#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("打開文件失?。?); // 輸出錯誤信息
return -1;
}
int flags = fcntl(fd, F_GETFL); // 獲取文件狀態(tài)標志
if (flags == -1) {
std::perror("獲取文件狀態(tài)標志失敗:");
close(fd);
return -1;
}
// 設(shè)置 O_NONBLOCK 標志,使文件描述符處于非阻塞模式
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
int new_flags = fcntl(fd, F_GETFL); // 再次獲取文件狀態(tài)標志
if (new_flags == -1) {
std::perror("獲取文件狀態(tài)標志失?。?);
close(fd);
return -1;
}
std::cout << "設(shè)置 O_NONBLOCK 前的標志:" << flags << std::endl; // 輸出設(shè)置前的標志
std::cout << "設(shè)置 O_NONBLOCK 后的標志:" << new_flags << std::endl; // 輸出設(shè)置后的標志
close(fd); // 關(guān)閉文件描述符
return 0;
}
?F_GETFL:獲取文件狀態(tài)標志。F_SETFL:設(shè)置文件狀態(tài)標志。
2.2 阻塞模式 vs 非阻塞模式
阻塞模式:(上面已有介紹)
- 默認情況下,文件描述符處于阻塞模式。
- 當你嘗試從一個沒有數(shù)據(jù)可讀的文件描述符中讀取數(shù)據(jù),或者嘗試向一個寫緩沖區(qū)已滿的文件描述符寫入數(shù)據(jù)時,進程會被阻塞,直到操作可以完成。
非阻塞模式:
非阻塞I/O模型下,I/O請求立即返回,不會阻塞程序。如果數(shù)據(jù)沒有準備好,程序會收到錯誤或“沒有數(shù)據(jù)”的通知,之后可以重新嘗試。
- 如果設(shè)置了
O_NONBLOCK標志,那么當嘗試讀取沒有數(shù)據(jù)可讀或?qū)懢彌_區(qū)已滿的情況下,不會阻塞進程,而是立即返回一個錯誤。 - 典型的錯誤碼是
EAGAIN或EWOULDBLOCK,這表明操作不能立即完成。 - 優(yōu)點:線程可以進行其他任務(wù),而不是等待I/O操作完成。
- 缺點:需要不斷輪詢(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)標志失敗: " << 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 會被設(shè)置為 EWOULDBLOCK 或 EAGAIN
if (errno == EWOULDBLOCK || errno == EAGAIN) {
cout << "沒有可用連接,繼續(xù)等待..." << endl;
sleep(1); // 延時 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 會被設(shè)置為 EWOULDBLOCK 或 EAGAIN
if (errno == EWOULDBLOCK || errno == EAGAIN) {
cout << "沒有可用數(shù)據(jù),執(zhí)行其他任務(wù)..." << endl;
usleep(100000); // 假設(shè)其他任務(wù)的延時(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會立即返回-1并設(shè)置errno為EWOULDBLOCK或EAGAIN。 - 程序檢測到
EWOULDBLOCK錯誤后,執(zhí)行其他任務(wù)(如打印信息和休眠),然后繼續(xù)嘗試接收數(shù)據(jù)。 - 這對應(yīng)于用戶程序調(diào)用
recv發(fā)起 I/O 請求,讀取 socket 緩沖區(qū)數(shù)據(jù)。由于 socket 緩沖區(qū)沒有就緒數(shù)據(jù)包,非阻塞 I/Orecv直接返回EWOULDBLOCK錯誤碼,用戶如果一直調(diào)用recv函數(shù)則一直返回EWOULDBLOCK錯誤碼,直到數(shù)據(jù)準備好。
非阻塞 I/O/階段2:數(shù)據(jù)到達并喚醒進程
- 當數(shù)據(jù)通過網(wǎng)絡(luò)接口卡(NIC)接收并被內(nèi)核處理后,數(shù)據(jù)被復(fù)制到套接字接收緩沖區(qū)。
- 隨后,
recv調(diào)用會成功讀取數(shù)據(jù),返回接收到的字節(jié)數(shù),程序繼續(xù)處理數(shù)據(jù)。 - 這與阻塞 I/O 的階段2 相同,即網(wǎng)卡收到數(shù)據(jù)包并喚醒進程,包括數(shù)據(jù)復(fù)制到套接字接收緩沖區(qū)和喚醒等待的進程。
拓展知識:Ctrl+C 和 Ctrl+D:
| 組合鍵 | 作用 | 退出狀態(tài)碼 | 典型場景 |
|---|---|---|---|
| Ctrl+C | 發(fā)送 SIGINT 信號給前臺進程,請求程序終止 | 通常為 130(128 + 2) | 用于中斷正在運行的程序 |
| Ctrl+D | 發(fā)送 EOF 信號,表示輸入結(jié)束 | 通常為 0 | 用于結(jié)束輸入或退出交互式 shell |
3.IO多路復(fù)用(Multiplexing)
IO多路復(fù)用是一種高效的IO處理方式,它可以讓一個進程同時監(jiān)控多個文件描述符,當其中任意一個文件描述符就緒時,就可以進行相應(yīng)的IO操作。
相比于傳統(tǒng)的阻塞IO和非阻塞IO,IO復(fù)用可以打打提高IO效率,減少CPU資源的浪費。
在Linux中,常用的IO復(fù)用模型有select、poll、epoll等。
IO復(fù)用模型請求由用戶程序發(fā)起,所以IO復(fù)用模型為同步IO。
3.1 IO復(fù)用select模型
3.1.1 認識select函數(shù)
系統(tǒng)調(diào)用select()會一直阻塞,直到一個或多個文件描述符集合成為就緒態(tài)。
它通過傳遞三個文件描述符集合(讀集合、寫集合和異常集合)給內(nèi)核,內(nèi)核會在這些集合中等待任意一個文件描述符就緒。
#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è)為比下面三個文件描述符集合中所包含的最大文件描述符號 +1。readfds:是用來檢測輸入是否就緒的文件描述符集合。writefds:是用來檢測輸出是否就緒的文件描述符集合。exceptfds:是用來檢測異常情況是否發(fā)生的文件描述符集合。timeout:超時時間,這個參數(shù)控制著select()的阻塞行為。
該參數(shù)可指定為NULL,此時select()會一直阻塞。又或者指向一個timeval結(jié)構(gòu)體。
如果結(jié)構(gòu)體timeval的兩個域都為0的話,此時select()不會阻塞。它只是簡單低輪詢指定的文件描述符集合,查看其中是否有就緒的文件描述符并立即返回。
否則,timeval的兩個域都不為0的話,timeval將為select()指定一個等待時間上限值。代表多久之后返回,即超時時間。
返回值:
- 如果有文件描述符變得可讀、可寫或發(fā)生異常,返回值為準備就緒的文件描述符的數(shù)目。
- 如果
timeout到達,返回值為 0。這種情況下每個返回的文件描述符集合將被清空。 - 如果發(fā)生錯誤,返回值為 -1。
timeval結(jié)構(gòu)體原型:
struct timeval
{
__time_t tv_sec; /* 秒. */
__suseconds_t tv_usec; /* 微秒. */
};
當timeout設(shè)為NULL,或其指向的結(jié)構(gòu)體字段非零時,select()將阻塞直到下列事件發(fā)生:
readfds、writefds或exceptfds中指定的文件描述符中至少有一個成為就緒態(tài)。- 該調(diào)用被信號處理中斷。
timeout中指定的時間上限已超時。
關(guān)于 fd_set:fd_set 是一個位圖結(jié)構(gòu),用于標識一組文件描述符(通常是網(wǎng)絡(luò)套接字)。fd_set 的定義通常是由底層實現(xiàn)細節(jié)決定的,但通常它是一個足夠大的位字段,能夠容納系統(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)的比特位會被設(shè)置為1。你可以通過FD_ISSET宏來檢查特定的文件描述符是否已經(jīng)就緒。
3.1.2fd_set類型和宏
通常數(shù)據(jù)類型fd_set以位掩碼的形式來實現(xiàn)。但是,我們并不需要知道這些細節(jié),因為所有關(guān)于文件描述符集合的操作都是通過四個宏完成的:
#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)用會修改這些結(jié)構(gòu)體,當select()返回時,它們包含的就是已處于就緒態(tài)的文件描述符集合了(由于這些結(jié)構(gòu)體會在調(diào)用中被修改,如果在循環(huán)中重復(fù)調(diào)用select(),我們必須保證每次都要重新初始化它們)。
之后這些結(jié)構(gòu)體可以通過FD_ISSET()來檢查是否有數(shù)據(jù)可讀。
3.1.3 select特點
文件描述符上限:
select函數(shù)能同時等待的文件描述符(fd)是有上限的。- 這個上限在很多操作系統(tǒng)中默認為 1024,可以通過調(diào)整內(nèi)核參數(shù)來增加這個上限,但即使增加,仍然存在一個固定的上限。
- 這是因為
select使用位圖來表示文件描述符集合,而位圖的大小是固定的。 - 如果需要更大的文件描述符數(shù)量,需要修改內(nèi)核或使用其他 I/O 多路復(fù)用技術(shù),如
epoll。
維護合法的文件描述符:
- 使用
select時,需要維護一個第三方數(shù)組來保存合法的文件描述符。 - 這是因為在實際應(yīng)用中,文件描述符可能會動態(tài)增加或減少,而
select本身并不提供任何機制來自動跟蹤這些變化。因此,程序員需要自己維護這樣一個列表,確保每次調(diào)用select時提供的文件描述符集合是最新的。
輸入輸出型參數(shù):
select的參數(shù)是輸入輸出型的。這意味著在調(diào)用select之前,你需要設(shè)置好各個集合(readfds、writefds、exceptfds),告訴內(nèi)核你需要監(jiān)控哪些文件描述符的狀態(tài)。而在select返回后,你需要檢查這些集合,確定哪些文件描述符就緒。- 這種模式要求在每次調(diào)用
select之前重置集合,并在調(diào)用之后檢查集合,這增加了用戶的負擔。
第一個參數(shù)是最大 fd+1:
select的第一個參數(shù)nfds是最大的文件描述符值加 1。- 這是因為
select在內(nèi)核中需要遍歷從 0 到最大文件描述符值的范圍,來檢查哪些文件描述符處于就緒狀態(tài)。 - 如果最大文件描述符是
n,那么實際上需要檢查的范圍是從0到n,共n+1個文件描述符。 - 例如,如果最大的文件描述符是 5,那么
select實際上需要檢查 0 到 5 的文件描述符,共 6 個。
位圖的使用:
select使用位圖(fd_set)來表示文件描述符集合。位圖是一種高效的數(shù)據(jù)結(jié)構(gòu),每個比特位代表一個文件描述符的狀態(tài)。- 當用戶向內(nèi)核傳遞文件描述符集合時,實際上是傳遞了一個位圖。內(nèi)核在監(jiān)控過程中會修改這個位圖,將就緒的文件描述符標記出來。
- 當
select返回后,用戶可以根據(jù)位圖來確定哪些文件描述符已經(jīng)準備好。這種方式簡化了內(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,因為 select() 會修改 read_fds
read_fds = master_fds;
// 設(shè)置超時,阻塞 5 秒
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// 調(diào)用 select() 進行 I/O 多路復(fù)用
int activity = select(max_fd + 1, &read_fds, nullptr, nullptr, &timeout);
if (activity == -1) {
perror("select 錯誤");
break;
} else if (activity == 0) {
std::cout << "沒有活動,繼續(xù)等待...\n";
continue;
}
// 遍歷所有文件描述符
for (int fd = 0; fd <= max_fd; ++fd) {
// 如果 fd 是活動的文件描述符
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("讀取錯誤");
close(fd);
FD_CLR(fd, &master_fds);
}
}
}
}
}
close(server_fd);
return 0;
}
原理分析:

3.2 IO復(fù)用poll模型
poll是一種改進的 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ù)組的指針,每個結(jié)構(gòu)體代表一個需要監(jiān)視的文件描述符及其事件。nfds:pollfd數(shù)組中元素的數(shù)量,即有多少個文件描述符需要被監(jiān)視。timeout:等待事件的超時時間,單位為毫秒。-1表示無限等待直到至少有一個文件描述符準備好;0表示非阻塞;如果是正數(shù),則表示最大等待的時間長度。
pollfd 結(jié)構(gòu)體:
struct pollfd {
int fd; // 要監(jiān)視的文件描述符
short events; // 監(jiān)視的事件
short revents; // 實際發(fā)生的事件
};
事件類型常用值:
POLLIN:數(shù)據(jù)可讀。POLLOUT:可以寫數(shù)據(jù)。POLLERR:發(fā)生錯誤。POLLHUP:掛斷。POLLPRI:高優(yōu)先級數(shù)據(jù)可讀。
3.2.2poll解決select存在的問題
poll相較于select在多個方面進行了改進,解決了select模型中的一些關(guān)鍵限制。以下是poll解決select存在問題的主要方面:
- 無需重新設(shè)定參數(shù):與
select每次調(diào)用前都需要重新初始化監(jiān)視的文件描述符集合不同,poll使用一個獨立的結(jié)構(gòu)體數(shù)組(pollfd),該數(shù)組在函數(shù)調(diào)用后保持不變,只需更新事件結(jié)果。這使得代碼更簡潔、易于維護。 - 消除文件描述符數(shù)量的上限:
select受限于系統(tǒng)定義的最大文件描述符數(shù)(如FD_SETSIZE),而poll通過動態(tài)管理文件描述符數(shù)組,僅受系統(tǒng)資源和內(nèi)核能力的限制,適合處理大量并發(fā)連接。 - 更高效的事件通知機制:盡管兩者都可能需要掃描所有文件描述符,
poll得益于其靈活的結(jié)構(gòu)和操作系統(tǒng)的優(yōu)化,在實際應(yīng)用中通常性能更優(yōu)。 - 支持更多事件類型:相較于
select僅支持基本事件類型,poll支持更多種類的事件,如高優(yōu)先級數(shù)據(jù)和掛斷事件,提供更細致的監(jiān)控能力。 - 更好的跨平臺兼容性:
poll在多種現(xiàn)代操作系統(tǒng)中的實現(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è)置套接字選項
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ā)生錯誤
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通過鏈表實現(xiàn)IO復(fù)用,將 socket 注冊 poll_list 鏈表,通過poll系統(tǒng)調(diào)用輪詢鏈表,獲取 socket 事件。
成功獲取到 socket 事件后,poll成功返回,此時可以通過接收函數(shù)讀取 socket 緩沖區(qū)數(shù)據(jù)。

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

4.信號驅(qū)動式IO
信號驅(qū)動式IO是Linux提供的一種IO模型,通過信號通知應(yīng)用程序某個文件描述符的狀態(tài)發(fā)生變化,適用于需要非阻塞IO操作的場景。它利用信號機制將內(nèi)核的事件通知傳遞給用戶空間,使得程序無需主動倫旭即可對IO事件做出響應(yīng)。
4.1 工作原理
- 信號和信號處理:
- 在信號驅(qū)動I/O中,當某個I/O事件(如數(shù)據(jù)準備好讀或?qū)懀┌l(fā)生時,內(nèi)核會向進程發(fā)送一個信號。進程通過信號處理函數(shù)來處理該事件。
- 該信號通常是
SIGIO(I/O信號)或SIGURG(緊急數(shù)據(jù)的信號)等。
- 非阻塞模式:
- 為了使信號驅(qū)動I/O工作,通常需要將相關(guān)的套接字設(shè)置為非阻塞模式。這樣,I/O操作(如
recv或send)不會因為沒有數(shù)據(jù)而阻塞。 - 一旦數(shù)據(jù)準備好,操作系統(tǒng)會發(fā)送
SIGIO信號,通知進程可以進行讀取或?qū)懭搿?/li>
- 信號處理函數(shù):
- 在信號觸發(fā)時,操作系統(tǒng)會中斷進程的正常執(zhí)行流程,并轉(zhuǎn)到預(yù)先注冊的信號處理函數(shù)中執(zhí)行。這使得信號驅(qū)動I/O成為一種異步機制。
- 信號處理函數(shù)中通常會包含對
recv或send等函數(shù)的調(diào)用,以便處理就緒的I/O數(shù)據(jù)。
4.2 為什么使用信號驅(qū)動IO
- **避免輪詢:**傳統(tǒng)的非阻塞I/O通常需要通過
select、poll或epoll等方式輪詢文件描述符,檢查是否有數(shù)據(jù)可以讀取。信號驅(qū)動I/O消除了這種輪詢機制,避免了CPU時間的浪費。 - **減少阻塞:**通過信號驅(qū)動I/O,應(yīng)用程序不再需要阻塞等待I/O事件發(fā)生,而是通過信號觸發(fā)事件,從而使得程序可以在等待I/O的同時執(zhí)行其他任務(wù)。
- **資源高效:**信號驅(qū)動I/O能夠?qū)崿F(xiàn)資源的高效利用,因為它允許應(yīng)用程序在I/O事件到達時立即處理,而不需要檢查文件描述符狀態(tài)。
- **適用于實時應(yīng)用:**對于一些需要及時響應(yīng)的實時應(yīng)用,信號驅(qū)動I/O提供了一種快速響應(yīng)數(shù)據(jù)的方式。
4.3 實現(xiàn)流程
信號驅(qū)動式I/O的核心思想是預(yù)先告知內(nèi)核當某個描述符準備發(fā)生某件事情(如數(shù)據(jù)到達)時發(fā)送一個信號(SIGIO)給進程。這使得進程可以在等待數(shù)據(jù)的過程中不被阻塞,只有在接收到SIGIO信號后才去處理I/O事件。程序需要按照如下步驟執(zhí)行:
- 為內(nèi)核發(fā)送的通知信號安裝一個信號處理例程。默認情況下,這個通知信號為
SIGIO - 設(shè)定文件符的屬主,也就是當文件描述符山可執(zhí)行IO時會接收通知信號的進程或進程組。通常我們讓調(diào)用進程成為屬主。設(shè)定屬主可通過
fcntl()的F_SETOWN操作來完成
fcntl(fd, F_SETOWN , pid);
- 通過設(shè)定
O_NONBLOCK標識使能非阻塞IO - 通過打開
O_ASYNC標志使能信號驅(qū)動IO。這可以和上一步合并為一個操作,因為它們都需要用到fcntl()的F_SETFL操作
flags = fcntl(fd, F_GETFL); // get current flags fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
- 調(diào)用進程線程可以執(zhí)行其他任務(wù)了。當IO操作就緒時,內(nèi)核為進程發(fā)送一個信號,然后調(diào)用在第1步中安裝好的信號處理例程
- 信號驅(qū)動IO提供的是邊緣觸發(fā)通知。這表示一旦進程被通知IO就緒,它就應(yīng)該盡可能的能多地執(zhí)行 I/O(例如盡可能多地讀取字節(jié))。假設(shè)文件描述符時非阻塞的,這表示需要在循環(huán)中執(zhí)行IO系統(tǒng)調(diào)用直到失敗位置,此時的錯誤碼為
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)錯誤\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; // 重新啟動被信號中斷的系統(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è)置文件描述符的擁有者為當前進程
fcntl(sockfd, F_SETOWN, getpid());
int flags = fcntl(sockfd, F_GETFL, 0);
// 啟用信號驅(qū)動模式 | 設(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è)置文件描述符的異步通知機制:
- 設(shè)置 O_ASYNC 標志:使用
fcntl(fd, F_SETFL, flags | O_ASYNC)來啟用異步通知。 - 設(shè)置接收 SIGIO 信號的進程 ID:使用
fcntl(fd, F_SETOWN, getpid())來指定哪個進程應(yīng)該接收SIGIO信號,通知它可以進行IO操作了。
- 內(nèi)核空間處理
在內(nèi)核空間,驅(qū)動程序需要支持異步通知機制。具體步驟如下:
- 管理
fasync_struct隊列:使用fasync_helper函數(shù)來管理一個鏈表(隊列),該鏈表存儲了所有注冊了異步通知的進程信息。 - 觸發(fā)事件:當某個事件發(fā)生時(如數(shù)據(jù)到達),驅(qū)動程序調(diào)用
kill_fasync函數(shù)來通知所有注冊了異步通知的進程。
- 觸發(fā)事件
當驅(qū)動程序檢測到某個事件(如數(shù)據(jù)到達)時,它會調(diào)用 kill_fasync 函數(shù)來通知所有注冊了異步通知的進程。kill_fasync 函數(shù)會遍歷 fasync_struct 隊列,并向每個進程發(fā)送 SIGIO 信號。
- 處理信號
用戶空間的應(yīng)用程序收到 SIGIO 信號后,可以在其信號處理函數(shù)中執(zhí)行相應(yīng)的操作。例如,處理接收到的數(shù)據(jù)或進行其他必要的操作。
5.異步IO
異步IO(Asynchronous IO,AIO)是一種處理輸入/輸出操作的方式,它允許程序在發(fā)起IO操作后立即返回,而不是等待操作完成。
這種方式可以顯著提高應(yīng)用程序的并發(fā)性和吞吐量,特別是在IO密集型的應(yīng)用場景中。下面將從多個角度深入探討異步IO的概念,實現(xiàn)機制及其應(yīng)用場景。
5.1 異步I/O的基本概念
5.1.1 同步 vs 異步
- 同步I/O:當一個進程發(fā)起I/O請求時,它會被阻塞直到該請求完成。這意味著在此期間,進程不能執(zhí)行其他任務(wù)。
- 異步I/O:當一個進程發(fā)起I/O請求時,它可以立即繼續(xù)執(zhí)行其他任務(wù),而不需要等待I/O操作完成。一旦I/O操作完成,系統(tǒng)會通過某種方式通知進程結(jié)果。
5.1.2 阻塞 vs 非阻塞
- 阻塞I/O:如果文件描述符未準備好進行讀寫操作,調(diào)用將被掛起,直到操作準備好為止。
- 非阻塞I/O:如果文件描述符未準備好進行讀寫操作,調(diào)用會立即返回一個錯誤碼,允許進程嘗試其他操作或稍后再試。
雖然“異步”和“非阻塞”聽起來相似,但它們實際上是不同的概念。異步I/O指的是整個操作由內(nèi)核而非用戶進程來完成,并且在完成后通知用戶進程;而非阻塞I/O則是指用戶進程可以在沒有數(shù)據(jù)可讀/寫時不被阻塞,但仍需主動輪詢檢查狀態(tài)
5.1.3 異步IO的工作原理
異步 I/O 的實現(xiàn)通常依賴于以下幾個關(guān)鍵組件:
事件通知機制:內(nèi)核提供一種機制,能夠在 I/O 操作完成時通知應(yīng)用程序。常見的事件通知機制有:
- 信號通知:如
SIGIO。 - 回調(diào)通知:如
io_uring或AIO中的回調(diào)機制。 - 輪詢機制:應(yīng)用程序主動檢查 I/O 狀態(tài),類似于
select()和poll()。
文件描述符和 I/O 操作:文件描述符是 I/O 操作的基礎(chǔ)。內(nèi)核會根據(jù)文件描述符的狀態(tài)來決定 I/O 操作是否可以完成,并在完成時通知程序。
非阻塞模式:異步 I/O 需要文件描述符處于非阻塞模式。通過設(shè)置 O_NONBLOCK 或其他標志,程序可以立即返回,而不是等待 I/O 完成。
POSIX AIO 示例:
POSIX AIO 是 Linux 提供的一套接口,用于執(zhí)行異步 I/O 操作。
在異步 I/O 中,程序可以發(fā)起 I/O 操作(如 aio_read 或 aio_write),然后繼續(xù)執(zhí)行其他任務(wù),而不是等待 I/O 操作完成。
當 I/O 操作完成時,程序可以通過輪詢、信號或回調(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é)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
CentOS 7 x64下Apache+MySQL(Mariadb)+PHP56的安裝教程詳解
這篇文章主要介紹了CentOS 7 x64下Apache+MySQL(Mariadb)+PHP56的安裝教程,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-12-12
Linux中文件描述符fd與文件指針FILE*互相轉(zhuǎn)換實例解析
這篇文章主要介紹了Linux中文件描述符fd與文件指針FILE*互相轉(zhuǎn)換實例解析,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下2018-01-01
environments was not found on the java.library.path 問題的解決方法
The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path 問題的解決方法,需要的朋友可以參考下2016-08-08
Linux環(huán)境(CentOS6.7 64位)下安裝subversion1.9.5的方法
這篇文章主要介紹了Linux環(huán)境(CentOS6.7 64位)下安裝subversion1.9.5的方法,結(jié)合實例形式分析了CentOS下安裝subversion1.9.5的相關(guān)步驟、命令及操作注意事項,需要的朋友可以參考下2018-04-04
解決linux?ping命令報錯name?or?service?not?known問題
文章詳細介紹了兩種解決CentOS?7無法上網(wǎng)的問題的步驟:首先,通過VMware的NAT模式配置網(wǎng)絡(luò),并編輯網(wǎng)絡(luò)配置文件以靜態(tài)IP地址設(shè)置;其次,通過克隆CentOS?7并進行相應(yīng)的IP、UUID和主機名修改,同時更新DNS和網(wǎng)絡(luò)配置,最終實現(xiàn)聯(lián)網(wǎng)2024-11-11
Linux下用dnsmasq做dns cache server的配置方法
最近國外的服務(wù)器本地DNS總是出故障,閃斷一會兒都會影響業(yè)務(wù)。于是在機房里找了兩臺Server,安裝上keepalived和dnsmasq實際本地的DNS緩存2014-08-08
Linux內(nèi)核啟動流程之start_kernel問題
這篇文章主要介紹了Linux內(nèi)核啟動流程之start_kernel問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01

