c語言 select poll epoll 區別 總結

IO複用

​ 爲了解決大量客戶端訪問的問題,引入IO技術:一個進程可以同事對多個客戶請求進行服務,複用一個進程對多個IO進行服務。IO讀寫的數據多數情況下沒準備好,需要通過一個函數監聽這些數據狀態,一旦有數據可以讀寫就服務。

​ select,poll,epoll都是IO多路複用的機制,監視多個描述符,一旦某個描述符就緒,通知程序進行操作。

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

select

頭文件

/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#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);
void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

描述

​ select 允許程序監視多個文件描述符,直到一個或多個文件描述符對於某種I / O操作。如果可以執行文件描述符,則認爲文件描述符已準備就緒相應的I / O操作。

​ select()只能監視小於FD_SETSIZE的文件描述符號。使用的超時時間是結構體時間,以秒和微秒爲單位。可以更新超時參數以指示還剩多少時間。沒有sigmask參數。

​ 在readfds中列出,監視三個獨立的文件描述符集:

​ 觀察是否可以讀取字符、查看是否有可用空間來寫入、監視exceptfds中的異常。如果沒有文件描述符,則三個文件描述符集的每一個都可以指定爲NULL。

​ FD_CLR()分別從集合中添加和刪除給定的文件描述符。

​ FD_ISSET() 查看文件描述符是否是集合的一部分;

​ nfds是三組中編號最高的文件描述符,加1。

​ timeout參數指定select()應該等待文件描述符準備就緒的間隔,直到發生以下任何一種情況:文件描述符準備就緒、呼叫被信號處理程序中斷、超時到期。超時間隔將四捨五入爲系統時鐘的粒度,並且內核調度延遲意味着阻塞間隔可能會少量溢出。如果timeval結構的兩個字段均爲零,則select()立即返回。

​ <sys/time.h> 中時間結構體定義如下:

struct timeval {
	long    tv_sec;         /* seconds */
	long    tv_usec;        /* microseconds */
};	

​ 一些代碼調用select(),將所有三個集合都設爲空,nfds爲零,並且將非NULL超時作爲相當便攜式的亞秒級精度睡眠方式。

返回值

​ 成功後,select()返回包含在文件中的文件描述符的數量。返回的三個描述符集(即readfds中設置的總位數,writefds,exceptfds),如果超時在事情之前到期,則可能爲零發生。

​ 如果出錯,則返回-1,並且將errno設置爲指示錯誤;

​ 否則爲0。 文件描述符集未修改,並且超時變未定義。

錯誤

EBADF	在一組中給出了無效的文件描述符。(也許是一個文件描述-已經關閉的Tor,或發生錯誤的Tor。)
EINTR	捕獲到一個信號; 參見signal(7)。
EINVAL	nfds爲負或超過RLIMIT_NOFILE資源限制(getrlimit(2))。
EINVAL	超時中包含的值無效。
ENOMEM	無法爲內部表分配內存。

例子

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main(void) {
    fd_set rfds;
    struct timeval tv;
    int retval;

    /* Watch stdin (fd 0) to see when it has input. */

    FD_ZERO(&rfds);
    FD_SET(0, &rfds);

    /* Wait up to five seconds. */

    tv.tv_sec = 5;
    tv.tv_usec = 0;

    retval = select(1, &rfds, NULL, NULL, &tv);
    /* Don't rely on the value of tv now! */

    if (retval == -1)
        perror("select()");
    else if (retval) {
        char buf[512] = {0};
        printf("Data is available now.\n");
        scanf("%[^\n]s", buf);
        /* FD_ISSET(0, &rfds) will be true. */
    }
    else
        printf("No data within five seconds.\n");

    exit(EXIT_SUCCESS);
}

poll

頭文件

#include <poll.h>

函數

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

描述

​ poll()執行與select(2)類似的任務:它等待一組文件描述符中的準備執行I / O。

struct pollfd {
	int   fd;         /* file descriptor */
	short events;     /* requested events */
	short revents;    /* returned events */
};

​ 調用者應在nfds中指定fds數組中的項目數。

​ 字段fd包含打開文件的文件描述符。如果該字段爲負,則相應的events字段將被忽略,而revents字段將返回零。

​ 字段events是輸入參數,位掩碼指定應用程序的事件對文件描述符fd。該字段可以指定爲零,在這種情況下,只能在清除中返回的事件是POLLHUP,POLLERR和POLLNVAL

​ 字段revents是一個輸出參數,由內核填充執行該事件的事件。revents中返回的位可以包括事件中指定的任何位,或值POLLERR,POLLHUP或POLLNVAL之一。 (這三位在事件字段,並且只要相應的條件是正確的。)

​ 如果任何文件描述都未發生請求的事件(也沒有錯誤),poll()阻塞,直到其中一個事件發生:文件描述符準備就緒、呼叫被信號處理程序中斷、超時到期。

​ <poll.h>中定義了可以在事件和清除中設置/返回的位:

​ POLLIN 有要讀取的數據。

​ POLLPRI 有緊急數據要讀取(例如,TCP套接字上的帶外數據

​ POLLOUT現在可以進行寫操作,儘管寫操作要比套接字或管道仍將阻塞(除非設置了O_NONBLOCK)。

​ POLLRDHUP流套接字對等體關閉連接,或關閉寫入一半連接。必須定義_GNU_SOURCE功能測試宏

​ POLLERR 錯誤條件僅在revent中返回;在events中忽略)。

​ POLLHUP

​ POLLNVAL 無效的請求:fd未打開

返回值

​ 成功時,將返回正數;非零的revent字段的結構的數量

​ 值爲0表示調用超時並且沒有文件描述符準備好。

​ 發生錯誤時,返回-1,並正確設置errno。

錯誤

​ EFAULT作爲參數給出的數組未包含在調用程序的地址中空間。

    EINTR在任何請求的事件之前發生信號;參見signal(7)。

    EINVAL nfds值超過RLIMIT_NOFILE值。

    ENOMEM沒有空間分配文件描述符表。
#include "../common/color.h"
#include "../common/common.h"
#include "../common/tcp_server.h"
#include "../common/head.h"

#define POLLSIZE 100
#define BUFSIZE 512

char ch_char(char c) {
    if (c >= 'a' && c <= 'z')
        return c - 32;
    return  c;
}

int main(int argc, char **argv){
    if (argc != 2) {
        fprintf(stderr, "Usage: %s port!\n", argv[0]);
        exit(1);
    }
    int server_listen, fd;

    if ((server_listen = socket_create(atoi(argv[1]))) < 0) {
        perror("socket_create");
        exit(1);
    }
    
    struct pollfd event_set[POLLSIZE];
    
    for (int i = 0; i < POLLSIZE; i++) {
        event_set[i].fd = -1;
    }
    
    event_set[0].fd = server_listen;
    event_set[0].events = POLLIN;

    while(1) {
        int retval;
        if ((retval = poll(event_set, POLLSIZE, -1)) < 0) {
            perror("poll");
            return 1;
        }
        if (event_set[0].revents & POLLIN) {
            if ((fd = accept(server_listen, NULL, NULL)) < 0) {
                perror("accept");
                continue;
            }
            retval--;
            int i;
            for (i = 1; i < POLLSIZE; i++) {
                if (event_set[i].fd < 0) {
                    event_set[i].fd = fd;
                    event_set[i].events = POLLIN;
                    break;
                }
            }

            if (i == POLLSIZE) {
                printf("Too many clients!\n");
            }
        }
        
        for (int i = 0; i < POLLSIZE; i++) {
            if (event_set[i].fd < 0) continue;
            
            if (event_set[i].revents & (POLLIN | POLLHUP | POLLERR)) {
                retval--;
                char buff[BUFSIZE] = {0};
                if (recv(event_set[i].fd, buff, BUFSIZE, 0) > 0) {
                    printf("Recv : %s \n", buff);
                    for (int i = 0; i < strlen(buff); i++) {
                        buff[i] = ch_char(buff[i]);
                    }
                    send(event_set[i].fd, buff, strlen(buff), 0);
                }
                else {
                    close(event_set[i].fd);
                    event_set[i].fd = -1;
                }
            }

            if (retval <= 0) {
                break;
            }
        }
    }
    return 0;
}

epoll

頭文件

#include <sys/epoll.h>

描述

​ epoll API執行與poll(2)類似的任務:監視多個文件描述符以看看是否可以在其中任何一個上進行I / O。 epoll API可以用作邊緣觸發或級別觸發的界面,可以很好地擴展到大量受監視的文件描述符。 提供了以下系統調用來創建和管理epoll實例

​ epoll_create(2)創建一個epoll實例並返回一個引用的文件描述符

   通過epoll_ctl(2)註冊對特定文件描述符的興趣。

​ epoll_wait(2)等待I / O事件,如果沒有事件發生則阻塞調用線程

區別

select:**

select本質上是通過設置或者檢查存放fd標誌位的數據結構來進行下一步處理。這樣所帶來的缺點是:

1、 單個進程可監視的fd數量被限制,即能監聽端口的大小有限。

​ 一般來說這個數目和系統內存關係很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.

2、 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低:

​ 當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字註冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。

3、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大

poll:

poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。

它沒有最大連接數的限制,原因是它是基於鏈表來存儲的,但是同樣有一個缺點:

1、大量的fd的數組被整體複製於用戶態和內核地址空間之間,而不管這樣的複製是不是有意義。

2、poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。

epoll:

epoll有EPOLLLT和EPOLLET兩種觸發模式,LT是默認的模式,ET是“高速”模式。LT模式下,只要這個fd還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作,而在ET(邊緣觸發)模式中,它只會提示一次,直到下次再有數據流入之前都不會再提示了,無 論fd中是否還有數據可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀光,也就是說一直讀到read的返回值小於請求值,或者 遇到EAGAIN錯誤。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,內核就會採用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。

總結:

綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。

1、表面上看epoll的性能最好,但是在連接數少並且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。

2、select低效是因爲每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章