1.背景

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正是因为这两个阶段,linux系统产生了下面五种网络模式的方案。

  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路复用( IO multiplexing)
  • 信号驱动 I/O( signal driven IO)
  • 异步 I/O(asynchronous IO)

1.1 I/O多路复用(IO multiplexing)

IO multiplexing就是我们说的select/poll/epoll,有些地方也称这种IO方式为event driven IO。IO多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。

select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
image.png

当用户进程调用了select,那么整个进程会被block,而同时,kernel会监视所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

1.2 select函数

  1. #include <sys/select.h>
  2. #include <sys/time.h>
  3. int select(int maxfd_add_1, fd_set *readset, fd_set *write_set, fd_set * exceptset,
  4. const struct timeval *timeout);
  5. #define __FD_SETSIZE 1024
  6. typedef struct {
  7. unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
  8. } fd_set;
  9. struct timeval{
  10. long tv_sec;
  11. long tv_usec;
  12. };
  13. void FD_ZERO(fd_set *fdset); // 清空
  14. void FD_SET(int fd, fd_set *fdset); // 设置一个位
  15. void FD_CLR(int fd, fd_set *fdset); // 清除一个位
  16. int FD_ISSET(int fd,fd_set *fdset); // 检查某个位是否被设置,可用于函数返回时判断那个文件描述符就绪

maxfd_add_1后面三个集中设置了的文件描述符的最大值+1,因为这个值表示的是个数。这个参数存在的意义就是内核在每次唤醒的时候需要遍历的文件描述符个数,因而不用全部遍历所有1024个文件描述符的状态
存在这个参数的原因纯粹是为了效率。
readsetwrite_setexceptset这三个fd_set类型是一个位图。返回的时候,如果对应的位被设置,那么表示文件描述符就绪,因此需要用户再次手动遍历依次。

select的缺点

  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小了,默认是1024


    1.3 poll函数

    ```c

    include

int poll(struct pollfd *fdarray, unsigned long n, int timeout); // 成功个数,错误-1,超时0

struct pollfd { int fd; / file descriptor / short events; / requested events to watch / short revents; / returned events witnessed / };

  1. `fdarray`是一个pollfd类型的数组(首指针),n表示这个数组的个数。结构体`pollfd``events`成员是要测试的条件,而`revents`是内核要填充的,表示文件描述符当前的读写状态。<br />`pollfd`结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。和select函数一样,poll返回后,也需要轮询`pollfd`来获取就绪的描述符。
  2. <a name="AsiCY"></a>
  3. ## 1.4 epoll函数
  4. epoll是在2.6内核中提出的,是之前的selectpoll的增强版本。相对于selectpoll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
  5. ```c
  6. #include <sys/epoll.h>
  7. // 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
  8. int epoll_create(int size);
  9. // 对要监听的文件描述符的的增加/修改/删除
  10. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  11. // 开始阻塞
  12. // 参数 evlist 所指向的结构体数组中返回的是有关就绪态文件描述符的信息。
  13. // 数组 evlist 的空间由调用者负责申请,所包含的元素个数为 maxevents
  14. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  15. // epoll_wait 返回的时候, events 表示发生的事件
  16. // data 则表示与文件描述符相关的信息,可以指向一个结构体,也可以是直接的文件描述符
  17. typedef union epoll_data {
  18. void *ptr; // 常用这个
  19. int fd;
  20. uint32_t u32;
  21. uint64_t u64;
  22. } epoll_data_t;
  23. struct epoll_event {
  24. uint32_t events; // bit mask, 感兴趣的事件如EPOLLIN,EPOLLOUT,EPOLLPRI等
  25. epoll_data_t data; // 一般使用data的fd成员表示感兴趣的socket
  26. };
  27. /* events可以是以下几个宏的集合:
  28. EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  29. EPOLLOUT:表示对应的文件描述符可以写;
  30. EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  31. EPOLLERR:表示对应的文件描述符发生错误;
  32. EPOLLHUP:表示对应的文件描述符被挂断;
  33. EPOLLET :将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  34. EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  35. */

2.总结

2.1 epoll优点

在 select/poll中,进程只有在调用一定的方法后,内核才会遍历文件描述符。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)

epoll的优点主要是以下个方面:

  1. 监视的描述符数量不受限制。它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。
  2. IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。

2.2 性能比较

如果没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle-connection,就会发现epoll的效率大大高于select/poll。image.png

3.例子

  1. #define IPADDRESS "127.0.0.1"
  2. #define PORT 8787
  3. #define MAXSIZE 1024
  4. #define LISTENQ 5
  5. #define FDSIZE 1000
  6. #define EPOLLEVENTS 100
  7. listenfd = socket_bind(IPADDRESS,PORT);
  8. struct epoll_event events[EPOLLEVENTS];
  9. //创建一个描述符
  10. epollfd = epoll_create(FDSIZE);
  11. //添加监听描述符事件
  12. add_event(epollfd,listenfd,EPOLLIN);
  13. //循环等待
  14. for ( ; ; ){
  15. //该函数返回已经准备好的描述符事件数目
  16. ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
  17. //处理接收到的连接
  18. handle_events(epollfd,events,ret,listenfd,buf);
  19. }
  20. //事件处理函数
  21. static void handle_events(int epollfd,struct epoll_event *events,int num,
  22. int listenfd,char *buf)
  23. {
  24. int i;
  25. int fd;
  26. //进行遍历;这里只要遍历已经准备好的io事件。num并不是当初epoll_create时的FDSIZE。
  27. for (i = 0;i < num;i++)
  28. {
  29. fd = events[i].data.fd;
  30. //根据描述符的类型和事件类型进行处理
  31. if ((fd == listenfd) &&(events[i].events & EPOLLIN))
  32. handle_accpet(epollfd,listenfd);
  33. else if (events[i].events & EPOLLIN)
  34. do_read(epollfd,fd,buf);
  35. else if (events[i].events & EPOLLOUT)
  36. do_write(epollfd,fd,buf);
  37. }
  38. }
  39. //添加事件
  40. static void add_event(int epollfd,int fd,int state){
  41. struct epoll_event ev;
  42. ev.events = state;
  43. ev.data.fd = fd;
  44. epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
  45. }
  46. //处理接收到的连接
  47. static void handle_accpet(int epollfd,int listenfd){
  48. int clifd;
  49. struct sockaddr_in cliaddr;
  50. socklen_t cliaddrlen;
  51. clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
  52. if (clifd == -1)
  53. perror("accpet error:");
  54. else {
  55. //添加一个客户描述符和事件
  56. printf("accept a new client: %s:%d\n",
  57. inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
  58. add_event(epollfd,clifd,EPOLLIN);
  59. }
  60. }
  61. //读处理
  62. static void do_read(int epollfd,int fd,char *buf){
  63. int nread;
  64. nread = read(fd,buf,MAXSIZE);
  65. if (nread == -1) {
  66. perror("read error:");
  67. close(fd); //记住close fd
  68. delete_event(epollfd,fd,EPOLLIN); //删除监听
  69. }
  70. else if (nread == 0) {
  71. fprintf(stderr,"client close.\n");
  72. close(fd); //记住close fd
  73. delete_event(epollfd,fd,EPOLLIN); //删除监听
  74. }
  75. else {
  76. printf("read message is : %s",buf);
  77. //修改描述符对应的事件,由读改为写
  78. modify_event(epollfd,fd,EPOLLOUT);
  79. }
  80. }
  81. //写处理
  82. static void do_write(int epollfd,int fd,char *buf) {
  83. int nwrite;
  84. nwrite = write(fd,buf,strlen(buf));
  85. if (nwrite == -1){
  86. perror("write error:");
  87. close(fd); //记住close fd
  88. delete_event(epollfd,fd,EPOLLOUT); //删除监听
  89. }else{
  90. modify_event(epollfd,fd,EPOLLIN);
  91. }
  92. memset(buf,0,MAXSIZE);
  93. }
  94. //删除事件
  95. static void delete_event(int epollfd,int fd,int state) {
  96. struct epoll_event ev;
  97. ev.events = state;
  98. ev.data.fd = fd;
  99. epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
  100. }
  101. //修改事件
  102. static void modify_event(int epollfd,int fd,int state){
  103. struct epoll_event ev;
  104. ev.events = state;
  105. ev.data.fd = fd;
  106. epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
  107. }

参考文献

  1. https://zhuanlan.zhihu.com/p/148972109
  2. https://www.cnblogs.com/perfy576/p/8554734.html