亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

手把手寫C++服務(wù)器(31):服務(wù)器性能提升關(guān)鍵——IO復(fù)用技術(shù)【兩萬字長文】

big_cat / 3721人閱讀

摘要:前面幾講手撕了網(wǎng)關(guān)服務(wù)器回顯服務(wù)器服務(wù)的代碼,但是這幾個(gè)一次只能監(jiān)聽一個(gè)文件描述符,因此性能非常原始低下。復(fù)用能使服務(wù)器同時(shí)監(jiān)聽多個(gè)文件描述符,是服務(wù)器性能提升的關(guān)鍵。表示要操作的文件描述符,指定操作類型,指定事件。

?本系列文章導(dǎo)航: 手把手寫C++服務(wù)器(0):專欄文章-匯總導(dǎo)航【更新中】?

前言: Linux中素有“萬物皆文件,一切皆IO”的說法。前面幾講手撕了CGI網(wǎng)關(guān)服務(wù)器、echo回顯服務(wù)器、discard服務(wù)的代碼,但是這幾個(gè)一次只能監(jiān)聽一個(gè)文件描述符,因此性能非常原始、低下。IO復(fù)用能使服務(wù)器同時(shí)監(jiān)聽多個(gè)文件描述符,是服務(wù)器性能提升的關(guān)鍵。雖然IO復(fù)用本身是阻塞的,但是和并發(fā)技術(shù)結(jié)合起來,再加上一點(diǎn)設(shè)計(jì)模式,一個(gè)高性能服務(wù)器的基石就基本搭建完成了。

目錄

1、預(yù)備知識(shí)

(1)文件描述符

(2)進(jìn)程阻塞

(3)緩存IO

(4)什么是IO多路復(fù)用?

2、Linux五大IO模型

(1)阻塞IO

(2)非阻塞IO

(3)IO多路復(fù)用

(4)信號(hào)驅(qū)動(dòng)IO

(5)異步IO

3、select

函數(shù)返回

參數(shù)詳解

重要結(jié)構(gòu)體詳解

使用流程

代碼實(shí)例

4、poll

函數(shù)原型

重要結(jié)構(gòu)體詳解

事件類型

使用流程

代碼實(shí)例

5、epoll

函數(shù)原型

函數(shù)返回

LT水平觸發(fā)模式和ET邊沿觸發(fā)模式

代碼實(shí)例

6、三組IO復(fù)用函數(shù)對(duì)比

1. 用戶態(tài)將文件描述符傳入內(nèi)核的方式

2. 內(nèi)核態(tài)檢測文件描述符讀寫狀態(tài)的方式

3. 找到就緒的文件描述符并傳遞給用戶態(tài)的方式

4. 重復(fù)監(jiān)聽的處理方式

7、經(jīng)典面試題:epoll更高效的原因

寫在最后

參考


1、預(yù)備知識(shí)

(1)文件描述符

強(qiáng)烈推薦看一下本系列的第25講《手把手寫C++服務(wù)器(25):萬物皆可文件之socket fd

文件描述符(File descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語,是一個(gè)用于表述指向文件的引用的抽象化概念。 文件描述符在形式上是一個(gè)非負(fù)整數(shù)。實(shí)際上,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。當(dāng)程序打開一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。在程序設(shè)計(jì)中,一些涉及底層的程序編寫往往會(huì)圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。

(2)進(jìn)程阻塞

正在執(zhí)行的進(jìn)程,由于期待的某些事件未發(fā)生,如請(qǐng)求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達(dá)或無新工作做等,則由系統(tǒng)自動(dòng)執(zhí)行阻塞原語(Block),使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)??梢?,進(jìn)程的阻塞是進(jìn)程自身的一種主動(dòng)行為,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得了CPU資源),才可能將其轉(zhuǎn)為阻塞狀態(tài)。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài),是不占用CPU資源的。

(3)緩存IO

緩存I/O又稱為標(biāo)準(zhǔn)I/O,大多數(shù)文件系統(tǒng)的默認(rèn)I/O操作都是緩存I/O。在Linux的緩存I/O機(jī)制中,操作系統(tǒng)會(huì)將I/O的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存中,即數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。

緩存 I/O 的缺點(diǎn):

數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的。

(4)什么是IO多路復(fù)用?

IO 多路復(fù)用是一種同步IO模型,實(shí)現(xiàn)一個(gè)線程可以監(jiān)視多個(gè)文件句柄;一旦某個(gè)文件句柄就緒,就能夠通知應(yīng)用程序進(jìn)行相應(yīng)的讀寫操作;沒有文件句柄就緒就會(huì)阻塞應(yīng)用程序,交出CPU。

2、Linux五大IO模型

(1)阻塞IO

這是最常用的簡單的IO模型。阻塞IO意味著當(dāng)我們發(fā)起一次IO操作后一直等待成功或失敗之后才返回,在這期間程序不能做其它的事情。阻塞IO操作只能對(duì)單個(gè)文件描述符進(jìn)行操作,詳見readwrite

(2)非阻塞IO

我們?cè)诎l(fā)起IO時(shí),通過對(duì)文件描述符設(shè)置O_NONBLOCK flag來指定該文件描述符的IO操作為非阻塞。非阻塞IO通常發(fā)生在一個(gè)for循環(huán)當(dāng)中,因?yàn)槊看芜M(jìn)行IO操作時(shí)要么IO操作成功,要么當(dāng)IO操作會(huì)阻塞時(shí)返回錯(cuò)誤EWOULDBLOCK/EAGAIN,然后再根據(jù)需要進(jìn)行下一次的for循環(huán)操作,這種類似輪詢的方式會(huì)浪費(fèi)很多不必要的CPU資源,是一種糟糕的設(shè)計(jì)。和阻塞IO一樣,非阻塞IO也是通過調(diào)用readwrite來進(jìn)行操作的,也只能對(duì)單個(gè)描述符進(jìn)行操作。

(3)IO多路復(fù)用

IO多路復(fù)用在Linux下包括了三種,select、poll、epoll,抽象來看,他們功能是類似的,但具體細(xì)節(jié)各有不同:首先都會(huì)對(duì)一組文件描述符進(jìn)行相關(guān)事件的注冊(cè),然后阻塞等待某些事件的發(fā)生或等待超時(shí)。IO多路復(fù)用都可以關(guān)注多個(gè)文件描述符,但對(duì)于這三種機(jī)制而言,不同數(shù)量級(jí)文件描述符對(duì)性能的影響是不同的,下面會(huì)詳細(xì)介紹。

(4)信號(hào)驅(qū)動(dòng)IO

信號(hào)驅(qū)動(dòng)IO是利用信號(hào)機(jī)制,讓內(nèi)核告知應(yīng)用程序文件描述符的相關(guān)事件。

但信號(hào)驅(qū)動(dòng)IO在網(wǎng)絡(luò)編程的時(shí)候通常很少用到,因?yàn)樵诰W(wǎng)絡(luò)環(huán)境中,和socket相關(guān)的讀寫事件太多了,比如下面的事件都會(huì)導(dǎo)致SIGIO信號(hào)的產(chǎn)生:

  1. TCP連接建立
  2. 一方斷開TCP連接請(qǐng)求
  3. 斷開TCP連接請(qǐng)求完成
  4. TCP連接半關(guān)閉
  5. 數(shù)據(jù)到達(dá)TCP socket
  6. 數(shù)據(jù)已經(jīng)發(fā)送出去(如:寫buffer有空余空間)

上面所有的這些都會(huì)產(chǎn)生SIGIO信號(hào),但我們沒辦法在SIGIO對(duì)應(yīng)的信號(hào)處理函數(shù)中區(qū)分上述不同的事件,SIGIO只應(yīng)該在IO事件單一情況下使用,比如說用來監(jiān)聽端口的socket,因?yàn)橹挥锌蛻舳税l(fā)起新連接的時(shí)候才會(huì)產(chǎn)生SIGIO信號(hào)。

(5)異步IO

異步IO和信號(hào)驅(qū)動(dòng)IO差不多,但它比信號(hào)驅(qū)動(dòng)IO可以多做一步:相比信號(hào)驅(qū)動(dòng)IO需要在程序中完成數(shù)據(jù)從用戶態(tài)到內(nèi)核態(tài)(或反方向)的拷貝,異步IO可以把拷貝這一步也幫我們完成之后才通知應(yīng)用程序。我們使用?aio_read?來讀,aio_write?寫。

同步IO vs 異步IO

1. 同步IO指的是程序會(huì)一直阻塞到IO操作如read、write完成

2. 異步IO指的是IO操作不會(huì)阻塞當(dāng)前程序的繼續(xù)執(zhí)行
所以根據(jù)這個(gè)定義,上面阻塞IO當(dāng)然算是同步的IO,非阻塞IO也是同步IO,因?yàn)楫?dāng)文件操作符可用時(shí)我們還是需要阻塞的讀或?qū)懀鞩O多路復(fù)用和信號(hào)驅(qū)動(dòng)IO也是同步IO,只有異步IO是完全完成了數(shù)據(jù)的拷貝之后才通知程序進(jìn)行處理,沒有阻塞的數(shù)據(jù)讀寫過程。

3、select

select的作用是在一段指定的時(shí)間內(nèi),監(jiān)聽用戶感興趣的文件描述符上的可讀、可寫、異常等事件。函數(shù)原型如下:

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

函數(shù)返回

  • select成功時(shí)返回就緒文件描述符的總數(shù);
  • 如果在超時(shí)時(shí)間內(nèi)沒有任何文件描述符就緒,select將返回0;
  • select失敗時(shí)返回-1并設(shè)置errno。;
  • 如果在select等待期間,程序接收到信號(hào),select立即返回-1,并將errno設(shè)置為EINTR。

參數(shù)詳解

  • nfds:指定被監(jiān)聽文件描述符總數(shù)。通常被設(shè)置為select監(jiān)聽所有文件描述符中的最大值+1。
  • readfds:可讀事件對(duì)應(yīng)文件描述符集合。
  • writefds:可寫事件對(duì)應(yīng)文件描述符集合。
  • exceptfds:異常事件對(duì)應(yīng)文件描述符集合。
  • timeout:設(shè)置select超時(shí)時(shí)間。

重要結(jié)構(gòu)體詳解

readfds、writefds、exceptfds都是fd_set結(jié)構(gòu)體,timeout是timeval結(jié)構(gòu)體,這里詳解一下這兩個(gè)結(jié)構(gòu)體。

1、fd_set

fd_set結(jié)構(gòu)體定義比較復(fù)雜,涉及到位操作,比較復(fù)雜。所以通常用宏來訪問fd_set中的位。

#include FD_ZERO(fd_set* fdset);    // 清除fdset中的所有位FD_SET(int fd, fd_set* fdset); // 設(shè)置fdset中的位FD_CLR(int fd, fd_set* fdset); // 清除fdset中的位int FD_ISSET(int fd, fd_set* fdset);  // 測試fdset的位fd是否被設(shè)置
  • FD_ZERO用來清空文件描述符組。每次調(diào)用select前都需要清空一次。
  • FD_SET添加一個(gè)文件描述符到組中,F(xiàn)D_CLR對(duì)應(yīng)將一個(gè)文件描述符移出組中。
  • FD_ISSET檢測一個(gè)文件描述符是否在組中,我們用這個(gè)來檢測一次select調(diào)用之后有哪些文件描述符可以進(jìn)行IO操作。

2、timeval

struct timeval {    long tv_sec; // 秒數(shù)    long tv_usec; // 微妙數(shù)};

使用流程

綜上所述,我們一般的使用流程是:

  1. 準(zhǔn)備工作——定義readfds、timeval等
  2. 使用FD_ZERO清零,使用FD_SET設(shè)置文件描述符。因?yàn)槭录l(fā)生后,文件描述符集合都將被內(nèi)核修改。
  3. 調(diào)用select
  4. 使用FD_ISSET檢測文件描述符是否在組中

代碼實(shí)例

根據(jù)使用流程,給出一個(gè)代碼示例:

#include #include #include #include #define TIMEOUT 5 /* select timeout in seconds */#define BUF_LEN 1024 /* read buffer in bytes */int main (void) {  struct timeval tv;  fd_set readfds;  int ret;    /* Wait on stdin for input. */  FD_ZERO(&readfds);  FD_SET(STDIN_FILENO, &readfds);  /* Wait up to five seconds. */  tv.tv_sec = TIMEOUT;  tv.tv_usec = 0;    /* All right, now block! */  ret = select (STDIN_FILENO + 1, &readfds,                NULL,                NULL,                 &tv);  if (ret == ?1) {    perror ("select");    return 1;   } else if (!ret) {    printf ("%d seconds elapsed./n", TIMEOUT);    return 0;   }  /*  * Is our file descriptor ready to read?  * (It must be, as it was the only fd that  * we provided and the call returned  * nonzero, but we will humor ourselves.)  */  if (FD_ISSET(STDIN_FILENO, &readfds)) {    char buf[BUF_LEN+1];    int len;    /* guaranteed to not block */    len = read (STDIN_FILENO, buf, BUF_LEN);    if (len == ?1) {      perror ("read");      return 1;     }    if (len) {      buf[len] = "/0";      printf ("read: %s/n", buf);    }    return 0;   }  fprintf (stderr, "This should not happen!/n");  return 1; }

后面一講會(huì)給出一些實(shí)用的例子,有了select之后我們可以同時(shí)監(jiān)聽很多個(gè)請(qǐng)求,系統(tǒng)的處理能力大大增強(qiáng)了。

4、poll

和select類似,在一定時(shí)間內(nèi)輪詢一定數(shù)量的文件描述符。

函數(shù)原型

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

但是和select不同的是,select需要用三組文件描述符,poll只有一個(gè)pollfd文件數(shù)組,數(shù)組中的每個(gè)元素都表示一個(gè)需要監(jiān)聽IO操作事件的文件描述符。而且我們只需要關(guān)心數(shù)組中events參數(shù),revents由內(nèi)核自動(dòng)填充。

重要結(jié)構(gòu)體詳解

    struct pollfd {        int fd;    // 文件描述符        short events;    // 注冊(cè)的事件         short revents;   // 實(shí)際發(fā)生的事件,由內(nèi)核填充    };

事件類型

具體的事件類型參看手冊(cè):https://man7.org/linux/man-pages/man2/poll.2.html

       POLLIN There is data to read.       POLLPRI              There is some exceptional condition on the file              descriptor.  Possibilities include:              ? There is out-of-band data on a TCP socket (see tcp(7)).              ? A pseudoterminal master in packet mode has seen a state                change on the slave (see ioctl_tty(2)).              ? A cgroup.events file has been modified (see cgroups(7)).       POLLOUT              Writing is now possible, though a write larger than the              available space in a socket or pipe will still block              (unless O_NONBLOCK is set).       POLLRDHUP (since Linux 2.6.17)              Stream socket peer closed connection, or shut down writing              half of connection.  The _GNU_SOURCE feature test macro              must be defined (before including any header files) in              order to obtain this definition.       POLLERR              Error condition (only returned in revents; ignored in              events).  This bit is also set for a file descriptor              referring to the write end of a pipe when the read end has              been closed.       POLLHUP              Hang up (only returned in revents; ignored in events).              Note that when reading from a channel such as a pipe or a              stream socket, this event merely indicates that the peer              closed its end of the channel.  Subsequent reads from the              channel will return 0 (end of file) only after all              outstanding data in the channel has been consumed.       POLLNVAL              Invalid request: fd not open (only returned in revents;              ignored in events).       When compiling with _XOPEN_SOURCE defined, one also has the       following, which convey no further information beyond the bits       listed above:       POLLRDNORM              Equivalent to POLLIN.       POLLRDBAND              Priority band data can be read (generally unused on              Linux).       POLLWRNORM              Equivalent to POLLOUT.       POLLWRBAND              Priority data may be written.

使用流程

綜上所述,我們一般的使用流程是:

  1. 定義pollfd數(shù)組,并設(shè)置poll數(shù)組相關(guān)參數(shù)。
  2. 設(shè)置超時(shí)時(shí)間
  3. 調(diào)用poll

代碼實(shí)例

根據(jù)使用流程,給出一個(gè)代碼示例:

#include #include #include #define TIMEOUT 5 /* poll timeout, in seconds */int main (void) {  struct pollfd fds[2];  int ret;  /* watch stdin for input */  fds[0].fd = STDIN_FILENO;  fds[0].events = POLLIN;  /* watch stdout for ability to write (almost always true) */  fds[1].fd = STDOUT_FILENO;  fds[1].events = POLLOUT;  /* All set, block! */  ret = poll (fds, 2, TIMEOUT * 1000);  if (ret == ?1) {    perror ("poll");    return 1;   }  if (!ret) {    printf ("%d seconds elapsed./n", TIMEOUT);    return 0;   }  if (fds[0].revents & POLLIN)    printf ("stdin is readable/n");  if (fds[1].revents & POLLOUT)    printf ("stdout is writable/n");  return 0; }

5、epoll

epoll是Linux特有的IO復(fù)用函數(shù),使用一組函數(shù)來完成任務(wù),而不是單個(gè)函數(shù)。

epoll把用戶關(guān)心的文件描述符上的事件放在內(nèi)核的一個(gè)事件表中,不需要像select、poll那樣每次調(diào)用都要重復(fù)傳入文件描述符集或事件集。

epoll需要使用一個(gè)額外的文件描述符,來唯一標(biāo)識(shí)內(nèi)核中的時(shí)間表,由epoll_create創(chuàng)建。

函數(shù)原型

    #include     int epoll_create(int size);    int epoll_create1(int flags);    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);    int epoll_wait(int epfd, struct epoll_event *events,                int maxevents, int timeout);    int epoll_pwait(int epfd, struct epoll_event *events,                int maxevents, int timeout,                const sigset_t *sigmask);
  • epoll_create:創(chuàng)建一個(gè)epoll實(shí)例,size參數(shù)給內(nèi)核一個(gè)提示,標(biāo)識(shí)事件表的大小。函數(shù)返回的文件描述符將作用其他所有epoll系統(tǒng)調(diào)用的第一個(gè)參數(shù),以指定要訪問的內(nèi)核事件表。
  • epoll_ctl:操作文件描述符。fd表示要操作的文件描述符,op指定操作類型,event指定事件。
  • epoll_wait:在一段超時(shí)時(shí)間內(nèi)等待一組文件描述符上的事件。如果監(jiān)測到事件,就將所有就緒的事件從內(nèi)核事件表(epfd參數(shù)指定)中復(fù)制到第二個(gè)參數(shù)events指向的數(shù)組中。因?yàn)閑vents數(shù)組只用于輸出epoll_wait監(jiān)測到的就緒事件,而不像select、poll那樣就用于傳入用戶注冊(cè)的事件,又用于輸出內(nèi)核檢測到的就緒事件。這樣極大提高了應(yīng)用程序索引就緒文件描述符的效率。

函數(shù)返回

特別注意epoll_wait函數(shù)成功時(shí)返回就緒的文件描述符總數(shù)。select和poll返回文件描述符總數(shù)。

以尋找已經(jīng)就緒的文件描述符,舉個(gè)例子如下:

epoll_wait只需要遍歷返回的文件描述符,但是poll和select需要遍歷所有文件描述符

//  pollint ret = poll(fds, MAX_EVENT_NUMBER, -1);// 必須遍歷所有已注冊(cè)的文件描述符for (int i = 0; i < MAX_EVENT_NUMBER; i++) {    if (fds[i].revents & POLLIN) {        int sockfd = fds[i].fd;    }}// epoll_waitint ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);// 僅需要遍歷就緒的ret個(gè)文件描述符for (int i = 0; i < ret; i++) {    int sockfd = events[i].data.fd;}

LT水平觸發(fā)模式和ET邊沿觸發(fā)模式

epoll監(jiān)控多個(gè)文件描述符的I/O事件。epoll支持邊緣觸發(fā)(edge trigger,ET)或水平觸發(fā)(level trigger,LT),通過epoll_wait等待I/O事件,如果當(dāng)前沒有可用的事件則阻塞調(diào)用線程。

select和poll只支持LT工作模式,epoll的默認(rèn)的工作模式是LT模式。

水平觸發(fā):

  • 當(dāng)epoll_wait檢測到其上有事件發(fā)生并將此事件通知應(yīng)用程序后,應(yīng)用程序可以不立即處理此事件。這樣應(yīng)用程序下一次調(diào)用epoll_wait的時(shí)候,epoll_wait還會(huì)再次向應(yīng)用程序通告此事件,直到事件被處理。

邊沿觸發(fā):

  • 當(dāng)epoll_wait檢測到其上有事件發(fā)生并將此事件通知應(yīng)用程序后,應(yīng)用程序必須立即處理此事件,后續(xù)的epoll_wait調(diào)用將不再向應(yīng)用程序通知這一事件。

所以,邊沿觸發(fā)模式很大程度上降低了同一個(gè)epoll事件被重復(fù)觸發(fā)的次數(shù),所以效率更高。

代碼實(shí)例

#include #include #include #include #include #include #include #include #include #include #define MAXEVENTS 64static int make_socket_non_blocking (int sfd){  int flags, s;  flags = fcntl (sfd, F_GETFL, 0);  if (flags == -1)    {      perror ("fcntl");      return -1;    }  flags |= O_NONBLOCK;  s = fcntl (sfd, F_SETFL, flags);  if (s == -1)    {      perror ("fcntl");      return -1;    }  return 0;}static int create_and_bind (char *port){  struct addrinfo hints;  struct addrinfo *result, *rp;  int s, sfd;  memset (&hints, 0, sizeof (struct addrinfo));  hints.ai_family = AF_UNSPEC;     /* Return IPv4 and IPv6 choices */  hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */  hints.ai_flags = AI_PASSIVE;     /* All interfaces */  s = getaddrinfo (NULL, port, &hints, &result);  if (s != 0)    {      fprintf (stderr, "getaddrinfo: %s/n", gai_strerror (s));      return -1;    }  for (rp = result; rp != NULL; rp = rp->ai_next)    {      sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);      if (sfd == -1)        continue;      s = bind (sfd, rp->ai_addr, rp->ai_addrlen);      if (s == 0)        {          /* We managed to bind successfully! */          break;        }      close (sfd);    }  if (rp == NULL)    {      fprintf (stderr, "Could not bind/n");      return -1;    }  freeaddrinfo (result);  return sfd;}int main (int argc, char *argv[]){  int sfd, s;  int efd;  struct epoll_event event;  struct epoll_event *events;  if (argc != 2)    {      fprintf (stderr, "Usage: %s [port]/n", argv[0]);      exit (EXIT_FAILURE);    }  sfd = create_and_bind (argv[1]);  if (sfd == -1)    abort ();  s = make_socket_non_blocking (sfd);  if (s == -1)    abort ();  s = listen (sfd, SOMAXCONN);  if (s == -1)    {      perror ("listen");      abort ();    }  efd = epoll_create1 (0);  if (efd == -1)    {      perror ("epoll_create");      abort ();    }  event.data.fd = sfd;  event.events = EPOLLIN | EPOLLET;  s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);  if (s == -1)    {      perror ("epoll_ctl");      abort ();    }  /* Buffer where events are returned */  events = calloc (MAXEVENTS, sizeof event);  /* The event loop */  while (1)    {      int n, i;      n = epoll_wait (efd, events, MAXEVENTS, -1);      for (i = 0; i < n; i++)	{	  if ((events[i].events & EPOLLERR) ||              (events[i].events & EPOLLHUP) ||              (!(events[i].events & EPOLLIN)))	    {              /* An error has occured on this fd, or the socket is not                 ready for reading (why were we notified then?) */	      fprintf (stderr, "epoll error/n");	      close (events[i].data.fd);	      continue;	    }	  else if (sfd == events[i].data.fd)	    {              /* We have a notification on the listening socket, which                 means one or more incoming connections. */              while (1)                {                  struct sockaddr in_addr;                  socklen_t in_len;                  int infd;                  char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];                  in_len = sizeof in_addr;                  infd = accept (sfd, &in_addr, &in_len);                  if (infd == -1)                    {                      if ((errno == EAGAIN) ||                          (errno == EWOULDBLOCK))                        {                          /* We have processed all incoming                             connections. */                          break;                        }                      else                        {                          perror ("accept");                          break;                        }                    }                  s = getnameinfo (&in_addr, in_len,                                   hbuf, sizeof hbuf,                                   sbuf, sizeof sbuf,                                   NI_NUMERICHOST | NI_NUMERICSERV);                  if (s == 0)                    {                      printf("Accepted connection on descriptor %d "                             "(host=%s, port=%s)/n", infd, hbuf, sbuf);                    }                  /* Make the incoming socket non-blocking and add it to the                     list of fds to monitor. */                  s = make_socket_non_blocking (infd);                  if (s == -1)                    abort ();                  event.data.fd = infd;                  event.events = EPOLLIN | EPOLLET;                  s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);                  if (s == -1)                    {                      perror ("epoll_ctl");                      abort ();                    }                }              continue;            }          else            {              /* We have data on the fd waiting to be read. Read and                 display it. We must read whatever data is available                 completely, as we are running in edge-triggered mode                 and won"t get a notification again for the same                 data. */              int done = 0;              while (1)                {                  ssize_t count;                  char buf[512];                  count = read (events[i].data.fd, buf, sizeof buf);                  if (count == -1)                    {                      /* If errno == EAGAIN, that means we have read all                         data. So go back to the main loop. */                      if (errno != EAGAIN)                        {                          perror ("read");                          done = 1;                        }                      break;                    }                  else if (count == 0)                    {                      /* End of file. The remote has closed the                         connection. */                      done = 1;                      break;                    }                  /* Write the buffer to standard output */                  s = write (1, buf, count);                  if (s == -1)                    {                      perror ("write");                      abort ();                    }                }              if (done)                {                  printf ("Closed connection on descriptor %d/n",                          events[i].data.fd);                  /* Closing the descriptor will make epoll remove it                     from the set of descriptors which are monitored. */                  close (events[i].data.fd);                }            }        }    }  free (events);  close (sfd);  return EXIT_SUCCESS;}

6、三組IO復(fù)用函數(shù)對(duì)比

1. 用戶態(tài)將文件描述符傳入內(nèi)核的方式

  • select:創(chuàng)建3個(gè)文件描述符集并拷貝到內(nèi)核中,分別監(jiān)聽讀、寫、異常動(dòng)作。這里受到單個(gè)進(jìn)程可以打開的fd數(shù)量限制,默認(rèn)是1024。
  • poll:將傳入的struct pollfd結(jié)構(gòu)體數(shù)組拷貝到內(nèi)核中進(jìn)行監(jiān)聽。
  • epoll:執(zhí)行epoll_create會(huì)在內(nèi)核的高速cache區(qū)中建立一顆紅黑樹以及就緒鏈表(該鏈表存儲(chǔ)已經(jīng)就緒的文件描述符)。接著用戶執(zhí)行的epoll_ctl函數(shù)添加文件描述符會(huì)在紅黑樹上增加相應(yīng)的結(jié)點(diǎn)。

2. 內(nèi)核態(tài)檢測文件描述符讀寫狀態(tài)的方式

  • select:采用輪詢方式,遍歷所有fd,最后返回一個(gè)描述符讀寫操作是否就緒的mask掩碼,根據(jù)這個(gè)掩碼給fd_set賦值。
  • poll:同樣采用輪詢方式,查詢每個(gè)fd的狀態(tài),如果就緒則在等待隊(duì)列中加入一項(xiàng)并繼續(xù)遍歷。
  • epoll:采用回調(diào)機(jī)制。在執(zhí)行epoll_ctl的add操作時(shí),不僅將文件描述符放到紅黑樹上,而且也注冊(cè)了回調(diào)函數(shù),內(nèi)核在檢測到某文件描述符可讀/可寫時(shí)會(huì)調(diào)用回調(diào)函數(shù),該回調(diào)函數(shù)將文件描述符放在就緒鏈表中。

3. 找到就緒的文件描述符并傳遞給用戶態(tài)的方式

  • select:將之前傳入的fd_set拷貝傳出到用戶態(tài)并返回就緒的文件描述符總數(shù)。用戶態(tài)并不知道是哪些文件描述符處于就緒態(tài),需要遍歷來判斷。
  • poll:將之前傳入的fd數(shù)組拷貝傳出用戶態(tài)并返回就緒的文件描述符總數(shù)。用戶態(tài)并不知道是哪些文件描述符處于就緒態(tài),需要遍歷來判斷。
  • epoll:epoll_wait只用觀察就緒鏈表中有無數(shù)據(jù)即可,最后將鏈表的數(shù)據(jù)返回給數(shù)組并返回就緒的數(shù)量。內(nèi)核將就緒的文件描述符放在傳入的數(shù)組中,所以只用遍歷依次處理即可。

4. 重復(fù)監(jiān)聽的處理方式

  • select:將新的監(jiān)聽文件描述符集合拷貝傳入內(nèi)核中,繼續(xù)以上步驟。
  • poll:將新的struct pollfd結(jié)構(gòu)體數(shù)組拷貝傳入內(nèi)核中,繼續(xù)以上步驟。
  • epoll:無需重新構(gòu)建紅黑樹,直接沿用已存在的即可。

7、經(jīng)典面試題:epoll更高效的原因?

select和poll的動(dòng)作基本一致,只是poll采用鏈表來進(jìn)行文件描述符的存儲(chǔ),而select采用fd標(biāo)注位來存放,所以select會(huì)受到最大連接數(shù)的限制,而poll不會(huì)。

select、poll、epoll雖然都會(huì)返回就緒的文件描述符數(shù)量。但是select和poll并不會(huì)明確指出是哪些文件描述符就緒,而epoll會(huì)。造成的區(qū)別就是,系統(tǒng)調(diào)用返回后,調(diào)用select和poll的程序需要遍歷監(jiān)聽的整個(gè)文件描述符找到是誰處于就緒,而epoll則直接處理即可。

select、poll都需要將有關(guān)文件描述符的數(shù)據(jù)結(jié)構(gòu)拷貝進(jìn)內(nèi)核,最后再拷貝出來。而epoll創(chuàng)建的有關(guān)文件描述符的數(shù)據(jù)結(jié)構(gòu)本身就存于內(nèi)核態(tài)中。

select、poll采用輪詢的方式來檢查文件描述符是否處于就緒態(tài),而epoll采用回調(diào)機(jī)制。造成的結(jié)果就是,隨著fd的增加,select和poll的效率會(huì)線性降低,而epoll不會(huì)受到太大影響,除非活躍的socket很多。

epoll的邊緣觸發(fā)模式效率高,系統(tǒng)不會(huì)充斥大量不關(guān)心的就緒文件描述符。

雖然epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機(jī)制需要很多函數(shù)回調(diào)。

寫在最后

這一講偏理論,主要講了Linux中三種IO復(fù)用。后面幾講會(huì)在這一講的基礎(chǔ)上,圍繞IO寫一些有趣的實(shí)戰(zhàn)demo,敬請(qǐng)期待。

參考

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/118798.html

相關(guān)文章

  • 【W(wǎng)ebpack 性能優(yōu)化系列(3) - oneOf】

    摘要:當(dāng)一個(gè)文件要被多個(gè)處理,那么一定要指定執(zhí)行的先后順序先執(zhí)行在執(zhí)行參考 webpack系列文章: 【W(wǎng)ebpack 性能優(yōu)化系列(2) - source-map】【W(wǎng)...

    myshell 評(píng)論0 收藏0
  • ES6指北【3】——5000長文帶你徹底搞懂ES6模塊

    摘要:模塊什么是模塊什么是模塊化玩過游戲的朋友應(yīng)該知道,一把裝配完整的步槍,一般是槍身消音器倍鏡握把槍托。更重要的是,其它大部分語言都支持模塊化。這一點(diǎn)與規(guī)范完全不同。模塊輸出的是值的緩存,不存在動(dòng)態(tài)更新。 1.模塊 1.1 什么是模塊?什么是模塊化? 玩過FPS游戲的朋友應(yīng)該知道,一把裝配完整的M4步槍,一般是槍身+消音器+倍鏡+握把+槍托。 如果把M4步槍看成是一個(gè)頁面的話,那么我們可以...

    ygyooo 評(píng)論0 收藏0
  • 從小白程序員一路晉升為大廠高級(jí)技術(shù)專家我看過哪些書籍?(建議收藏)

    摘要:大家好,我是冰河有句話叫做投資啥都不如投資自己的回報(bào)率高。馬上就十一國慶假期了,給小伙伴們分享下,從小白程序員到大廠高級(jí)技術(shù)專家我看過哪些技術(shù)類書籍。 大家好,我是...

    sf_wangchong 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<