详解C/C++中的select、poll和epoll

  目录

  1. select

  1.1 概述

  select函数是UNIX和Linux中常用的多路复用IO机制,它允许程序同时监控多个文件描述符(可以是套接字socket,也可以是普通文件)的读、写和异常事件。它使进程能够告诉内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它。这样做的优点是,不需要应用程序自行检测和处理每个客户端连接的状态,可以节省大量的系统资源,提高应用程序的效率。

  1.2 函数详解

  首先,我们需要包含一些必要的头文件以使用select函数和相关的数据结构:

  #include

  #include

  #include

  #include

  接下来是select函数的原型:

  int select(int maxfdpl, fd_set *readset, fd_set *writeset,

  fd_set *exceptset, struct timeval *timeout);

  select函数接收以下参数:

  struct timeval {

  long tv_sec; /* seconds */

  long tv_usec; /* microseconds */

  };

  关于文件描述符的操作,以下四个函数是用于处理fd_set类型数据的:

  使用这些函数,我们可以方便地对文件描述符集合进行操作,以便于使用select函数进行IO操作的复用。

  1.3 例子

  下面给出一个使用select创建一个可以同时处理多个客户端的连接的服务器:

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #include

  #define MAX_CLIENTS 5

  #define BUFFER_SIZE 1024

  int main(int argc , char *argv[])

  {

  int listener, newsockfd, portno, clilen;

  char buffer[BUFFER_SIZE];

  fd_set master; // 主文件描述符列表

  fd_set read_fds; // 用select()的临时文件描述符列表

  struct sockaddr_in serv_addr, cli_addr;

  int FD_MAX; // 最大文件描述符号

  listener = socket(AF_INET, SOCK_STREAM, 0);

  memset(&serv_addr, '0', sizeof(serv_addr));

  portno = 5000;

  serv_addr.sin_family = AF_INET;

  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

  serv_addr.sin_port = htons(portno);

  bind(listener, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

  listen(listener, 10);

  FD_ZERO(&master); // 清除主监听输入端口

  FD_ZERO(&read_fds); // 清除临时集合

  // 添加监听器到主集合

  FD_SET(listener, &master);

  // 追踪最大的文件描述符

  FD_MAX = listener;

  while(1)

  {

  read_fds = master; // 拷贝它

  if(select(FD_MAX+1, &read_fds, NULL, NULL, NULL) == -1)

  {

  perror("Select error!");

  exit(1);

  }

  for(int i = 0; i <= FD_MAX; i++)

  {

  if(FD_ISSET(i, &read_fds))

  {

  if(i == listener)

  {

  // handle new connections

  clilen = sizeof(cli_addr);

  newsockfd = accept(listener, (struct sockaddr*)&cli_addr, &clilen);

  if(newsockfd == -1)

  {

  perror("accept error");

  }

  else

  {

  FD_SET(newsockfd, &master); // 添加到主集合

  if(newsockfd > FD_MAX)

  {

  FD_MAX = newsockfd; // 持续追踪最大的文件描述符

  }

  printf("selectserver: new connection from %s on socket %d

  ",inet_ntoa(cli_addr.sin_addr), newsockfd);

  }

  }

  else

  {

  // 处理来自客户端的数据

  if((recv(i, buffer, sizeof(buffer), 0)) <= 0)

  {

  // got error or connection closed by client

  close(i);

  FD_CLR(i, &master); // 从主集合中移除

  }

  else

  {

  // 我们得到了一些数据!

  for(int j = 0; j <= FD_MAX; j++)

  {

  // 发送数据到所有连接

  if(FD_ISSET(j, &master))

  {

  if(j != listener && j != i)

  {

  send(j, buffer, strlen(buffer), 0);

  }

  }

  }

  }

  }

  }

  }

  }

  return 0;

  }

  上面这个例子会创建一个在端口5000监听的服务器。使用select,我们可以在单个线程中同时处理多个客户端的连接。这就是使用select的优点:我们可以同时处理多个连接,而不需要为每个连接创建一个单独的线程或进程。

  1.4 总结

  2. poll

  1.1 概述

  poll函数提供了类似于select的功能,允许进程向内核指示等待多个事件中的任何一个发生,它只在有一个或多个事件发生或经历一段指定时间后才唤醒进程。不过,与select相比,poll在处理流设备时能够提供更丰富的信息。它能有效地管理多个输入/输出源,并且在特定事件发生时进行响应,这使得对多任务并发处理的支持更为高效。

  1.2 函数详解

  poll函数的声明如下:

  #include

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

  这里是poll函数的参数列表:

  结构体的定义如下:

  struct pollfd {

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

  short events; /* 要监控的事件 */

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

  };

  当调用poll函数时,内核会检查每个结构体中列出的文件描述符,看看是否有任何指定的事件发生。如果有,内核将会在字段中设置相应的位,以指示哪些事件已经发生。然后poll函数返回,应用程序可以检查每个结构体的字段来确定每个文件描述符上发生了哪些事件。

  1.3 例子

  在下面这个示例中,我们会创建两个管道(pipe),然后使用 poll 来等待这两个管道中的任何一个变得可读。

  #include

  #include

  #include

  #define TIMEOUT 5

  int main (void)

  {

  struct pollfd fds[2];

  int ret;

  // 创建两个管道

  int pipefd[2];

  pipe(pipefd);

  // watch stdin (fd 0) for input

  fds[0].fd = 0;

  fds[0].events = POLLIN;

  // watch pipe for input

  fds[1].fd = pipefd[0];

  fds[1].events = POLLIN;

  ret = poll(fds, 2, TIMEOUT * 1000);

  if (ret == -1) {

  perror ("poll");

  return 1;

  }

  if (!ret) {

  printf ("%d seconds elapsed.

  ", TIMEOUT);

  return 0;

  }

  if (fds[0].revents & POLLIN)

  printf ("stdin is readable

  ");

  if (fds[1].revents & POLLIN)

  printf ("pipe is readable

  ");

  return 0;

  }

  在上述示例中,我们使用 poll 来同时监听标准输入和管道的输入。如果在5秒钟内,标准输入或管道有任何数据可读,那么 就会返回,并通过检查 标志来通知我们哪一个文件描述符已经就绪。如果在5秒内没有任何数据可读,那么 poll 也会返回,此时我们可以打印一个超时信息。这就是使用 poll 的优点:我们可以同时处理多个输入源,而不需要为每个输入源创建一个单独的线程或进程。

  1.4 总结

  3. epoll

  1.1 概述

  在许多并发连接中只有少数活路的场景下,epoll是Linux下I/O多路复用接口select/poll的增强版本,能有效提升系统CPU的使用率。区别于select和poll每次等待事件之前都需要重新设置监视的文件描述符集,epoll能复用文件描述符集来传递结果,减少了重复的准备工作。

  获取事件时,epoll无需像select和poll一样遍历整个被侦听的描述符集,只需遍历被内核IO事件异步唤醒并加入到就绪队列的描述符集即可。这使得处理大量文件描述符时,只有实际产生活动的文件描述符才需要被处理,从而大大提升了效率。

  当前,在大规模并发网络程序中,epoll已经成为首选模型。除了提供select/poll的IO事件电平触发(Level Triggered)模式,epoll还额外提供了边沿触发(Edge Triggered)模式,这使得用户空间程序可以缓存IO状态,减少epoll_wait/epoll_pwait的调用,从而进一步提升了程序的运行效率。

  1.2 函数详解

  是 Linux 中的 I/O 多路复用接口,常用的API有 、 和 。以下是这些API的详细介绍:

  :创建一个epoll的句柄。

  #include

  int epoll_create(int size);

  参数:参数现在并不起作用,但是必须大于0。

  返回值:如果成功,返回一个非负的文件描述符。失败时,返回-1。

  :控制某个epoll文件描述符上的事件,可以注册、修改、删除。

  #include

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

  参数:

  ​​​​​​struct epoll_event {

  uint32_t events; /* Epoll events */

  epoll_data_t data; /* User data variable */

  };

  ​

  返回值:成功时,返回0。失败时,返回-1。

  :等待epoll上的I/O事件。

  #include

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

  参数:

  返回值:成功时,返回需要处理的事件数目。如返回0表示已经超时。失败时,返回-1。

  1.3 例子

  以下是一个使用epoll的示例,其中包含了、和等函数的使用,主要展示了epoll的IO多路复用和边缘触发(ET)模式特性:

  #include

  #include

  #include

  #include

  #include

  #include

  #define MAX_EVENTS 10

  void set_nonblock(int fd) {

  int flags = fcntl(fd, F_GETFL, 0);

  flags |= O_NONBLOCK;

  fcntl(fd, F_SETFL, flags);

  }

  int main() {

  struct epoll_event ev, events[MAX_EVENTS];

  int listen_sock, conn_sock, nfds, epollfd;

  /* Code to set up listening socket, 'listen_sock',

  (socket(), bind(), listen()) omitted */

  epollfd = epoll_create1(0);

  if (epollfd == -1) {

  perror("epoll_create1");

  exit(EXIT_FAILURE);

  }

  ev.events = EPOLLIN | EPOLLET;

  ev.data.fd = listen_sock;

  if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {

  perror("epoll_ctl: listen_sock");

  exit(EXIT_FAILURE);

  }

  for (;;) {

  nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);

  if (nfds == -1) {

  perror("epoll_wait");

  exit(EXIT_FAILURE);

  }

  for (int n = 0; n < nfds; ++n) {

  if (events[n].data.fd == listen_sock) {

  conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen);

  if (conn_sock == -1) {

  perror("accept");

  exit(EXIT_FAILURE);

  }

  set_nonblock(conn_sock);

  ev.events = EPOLLIN | EPOLLET;

  ev.data.fd = conn_sock;

  if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {

  perror("epoll_ctl: conn_sock");

  exit(EXIT_FAILURE);

  }

  } else {

  do_use_fd(events[n].data.fd);

  }

  }

  }

  close(epollfd);

  return 0;

  }

  在上述代码中,首先创建了一个epoll对象,然后将监听套接字添加到epoll事件集合中,并注册了和事件,代表对应的文件描述符可读,代表以边缘触发模式对事件进行处理。

  在无限循环中,调用来等待I/O事件的发生,当新的连接进来时,使用接受新的连接,然后将新的连接设为非阻塞模式,并添加到epoll事件集合中。当连接上有数据可读时,调用函数进行处理。

  此代码展示了epoll可以动态地添加、修改和删除关注的文件描述符,也展示了边缘触发模式的使用,这些都是epoll的主要特点。

  1.4 总结

  1.优点:

  2.缺点:

  3.注意事项:

  4. 三者的区别

  选择方式selectpollepoll操作方式遍历遍历回调底层实现数组链表红黑树IO效率每次调用都进行线性遍历,时间复杂度为O(n)每次调用都进行线性遍历,时间复杂度为O(n)事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)最大连接数1024(x86)或2048(x64)无上限无上限fd拷贝每次调用select,都需要把fd集合从用户态拷贝到内核态每次调用poll,都需要把fd集合从用户态拷贝到内核态调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

  到此这篇关于C/C++中的select、poll和epoll的文章就介绍到这了,更多相关C++ select、poll和epoll内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

  您可能感兴趣的文章: