select/poll/epool linux多路復(fù)用解析

Linux(實(shí)際上市Unix)的一個(gè)基本概念是Unix/Linux中一切都是文件。每個(gè)進(jìn)程都有一個(gè)指向文件坑赡,套接字,設(shè)備或其他操作系統(tǒng)對(duì)象的文件描述符

與許多IO源一起工作的典型系統(tǒng)都要經(jīng)歷一個(gè)初始化階段么抗,然后進(jìn)入某種待機(jī)模式------等待客戶端發(fā)送請(qǐng)求并響應(yīng)


image.png

linux下有3種方案輪訓(xùn)一組文件描述符.

  • select
  • poll
  • epoll

Select系統(tǒng)調(diào)用

select()系統(tǒng)調(diào)用提供了一種實(shí)現(xiàn)同步多路復(fù)用 I/O 的機(jī)制毅否。

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

對(duì) select()的調(diào)用會(huì)阻塞,直到給定的文件描述符準(zhǔn)備好執(zhí)行 I/O 操作蝇刀,或者到達(dá)了可選的指定超時(shí)時(shí)間螟加。

被監(jiān)視的文件描述符被分成三組:

readfds 集合中列出的文件描述符將被監(jiān)視,以查看數(shù)據(jù)是否可用于讀刃鼙谩仰迁;
writefds 集合中列出的文件描述符將被監(jiān)視,以查看寫(xiě)入操作是否會(huì)在沒(méi)有阻塞的情況下完成顽分;
exceptfds 集合中的文件描述符將被監(jiān)視徐许,以查看是否發(fā)生了異常,或者帶外數(shù)據(jù)是否可用(這些狀態(tài)僅適用于套接字)卒蘸。

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
 
#define MAXBUF 256
 
void child_process(void)
{
  sleep(2);
  char msg[MAXBUF];
  struct sockaddr_in addr = {0};
  int n, sockfd,num=1;
  srandom(getpid());
  /* Create socket and connect to server */
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = inet_addr("127.0.0.1");
 
  connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
 
  printf("child {%d} connected \n", getpid());
  while(1){
        int sl = (random() % 10 ) +  1;
        num++;
        sleep(sl);
    sprintf (msg, "Test message %d from client %d", num, getpid());
    n = write(sockfd, msg, strlen(msg));    /* Send message */
  }
 
}
 
int main()
{
  char buffer[MAXBUF];
  int fds[5];
  struct sockaddr_in addr;
  struct sockaddr_in client;
  int addrlen, n,i,max=0;;
  int sockfd, commfd;
  fd_set rset;
  for(i=0;i<5;i++)
  {
    if(fork() == 0)
    {
        child_process();
        exit(0);
    }
  }
 
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  memset(&addr, 0, sizeof (addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = INADDR_ANY;
  bind(sockfd,(struct sockaddr*)&addr ,sizeof(addr));
  listen (sockfd, 5); 
 
  for (i=0;i<5;i++) 
  {
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    if(fds[i] > max)
        max = fds[i];
  }
  
  while(1){
    FD_ZERO(&rset);
    for (i = 0; i< 5; i++ ) {
        FD_SET(fds[i],&rset);
    }
 
    puts("round again");
    select(max+1, &rset, NULL, NULL, NULL);
 
    for(i=0;i<5;i++) {
        if (FD_ISSET(fds[i], &rset)){
            memset(buffer,0,MAXBUF);
            read(fds[i], buffer, MAXBUF);
            puts(buffer);
        }
    }   
  }
  return 0;
}

我們從創(chuàng)建 5 個(gè)子進(jìn)程開(kāi)始雌隅,每個(gè)進(jìn)程連接到服務(wù)器并將消息發(fā)送到服務(wù)器,服務(wù)器進(jìn)程使用accept(2) 為每個(gè)客戶端創(chuàng)建不同的文件描述符缸沃,select(2) 中的第一個(gè)參數(shù)應(yīng)該是三個(gè)集合中編號(hào)最大的文件描述符恰起,再加上 1,就可以知道最大的文件描述符編號(hào)趾牧。

主無(wú)限循環(huán)創(chuàng)建一組所有文件描述符检盼,調(diào)用 select 和 on 返回檢查哪個(gè)文件描述符已準(zhǔn)備好讀取,為了簡(jiǎn)單起見(jiàn)翘单,沒(méi)有添加錯(cuò)誤檢查吨枉。

返回時(shí)蹦渣,選擇將該集合更改為僅包含準(zhǔn)備好的文件描述符,因此我們需要在每次迭代中重新構(gòu)建集合貌亭。

Select – summary:

  • 我們需要在每次調(diào)用之前構(gòu)建每組集合柬唯;
  • 這個(gè)函數(shù)檢查任何 bit 到更高的數(shù)字 —— O(n);
  • 我們需要遍歷文件描述符來(lái)檢查它是否存在于從 select() 返回的集合中圃庭;
  • select 的主要優(yōu)點(diǎn)在于它的可移植性 —— 每個(gè)類(lèi) unix 操作系統(tǒng)的都有锄奢。

Poll 系統(tǒng)調(diào)用

不像 select() 低效的三個(gè)基于位掩碼的文件描述符集合,poll() 采用了一個(gè) nfds pollfd 結(jié)構(gòu)的單個(gè)數(shù)組剧腻,函數(shù)原型更簡(jiǎn)單:

int poll (struct pollfd *fds, unsigned int nfds, int timeout)拘央;

pollfd 結(jié)構(gòu)對(duì)事件和返回事件有不同的字段,所以我們不需要每次都創(chuàng)建它:

struct pollfd {
      int fd;
      short events; 
      short revents;
};

修改上面的例子:


#include <errno.h>
#include <netinet/in.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAXBUF 256

void child_process(void)
{
  sleep(2);
  char msg[MAXBUF];
  struct sockaddr_in addr = {0};
  int n, sockfd, num = 1;
  srandom(getpid());
  /* Create socket and connect to server */
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = inet_addr("127.0.0.1");

  connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));

  printf("child {%d} connected \n", getpid());
  while (1)
  {
    int sl = (random() % 10) + 1;
    num++;
    sleep(sl);
    sprintf(msg, "Test message %d from client %d", num, getpid());
    n = write(sockfd, msg, strlen(msg)); /* Send message */
  }
}

int main()
{
  char buffer[MAXBUF];
  int fds[5];
  struct sockaddr_in addr;
  struct sockaddr_in client;
  int addrlen, n, i, max = 0;
  ;
  int sockfd, commfd;
  fd_set rset;
  for (i = 0; i < 5; i++)
  {
    if (fork() == 0)
    {
      child_process();
      exit(0);
    }
  }

  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = INADDR_ANY;
  bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
  listen(sockfd, 5);

  struct pollfd pollfds[5];
  for (int i = 0; i < 5; i++)
  {
    memset(&client, 0, sizeof(client));
    addrlen = sizeof(client);
    pollfds[i].fd = accept(sockfd, (struct sockaddr *)&client, &addrlen);
    pollfds[i].events = POLLIN;
  }
  sleep(1);
  while (1)
  {
    puts("round again");
    poll(pollfds, 5, 50000);

    for (i = 0; i < 5; i++)
    {
      if (pollfds[i].revents & POLLIN)
      {
        pollfds[i].revents = 0;
        memset(buffer, 0, MAXBUF);
        read(pollfds[i].fd, buffer, MAXBUF);
        puts(buffer);
      }
    }
  }
  return 0;
}

就像使用 select 所做的那樣恕酸,我們需要檢查每個(gè) pollfd 對(duì)象堪滨,看看它的文件描述符是否準(zhǔn)備好胯陋,但不需要在每次迭代時(shí)構(gòu)建集合蕊温。

Poll vs Select

poll() 不要求用戶計(jì)算編號(hào)最高的文件描述符 +1 的值;
poll() 對(duì)于大值文件描述符更有效遏乔。假設(shè)我們通過(guò) select() 方法監(jiān)視一個(gè)值為 900 的單個(gè)文件描述符 —— 內(nèi)核將不得不檢查傳入集合的每個(gè)值的每一位义矛,直到第 900 位;
select() 的文件描述符集合是靜態(tài)大小的盟萨;
使用 select()凉翻,文件描述符集合會(huì)在返回時(shí)重建,因此每個(gè)后續(xù)調(diào)用都必須重新初始化它們捻激。 poll() 系統(tǒng)調(diào)用將輸入(events 字段)與輸出(revents 字段)分隔開(kāi)制轰,允許在不更改的情況下重新使用該數(shù)組。
返回時(shí)胞谭,select() 的 timeout 參數(shù)未定義垃杖。 可移植性代碼需要重新初始化它,這不是pselect() 的問(wèn)題丈屹;
select() 更具可移植性调俘,因?yàn)槟承?Unix 系統(tǒng)不支持 poll()。

Epoll 系統(tǒng)調(diào)用

在使用 select 和 poll 時(shí)旺垒,我們管理用戶空間上的所有內(nèi)容彩库,并在每個(gè)調(diào)用上發(fā)送集合以等待,要添加另一個(gè)套接字先蒋,我們需要將它添加到集合中并再次調(diào)用 select/poll骇钦。

Epoll* 系統(tǒng)調(diào)用幫助我們創(chuàng)建和管理內(nèi)核中的上下文,我們將任務(wù)分為 3 個(gè)步驟:

  • 使用 epoll_create 在內(nèi)核中創(chuàng)建一個(gè)上下文竞漾;
  • 使用 epoll_ctl 向/從上下文添加/移除文件描述符眯搭;
  • 使用 epoll_wait 等待上下文中的事件皇忿。

將上面的示例改用 epoll:


#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/epoll.h>
#define MAXBUF 256

void child_process(void)
{
  sleep(2);
  char msg[MAXBUF];
  struct sockaddr_in addr = {0};
  int n, sockfd, num = 1;
  srandom(getpid());
  /* Create socket and connect to server */
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = inet_addr("127.0.0.1");

  connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));

  printf("child {%d} connected \n", getpid());
  while (1)
  {
    int sl = (random() % 10) + 1;
    num++;
    sleep(sl);
    sprintf(msg, "Test message %d from client %d", num, getpid());
    n = write(sockfd, msg, strlen(msg)); /* Send message */
  }
}

int main()
{
  char buffer[MAXBUF];
  struct sockaddr_in addr;
  struct sockaddr_in client;
  int addrlen, n, i, max = 0;
  ;
  int sockfd, commfd;
  fd_set rset;
  for (i = 0; i < 5; i++)
  {
    if (fork() == 0)
    {
      child_process();
      exit(0);
    }
  }

  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(2000);
  addr.sin_addr.s_addr = INADDR_ANY;
  bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
  listen(sockfd, 5);

struct epoll_event events[5];
int epfd = epoll_create(10);
int nfds;

for (i=0;i<5;i++) 
  {
    static struct epoll_event ev;
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); 
  }
  
  while(1){
    puts("round again");
    nfds = epoll_wait(epfd, events, 5, 10000);
    printf("ngfs = {%d}  \n", nfds);
    for(i=0;i<nfds;i++) {
            memset(buffer,0,MAXBUF);
            read(events[i].data.fd, buffer, MAXBUF);
            puts(buffer);
    }
  }

  return 0;
}

Epoll vs Select/Poll

  • 我們可以在等待時(shí)添加或刪除文件描述符;
  • epoll_wait 僅返回具有準(zhǔn)備文件描述符的對(duì)象坦仍;
  • epoll 有更好的性能 —— O(1) 而不是O(n)鳍烁;
  • epoll 可以表現(xiàn)為級(jí)別觸發(fā)或邊緣觸發(fā)(請(qǐng)參見(jiàn)手冊(cè)頁(yè));
  • epoll 是 Linux 特有的繁扎,因此可移植性一般幔荒。

參考:
https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/#.XwMCHJMzZTY

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市梳玫,隨后出現(xiàn)的幾起案子爹梁,更是在濱河造成了極大的恐慌,老刑警劉巖提澎,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姚垃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡盼忌,警方通過(guò)查閱死者的電腦和手機(jī)积糯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谦纱,“玉大人看成,你說(shuō)我怎么就攤上這事】缂危” “怎么了川慌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)祠乃。 經(jīng)常有香客問(wèn)我梦重,道長(zhǎng),這世上最難降的妖魔是什么亮瓷? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任琴拧,我火速辦了婚禮,結(jié)果婚禮上寺庄,老公的妹妹穿的比我還像新娘艾蓝。我一直安慰自己,他們只是感情好斗塘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布赢织。 她就那樣靜靜地躺著,像睡著了一般馍盟。 火紅的嫁衣襯著肌膚如雪于置。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天贞岭,我揣著相機(jī)與錄音八毯,去河邊找鬼搓侄。 笑死,一個(gè)胖子當(dāng)著我的面吹牛话速,可吹牛的內(nèi)容都是我干的讶踪。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼泊交,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼乳讥!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起廓俭,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤云石,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后研乒,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體汹忠,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年雹熬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宽菜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡橄唬,死狀恐怖赋焕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情仰楚,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布犬庇,位于F島的核電站僧界,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏臭挽。R本人自食惡果不足惜捂襟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望欢峰。 院中可真熱鬧葬荷,春花似錦、人聲如沸纽帖。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)懊直。三九已至扒吁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間室囊,已是汗流浹背雕崩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工魁索, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盼铁。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓粗蔚,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親饶火。 傳聞我的和親對(duì)象是個(gè)殘疾皇子支鸡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345