20分鐘瞭解Epoll + 聊天室實戰

来源:https://www.cnblogs.com/skyfynn/archive/2019/04/02/10644243.html
-Advertisement-
Play Games

瞭解IO多路復用,瞭解poll和select存在的問題,深入瞭解epoll如何高效,最後使用epoll實現一個聊天室鞏固學習 ...


我們知道,電腦的硬體資源由操作系統管理、調度,我們的應用程式運行在操作系統之上,我們的程式運行需要訪問電腦上的資源(如讀取文件,接收網路請求),操作系統有內核空間和用戶空間之分,所以數據讀取,先由內核讀取數據到內核緩衝區,然後才會從操作系統的內核空間拷貝到用戶空間,這個就是緩存I/O,又被稱作標準I/O。

幾種常見的IO模式:阻塞I/O、非阻塞I/O、I/O多路復用

1、阻塞I/O

用戶進程向內核發起I/O系統調用,內核去準備所需的數據,直到數據都準備好了(需要一段時間)返回給用戶進程,在這期間,用戶進程一直處於阻塞狀態,拿到所需數據,才會繼續向下執行。

2、非阻塞I/O

用戶進程向內核發起I/O系統調用,內核發現數據還沒準備好,立即返回error,用戶進程拿到error,可以再次向內核發起請求,直到獲取所需數據

3、I/O多路復用

下麵詳細介紹

一、I/O多路復用

定義:I/O multiplexing allows us to simultaneously monitor multiple file descriptors to see if I/O is possible on any of them.

1、為什麼要同時監視多個文件描述符?

傳統的阻塞I/O模型可以滿足大部分的應用程式使用場景,但有的時候,一些應用程式會同時需要如下特性:

  • 檢查文件I/O是否已經ready,如果沒有,不阻塞,直接返回
  • 同時監視多個文件描述符,判斷他們中的任意一個的I/O是否已經ready

比如一個web伺服器,可能同時打開了幾千個連接,每accept一個連接,操作系統產生一個fd(文件描述符),我們的伺服器需要監聽這些文件描述符,當客戶端發來新的數據的時候,我們需要處理請求數據,並給出響應,正常的實現方式可能如下:

connections = [fd1, fd2, fdn];
for (c in connections) {
    if (hasNewInput(x)) {
        processInput(x);
    }
}

這樣實現的問題是,我們自己維護著所有的連接,需要主動的去輪尋判斷I/O是否已經ready以便進行讀寫,但事實上並不是所有連接都是活躍狀態(建立的連接並沒有數據交互),如果我們輪尋的頻率很低,那用戶獲取響應的時間可能長的無法忍受,如果輪尋的頻率非常的高,則會浪費CPU的時間。

所以如果可以將主動遍歷所有連接,判斷每一個的IO狀態,改為將這些文件描述符(fd)交給內核去監視,然後當一個或多個文件描述符IO ready的時候,內核來告訴我們,這樣效率就大大提高了,因此我們需要IO多路復用。

2、I/O多路復用的幾種實現

IO多路復用的實現比較普遍的有兩個:select和poll,相比較poll,select更為普遍,最早與BSD socket api一起出現,已被納入SUSV3標準規範,epoll是只屬於Linux的特性,最早的API出現在Linux2.6。

名字叫I/O多路復用,所謂的復用,復用的是同一個進程(線程),也就是在同一個進程中“併發“的完成多個文件描述符的I/O。

2.1、select

select系統調用會阻塞,直到一個或多個文件描述符I/O ready

1>定義:

#include <sys/time.h>         /* For portability */ 
#include <sys/select.h> 

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

2>參數:

  • nfds:應設置為大於等於下麵三個fds監聽的文件描述符的總和
  • readfds:需要監聽輸入事件的文件描述符集合
  • writefds:需要監聽輸出事件的文件描述符集合
  • exceptfds:需要被監聽是否有異常情況發生的文件描述符集合
  • timeout:,阻塞;設置為0,檢查一遍文件描述符狀態立即返回;設置為NULL或者非0值,會一直阻塞直到:
    • 三種fds中有至少一個文件描述符進入ready狀態
    • 被信號處理中斷
    • 到達指定的超時時間

3>返回值:

  • 返回-1:產生錯誤
  • 返回0:超時返回了,這期間沒有文件描述符ready
  • 返回正數:返回的數值就是已經ready的文件描述符的數量

4>小示例

#include <stdio.h>
#include <string.h>
#include <sys/time.h> 
#include <sys/select.h>

int main(int argc, char *argv[])
{
    fd_set readfds, writefds;
    int ready, nfds, fd, numRead, j;
    struct timeval timeout;
    struct timeval *pto;
    char buf[10];
    
    //判斷參數是否包含短斜線,包含timeout設置NULL
    if (strcmp(argv[1], "-") == 0) {
        pto = NULL;
    } else {
        pto = &timeout;
        pto.tv_sec = atoi(argv[1]);
        pto.tv_usec = 0;
    }
    //準備監聽文件描述符集合
    nfds = 0;
    //用select 提供的巨集初始化fd_set
    FD_ZERO(&readfds);
    FD_ZERO(&writefds);

    for (j = 2; j < argc; j++) {
        numRead = sscanf(argv[j], "%d%2[rw]", &fd, buf);
        if (numRead != 2) {
            printf("error");
            return 0;
        }
        if (fd >= nfds) {
            nfds += 1;
        }
        //判斷是監聽讀還是寫
        if (strchr(buf, 'r') != NULL) {
            FD_SET(fd, &readfds);
        }
        if (strchr(buf, 'w') != NULL) {
            FD_SET(fd, &writefds);
        }
    }
    //調用select函數
    ready = select(nfds, &readfds, &writefds, NULL, pto);
    if (ready == -1) {
        printf("select error\n");
        return 0;
    }
    for (fd = 0; fd < nfds; fd++) {
        //列印ready 狀態
        printf("%d: %s%s\n", fd, FD_ISSET(fd, &readfds) ? "r" : "", FD_ISSET(fd, &writefds) ? "w" : "");
    }
    return 0;
}

使用文件描述符0 也就是標準輸入測試,首先編譯一下這段代碼gcc -o select select.c

運行./select 10 0r 代表select阻塞最多10秒超時返回,監聽標準輸入的 讀ready狀態,會發現程式處於阻塞狀態,直到10秒後select返回,列印 0:,也就是沒有ready

運行./select - 0r 代表select不會超時直到 標準輸入 讀ready,程式會一直掛起,直到我們敲回車,返回0:r

運行./select 5 0r 1w 代表最多5秒超時,同時監聽標準輸入的讀狀態,和標準輸出的寫狀態,返回0: 1:w

2.2、poll

poll跟select機制基本一樣,不同點在於select將監聽的文件描述符分開到3個集合中,poll只需一個文件描述符列表

1>定義

#include <poll.h> 

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

2>參數

  • struct pollfd fds:文件描述符數組
struct pollfd {     
    int   fd;               /* File descriptor */     
    short events;           /* Requested events bit mask */     
    short revents;          /* Returned events bit mask */ 
};
  • nfds:fds數組中文件描述符的總數
  • timeout:超時時間,傳-1阻塞直到有文件描述符ready,傳0不阻塞,傳>0的值,不管有沒有ready的文件描述符,到指定時間超時返回

3>返回值

  • 返回-1:有錯誤產生
  • 返回0:沒有任何文件描述符ready
  • 返回正數:fds元素revents不為0的文件描述的個數

3、poll和select存在哪些問題?

1、每次調用poll()或select(),內核必須檢查傳過來的所有文件描述符,當文件描述符超過一定數量後,光是檢查這一步就已經非常耗時了

2、每次調用poll()或select(),需要從用戶態初始化文件描述符的數據結構,然後傳遞到內核態,在內核態檢查IO狀態,然以將狀態更新到文件描述符數據結構,再返回這個數據結構,數據需不斷的在用戶態和內核態間拷貝,同樣當文件描述符數量很大時,非常耗費CPU時間

3、每次調用完poll()或select(),還要遍歷返回的所有文件描述符,判斷狀態,浪費時間

解決以上問題的關鍵是:1)不要每次都在內核態和用戶態複製這些監聽的文件描述符數據 2)只返回IO ready 的文件描述符,不要只標識狀態,自己還需要再遍歷一遍,因此,Linux給了我們epoll。

二、Epoll

Linux的epoll,也是I/O多路復用的一種實現方式,同樣是同時監聽多個文件描述符的I/O狀態,他有如下幾個優勢:

  • 同時監聽的大量文件描述符,效率高於select和epoll
  • epoll api同時提供了水平觸發通知和邊緣觸發通知可選,select和epoll只有水平觸發通知的方式(兩種通知方式後面會講)
  • 對於監聽文件描述的具體事件(讀、寫等,通過bit mask的方式),更為靈活

不像select和poll直接就是系統調用的函數,epoll由3個系統調用函數組成

1、epoll的3個函數

1.1、epoll_create

通過調用epoll_create,創建一個新的epoll內核數據結構實例,返回該實例的文件描述符,然後,調用進程可以使用這個文件描述符向epoll實例添加、刪除或修改它想要監視的其他文件描述符

#include <sys/epoll.h>
int epoll_create(int size);

1.2、epoll_ctl

進程可以通過調用epoll_ctl向epoll實例添加它希望監視的文件描述符,所有這些文件描述符由epoll實例維護在interest list中。當被監視的文件描述符為I/O做好準備時,他們就進入到ready list,ready list是interest list的一個子集

定義:

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

參數:

  • epfd:epoll_create返回的文件描述符
  • fd:準備加入監視列表(interest list)的文件描述符
  • op:要對這個fd做什麼操作,有3種
    • 添加:EPOLL_CTL_ADD
    • 刪除:EPOLL_CTL_DEL
    • 修改:EPOLL_CTL_MOD
  • event:指向epoll_event結構體的指針,結構體中存儲著我們實際想要監視fd的哪些事件

epoll_event結構:

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

events:
事件的成員是位掩碼,比如fd是一個socket,我們可能希望監視他,以獲取套接字緩衝區上的新數據(使用EPOLLIN),如果希望事件的通知機制使用邊緣觸發的方式,可以使用EPOLLET。也就是讀操作就緒,使用邊緣觸發的通知方式,需要這樣指定events:EPOLLIN | EPOLLET

所有可以指定的events見http://man7.org/linux/man-pages/man2/epoll_ctl.2.html

data:
epoll_data_t 是一個union,可以存儲一些數據,當該文件描述符ready的時候,返回給調用監聽的進程

typedef union epoll_data {     
    void *ptr;          /* Pointer to user-defined data */     
    int fd;             /* File descriptor */     
    uint32_t u32;        /* 32-bit integer */     
    uint64_t u64;        /* 64-bit integer */ 
} epoll_data_t;

返回值:返回0執行成功,-1發生錯誤

1.3、epoll_wait

通過調用epoll_wait,可以獲取之前監聽的所有事件(interest list)中I/O準備好的事件(ready list)

定義:

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);

參數:

  • epfd:epoll_create創建epoll實例,返回的文件描述符
  • evlist:epoll_event數組,epoll_wait調用會阻塞,直到有文件描述符ready,ready的文件描述符及發生的事件會保存到evlist
  • maxevents:evlist數組的長度
  • timeout:指定epoll_wait將阻塞多長時間
    • 設置為0,非阻塞模式,檢查interest list是否有ready的文件描述符後立即返回
    • 設置為-1,永遠的阻塞直到1>interest list中的一個或多個文件描述符就緒 2>被信號處理程式終斷
    • 設置為正數:永遠的阻塞直到1>interest list中的一個或多個文件描述符就緒 2>被信號處理程式終斷 3> 指定的timeout毫秒到了,過期返回

2、深入epoll實例

2.1、先弄清文件描述符和打開文件的關係

為了弄清楚打開文件、文件描述符和epoll之間的關係,我們先看一下Linux操作系統中文件描述符(file descriptors)、打開文件描述(open file description)、和系統級文件inode表之間的關係,如圖所示:

內核維護了3個數據結構:

  • 每個進程打開的文件描述符(file descriptor table)
    • fd flags:文件的flag,就是只讀、只寫等這些flag
    • file ptr:指向文件描述的指針
  • 系統中打開文件的描述(open file table)
    • file offset:當前文件的offset
    • status flags:對應open的flags參數
    • inode ptr:指向i-node對象的指針
  • 操作系統維護文件系統 (i-node table)
    • file type:文件類型
    • file locks:一個指向該文件鎖相關的列表的指針
    • 以及其它各種標識這個文件相關信息

圖中可以看出:

  • A進程的fd1和fd20這兩個文件描述符的file ptr指向同一個同一個open file description(可能執行了dup系統調用複製了文件描述符)
  • A進程fd2和B進程的fd2的file ptr也指向同一個open file description(B可能是A調用系統調用fork出來的子進程)
  • A進程的fd0和B進程的fd3指向不同的file description,但是指向的是同一個i-node,也就說打開的是同一個文件

2.2、再看epoll_create

當調用了epoll_create,事實上內核在inode-table創建了一個新的inode(也就是epoll實例),以及在file description table中添加一條打開文件描述記錄,並且返回給調用者一個文件描述符。

接下來我們就可以使用這個文件描述符,向epoll實例中添加需要監聽的文件描述符,當調用epoll_ctl添加一個文件描述符到epoll實例的監聽列表(interest list)的時候,事實上真正添加的不是文件描述符,而是其對應的文件描述(file description table)。基於這一點,結合上面文件描述符和打開文件關係的圖:

假如進程A調用epoll_create,返迴文件描述符fd8。

  • 如果B是A fork出來的子進程,那麼會繼承A所有打開的文件描述符,包括fd8,所以進程A和進程B共用interest list,
  • A fork完 B 之後,又打開了一個文件,返迴文件描述符fd3,然後調用epoll_ctl添加到interest list中,這時B中並不包括A新打開的文件描述符fd3,但是當fd3有I/O 事件的時候,A或B進程調用epoll_wait,都會收到fd3 IO ready的通知

2.3、epoll為什麼性能高於poll和select?

回顧上述poll()和select()存在的問題,可以看到epoll剛好解決了這些問題:

  • 相比較poll和select,每次調用迴圈檢查每一個文件描述符I/O狀態,時間複雜度O(N),不管N大小,調用一次迴圈檢查一次;epoll得益於監聽的是文件描述符下一級的打開文件描述,並且每次調用epoll_wait的時候直接返回ready list就可以了,不需要迴圈
  • 相比較poll和select,每次調用都需要傳遞監聽文件描述符數據到內核,epoll調用epoll_ctl一次性添加到epoll實例,接下來調用epoll_wait不需要再傳遞這些文件描述符信息過去了
  • 相比較poll()和select()返回所有的文件描述,epoll只返回ready的文件描述符

監聽不同數量文件描述符,執行100000次狀態檢查select、poll、epoll性能對比:

3、關於level-trigger 和 edge-trigger

3.1、文件描述符準備就緒通知的兩種模型:

  • 水平觸發:如果可以在不阻塞的情況下執行I/O系統調用,則認為文件描述符已經準備好了,觸發通知
  • 邊緣觸發:文件描述符被監控以來,有新的I/O活動(例如 新的輸入),觸發通知

3.2、不同的通知模型如何影響我們的程式設計?

水平觸發

  • 確定文件描述符的I/O狀態已經ready
  • 已經ready,對這個文件描述符執行一些I/O操作(比如讀取幾個位元組數據),然後繼續監視它的IO狀態
  • 仍然有未讀取的數據,還會繼續觸發通知,也就是說不用一次執行完所有的IO操作(比如一次讀取緩衝區的全部內容)

邊緣觸發:

  • 只有新的IO事件發生時,才會觸發通知
  • 直到下次IO事件發生,不會再觸發通知

註:對於邊緣觸發,當觸發通知時,我們並不知道有多少IO可用(例如有多少位元組可以讀),所以一般使用邊緣觸發通知方式要遵循如下規則:

  1. 接收到事件通知後,程式應該儘可能多的執行IO(讀寫),因為僅僅通知這一次,不讀取完畢,數據可能就丟失了
  2. 為了避免IO阻塞,每個被監視的文件描述符應該是非阻塞模式打開的,然後收到事件通知後,重覆的執行IO直到返回錯誤信息

接下來的聊天室的實例,我們使用邊緣觸發的方式

三、Epoll實戰:聊天室

瞭解完epoll的基礎和原理,使用一個精簡版的聊天室的小實例來鞏固學習。

主要思路:一個服務端,可以接收多個客戶端的連接,客戶端發送的消息會同步到所有聊天室內的客戶端

1、服務端設計與實現

第一步:服務端創建socket服務

第二步:創建epoll實例,將socket服務fd加入interest list

第三步:迴圈調用epoll_wait看是否有ready的文件描述符,如果有並且是我們的socket服務fd,說明是有新的客戶端連接加入,保存客戶端fd,並將fd加入interest list;如果非socket 服務fd,說明是客戶端有新消息發送至服務端,接收消息然後廣播給保存的所有客戶端fd

2、客戶端設計與實現

第一步:創建socket連接服務端

第二步:創建管道,以便獲取客戶端輸入

第三步:創建epoll實例,並把socket fd和管道fd加入interest list

第四步:fork子進程

第五步:父進程調用epoll_wait獲取ready list,如果fd是服務端sockt,則直接列印廣播消息;如果fd是管道則為用戶輸入,讀取管道中的消息,發到服務端

3、涉及其他知識點

  • socket
  • fork
  • pipe
  • linked list(保存連接的客戶端fd)

運行如圖:

源碼放到了Githubhttps://github.com/bigbignerd/epollchat

四、參考文獻

1、《The linux programming interface》

2、Medium: The method to epoll's madness

如有表述錯誤之處,歡迎指正!

原文地址https://www.cnblogs.com/skyfynn/p/10644243.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在項目中,我們常常需要自定義錯誤頁面,但往往返回的狀態碼都變成了200,對SEO很不友好。我嘗試過在百度上尋找解決方案,但找到的資料中說的方法都試過了,發現都是無法返回正確的狀態碼的。 最後,只好自已摸索了。經過多次嘗試,總算實現了。現在把實現的方法總結出來,供大家參考: 解決方法: 在項目根目錄下 ...
  • vi編輯器是Linux和Unix上最基本的文本編輯器,工作在字元模式下。由於不需要圖形界面,vi是效率很高的文本編輯器。儘管在Linux上也有很多圖形界面的編輯器可用,但vi在系統和伺服器管理中的功能是那些圖形編輯器所無法比擬的。 ...
  • 今天,朋友買了一個新的聯想電腦,自帶出廠系統。進入PE後發現居然有聯想官方的OEM分區,於是直接拷貝過來,然後裝在另一個電腦里可以正常使用,這裡給大家分享一下。 工程下載: 鏈接:https://pan.baidu.com/s/1p2nM3n1ZuzZeBPGN2DcCaQ 提取碼:pbtw 1.下 ...
  • 安裝mysql 檢查安裝是否成功 登錄mysql 啟動關閉mysql 配置遠程連接 1. 檢查是否有防火牆限制 2. 檢查mysql是否有訪問許可權 3. 修改 /etc/mysql/mysql.conf.d/mysqld.cnf,註釋bind address 4. 開啟阿裡雲安全組埠策略,開放33 ...
  • 相對來講嵌入式相關的人群總體來說比較少,那麼有沒有一個聚集地呢? ...
  • 自己在Ubuntu下練習C程式時,用到了庫函數math.h,雖然在源程式中已添加頭文件“math.h”,但仍提示所用函數未定義,原本以為是程式出錯了,找了好久,這是怎麼回事呢?後來上網查了下,發現是Linux系統的原因,在Linux下,若要調用C中的math庫里的函數,必須在編譯時加上“-lm”,表 ...
  • PuTTY自帶的pscp非常好用。 使用方式和Linux中的scp命令很像,格式如下: pscp 文件 用戶名@LinuxIP:目錄 例如: pscp d:/a.iso [email protected]:/home/ 按提示輸入密碼即可。 ...
  • iDEA啟動後報Error:Abnormal build process terminatio 報錯的原因如下:Error:Abnormal build process termination: "C:\Program Files\Java\jdk1.8.0_121\bin\java" -Xmx70 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...