资讯专栏INFORMATION COLUMN

手把手写C++服务器(31):服务器性能提升关键——IO复用技术【两万字长文】

big_cat / 3333人阅读

摘要:前面几讲手撕了网关服务器回显服务器服务的代码,但是这几个一次只能监听一个文件描述符,因此性能非常原始低下。复用能使服务器同时监听多个文件描述符,是服务器性能提升的关键。表示要操作的文件描述符,指定操作类型,指定事件。

 本系列文章导航: 手把手写C++服务器(0):专栏文章-汇总导航【更新中】 

前言: Linux中素有“万物皆文件,一切皆IO”的说法。前面几讲手撕了CGI网关服务器、echo回显服务器、discard服务的代码,但是这几个一次只能监听一个文件描述符,因此性能非常原始、低下。IO复用能使服务器同时监听多个文件描述符,是服务器性能提升的关键。虽然IO复用本身是阻塞的,但是和并发技术结合起来,再加上一点设计模式,一个高性能服务器的基石就基本搭建完成了。

目录

1、预备知识

(1)文件描述符

(2)进程阻塞

(3)缓存IO

(4)什么是IO多路复用?

2、Linux五大IO模型

(1)阻塞IO

(2)非阻塞IO

(3)IO多路复用

(4)信号驱动IO

(5)异步IO

3、select

函数返回

参数详解

重要结构体详解

使用流程

代码实例

4、poll

函数原型

重要结构体详解

事件类型

使用流程

代码实例

5、epoll

函数原型

函数返回

LT水平触发模式和ET边沿触发模式

代码实例

6、三组IO复用函数对比

1. 用户态将文件描述符传入内核的方式

2. 内核态检测文件描述符读写状态的方式

3. 找到就绪的文件描述符并传递给用户态的方式

4. 重复监听的处理方式

7、经典面试题:epoll更高效的原因

写在最后

参考


1、预备知识

(1)文件描述符

强烈推荐看一下本系列的第25讲《手把手写C++服务器(25):万物皆可文件之socket fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。 文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

(2)进程阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得了CPU资源),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

(3)缓存IO

缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存 I/O 的缺点:

数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

(4)什么是IO多路复用?

IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪就会阻塞应用程序,交出CPU。

2、Linux五大IO模型

(1)阻塞IO

这是最常用的简单的IO模型。阻塞IO意味着当我们发起一次IO操作后一直等待成功或失败之后才返回,在这期间程序不能做其它的事情。阻塞IO操作只能对单个文件描述符进行操作,详见readwrite

(2)非阻塞IO

我们在发起IO时,通过对文件描述符设置O_NONBLOCK flag来指定该文件描述符的IO操作为非阻塞。非阻塞IO通常发生在一个for循环当中,因为每次进行IO操作时要么IO操作成功,要么当IO操作会阻塞时返回错误EWOULDBLOCK/EAGAIN,然后再根据需要进行下一次的for循环操作,这种类似轮询的方式会浪费很多不必要的CPU资源,是一种糟糕的设计。和阻塞IO一样,非阻塞IO也是通过调用readwrite来进行操作的,也只能对单个描述符进行操作。

(3)IO多路复用

IO多路复用在Linux下包括了三种,selectpollepoll,抽象来看,他们功能是类似的,但具体细节各有不同:首先都会对一组文件描述符进行相关事件的注册,然后阻塞等待某些事件的发生或等待超时。IO多路复用都可以关注多个文件描述符,但对于这三种机制而言,不同数量级文件描述符对性能的影响是不同的,下面会详细介绍。

(4)信号驱动IO

信号驱动IO是利用信号机制,让内核告知应用程序文件描述符的相关事件。

但信号驱动IO在网络编程的时候通常很少用到,因为在网络环境中,和socket相关的读写事件太多了,比如下面的事件都会导致SIGIO信号的产生:

  1. TCP连接建立
  2. 一方断开TCP连接请求
  3. 断开TCP连接请求完成
  4. TCP连接半关闭
  5. 数据到达TCP socket
  6. 数据已经发送出去(如:写buffer有空余空间)

上面所有的这些都会产生SIGIO信号,但我们没办法在SIGIO对应的信号处理函数中区分上述不同的事件,SIGIO只应该在IO事件单一情况下使用,比如说用来监听端口的socket,因为只有客户端发起新连接的时候才会产生SIGIO信号。

(5)异步IO

异步IO和信号驱动IO差不多,但它比信号驱动IO可以多做一步:相比信号驱动IO需要在程序中完成数据从用户态到内核态(或反方向)的拷贝,异步IO可以把拷贝这一步也帮我们完成之后才通知应用程序。我们使用 aio_read 来读,aio_write 写。

同步IO vs 异步IO

1. 同步IO指的是程序会一直阻塞到IO操作如read、write完成

2. 异步IO指的是IO操作不会阻塞当前程序的继续执行
所以根据这个定义,上面阻塞IO当然算是同步的IO,非阻塞IO也是同步IO,因为当文件操作符可用时我们还是需要阻塞的读或写,同理IO多路复用和信号驱动IO也是同步IO,只有异步IO是完全完成了数据的拷贝之后才通知程序进行处理,没有阻塞的数据读写过程。

3、select

select的作用是在一段指定的时间内,监听用户感兴趣的文件描述符上的可读、可写、异常等事件。函数原型如下:

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

函数返回

  • select成功时返回就绪文件描述符的总数;
  • 如果在超时时间内没有任何文件描述符就绪,select将返回0;
  • select失败时返回-1并设置errno。;
  • 如果在select等待期间,程序接收到信号,select立即返回-1,并将errno设置为EINTR。

参数详解

  • nfds:指定被监听文件描述符总数。通常被设置为select监听所有文件描述符中的最大值+1。
  • readfds:可读事件对应文件描述符集合。
  • writefds:可写事件对应文件描述符集合。
  • exceptfds:异常事件对应文件描述符集合。
  • timeout:设置select超时时间。

重要结构体详解

readfds、writefds、exceptfds都是fd_set结构体,timeout是timeval结构体,这里详解一下这两个结构体。

1、fd_set

fd_set结构体定义比较复杂,涉及到位操作,比较复杂。所以通常用宏来访问fd_set中的位。

#include FD_ZERO(fd_set* fdset);    // 清除fdset中的所有位FD_SET(int fd, fd_set* fdset); // 设置fdset中的位FD_CLR(int fd, fd_set* fdset); // 清除fdset中的位int FD_ISSET(int fd, fd_set* fdset);  // 测试fdset的位fd是否被设置
  • FD_ZERO用来清空文件描述符组。每次调用select前都需要清空一次。
  • FD_SET添加一个文件描述符到组中,FD_CLR对应将一个文件描述符移出组中。
  • FD_ISSET检测一个文件描述符是否在组中,我们用这个来检测一次select调用之后有哪些文件描述符可以进行IO操作。

2、timeval

struct timeval {    long tv_sec; // 秒数    long tv_usec; // 微妙数};

使用流程

综上所述,我们一般的使用流程是:

  1. 准备工作——定义readfds、timeval等
  2. 使用FD_ZERO清零,使用FD_SET设置文件描述符。因为事件发生后,文件描述符集合都将被内核修改。
  3. 调用select
  4. 使用FD_ISSET检测文件描述符是否在组中

代码实例

根据使用流程,给出一个代码示例:

#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; }

后面一讲会给出一些实用的例子,有了select之后我们可以同时监听很多个请求,系统的处理能力大大增强了。

4、poll

和select类似,在一定时间内轮询一定数量的文件描述符。

函数原型

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

但是和select不同的是,select需要用三组文件描述符,poll只有一个pollfd文件数组,数组中的每个元素都表示一个需要监听IO操作事件的文件描述符。而且我们只需要关心数组中events参数,revents由内核自动填充。

重要结构体详解

    struct pollfd {        int fd;    // 文件描述符        short events;    // 注册的事件         short revents;   // 实际发生的事件,由内核填充    };

事件类型

具体的事件类型参看手册: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数组,并设置poll数组相关参数。
  2. 设置超时时间
  3. 调用poll

代码实例

根据使用流程,给出一个代码示例:

#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复用函数,使用一组函数来完成任务,而不是单个函数。

epoll把用户关心的文件描述符上的事件放在内核的一个事件表中,不需要像select、poll那样每次调用都要重复传入文件描述符集或事件集。

epoll需要使用一个额外的文件描述符,来唯一标识内核中的时间表,由epoll_create创建。

函数原型

    #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:创建一个epoll实例,size参数给内核一个提示,标识事件表的大小。函数返回的文件描述符将作用其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
  • epoll_ctl:操作文件描述符。fd表示要操作的文件描述符,op指定操作类型,event指定事件。
  • epoll_wait:在一段超时时间内等待一组文件描述符上的事件。如果监测到事件,就将所有就绪的事件从内核事件表(epfd参数指定)中复制到第二个参数events指向的数组中。因为events数组只用于输出epoll_wait监测到的就绪事件,而不像select、poll那样就用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这样极大提高了应用程序索引就绪文件描述符的效率。

函数返回

特别注意epoll_wait函数成功时返回就绪的文件描述符总数。select和poll返回文件描述符总数。

以寻找已经就绪的文件描述符,举个例子如下:

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

//  pollint ret = poll(fds, MAX_EVENT_NUMBER, -1);// 必须遍历所有已注册的文件描述符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个文件描述符for (int i = 0; i < ret; i++) {    int sockfd = events[i].data.fd;}

LT水平触发模式和ET边沿触发模式

epoll监控多个文件描述符的I/O事件。epoll支持边缘触发(edge trigger,ET)或水平触发(level trigger,LT),通过epoll_wait等待I/O事件,如果当前没有可用的事件则阻塞调用线程。

select和poll只支持LT工作模式,epoll的默认的工作模式是LT模式。

水平触发:

  • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理此事件。这样应用程序下一次调用epoll_wait的时候,epoll_wait还会再次向应用程序通告此事件,直到事件被处理。

边沿触发:

  • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理此事件,后续的epoll_wait调用将不再向应用程序通知这一事件。

所以,边沿触发模式很大程度上降低了同一个epoll事件被重复触发的次数,所以效率更高

代码实例

#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复用函数对比

1. 用户态将文件描述符传入内核的方式

  • select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的fd数量限制,默认是1024。
  • poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。
  • epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。

2. 内核态检测文件描述符读写状态的方式

  • select:采用轮询方式,遍历所有fd,最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。
  • poll:同样采用轮询方式,查询每个fd的状态,如果就绪则在等待队列中加入一项并继续遍历。
  • epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。

3. 找到就绪的文件描述符并传递给用户态的方式

  • select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。
  • poll:将之前传入的fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。
  • epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。

4. 重复监听的处理方式

  • select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。
  • poll:将新的struct pollfd结构体数组拷贝传入内核中,继续以上步骤。
  • epoll:无需重新构建红黑树,直接沿用已存在的即可。

7、经典面试题:epoll更高效的原因?

select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。

select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。

select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中。

select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。

epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符。

虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

写在最后

这一讲偏理论,主要讲了Linux中三种IO复用。后面几讲会在这一讲的基础上,围绕IO写一些有趣的实战demo,敬请期待。

参考

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/118798.html

相关文章

  • 【Webpack 性能优化系列(3) - oneOf】

    摘要:当一个文件要被多个处理,那么一定要指定执行的先后顺序先执行在执行参考 webpack系列文章: 【Webpack 性能优化系列(2) - source-map】【W...

    myshell 评论0 收藏0
  • OpenCV-Python实战(4)——OpenCV常见图像处理技术(❤️万长文,含大量示例❤️)

    OpenCV-Python实战(4)——OpenCV常见图像处理技术(❤️万字长文,含大量示例❤️) 0. 前言1. 拆分与合并通道2. 图像的几何变换2.1 缩放图像2.2 平移图像2.3 旋转图像2.4 图像的仿射变换2.5 图像的透视变换2.6 裁剪图像 3. 图像滤波3.1 应用滤波器(卷积核或简称为核)3.2 图像平滑3.2.1 均值滤波3.2.2 高斯滤波3.2.3 中值滤波3.2...

    Amos 评论0 收藏0
  • 金九银十中,看看这31道Android面试题

    摘要:静态集合类引起内存泄露主要是,等,如果是静态集合这些集合没有及时的话,就会一直持有这些对象。关于合理使用内存,其实就是避免内存泄露中已经说明。参数原生参数元素需要支持机制参考进程线程管理一消息机制的框架这个系类。 阅读目录 1.如何对 Android 应用进行性能分析 2.什么情况下会导致内存泄露 3.如何避免 OOM 异常 4.Android 中如何捕获未捕获的异常 5.ANR 是...

    call_me_R 评论0 收藏0
  • ES6指北【3】——5000长文带你彻底搞懂ES6模块

    摘要:模块什么是模块什么是模块化玩过游戏的朋友应该知道,一把装配完整的步枪,一般是枪身消音器倍镜握把枪托。更重要的是,其它大部分语言都支持模块化。这一点与规范完全不同。模块输出的是值的缓存,不存在动态更新。 1.模块 1.1 什么是模块?什么是模块化? 玩过FPS游戏的朋友应该知道,一把装配完整的M4步枪,一般是枪身+消音器+倍镜+握把+枪托。 如果把M4步枪看成是一个页面的话,那么我们可以...

    ygyooo 评论0 收藏0
  • 从小白程序员一路晋升为大厂高级技术专家我看过哪些书籍?(建议收藏)

    摘要:大家好,我是冰河有句话叫做投资啥都不如投资自己的回报率高。马上就十一国庆假期了,给小伙伴们分享下,从小白程序员到大厂高级技术专家我看过哪些技术类书籍。 大家好,我是...

    sf_wangchong 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<