1. 什么是IO多路复用

一句话解释:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力。

2. 解决什么问题

说在前头

应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标的输入、中断信号等等事件,再比如web服务器如nginx,需要同时处理来来自N个客户端的事件。

逻辑控制流在时间上的重叠叫做 并发

而CPU单核在同一时刻只能做一件事情,一种解决办法是对CPU进行时分复用(多个事件流将CPU切割成多个时间片,不同事件流的时间片交替进行)。在计算机系统中,我们用线程或者进程来表示一条执行流,通过不同的线程或进程在操作系统内部的调度,来做到对CPU处理的时分复用。这样多个事件流就可以并发进行,不需要一个等待另一个太久,在用户看起来他们似乎就是并行在做一样。

但凡事都是有成本的。线程/进程也一样,有这么几个方面:

  1. 线程/进程创建成本
  2. CPU切换不同线程/进程成本 Context Switch
  3. 多线程的资源竞争

有没有一种可以在单线程/进程中处理多个事件流的方法呢?一种答案就是IO多路复用。

因此IO多路复用解决的本质问题是在用更少的资源完成更多的事

为了更全面的理解,先介绍下在Linux系统下所有IO模型。

I/O模型

目前Linux系统中提供了5种IO处理模型

  1. 阻塞IO
  2. 非阻塞IO
  3. IO多路复用
  4. 信号驱动IO
  5. 异步IO

阻塞IO

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

非阻塞IO

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

IO多路复用

IO多路复用在Linux下包括了三种,select、poll、epoll,抽象来看,他们功能是类似的,但具体细节各有不同:首先都会对一组文件描述符进行相关事件的注册,然后阻塞等待某些事件的发生或等待超时。

信号驱动IO

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

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

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

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

异步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是完全完成了数据的拷贝之后才通知程序进行处理,没有阻塞的数据读写过程。

概念解释

同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

3. 目前有哪些IO多路复用的方案

解决方案总览

Linux: select、poll、epoll

MacOS/FreeBSD: kqueue

Windows/Solaris: IOCP

4. 具体怎么用

我在工作中接触的都是Linux系统的服务器,所以在这里只介绍Linux系统的解决方案

select

相关函数定义如下

/* According to POSIX.1-2001, POSIX.1-2008 */#include <sys/select.h>/* According to earlier standards */#include <sys/time.h>#include <sys/types.h>#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);void FD_CLR(int fd, fd_set *set);int  FD_ISSET(int fd, fd_set *set);void FD_SET(int fd, fd_set *set);void FD_ZERO(fd_set *set);

select的调用会阻塞到有文件描述符可以进行IO操作或被信号打断或者超时才会返回。

select将监听的文件描述符分为三组,每一组监听不同的需要进行的IO操作。readfds是需要进行读操作的文件描述符,writefds是需要进行写操作的文件描述符,exceptfds是需要进行异常事件处理的文件描述符。这三个参数可以用NULL来表示对应的事件不需要监听。

当select返回时,每组文件描述符会被select过滤,只留下可以进行对应IO操作的文件描述符。

FD_xx系列的函数是用来操作文件描述符组和文件描述符的关系。

FD_ZERO用来清空文件描述符组。每次调用select前都需要清空一次。

fd_set writefds;
FD_ZERO(&writefds)

FD_SET添加一个文件描述符到组中,FD_CLR对应将一个文件描述符移出组中

FD_SET(fd, &writefds);
FD_CLR(fd, &writefds);

FD_ISSET检测一个文件描述符是否在组中,我们用这个来检测一次select调用之后有哪些文件描述符可以进行IO操作

if (FD_ISSET(fd, &readfds)){/* fd可读 */
}

select基本用法:

创建
      fd_set rset , allset;
      FD_ZERO(&allset);
      FD_SET(listenfd, &allset);
  监听
      /*只select出用于读的描述字,阻塞无timeout*/
      nready = select(maxfd+1 , &rset , NULL , NULL , NULL);
  获取
      if(FD_ISSET(listenfd,&rset))

select限制

select可同时监听的文件描述符数量是通过FS_SETSIZE来限制的,在Linux系统中,该值为1024,当然我们可以增大这个值,但随着监听的文件描述符数量增加,select的效率会降低,我们会在『不同IO多路复用方案优缺点』一节中展开。

pselect和select大体上是一样的,但有一些细节上的区别。

打开链接查看完整的使用select的例子

poll

相关函数定义

    #include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);#include <signal.h>#include <poll.h>int ppoll(struct pollfd *fds, nfds_t nfds,const struct timespec *tmo_p, const sigset_t *sigmask);struct pollfd {int fd; /* file descriptor */short events; /* requested events to watch */short revents; /* returned events witnessed */};

和select用三组文件描述符不同的是,poll只有一个pollfd数组,数组中的每个元素都表示一个需要监听IO操作事件的文件描述符。events参数是我们需要关心的事件,revents是所有内核监测到的事件。合法的事件可以参考这里。

poll基本用法:

  创建
      struct pollfd client[OPEN_MAX];
      client[0].fd = listenfd;
      client[0].events = POLLRDNORM;
      for(i=1;i<OPEN_MAX;i++)
      {
        client[i].fd = -1;
      }
      maxi = 0;
  监听
       nready = poll(client,maxi+1,INFTIM);
  获取
      sockfd = client[i].fd;
      if(client[i].revents & (POLLRDNORM|POLLERR))

打开链接查看完整的使用poll的例子

epoll

相关函数定义如下

    #include <sys/epoll.h>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_create1用于创建一个epoll实例,而epoll_ctl用于往epoll实例中增删改要监测的文件描述符,epoll_wait则用于阻塞的等待可以执行IO操作的文件描述符直到超时。

epoll基本用法:

  创建
      int epfd;
      struct epoll_event ev, events[20];
      epfd = epoll_create(256);
      ev.data.fd=listenfd;
      ev.events=EPOLLIN|EPOLLET;
      epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
  监听
      nfds=epoll_wait(epfd,events,20,500);
  获取
      if (events[i].events & EPOLLIN)

打开链接查看完整的使用epoll的例子

level-triggered and edge-triggered

这两种底层的事件通知机制通常被称为水平触发和边沿触发,真是翻译的词不达意,如果我来翻译,我会翻译成:状态持续通知和状态变化通知。

这两个概念来自电路,triggered代表电路激活,也就是有事件通知给程序,level-triggered表示只要有IO操作可以进行比如某个文件描述符有数据可读,每次调用epoll_wait都会返回以通知程序可以进行IO操作,edge-triggered表示只有在文件描述符状态发生变化时,调用epoll_wait才会返回,如果第一次没有全部读完该文件描述符的数据而且没有新数据写入,再次调用epoll_wait都不会有通知给到程序,因为文件描述符的状态没有变化。

select和poll都是状态持续通知的机制,且不可改变,只要文件描述符中有IO操作可以进行,那么select和poll都会返回以通知程序。而epoll两种通知机制可选。

状态变化通知(edge-triggered)模式下的epoll

在epoll状态变化通知机制下,有一些的特殊的地方需要注意。考虑下面这个例子

  1. 服务端文件描述符rfd代表要执行read操作的TCP socket,rfd已被注册到一个epoll实例中
  2. 客户端向rfd写了2kb数据
  3. 服务端调用epoll_wait返回,rfd可执行read操作
  4. 服务端从rfd中读取了1kb数据
  5. 服务端又调用了一次epoll_wait

在第5步的epoll_wait调用不会返回,而对应的客户端会因为服务端没有返回对应的response而超时重试,原因就是我上面所说的,epoll_wait只会在状态变化时才会通知程序进行处理。第3步epoll_wait会返回,是因为客户端写了数据,导致rfd状态被改变了,第3步的epoll_wait已经消费了这个事件,所以第5步的epoll_wait不会返回。

我们需要配合非阻塞IO来解决上面的问题:

  1. 对需要监听的文件描述符加上非阻塞IO标识
  2. 只在read或者write返回EAGAIN或EWOULDBLOCK错误时,才调用epoll_wait等待下次状态改变发生

通过上述方式,我们可以确保每次epoll_wait返回之后,我们的文件描述符中没有读到一半或写到一半的数据。

5. 不同IO多路复用方案优缺点

poll vs select

poll和select基本上是一样的,poll相比select好在如下几点:

  1. poll传参对用户更友好。比如不需要和select一样计算很多奇怪的参数比如nfds(值最大的文件描述符+1),再比如不需要分开三组传入参数。
  2. poll会比select性能稍好些,因为select是每个bit位都检测,假设有个值为1000的文件描述符,select会从第一位开始检测一直到第1000个bit位。但poll检测的是一个数组。
  3. select的时间参数在返回的时候各个系统的处理方式不统一,如果希望程序可移植性更好,需要每次调用select都初始化时间参数。

而select比poll好在下面几点

  1. 支持select的系统更多,兼容更强大,有一些unix系统不支持poll
  2. select提供精度更高(到microsecond)的超时时间,而poll只提供到毫秒的精度。

但总体而言 select和poll基本一致。

epoll vs poll&select

epoll优于select&poll在下面几点:

  1. 在需要同时监听的文件描述符数量增加时,select&poll是O(N)的复杂度,epoll是O(1),在N很小的情况下,差距不会特别大,但如果N很大的前提下,一次O(N)的循环可要比O(1)慢很多,所以高性能的网络服务器都会选择epoll进行IO多路复用。
  2. epoll内部用一个文件描述符挂载需要监听的文件描述符,这个epoll的文件描述符可以在多个线程/进程共享,所以epoll的使用场景要比select&poll要多。

参考文章

select、poll、epoll程序实例 - 步孤天 - 博客园 (cnblogs.com)

(2条消息) socket编程以及select、epoll、poll示例详解_Hyacinth_Dy-CSDN博客

(2条消息) 深入理解select、poll和epoll及区别_$好记性还是要多记录$-CSDN博客

https://zhuanlan.zhihu.com/p/115220699

c++ IO多路复用相关推荐

  1. 漫谈五种IO模型(主讲IO多路复用)

    首先引用levin的回答让我们理清楚五种IO模型 1.阻塞I/O模型 老李去火车站买票,排队三天买到一张退票. 耗费:在车站吃喝拉撒睡 3天,其他事一件没干. 2.非阻塞I/O模型 老李去火车站买票, ...

  2. Python:通过一个小案例深入理解IO多路复用

    通过一个小案例深入理解IO多路复用 假如我们现在有这样一个普通的需求,写一个简单的爬虫来爬取校花网的主页 import requests import timestart = time.time()u ...

  3. 聊一个不常见的面试题:为什么数据库连接池不采用 IO 多路复用?

    欢迎关注方志朋的博客,回复"666"获面试宝典 今天我们聊一个不常见的 Java 面试题:为什么数据库连接池不采用 IO 多路复用? 这是一个非常好的问题.IO多路复用被视为是非常 ...

  4. 为什么数据库连接池不采用 IO 多路复用?

    欢迎关注方志朋的博客,回复"666"获面试宝典 接着,今天我们聊一个不常见的 Java 面试题:为什么数据库连接池不采用 IO 多路复用? 这是一个非常好的问题.IO多路复用被视为 ...

  5. Linux IO多路复用之Select简史

    内容目录 前言早期的UnixTCP/IP诞生后终端复用套接字章节回顾结论引用 前言 最近我一直在思考 Linux 中的多路复用,即 epoll(7)[1]系统调用.我很好奇 epoll与Windows ...

  6. 为什么数据库连接池不采用IO多路复用?

    文章来源:https://sourl.cn/q8fbw3 今天我们聊一个不常见的 Java 面试题:为什么数据库连接池不采用 IO 多路复用? 这是一个非常好的问题.IO多路复用被视为是非常好的性能助 ...

  7. 【NIO】IO多路复用

    上节,我们讲解了阻塞和非阻塞,我们今天讲第三种IO模型,这就是IO多路复用. 引入多路复用的原因 实际上,在服务端与客户端一对一通信的时候,同步阻塞式地读写并不会有太大的问题,最典型的就是两个对等机器 ...

  8. IO模式和IO多路复用

    前言 前天看redis相关的博文里面提到了epoll,就搜了一下,发现这篇文章 Linux IO模式及 select.poll.epoll详解,讲的很好,收获很大.这里根据自己的理解总结一下. IO模 ...

  9. IO多路复用之poll

    1.基本知识 poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制.poll和selec ...

  10. 理解操作系统IO多路复用

    在讲解IO多路复用之前,我们需要预习一下文件以及文件描述符. 什么是文件 程序员使用I/O最终都逃不过文件. 因为这篇同属于高性能.高并发系列,讲到高性能.高并发就离不开Linux/Unix,因此这里 ...

最新文章

  1. cocos2d-x学习笔记 动作 CCCallFunc家族(回调函数包装器)
  2. LeetCode-剑指 Offer 03. 数组中重复的数字
  3. 坦克大战c语言程序贴吧,坦克大战!
  4. C# webrequest 抓取数据时,多个域Cookie的问题
  5. 竞价推广账户创意撰写的技巧之核心思路
  6. 荒野行动系统推荐观战榜_荒野行动 观战延迟投票结果公示 更新计划抢先看!...
  7. 评分卡建模工具scorecardpy全解读
  8. 手把手教,使用VMware虚拟机安装Windows XP系统,爷青回
  9. Winedt为什么可以用pdfLaTex编译中文(pdfLaTex和XeLaTex的使用)
  10. Github创建的个人简历
  11. 宁夏政务网 紫图高拍仪控件和文件上传控件的若干问题及解决方法
  12. D - Plane 航空管制2 HYSBZ - 2535
  13. php 高洛峰 正则,PHP 自定义 Smarty 模板引擎类 高洛峰 细说PHP
  14. 天地伟业客户端服务器维护,天地伟业监控维保常见问题总结
  15. js原生写图片轮播和切换
  16. mybatis源码解析(一)
  17. AWS DynamoDB 常用操作
  18. APP同过ESP8266与51单片机通信
  19. 牵手家居品牌翘楚,瑞泰信息助力罗浮宫数字化营销管理能力持续升级
  20. 加密狗软加密方案离线绑定与解绑

热门文章

  1. 一般硬盘读取速度和写入速度是多少
  2. zerotier异地搭建组网
  3. 没有人会疼自己没人会懂,会理解:伤感空间日志
  4. 计算机怎样保存文件格式,word文档怎样保存为pdf格式
  5. 放弃理想,未必能成就现实
  6. matlab中lab颜色空间,使用Matlab绘制图像的rgb颜色空间和Lab颜色空间分量图和分量直方图...
  7. git无法push大文件:this exceeds GitHub‘s file size limit of 100.00 MB
  8. 蓝桥杯——瓷砖样式(第八届决赛)
  9. 约瑟夫生者死者游戏:有N个旅客同乘一条船,因为严重超载,加上风高浪大,危险万分;因此船长告诉乘客,只有将全船一半的旅客投入海中,其余人才能幸免于难;无奈,大家只得同意这种办法,并议定N个人围成一圈,由
  10. Fiddler抓取HTTPs流量