IO多路复用select/poll/epoll

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、io多路复用
  • 二、select/poll/epoll
    • 1.select(八几年的老东西了)
    • 2. poll
    • 注意问题
    • 3.epoll
    • 问题注意点
    • 缺点
    • 选择
  • libevent
    • 官网 概述
  • Redis I/O 多路复用模块
    • 子模块的选择
  • 仓库文章


前言

Linux(实际上是 Unix)的一个基本概念是 Unix/Linux 中的一切都是文件的规则。每个进程都有一个指向文件、套接字、设备和其他操作系统对象的文件描述符表。
与许多 IO 源一起工作的典型系统有一个初始化阶段,然后进入某种待机模式——等待任何客户端发送请求并响应它

我查看

一、io多路复用

使用场景:一个高性能的网络服务器,可以让多个客户端同时使用 (bs)并且可以获取这些客户端的请求 并处理

怎么处理这些请求的 高并发状况

  • 如果使用多线程 可以做 但是 多线程的使用需要上下文的切换(需要处理一些句柄) 客户端比较多的时候 代价是非常高的

  • 使用单线程的时候使用 多个客户端的请求,会通过DMA 专门处理iO, 保证了数据的不丢失

二、select/poll/epoll

以 传入五个io来举例子
select/poll/epoll
都服务于相同的想法,创建一组文件描述符,告诉内核你想对每个文件描述符做什么(读,写,…),并使用一个线程阻塞一个函数调用,直到至少一个文件描述符请求的操作可用

1.select(八几年的老东西了)

系统调用提供了一种实现同步多路复用 I/O 的机制
要使用select,开发人员需要使用描述符和要监视的事件来初始化和填充几个fd_set结构,然后调用select ()。IO多路复用select/poll/epoll_第1张图片

  • 我们需要在每次调用之前构建一个集合(还需要遍历集合中的所有描述符 select在linux上默认只支持 1024个)
  • 函数的时间复杂过高 O(n)
  • FD_SET 通过传递修改,因此不能被重用 ,需要在重新创建整个集合 或者通过 FD_COPY 从备份中恢复 ,每次select都需要重新做 我理解的是这个时候select 浪费了很大的资源 而 多路复用就一定程度的解决了这个问题(当时的人没有想到 会有这么多的线程应用程序为数千个连接提供服务)

2. poll

IO多路复用select/poll/epoll_第2张图片

  • poll可以监控的描述符数量没有硬性限制,(使用的是一个数组),因此 1024 的限制在这里不适用。
  • 它不会修改struct pollfd数据中传递的数据。因此,只要将生成事件的描述符的revents成员设置为零,就可以在 poll() 调用之间重用它。
  • 与select相比,它允许对事件进行更细粒度的控制。例如,它可以在不监视读取事件的情况下检测远程对等关闭。

注意问题

  • 象select一样,还需要遍历整个列表去检查 revents
  • 更糟糕的是,同样的情况也发生在内核空间中,因为内核必须遍历文件描述符列表以找出哪些套接字被监视,并再次遍历整个列表以设置事件。
  • 与select一样,无法动态修改 set 或关闭正在轮询的套接字
  • 如果以下情况属实,则poll应该是您的首选方法,甚至优于epoll :
    您需要支持的不仅仅是 Linux,并且不想使用 epoll 包装器,例如 libevent(epoll 仅适用于 Linux);
    您的应用程序一次需要监控少于 1000 个套接字(您不太可能看到使用 epoll 的任何好处);
    您的应用程序需要一次监视超过 1000 个套接字,但连接的寿命很短(这是一个很接近的情况,但在这种情况下,您很可能看不到使用 epoll 的任何好处,因为事件中的加速等待将浪费在将这些新描述符添加到集合中 - 见下文)
    您的应用程序的设计方式不是在另一个线程等待它们时更改事件的方式(即您没有使用 kqueue 或 IO 完成端口移植应用程序)。

3.epoll

epoll 是 Linux(并且只有 Linux)中最新、最好、最新的轮询方法。嗯,它实际上是在 2002 年添加到内核中的,所以它并不是那么新。它与poll和select的不同之处在于它将有关当前监视的描述符和相关事件的信息保存在内核中,并导出 API 以添加/删除/修改这些。

  struct epoll_event events[5];
  int epfd = epoll_create(10); 注意这个
  ...
  ...
  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); 到这了  
	
	for(i=0;i<nfds;i++) {
			memset(buffer,0,MAXBUF);
			read(events[i].data.fd, buffer, MAXBUF);
			puts(buffer);
	}
  }

问题注意点

  • 它使用起来更复杂,需要你编写更多的代码,与其他轮询方法相比,它需要更多的库调用。
  • epoll仅返回触发事件的描述符列表。不再需要遍历 10,000 个描述符来找到触发事件的描述符!
  • 您可以随时添加套接字或将其从监视中删除,即使epoll_wait函数中有另一个线程。您甚至可以修改描述符事件。一切都会正常工作,并且这种行为得到支持和记录。这为您提供了更大的实施灵活性。
  • 使用epoll_wait ()可以让多个线程在同一个 epoll 队列上等待,这是select/poll无法做到的。实际上不仅可以使用epoll,而是边缘触发模式下的推荐方法。

缺点

  • 更改事件标志(即从 READ 到 WRITE)需要epoll_ctl系统调用,而当使用poll时,这是一个完全在用户空间中完成的简单位掩码操作。使用epoll将5,000 个套接字从读取切换到写入将需要 5,000 次系统调用,因此上下文切换(
  • 截至 2014 年对epoll_ctl的调用仍然无法批处理,并且必须单独更改每个描述符

),而在轮询中则需要一个循环pollfd结构。

  • epoll是专门的 Linux 领域,虽然其他平台也有类似的机制,但它们并不完全相同——例如,边缘触发非常独特(不过 FreeBSD 的 kqueue 也支持它)。
  • 高性能处理逻辑更复杂,因此更难调试,特别是对于边缘触发,如果你错过额外的读/写,很容易出现死锁。

选择

  • 您的应用程序运行一个线程轮询,它通过少数线程处理许多网络连接。在单线程应用程序中,您将失去epoll的大部分优势,而且很可能它不会胜过poll。 (单线程用poll)
  • 您希望有相当多的套接字来监视(至少 1,000 个);数量较少的 epoll 不太可能比 poll 有任何性能优势,实际上可能会降低性能;

libevent

官网
概述

Libevent 是一个用C语言编写的、轻量级的开源高性能事件通知库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大;源代码相当精炼、易读;跨平台,支持 Windows、 Linux、 *BSD 和 Mac Os;支持多种 I/O 多路复用技术, epoll、 poll、 dev/poll、 select 和 kqueue 等;支持 I/O,定时器和信号等事件;注册事件优先级。

libebent是一个库,它将本文(以及其他一些)中列出的轮询方法包装在一个统一的 API 中。它的主要优点是它允许您编写一次代码并在许多操作系统上编译和运行它而无需更改代码。重要的是要理解 libevent 它只是构建在现有轮询方法之上的一个包装器,因此它继承了那些轮询方法所具有的问题。它不会在 Linux 上使select支持超过 1024 个套接字,也不会允许epoll在没有系统调用/上下文切换的情况下修改轮询事件。因此,了解每种方法的优缺点仍然很重要。

Redis I/O 多路复用模块

I/O 多路复用模块封装了底层的 select、epoll、avport 以及 kqueue 这些 I/O 多路复用函数,为上层提供了相同的接口。
IO多路复用select/poll/epoll_第3张图片

封装的 select 函数

int fd = /* file descriptor */

fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds)

for ( ; ; ) {
    select(fd+1, &rfds, NULL, NULL, NULL);
    if (FD_ISSET(fd, &rfds)) {
        /* file descriptor `fd` becomes readable */
    }
}

初始化一个可读的 fd_set 集合,保存需要监控可读性的 FD;
使用 FD_SET 将 fd 加入 rfds;
调用 select 方法监控 rfds 中的 FD 是否可读;
当 select 返回时,检查 FD 的状态并完成对应的操作。

子模块的选择

为了最大化执行的效率与性能,所以会根据编译平台的不同选择不同的 I/O 多路复用函数作为子模块,提供给上层统一的接口;在 Redis 中,我们通过宏定义的使用,合理的选择不同的子模块:

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

Redis 会优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的这些函数都使用了内核内部的结构,并且能够服务几十万的文件描述符。

但是如果当前编译环境没有上述函数,就会选择 select 作为备选方案,由于其在使用时会扫描全部监听的描述符,所以其时间复杂度较差 O(n),并且只能同时服务 1024 个文件描述符,所以一般并不会以 select 作为第一方案使用。

仓库文章

作者 free-coder 【并发】IO多路复用select/poll/epoll介绍

作者 不知道名字 LINUX – IO 多路复用 – SELECT VS POLL VS EPOLL

作者 待参考第 6 章 I/O 多路复用:函数select和poll函数

作者:乔治

作者: Redis 和 I/O 多路复用

你可能感兴趣的:(IO,Redis,linux,linux,服务器,redis,IO)