【Socket编程】篇六之IO多路复用——select、poll、epoll

本文详细介绍了IO多路复用的概念,对比了select、poll和epoll三种机制在处理多个文件描述符时的异同。select存在最大文件描述符限制,poll解决了这个问题但仍有轮询开销,而epoll通过事件回调机制提高了效率,尤其适合大量闲置连接的情况。同时,epoll提供了epoll_create、epoll_ctl和epoll_wait三个关键函数,实现了更高效的I/O管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【Socket编程】篇六之IO多路复用——select、poll、epoll

mick_seu 2016-06-12 22:02:09  9663  收藏 9

分类专栏: Socket网络编程 文章标签: socket编程 IO多路复用 select poll epoll

版权

文章参考自:http://blog.youkuaiyun.com/tennysonsky/article/details/45745887(秋叶原 — Mike VS 麦克《Linux系统编程——I/O多路复用select、poll、epoll的区别使用》)

此外,还有一篇好文:epoll机制:epoll_create、epoll_ctl、epoll_wait、close(鱼思故渊的专栏)

 

在上一篇中,我简单学习了 IO多路复用的基本概念,这里我将初学其三种实现手段:selectpollepoll

 

I/O 多路复用是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程或线程不阻塞于某个特定的 I/O 系统调用。

select(),poll(),epoll()都是I/O多路复用的机制。I/O多路复用通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪,就是这个文件描述符进行读写操作之前),能够通知程序进行相应的读写操作。但select(),poll(),epoll()本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

与多线程(TPC(Thread Per Connection)模型)和多进程(典型的Apache模型(Process Per Connection,简称PPC))相比,I/O 多路复用的最大优势是系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。

 

 

 

select()的使用

 

 

所需头文件:

 

#include <sys/select.h>

 

 

函数原型:

 

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

 

 

函数描述:

监视并等待多个文件描述符的属性变化(可读、可写或错误异常)。select()函数监视的文件描述符分 3 类,分别是writefds、readfds、和 exceptfds。调用后 select() 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间),函数才返回。当 select()函数返回后,可以通过遍历 fdset,来找到就绪的描述符。

 

参数描述:

 

nfds: 要监视的文件描述符的范围,一般取监视的描述符数的最大值+1,如这里写 10, 这样的话,描述符 0,1, 2 …… 9 都会被监视,在 Linux 上最大值一般为1024。

readfd: 监视的可读描述符集合,只要有文件描述符读操作准备就绪,这个文件描述符就存储到这。

writefds: 监视的可写描述符集合。

exceptfds: 监视的错误异常描述符集合。

 

三个参数 readfds、writefds 和 exceptfds 指定我们要让内核监测读、写和异常条件的描述字。如果不需要使用某一个的条件,就可以把它设为NULL 。

 

几个较为重要的宏:

 

 
  1. //清空集合

  2. void FD_ZERO(fd_set *fdset);

  3.  
  4. //将一个给定的文件描述符加入集合之中

  5. void FD_SET(int fd, fd_set *fdset);

  6.  
  7. //将一个给定的文件描述符从集合中删除

  8. void FD_CLR(int fd, fd_set *fdset);

  9.  
  10. //检查集合中指定的文件描述符是否可以读写

  11. int FD_ISSET(int fd, fd_set *fdset);

  12.  
  13. timeout: 超时时间,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其 timeval 结构用于指定这段时间的秒数和微秒数。

  14. struct timeval

  15. {

  16. time_t tv_sec; /* 秒 */

  17. suseconds_t tv_usec; /* 微秒 */

  18. };

 

 

 

三种可能的函数返回情况:

1)永远等待下去:timeout 设置为空指针 NULL,且没有一个描述符准备好。

2)等待固定时间:timeout 设置为某个固定时间,在有一个描述符准备好时返回,如果时间到了,就算没有文件描述符准备就绪,这个函数也会返回 0。

3)不等待(不阻塞):检查描述字后立即返回,这称为轮询。为此,struct timeval变量的时间值指定为 0 秒 0 微秒,文件描述符属性无变化返回 0,有变化返回准备好的描述符数量。

 

函数返回值:

成功:就绪描述符的数目(同时修改readfds、writefds 和 exceptfds 三个参数),超时返回 0;
出错:-1。

 

下面用 Socket 举例,两个客户端,其中一个每隔 5s 发一个固定的字符串到服务器,另一个采集终端的键盘输入,将其发给服务器,一个服务器,使用 IO 多路复用处理这两个客户端。代码如下:

 

服务器:

 

 
  1. #include <cstdio>

  2. #include <sys/select.h>

  3. #include <unistd.h>

  4. #include <stdlib.h>

  5. #include <cstring>

  6. #include <cassert>

  7. #include <sys/types.h>

  8. #include <sys/socket.h>

  9. #include <netinet/in.h>

  10. #include <arpa/inet.h>

  11.  
  12. const int BUFFER_SIZE = 4096;

  13. const int SERVER_PORT = 2222;

  14.  
  15. inline int max(int a, int b){ return (a > b ? : a, b);}

  16.  
  17. int main()

  18. {

  19. int server_socket;

  20. char buff1[BUFFER_SIZE];

  21. char buff2[BUFFER_SIZE];

  22. fd_set rfds;

  23. struct timeval tv;

  24. int ret;

  25. int n;

  26.  
  27. server_socket = socket(AF_INET, SOCK_STREAM, 0);

  28. assert(server_socket != -1);

  29.  
  30. struct sockaddr_in server_addr;

  31. memset(&server_addr, 0, sizeof(server_addr));

  32. server_addr.sin_family = AF_INET;

  33. server_addr.sin_port = htons(SERVER_PORT);

  34. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

  35.  
  36. assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

  37. assert(listen(server_socket, 5) != -1);

  38.  
  39. struct sockaddr_in client_addr1, client_addr2;

  40. socklen_t client_addr_len = sizeof(struct sockaddr_in);

  41.  
  42. printf("waiting...\n");

  43.  
  44. //此处先建立两个 TCP 连接

  45. int connfd1 = accept(server_socket, (struct sockaddr*)&client_addr1, &client_addr_len);

  46. assert(connfd1 != -1);

  47. printf("connect from %s:%d\n", inet_ntoa(client_addr1.sin_addr), ntohs(client_addr1.sin_port));

  48. int connfd2 = accept(server_socket, (struct sockaddr*)&client_addr2, &client_addr_len);

  49. assert(connfd2 != -1);

  50. printf("connect from %s:%d\n", inet_ntoa(client_addr2.sin_addr), ntohs(client_addr2.sin_port));

  51.  
  52. while(1)

  53. {

  54. FD_ZERO(&rfds);

  55. FD_SET(connfd1, &rfds);

  56. FD_SET(connfd2, &rfds);

  57.  
  58. tv.tv_sec = 10;

  59. tv.tv_usec = 0;

  60.  
  61. printf("select...\n");

  62. ret = select(max(connfd1, connfd2) + 1, &rfds, NULL, NULL, NULL);

  63. //ret = select(max(connfd1, connfd2) + 1, &rfds, NULL, NULL, &tv);

  64.  
  65. if(ret == -1)

  66. perror("select()");

  67. else if(ret > 0)

  68. {

  69. if(FD_ISSET(connfd1, &rfds))

  70. {

  71. n = recv(connfd1, buff1, BUFFER_SIZE, 0);

  72. buff1[n] = '\0'; //注意手动添加字符串结束符

  73. printf("connfd1: %s\n", buff1);

  74. }

  75. if(FD_ISSET(connfd2, &rfds))

  76. {

  77. n = recv(connfd2, buff2, BUFFER_SIZE, 0);

  78. buff2[n] = '\0'; //注意手动添加字符串结束符

  79. printf("connfd2: %s\n", buff2);

  80. }

  81. }

  82. else

  83. printf("time out\n");

  84. }

  85.  
  86. return 0;

  87. }

 

 

客户端1:

 

 
  1. #include <cstdio>

  2. #include <unistd.h>

  3. #include <stdlib.h>

  4. #include <cstring>

  5. #include <cassert>

  6. #include <sys/types.h>

  7. #include <sys/socket.h>

  8. #include <netinet/in.h>

  9. #include <arpa/inet.h>

  10.  
  11. const int BUFFER_SIZE = 4096;

  12. const int SERVER_PORT = 2222;

  13.  
  14. int main()

  15. {

  16. int client_socket;

  17. const char *server_ip = "127.0.0.1";

  18. char buffSend[BUFFER_SIZE] = "I'm from d.cpp";

  19.  
  20. client_socket = socket(AF_INET, SOCK_STREAM, 0);

  21. assert(client_socket != -1);

  22.  
  23. struct sockaddr_in server_addr;

  24. memset(&server_addr, 0, sizeof(server_addr));

  25. server_addr.sin_family = AF_INET;

  26. server_addr.sin_port = htons(SERVER_PORT);

  27. server_addr.sin_addr.s_addr = inet_addr(server_ip);

  28.  
  29. assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

  30.  
  31. while(1)

  32. {

  33. assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1);

  34. sleep(5);

  35. }

  36. close(client_socket);

  37.  
  38. return 0;

  39. }

 

 

客户端2:

 

 
  1. #include <cstdio>

  2. #include <unistd.h>

  3. #include <stdlib.h>

  4. #include <cstring>

  5. #include <cassert>

  6. #include <sys/types.h>

  7. #include <sys/socket.h>

  8. #include <netinet/in.h>

  9. #include <arpa/inet.h>

  10.  
  11. const int BUFFER_SIZE = 4096;

  12. const int SERVER_PORT = 2222;

  13.  
  14. int main()

  15. {

  16. int client_socket;

  17. const char *server_ip = "127.0.0.1";

  18. char buffSend[BUFFER_SIZE];

  19.  
  20. client_socket = socket(AF_INET, SOCK_STREAM, 0);

  21. assert(client_socket != -1);

  22.  
  23. struct sockaddr_in server_addr;

  24. memset(&server_addr, 0, sizeof(server_addr));

  25. server_addr.sin_family = AF_INET;

  26. server_addr.sin_port = htons(SERVER_PORT);

  27. server_addr.sin_addr.s_addr = inet_addr(server_ip);

  28.  
  29. assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

  30.  
  31. while(1)

  32. {

  33. fgets(buffSend, BUFFER_SIZE, stdin);

  34. assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1);

  35. }

  36. close(client_socket);

  37.  
  38. return 0;

  39. }

 

以上三份代码有缺陷,代码没有很好的结束方式,都是 while(1) 死循环,运行的结束需要用 Ctrl + c  ⊙﹏⊙

 

 

 

 

 

poll()的使用

 

 

select() 和 poll() 系统调用的本质一样,前者在 BSD UNIX 中引入的,后者在 System V 中引入的。poll() 的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

 

所需头文件:

 

#include <poll.h>


函数原型:

 

 

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


函数描述:

 

监视并等待多个文件描述符的属性变化。

 

函数参数:

1)fds:与 select() 使用三个 fd_set 的方式不同,poll() 使用一个 pollfd 的指针实现。一个 pollfd 结构体数组,其中包括了你想监视的文件描述符和事件, 事件由结构中事件域 events 来确定,调用后实际发生的事件将被填写在结构体的 revents 域。

 

 

 
  1. struct pollfd{

  2. int fd; /* 文件描述符 */

  3. short events; /* 等待的事件 */

  4. short revents; /* 实际发生了的事件 */

  5. }; 

 

_1、fd:每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。


_2、events:每个结构体的 events 域是监视该文件描述符的事件掩码,由用户来设置这个域。

 

_3、revents:revents 域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回。

 

事件的掩码取值如下:


POLLIN | POLLPRI 等价于 select() 的读事件,POLLOUT | POLLWRBAND 等价于 select() 的写事件。POLLIN 等价于 POLLRDNORM | POLLRDBAND,而 POLLOUT 则等价于 POLLWRNORM 。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events 为 POLLIN | POLLOUT。

每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件。

2)nfds:用来指定第一个参数数组元素个数。


3)timeout: 指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回。

 

 

 

 

函数返回值:

成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数,如果在超时前没有任何事件发生,poll()返回 0;

失败时,poll() 返回 -1。

 

 

此处我们将上面的例子用 poll() 重新实现如下,只用修改服务器端代码:

 

 
  1. #include <cstdio>

  2. #include <poll.h>

  3. #include <unistd.h>

  4. #include <stdlib.h>

  5. #include <cstring>

  6. #include <cassert>

  7. #include <sys/types.h>

  8. #include <sys/socket.h>

  9. #include <netinet/in.h>

  10. #include <arpa/inet.h>

  11.  
  12. const int BUFFER_SIZE = 4096;

  13. const int SERVER_PORT = 2222;

  14.  
  15. int main()

  16. {

  17. int server_socket;

  18. char buff1[BUFFER_SIZE];

  19. char buff2[BUFFER_SIZE];

  20. struct timeval tv;

  21. int ret;

  22. int n;

  23.  
  24. server_socket = socket(AF_INET, SOCK_STREAM, 0);

  25. assert(server_socket != -1);

  26.  
  27. struct sockaddr_in server_addr;

  28. memset(&server_addr, 0, sizeof(server_addr));

  29. server_addr.sin_family = AF_INET;

  30. server_addr.sin_port = htons(SERVER_PORT);

  31. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

  32.  
  33. assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

  34. assert(listen(server_socket, 5) != -1);

  35.  
  36. struct sockaddr_in client_addr1, client_addr2;

  37. socklen_t client_addr_len = sizeof(struct sockaddr_in);

  38.  
  39. printf("waiting...\n");

  40.  
  41. int connfd1 = accept(server_socket, (struct sockaddr*)&client_addr1, &client_addr_len);

  42. assert(connfd1 != -1);

  43. printf("connect from %s:%d\n", inet_ntoa(client_addr1.sin_addr), ntohs(client_addr1.sin_port));

  44. int connfd2 = accept(server_socket, (struct sockaddr*)&client_addr2, &client_addr_len);

  45. assert(connfd2 != -1);

  46. printf("connect from %s:%d\n", inet_ntoa(client_addr2.sin_addr), ntohs(client_addr2.sin_port));

  47.  
  48. struct pollfd rfds[2];

  49. rfds[0].fd = connfd1;

  50. rfds[0].events = POLLIN;

  51. rfds[1].fd = connfd2;

  52. rfds[1].events = POLLIN;

  53. tv.tv_sec = 10;

  54. tv.tv_usec = 0;

  55.  
  56. while(1)

  57. {

  58. printf("poll...\n");

  59. ret = poll(rfds, 2, -1);

  60.  
  61. if(ret == -1)

  62. perror("poll()");

  63. else if(ret > 0)

  64. {

  65. if((rfds[0].revents & POLLIN) == POLLIN)

  66. {

  67. n = recv(connfd1, buff1, BUFFER_SIZE, 0);

  68. buff1[n] = '\0';

  69. printf("connfd1: %s\n", buff1);

  70. }

  71. if((rfds[1].revents & POLLIN) == POLLIN)

  72. {

  73. n = recv(connfd2, buff2, BUFFER_SIZE, 0);

  74. buff2[n] = '\0';

  75. printf("connfd2: %s\n", buff2);

  76. }

  77. }

  78. else

  79. printf("time out\n");

  80. }

  81.  
  82. return 0;

  83. }

 

 

 

 

 

 

 

 

epoll()的使用

 

 

 

epoll 是在内核 2.6 中提出的,是之前的 select() 和 poll() 的增强版本。相对于 select() 和 poll() 来说,epoll 更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

 

epoll 操作过程需要三个接口,分别如下:

 

 
  1. #include <sys/epoll.h>

  2. int epoll_create(int size);

  3. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  4. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 

 

 

int epoll_create(int size);

 

功能:

该函数生成一个 epoll 专用的文件描述符。

参数:

size: 用来告诉内核这个监听的数目一共有多大,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值

 

返回值:
成功:epoll 专用的文件描述符
失败:-1

 

 

 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

 

 

 

功能:

epoll 的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

参数:

epfd: epoll 专用的文件描述符,epoll_create()的返回值

op: 表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从 epfd 中删除一个 fd;

fd: 需要监听的文件描述符

event: 告诉内核要监听什么事件,struct epoll_event 结构如:

 

 
  1. // 保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)

  2. typedef union epoll_data {

  3. void *ptr;

  4. int fd;

  5. __uint32_t u32;

  6. __uint64_t u64;

  7. } epoll_data_t;

  8.  
  9. // 感兴趣的事件和被触发的事件

  10. struct epoll_event {

  11. __uint32_t events; /* Epoll events */

  12. epoll_data_t data; /* User data variable */

  13. };

 

 

events 可以是以下几个宏的集合:

 

EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET :将 EPOLL 设为边缘触发(Edge Trigger)模式,这是相对于水平触发(Level Trigger)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里

返回值:

成功:0

失败:-1

 

 

int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout );

功能:

等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select() 调用。

参数:

epfd: epoll 专用的文件描述符,epoll_create()的返回值

events: 分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。

maxevents: maxevents 告之内核这个 events 有多少个 。

timeout: 超时时间,单位为毫秒,为 -1 时,函数为阻塞。

返回值:

成功:返回需要处理的事件数目,如返回 0 表示已超时

失败:-1

 

epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger)。LT 模式是默认模式,LT 模式与 ET 模式的区别如下:

LT 模式:支持block和no-block socket。当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。

 

ET 模式:只支持no-block socket。当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。该模式效率非常高,尤其在高并发,大流量的情况下,会比LT少很多epoll的系统调用。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。

 

接下来,我们将上面的例子,改为用 epoll 实现:

 

 
  1. #include <cstdio>

  2. #include <sys/epoll.h>

  3. #include <unistd.h>

  4. #include <stdlib.h>

  5. #include <cstring>

  6. #include <cassert>

  7. #include <sys/types.h>

  8. #include <sys/socket.h>

  9. #include <netinet/in.h>

  10. #include <arpa/inet.h>

  11.  
  12. const int BUFFER_SIZE = 4096;

  13. const int SERVER_PORT = 2222;

  14.  
  15. int main()

  16. {

  17. int server_socket;

  18. char buff1[BUFFER_SIZE];

  19. char buff2[BUFFER_SIZE];

  20. struct timeval tv;

  21. int ret;

  22. int n, i;

  23.  
  24. server_socket = socket(AF_INET, SOCK_STREAM, 0);

  25. assert(server_socket != -1);

  26.  
  27. struct sockaddr_in server_addr;

  28. memset(&server_addr, 0, sizeof(server_addr));

  29. server_addr.sin_family = AF_INET;

  30. server_addr.sin_port = htons(SERVER_PORT);

  31. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

  32.  
  33. assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

  34. assert(listen(server_socket, 5) != -1);

  35.  
  36. struct sockaddr_in client_addr1, client_addr2;

  37. socklen_t client_addr_len = sizeof(struct sockaddr_in);

  38.  
  39. printf("waiting...\n");

  40.  
  41. int connfd1 = accept(server_socket, (struct sockaddr*)&client_addr1, &client_addr_len);

  42. assert(connfd1 != -1);

  43. printf("connect from %s:%d\n", inet_ntoa(client_addr1.sin_addr), ntohs(client_addr1.sin_port));

  44. int connfd2 = accept(server_socket, (struct sockaddr*)&client_addr2, &client_addr_len);

  45. assert(connfd2 != -1);

  46. printf("connect from %s:%d\n", inet_ntoa(client_addr2.sin_addr), ntohs(client_addr2.sin_port));

  47.  
  48. tv.tv_sec = 10;

  49. tv.tv_usec = 0;

  50.  
  51. struct epoll_event event;

  52. struct epoll_event wait_event[2];

  53.  
  54. int epfd = epoll_create(10);

  55. assert(epfd != -1);

  56.  
  57. event.data.fd = connfd1;

  58. event.events = EPOLLIN;

  59. assert(epoll_ctl(epfd, EPOLL_CTL_ADD, connfd1, &event) != -1);

  60. event.data.fd = connfd2;

  61. event.events = EPOLLIN;

  62. assert(epoll_ctl(epfd, EPOLL_CTL_ADD, connfd2, &event) != -1);

  63.  
  64.  
  65. while(1)

  66. {

  67. printf("epoll...\n");

  68. ret = epoll_wait(epfd, wait_event, 2, -1);

  69.  
  70. if(ret == -1)

  71. perror("epoll()");

  72. else if(ret > 0)

  73. {

  74. for(i = 0; i < ret; ++i)

  75. {

  76. if(wait_event[i].data.fd == connfd1 && (wait_event[i].events & EPOLLIN) == EPOLLIN)

  77. {

  78. n = recv(connfd1, buff1, BUFFER_SIZE, 0);

  79. buff1[n] = '\0';

  80. printf("connfd1: %s\n", buff1);

  81. }

  82. else if(wait_event[i].data.fd == connfd2 && (wait_event[i].events & EPOLLIN) == EPOLLIN)

  83. {

  84. n = recv(connfd2, buff2, BUFFER_SIZE, 0);

  85. buff2[n] = '\0';

  86. printf("connfd2: %s\n", buff2);

  87. }

  88. }

  89. }

  90. else

  91. printf("time out\n");

  92. }

  93.  
  94. return 0;

  95. }

 

 

 

在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而 epoll() 事先通过 epoll_ctl() 来注册一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似 callback 的回调机制(软件中断 ),迅速激活这个文件描述符,当进程调用 epoll_wait() 时便得到通知。

 

 

 

 

下面分析 select、poll、epoll之间的优缺点:


select:

缺点:

1)每次调用select,都存在 fd 集合在用户态与内核态之间的拷贝,I/O 的效率会随着监视 fd 的数量的增长而线性下降。
2)select()调用的内部,需要用轮询的方式去完整遍历每一个 fd,如果遍历完所有 fd 后没有发现就绪 fd,则挂起当前进程,直到有 fd 就绪或者主动超时(使用 schedule_timeout 实现睡一会儿,判断一次(被定时器唤醒,注意与 select() 函数里面的 timeout 参数区分作用)的效果),被唤醒后它又要再次遍历 fd (直到有 fd 就绪或 select() 函数超时)。这个过程经历了多次无谓的遍历。CPU的消耗会随着监视 fd 的数量的增长而线性增加

 

 
  1. 步骤总结如下:

  2. 1)先把全部fd扫一遍;

  3. 2)如果发现有可用的fd,跳到5;

  4. 3)如果没有,当前进程去睡觉xx秒(schedule_timeout机制);

  5. 4)xx秒后自己醒了,或者状态变化的fd唤醒了自己,跳到1;

  6. 5)结束循环体,返回。(注意函数的返回也可能是 timeout 的超时)

 

 

3)select支持的文件描述符数量太小了,默认是1024。

4)由于 select 参数输入和输出使用同样的 fd_set ,导致每次 select()  之前都要重新初始化要监视的 fd_set,开销也会比较大。

 

poll:

poll 的实现和 select 非常相似,它同样存在 fd 集合在用户态和内核态间的拷贝,且在函数内部需要轮询整个 fd 集合。区别于select 的只是描述fd集合的方式不同,poll使用pollfd数组而不是select的fd_set结构,所以poll克服了select文件描述符数量的限制,此外,poll 的 polldf 结构体中分别用 events 和 revents 来存储输入和输出,较之 select() 不用每次调用 poll() 之前都要重新初始化需要监视的事件。

 

 

epoll:

epoll是一种 Reactor 模式,提供了三个函数,epoll_create(),epoll_ctl() 和 epoll_wait()。

优点:

1)对于上面的第一个缺点,epoll 的解决方案在 epoll_ctl() 函数中。每次注册新的事件到 epoll 描述符中时,会把该 fd 拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll 保证了每个fd在整个过程中只会拷贝一次。
2)对于第二个缺点,epoll 的解决方案不像 select 或 poll 一样轮询 fd,而只在 epoll_ctl 时把要监控的 fd 挂一遍,并为每个 fd 指定一个回调函数,当设备就绪,这个回调函数把就绪的 fd 加入一个就绪链表。epoll_wait 的工作实际上就是在这个就绪链表中查看有没有就绪的 fd,也即 epoll_wait 只关心“活跃”的描述符,而不用像 select() 和 poll() 需要遍历所有 fd,它需要不断轮询就绪链表,期间也可能多次睡眠和唤醒(类似与 select, poll),但终究它的轮询只用判断就续表是否为空即可,其CPU的消耗不会随着监视 fd 的数量的增长而线性增加,这就是回调机制的优势,也正是 epoll 的魅力所在。

 

同理,select() 和 poll() 函数返回后, 处理就绪 fd 的方法还是轮询,如下:

 

 
  1. int res = select(maxfd+1, &readfds, NULL, NULL, 120);

  2. if (res > 0)

  3. {

  4. for (int i = 0; i < MAX_CONNECTION; i++)

  5. {

  6. if (FD_ISSET(allConnection[i], &readfds))

  7. {

  8. handleEvent(allConnection[i]);

  9. }

  10. }

  11. }

  12. // if(res == 0) handle timeout, res < 0 handle error

 

 

而 epoll() 只需要从就绪链表中处理就绪的 fd:

 

 
  1. int res = epoll_wait(epfd, events, 20, -1);

  2. for (int i = 0; i < res;i++)

  3. {

  4. handleEvent(events[n]);

  5. }


此处的效率对比也是高下立判。

 

 

 

 

3)对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个和系统限制有关,linux里面可以用ulimit查看文件打开数限制。

 

缺点:epoll是 linux 特有的,而 select 和 poll 是在 POSIX 中规定的,跨平台支持更好。

 

 

 

 

综上:

select 、poll、epoll 的使用要根据具体的使用场合,并不是 epoll 的性能就一定好,因为回调函数也是有消耗的,当 socket 连接较少时或者是即使 socket 连接很多,但是连接基本都是活跃的情况下,select / poll 的性能与 epoll 是差不多的。即如果没有大量的 idle-connection 或者 dead-connection,epoll 的效率并不会比 select/poll 高很多,但是当遇到大量的 idle-connection,就会发现epoll 的效率大大高于 select/poll。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值