最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Linux其他備選高級IO模型用法詳解

 更新時間:2025年09月23日 16:26:59   作者:Truelon  
文章介紹了Linux系統(tǒng)中同步與異步I/O模型,包括阻塞I/O、非阻塞I/O、IO多路復(fù)用(select/poll/epoll)、信號驅(qū)動I/O和異步I/O,重點解析了各模型工作原理、優(yōu)缺點及適用場景,強調(diào)通過非阻塞、多路復(fù)用和事件驅(qū)動機制提升I/O效率,減少資源消耗,適用于高并發(fā)網(wǎng)絡(luò)服務(wù)等場景

其他高級 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)鍵目標(biāo)。

高效的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模型,在這種模型下,當(dāng)程序請求I/O操作時,會阻塞當(dāng)前線程直到I/O操作完成(如讀/寫數(shù)據(jù))。在等待期間,線程無法進行其他操作。
  • 優(yōu)點:簡單,易于理解和實現(xiàn)。
  • 缺點:性能瓶頸,當(dāng)進行大量I/O操作時,阻塞會導(dǎ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ù)準(zhǔn)備好,用戶進程會被阻塞,直到數(shù)據(jù)可用。

步驟 1: 發(fā)起 I/O 請求

ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE, 0);
  • 當(dāng)用戶程序調(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 將會把當(dāng)前進程放入 進程等待隊列,并且該進程不再占用 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_INTERRUPTIBLETASK_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è)置文件描述符標(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ù),可能還需要一個或多個附加參數(shù),具體取決于 cmd 的值。

返回值

  • 成功時,根據(jù)cmd的不同,可能需要一個或多個附加參數(shù)。
  • 失敗時,返回-1,并設(shè)置errno以只是錯誤原因。

fcntl的常見命令

復(fù)制文件描述符

示例代碼:

  • F_DUPFD:返回一個大于或等于指定值的最小可用文件描述符,該描述符與原來的描述符指向同一個文件。
  • 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("打開文件失?。?); // 輸出錯誤信息
        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("打開文件失?。?); // 輸出錯誤信息
        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 時關(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ù)時關(guān)閉該描述符。
  • O_NONBLOCK:非阻塞模式。
  • O_APPEND:追加模式。
  • O_ASYNC:啟用信號驅(qū)動 I/O。
  • O_DIRECT:直接 I/O(盡量繞過緩沖區(qū)緩存)。
  • O_NOATIME:不更新文件的訪問時間。

獲取/設(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("打開文件失?。?); // 輸出錯誤信息
        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 非阻塞模式

阻塞模式:(上面已有介紹)

  • 默認情況下,文件描述符處于阻塞模式。
  • 當(dāng)你嘗試從一個沒有數(shù)據(jù)可讀的文件描述符中讀取數(shù)據(jù),或者嘗試向一個寫緩沖區(qū)已滿的文件描述符寫入數(shù)據(jù)時,進程會被阻塞,直到操作可以完成。

非阻塞模式

非阻塞I/O模型下,I/O請求立即返回,不會阻塞程序。如果數(shù)據(jù)沒有準(zhǔn)備好,程序會收到錯誤或“沒有數(shù)據(jù)”的通知,之后可以重新嘗試。

  • 如果設(shè)置了 O_NONBLOCK 標(biāo)志,那么當(dāng)嘗試讀取沒有數(shù)據(jù)可讀或?qū)懢彌_區(qū)已滿的情況下,不會阻塞進程,而是立即返回一個錯誤。
  • 典型的錯誤碼是 EAGAINEWOULDBLOCK,這表明操作不能立即完成。
  • 優(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)標(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 會被設(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è)置 errnoEWOULDBLOCKEAGAIN。
  • 程序檢測到 EWOULDBLOCK 錯誤后,執(zhí)行其他任務(wù)(如打印信息和休眠),然后繼續(xù)嘗試接收數(shù)據(jù)。
  • 這對應(yīng)于用戶程序調(diào)用 recv 發(fā)起 I/O 請求,讀取 socket 緩沖區(qū)數(shù)據(jù)。由于 socket 緩沖區(qū)沒有就緒數(shù)據(jù)包,非阻塞 I/O recv 直接返回 EWOULDBLOCK 錯誤碼,用戶如果一直調(diào)用 recv 函數(shù)則一直返回 EWOULDBLOCK 錯誤碼,直到數(shù)據(jù)準(zhǔn)備好。

非阻塞 I/O/階段2:數(shù)據(jù)到達并喚醒進程

  • 當(dāng)數(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+CCtrl+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)控多個文件描述符,當(dāng)其中任意一個文件描述符就緒時,就可以進行相應(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ā)生異常,返回值為準(zhǔn)備就緒的文件描述符的數(shù)目。
  • 如果 timeout 到達,返回值為 0。這種情況下每個返回的文件描述符集合將被清空。
  • 如果發(fā)生錯誤,返回值為 -1。

timeval結(jié)構(gòu)體原型

struct timeval
{
__time_t tv_sec;		/* 秒.  */
__suseconds_t tv_usec;	/* 微秒.  */
};

當(dāng)timeout設(shè)為NULL,或其指向的結(jié)構(gòu)體字段非零時,select()將阻塞直到下列事件發(fā)生:

  • readfdswritefdsexceptfds中指定的文件描述符中至少有一個成為就緒態(tài)。
  • 該調(diào)用被信號處理中斷。
  • timeout 中指定的時間上限已超時。

關(guān)于 fd_setfd_set 是一個位圖結(jié)構(gòu),用于標(biāo)識一組文件描述符(通常是網(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ù)readfdswritefdsexceptfds所指向的結(jié)構(gòu)體都是保存結(jié)果值的地方。

在調(diào)用select()之前,這些指向的結(jié)構(gòu)體必須初始化(通過FD_ZERO()FD_SET()),以包含我們感興趣的文件描述符集合。

之后select()調(diào)用會修改這些結(jié)構(gòu)體,當(dāng)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è)置好各個集合(readfdswritefds、exceptfds),告訴內(nèi)核你需要監(jiān)控哪些文件描述符的狀態(tài)。而在 select 返回后,你需要檢查這些集合,確定哪些文件描述符就緒。
  • 這種模式要求在每次調(diào)用 select 之前重置集合,并在調(diào)用之后檢查集合,這增加了用戶的負擔(dān)。

第一個參數(shù)是最大 fd+1

  • select 的第一個參數(shù) nfds 是最大的文件描述符值加 1。
  • 這是因為 select 在內(nèi)核中需要遍歷從 0 到最大文件描述符值的范圍,來檢查哪些文件描述符處于就緒狀態(tài)。
  • 如果最大文件描述符是 n,那么實際上需要檢查的范圍是從 0n,共 n+1 個文件描述符。
  • 例如,如果最大的文件描述符是 5,那么 select 實際上需要檢查 0 到 5 的文件描述符,共 6 個。

位圖的使用:

  • select 使用位圖(fd_set)來表示文件描述符集合。位圖是一種高效的數(shù)據(jù)結(jié)構(gòu),每個比特位代表一個文件描述符的狀態(tài)。
  • 當(dāng)用戶向內(nèi)核傳遞文件描述符集合時,實際上是傳遞了一個位圖。內(nèi)核在監(jiān)控過程中會修改這個位圖,將就緒的文件描述符標(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,因為 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模型中的一些局限性,尤其是在處理大量文件描述符和提高性能方面。

pollselect類似,但它通過使用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)視的文件描述符及其事件。
  • nfdspollfd 數(shù)組中元素的數(shù)量,即有多少個文件描述符需要被監(jiān)視。
  • timeout:等待事件的超時時間,單位為毫秒。-1 表示無限等待直到至少有一個文件描述符準(zhǔn)備好;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存在問題的主要方面:

  1. 無需重新設(shè)定參數(shù):與select每次調(diào)用前都需要重新初始化監(jiān)視的文件描述符集合不同,poll使用一個獨立的結(jié)構(gòu)體數(shù)組(pollfd),該數(shù)組在函數(shù)調(diào)用后保持不變,只需更新事件結(jié)果。這使得代碼更簡潔、易于維護。
  2. 消除文件描述符數(shù)量的上限select受限于系統(tǒng)定義的最大文件描述符數(shù)(如FD_SETSIZE),而poll通過動態(tài)管理文件描述符數(shù)組,僅受系統(tǒng)資源和內(nèi)核能力的限制,適合處理大量并發(fā)連接。
  3. 更高效的事件通知機制:盡管兩者都可能需要掃描所有文件描述符,poll得益于其靈活的結(jié)構(gòu)和操作系統(tǒng)的優(yōu)化,在實際應(yīng)用中通常性能更優(yōu)。
  4. 支持更多事件類型:相較于select僅支持基本事件類型,poll支持更多種類的事件,如高優(yōu)先級數(shù)據(jù)和掛斷事件,提供更細致的監(jiān)控能力。
  5. 更好的跨平臺兼容性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ù)用機制,相較于selectpollepoll在處理大量并發(fā)連接時具有更好的性能和擴展性。

epoll的工作機制主要包括下面幾個步驟:

3.3.1 epoll的基本使用方法

  • 創(chuàng)建 epoll 實例

使用 epoll_createepoll_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)閉”標(biāo)志(close-on-exec),這意味著當(dāng)調(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_ctlepoll API 的核心組成部分之一,它用于控制 epoll 實例中的文件描述符集合。通過這個函數(shù),可以向 epoll 實例中添加、修改或刪除感興趣的文件描述符及其對應(yīng)的事件類型。

使用 epoll_ctl 系統(tǒng)調(diào)用向 epoll 實例中注冊需要監(jiān)控的文件描述符及其感興趣的事件類型(如可讀、可寫等)

參數(shù)

  • epfd: epoll_create1epoll_create 返回的 epoll 實例的文件描述符。

op: 操作類型,可以是以下三種之一:

  • EPOLL_CTL_ADD: 向 epoll 實例中添加新的文件描述符,并注冊感興趣的事件。
  • EPOLL_CTL_MOD: 修改已存在的文件描述符上的事件類型。
  • EPOLL_CTL_DEL: 從 epoll 實例中移除指定的文件描述符,不再監(jiān)聽其上的任何事件。
  • fd: 需要操作的目標(biāo)文件描述符。
  • 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ā)生。當(dāng)有事件就緒時,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 實例的文件描述符。
  • 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 實例

當(dāng)不再需要 epoll 實例時,使用 close 關(guān)閉 epoll 文件描述符,釋放資源。

close(epoll_fd);

3.3.2 epoll原理

  • 事件就緒的定義:底層的IO條件滿足了,可以進行某種IO行為了,就叫做事件就緒。select、pollepoll的工作方式都是基于“等待”到“IO就緒”的事件通知機制。它們通過不同的數(shù)據(jù)結(jié)構(gòu)和算法實現(xiàn)對文件描述符的高效管理和事件通知。
  • 紅黑樹epoll內(nèi)部使用紅黑樹來管理事件的注冊和監(jiān)控。紅黑樹作為一種自平衡的二叉搜索樹,能夠高效的執(zhí)行增、刪、改、查操作。紅黑樹的使用大大提高了epoll在大量文件描述符管理中的效率,尤其是在處理動態(tài)注冊和插銷事件時。
  • 回調(diào)通知機制:當(dāng) 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):當(dāng)一個文件描述符準(zhǔn)備好了,內(nèi)核會一直報告這個文件描述符,直到應(yīng)用程序處理這個事件。
  • 邊緣觸發(fā)(Edge-triggered ET):當(dāng)一個文件描述符從無數(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ù)事件通知,導(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ù)據(jù)。如果不處理完所有數(shù)據(jù),下一次數(shù)據(jù)到達時 epoll 可能不會再次通知,從而導(dǎo)致數(shù)據(jù)丟失。

例如,對于一個可讀事件的套接字,應(yīng)該不斷調(diào)用 read()recv() 來讀取所有數(shù)據(jù),直到 read() 返回 EAGAINEWOULDBLOCK,表示數(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è)置為適當(dāng)?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 事件通知機制,它通過以下方式提高效率:

  1. 紅黑樹管理文件描述符:使用 epoll_ctl 系統(tǒng)調(diào)用將感興趣的 socket(或其他文件描述符)注冊到 epoll 實例中。這些描述符被存儲在一個內(nèi)部的紅黑樹結(jié)構(gòu)中,這使得添加、刪除和查找操作都非常高效。
  2. 就緒隊列記錄活動事件:當(dāng)某個 socket 上有數(shù)據(jù)到達時,內(nèi)核會自動將該 socket 標(biāo)記為就緒,并將其加入到一個就緒隊列中。這里并沒有使用回調(diào)函數(shù),而是依賴于內(nèi)核的通知機制。
  3. 事件通知而非輪詢:與 selectpoll 不同,epoll 使用的是事件驅(qū)動的通知機制。這意味著只有當(dāng)有實際事件發(fā)生時(如可讀或可寫),epoll_wait 才會返回并告知應(yīng)用程序哪些 socket 已準(zhǔn)備好進行 I/O 操作。這樣避免了對所有文件描述符的重復(fù)檢查,提高了效率。
  4. 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 工作原理

  1. 信號和信號處理:
  • 在信號驅(qū)動I/O中,當(dāng)某個I/O事件(如數(shù)據(jù)準(zhǔn)備好讀或?qū)懀┌l(fā)生時,內(nèi)核會向進程發(fā)送一個信號。進程通過信號處理函數(shù)來處理該事件。
  • 該信號通常是 SIGIO(I/O信號)或 SIGURG(緊急數(shù)據(jù)的信號)等。
  1. 非阻塞模式:
  • 為了使信號驅(qū)動I/O工作,通常需要將相關(guān)的套接字設(shè)置為非阻塞模式。這樣,I/O操作(如 recvsend)不會因為沒有數(shù)據(jù)而阻塞。
  • 一旦數(shù)據(jù)準(zhǔn)備好,操作系統(tǒng)會發(fā)送 SIGIO 信號,通知進程可以進行讀取或?qū)懭搿?/li>
  1. 信號處理函數(shù):
  • 在信號觸發(fā)時,操作系統(tǒng)會中斷進程的正常執(zhí)行流程,并轉(zhuǎn)到預(yù)先注冊的信號處理函數(shù)中執(zhí)行。這使得信號驅(qū)動I/O成為一種異步機制。
  • 信號處理函數(shù)中通常會包含對 recvsend 等函數(shù)的調(diào)用,以便處理就緒的I/O數(shù)據(jù)。

4.2 為什么使用信號驅(qū)動IO

  1. **避免輪詢:**傳統(tǒng)的非阻塞I/O通常需要通過 select、pollepoll 等方式輪詢文件描述符,檢查是否有數(shù)據(jù)可以讀取。信號驅(qū)動I/O消除了這種輪詢機制,避免了CPU時間的浪費。
  2. **減少阻塞:**通過信號驅(qū)動I/O,應(yīng)用程序不再需要阻塞等待I/O事件發(fā)生,而是通過信號觸發(fā)事件,從而使得程序可以在等待I/O的同時執(zhí)行其他任務(wù)。
  3. **資源高效:**信號驅(qū)動I/O能夠?qū)崿F(xiàn)資源的高效利用,因為它允許應(yīng)用程序在I/O事件到達時立即處理,而不需要檢查文件描述符狀態(tài)。
  4. **適用于實時應(yīng)用:**對于一些需要及時響應(yīng)的實時應(yīng)用,信號驅(qū)動I/O提供了一種快速響應(yīng)數(shù)據(jù)的方式。

4.3 實現(xiàn)流程

信號驅(qū)動式I/O的核心思想是預(yù)先告知內(nèi)核當(dāng)某個描述符準(zhǔn)備發(fā)生某件事情(如數(shù)據(jù)到達)時發(fā)送一個信號(SIGIO)給進程。這使得進程可以在等待數(shù)據(jù)的過程中不被阻塞,只有在接收到SIGIO信號后才去處理I/O事件。程序需要按照如下步驟執(zhí)行:

  • 為內(nèi)核發(fā)送的通知信號安裝一個信號處理例程。默認情況下,這個通知信號為SIGIO
  • 設(shè)定文件符的屬主,也就是當(dāng)文件描述符山可執(zhí)行IO時會接收通知信號的進程或進程組。通常我們讓調(diào)用進程成為屬主。設(shè)定屬主可通過fcntl()F_SETOWN操作來完成
fcntl(fd, F_SETOWN , pid);
  • 通過設(shè)定O_NONBLOCK標(biāo)識使能非阻塞IO
  • 通過打開O_ASYNC標(biāo)志使能信號驅(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ù)了。當(dāng)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è)置文件描述符的擁有者為當(dāng)前進程
    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通知的工作流程

  1. 注冊異步通知

在用戶空間,應(yīng)用程序通過 fcntl 系統(tǒng)調(diào)用來設(shè)置文件描述符的異步通知機制:

  • 設(shè)置 O_ASYNC 標(biāo)志:使用 fcntl(fd, F_SETFL, flags | O_ASYNC) 來啟用異步通知。
  • 設(shè)置接收 SIGIO 信號的進程 ID:使用 fcntl(fd, F_SETOWN, getpid()) 來指定哪個進程應(yīng)該接收 SIGIO 信號,通知它可以進行IO操作了。
  1. 內(nèi)核空間處理

在內(nèi)核空間,驅(qū)動程序需要支持異步通知機制。具體步驟如下:

  • 管理 fasync_struct 隊列:使用 fasync_helper 函數(shù)來管理一個鏈表(隊列),該鏈表存儲了所有注冊了異步通知的進程信息。
  • 觸發(fā)事件:當(dāng)某個事件發(fā)生時(如數(shù)據(jù)到達),驅(qū)動程序調(diào)用 kill_fasync 函數(shù)來通知所有注冊了異步通知的進程。
  1. 觸發(fā)事件

當(dāng)驅(qū)動程序檢測到某個事件(如數(shù)據(jù)到達)時,它會調(diào)用 kill_fasync 函數(shù)來通知所有注冊了異步通知的進程。kill_fasync 函數(shù)會遍歷 fasync_struct 隊列,并向每個進程發(fā)送 SIGIO 信號。

  1. 處理信號

用戶空間的應(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:當(dāng)一個進程發(fā)起I/O請求時,它會被阻塞直到該請求完成。這意味著在此期間,進程不能執(zhí)行其他任務(wù)。
  • 異步I/O:當(dāng)一個進程發(fā)起I/O請求時,它可以立即繼續(xù)執(zhí)行其他任務(wù),而不需要等待I/O操作完成。一旦I/O操作完成,系統(tǒng)會通過某種方式通知進程結(jié)果。

5.1.2 阻塞 vs 非阻塞

  • 阻塞I/O:如果文件描述符未準(zhǔn)備好進行讀寫操作,調(diào)用將被掛起,直到操作準(zhǔn)備好為止。
  • 非阻塞I/O:如果文件描述符未準(zhǔn)備好進行讀寫操作,調(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_uringAIO 中的回調(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 或其他標(biāo)志,程序可以立即返回,而不是等待 I/O 完成。

POSIX AIO 示例:

POSIX AIO 是 Linux 提供的一套接口,用于執(zhí)行異步 I/O 操作。

在異步 I/O 中,程序可以發(fā)起 I/O 操作(如 aio_readaio_write),然后繼續(xù)執(zhí)行其他任務(wù),而不是等待 I/O 操作完成。

當(dāng) 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用vsftpd搭建FTP服務(wù)器

    阿里云基于CentOS用vsftpd搭建FTP服務(wù)器

    這篇文章主要介紹了阿里云CentOS系統(tǒng)yum方式安裝vsftpd的相關(guān)資料,最近想要了解的朋友可以了解一下。
    2016-10-10
  • 函數(shù)sync、fsync與fdatasync的總結(jié)整理(必看篇)

    函數(shù)sync、fsync與fdatasync的總結(jié)整理(必看篇)

    下面小編就為大家?guī)硪黄瘮?shù)sync、fsync與fdatasync的總結(jié)整理(必看篇)。小編覺得挺不錯的?,F(xiàn)在就分享給大家。也給大家做個參考。一起跟隨小編過來看看吧
    2016-12-12
  • Linux中一種友好的find替代工具(fd命令)

    Linux中一種友好的find替代工具(fd命令)

    這篇文章主要介紹了Linux中一種友好的find替代工具(fd命令),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-11-11
  • Virtualbox安裝Lubuntu 18.04 64位的圖文教程

    Virtualbox安裝Lubuntu 18.04 64位的圖文教程

    這篇文章主要為大家詳細介紹了Virtualbox安裝Lubuntu 18.04 64位的圖文教程,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-07-07
  • Linux下sshd服務(wù)及服務(wù)管理命令詳解

    Linux下sshd服務(wù)及服務(wù)管理命令詳解

    這篇文章主要介紹了Linux下sshd服務(wù)及服務(wù)管理命令,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • 詳解虛擬機下CentOS 6.5配置IP地址的三種方法

    詳解虛擬機下CentOS 6.5配置IP地址的三種方法

    本篇文章主要介紹了虛擬機下CentOS 6.5配置IP地址的三種方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2017-03-03
  • Linux之性能監(jiān)測命令解讀

    Linux之性能監(jiān)測命令解讀

    這篇文章主要介紹了Linux之性能監(jiān)測命令,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06
  • Linux文件和文件夾權(quán)限操作方法

    Linux文件和文件夾權(quán)限操作方法

    這篇文章主要介紹了Linux文件和文件夾權(quán)限操作方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05
  • Apache中如何設(shè)置默認首頁具體該怎么操作

    Apache中如何設(shè)置默認首頁具體該怎么操作

    這篇文章主要介紹了Apache中設(shè)置默認首頁的方法,需要的朋友可以參考下
    2014-02-02
  • Centos 7之Firewalld相關(guān)命令詳細介紹

    Centos 7之Firewalld相關(guān)命令詳細介紹

    這篇文章主要介紹了Centos 7之Firewalld相關(guān)命令詳細介紹的相關(guān)資料,需要的朋友可以參考下
    2017-02-02

最新評論

高台县| 长治市| 榕江县| 泰安市| 修武县| 靖安县| 西乌| 汉川市| 泗洪县| 饶阳县| 六枝特区| 新平| 自治县| 闽清县| 镇康县| 云阳县| 庄河市| 瓮安县| 通辽市| 正阳县| 长顺县| 栾城县| 习水县| 苗栗市| 罗平县| 临潭县| 凤冈县| 称多县| 太谷县| 徐闻县| 科技| 拜城县| 平泉县| 三门县| 天柱县| 眉山市| 安顺市| 福泉市| 酒泉市| 云龙县| 邛崃市|