大話「I-O 多路複用」

我是一名來自帝都 985 高校的機械研究生

本號專注於分享工科專業學習考試攻略、程序員學習求職經驗、教師求職考試經驗等相關內容,同時還會兼顧生活向,歡迎大家的關注和轉發!

目錄

select

poll

epoll

I/O多路複用使得程序能同時監聽多個文件描述符,能夠提高程序的性能,Linux 下實現 I/O 多路複用的系統調用主要有 selectpollepoll

select

函數調用過程:

  1. 首先要構造一個關於文件描述符的列表,將要監聽的文件描述符添加到該列表中。

  2. 調用一個系統函數,監聽該列表中的文件描述符,直到這些描述符中的一個或者多個進行 I/O 操作時,該函數才返回。

  1. 在返回時,它會告訴進程有多少(哪些)描述符要進行 I/O 操作。
// sizeof(fd_set) = 128   1024
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
     fd_set *exceptfds, struct timeval *timeout);
- 參數:
  - nfds : 委託內核檢測的最大文件描述符的值 + 1
  - readfds : 要檢測的文件描述符的讀的集合,委託內核檢測哪些文件描述符讀的屬性
      - 一般檢測讀操作
      - 對應的是對方發送過來的數據,因爲讀是被動的接收數據,檢測的就是讀緩衝區
      - 是一個傳入傳出參數
  - writefds : 要檢測的文件描述符的寫的集合,委託內核檢測哪些文件描述符寫的屬性
        - 委託內核檢測寫緩衝區是不是還可以寫數據(不滿的就可以寫)
  - exceptfds : 檢測發生異常的文件描述符的集合
  - timeout : 設置的超時時間
   
     struct timeval {
       long   tv_sec;     /* seconds */
       long   tv_usec;     /* microseconds */
     };    
      - NULL : 永久阻塞,直到檢測到了文件描述符有變化
      - tv_sec = 0 tv_usec = 0, 不阻塞
      - tv_sec > 0 tv_usec > 0, 阻塞對應的時間
         
   - 返回值 :
      - -1 : 失敗
      - >0(n) : 檢測的集合中有n個文件描述符發生了變化

// 將參數文件描述符fd對應的標誌位設置爲0
void FD_CLR(int fd, fd_set *set);

// 將參數文件描述符fd對應的標誌位設置爲1
void FD_SET(int fd, fd_set *set);

// 判斷fd對應的標誌位是0還是1,
// 返回值:fd 對應標誌位的值
// 0,返回0, 1,返回1
int  FD_ISSET(int fd, fd_set *set);

// fd_set一共有1024 bit, 全部初始化爲0
void FD_ZERO(fd_set *set);

  1. 每次調用 select,都需要把 fd 集合從用戶態拷貝到內核態,這個開銷在 fd 很多時會很大;

  2. 同時每次調用 select 都需要在內核遍歷傳遞進來的所有 fd,這個開銷在 fd 很多時也很大;

  3. select 支持的文件描述符數量太小了,默認是 1024;

  4. fds 集合不能重用,每次都需要重置。

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>

int main() {

    // 創建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 綁定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 監聽
    listen(lfd, 8);

    // 創建一個fd_set的集合,存放的是需要檢測的文件描述符
    fd_set rdset, tmp;
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;

    while(1) {

        tmp = rdset;

        // 調用select系統函數,讓內核幫檢測哪些文件描述符有數據
        int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
        if(ret == -1) {
            perror("select");
            exit(-1);
        } else if(ret == 0) {
            continue;
        } else if(ret > 0) {
            // 說明檢測到了有文件描述符的對應的緩衝區的數據發生了改變
            if(FD_ISSET(lfd, &tmp)) {
                // 表示有新的客戶端連接進來了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 將新的文件描述符加入到集合中
                FD_SET(cfd, &rdset);

                // 更新最大的文件描述符
                maxfd = maxfd > cfd ? maxfd : cfd;
            }

            for(int i = lfd + 1; i <= maxfd; i++) {
                if(FD_ISSET(i, &tmp)) {
                    // 說明這個文件描述符對應的客戶端發來了數據
                    char buf[1024] = {0};
                    int len = read(i, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(i);
                        FD_CLR(i, &rdset);
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                }
            }

        }

    }
    close(lfd);
    return 0;
}

poll

#include <poll.h>
struct pollfd {
int  fd;     /* 委託內核檢測的文件描述符 */
short events;   /* 委託內核檢測文件描述符的什麼事件 */
short revents;   /* 文件描述符實際發生的事件 */
};

//栗子
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 參數:
    - fds : 是一個struct pollfd 結構體數組,這是一個需要檢測的文件描述符的集合
    - nfds : 這個是第一個參數數組中最後一個有效元素的下標 + 1
    - timeout : 阻塞時長
      0 : 不阻塞
      -1 : 阻塞,當檢測到需要檢測的文件描述符有變化,解除阻塞
      >0 : 阻塞的時長
  - 返回值:
    -1 : 失敗
    >0(n) : 成功,n表示檢測到集合中有n個文件描述符發生變化

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>


int main() {

    // 創建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 綁定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 監聽
    listen(lfd, 8);

    // 初始化檢測的文件描述符數組
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++) {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;
    int nfds = 0;

    while(1) {

        // 調用poll系統函數,讓內核幫檢測哪些文件描述符有數據
        int ret = poll(fds, nfds + 1, -1);
        if(ret == -1) {
            perror("poll");
            exit(-1);
        } else if(ret == 0) {
            continue;
        } else if(ret > 0) {
            // 說明檢測到了有文件描述符的對應的緩衝區的數據發生了改變
            if(fds[0].revents & POLLIN) {
                // 表示有新的客戶端連接進來了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 將新的文件描述符加入到集合中
                for(int i = 1; i < 1024; i++) {
                    if(fds[i].fd == -1) {
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }

                // 更新最大的文件描述符的索引
                nfds = nfds > cfd ? nfds : cfd;
            }

            for(int i = 1; i <= nfds; i++) {
                if(fds[i].revents & POLLIN) {
                    // 說明這個文件描述符對應的客戶端發來了數據
                    char buf[1024] = {0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }

        }

    }
    close(lfd);
    return 0;
}

epoll

#include <sys/epoll.h>
// 創建一個新的epoll實例。在內核中創建了一個數據,這個數據中有兩個比較重要的數據:
// 一個是需要檢測的文件描述符的信息(紅黑樹)
// 還有一個是就緒列表,存放檢測到數據發生改變的文件描述符信息(雙向鏈表)

int epoll_create(int size);
- 參數:
    size : 目前沒有意義了。隨便寫一個數,必須大於0
- 返回值:
    -1 : 失敗
    > 0 : 文件描述符,操作epoll實例的

  
// 對epoll實例進行管理:添加文件描述符信息,刪除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 參數:
  - epfd : epoll實例對應的文件描述符
  - op : 要進行什麼操作
    EPOLL_CTL_ADD:  添加
    EPOLL_CTL_MOD: 修改
    EPOLL_CTL_DEL: 刪除
  - fd : 要檢測的文件描述符
  - event : 檢測文件描述符對應的檢測事件

    struct epoll_event {
    uint32_t   events;    /* Epoll events */
    epoll_data_t data;     /* User data variable */
    };

    常見的 Epoll 檢測事件:
      - EPOLLIN
      - EPOLLOUT
      - EPOLLERR
      
    typedef union epoll_data {
    void     *ptr;
    int      fd;
    uint32_t   u32;
    uint64_t   u64;
    } epoll_data_t;

// 檢測函數        
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 參數:
  - epfd : epoll實例對應的文件描述符
  - events : 傳出參數,保存發送變化的文件描述符的信息
  - maxevents : 第二個參數結構體數組的大小
  - timeout : 阻塞時間
    - 0 : 不阻塞
    - -1 : 阻塞,直到檢測到fd數據發生變化,解除阻塞
    - > 0 : 阻塞的時長(毫秒)
       
- 返回值:
  - 成功,返回發送變化的文件描述符的個數 > 0
  - 失敗 -1
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main() {

    // 創建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 綁定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 監聽
    listen(lfd, 8);

    // 調用epoll_create()創建一個epoll實例
    int epfd = epoll_create(100);

    // 將監聽的文件描述符相關的檢測信息添加到epoll實例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 監聽的文件描述符有數據達到,有客戶端連接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }   
                // 有數據到達,需要通信
                char buf[1024] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}
  1. LT 模式 (水平觸發)

假設委託內核檢測讀事件 -> 檢測 fd 的讀緩衝區

讀緩衝區有數據 - > epoll 檢測到了會給用戶通知

a. 用戶不讀數據,數據一直在緩衝區,epoll 會一直通知

b. 用戶只讀了一部分數據,epoll 會通知

c. 緩衝區的數據讀完了,不通知

LT(level - triggered)是缺省的工作方式,並且同時支持 block 和 no-block socket。在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的 fd 進行 IO 操作。如果你不作任何操作,內核還是會繼續通知你的。

  1. ET 模式(邊沿觸發)

假設委託內核檢測讀事件 -> 檢測 fd 的讀緩衝區

讀緩衝區有數據 - > epoll 檢測到了會給用戶通知

a. 用戶不讀數據,數據一直在緩衝區中,epoll 下次檢測的時候就不通知了

b. 用戶只讀了一部分數據,epoll 不通知

c. 緩衝區的數據讀完了,不通知

ET(edge - triggered)是高速工作方式,只支持 no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過 epoll 告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了。但是請注意,如果一直不對這個 fd 作 IO 操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)。

ET 模式在很大程度上減少了 epoll 事件被重複觸發的次數,因此效率要比 LT 模式高。epoll 工作在 ET 模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀 / 阻塞寫操作把處理多個文件描述符的任務餓死。

struct epoll_event {
uint32_t   events;    /* Epoll events */
epoll_data_t data;     /* User data variable */
};

//常見的Epoll檢測事件:
  - EPOLLIN
  - EPOLLOUT
  - EPOLLERR
  - EPOLLET
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CxymdtDLwE2G_J4KH3Leig