草庐IT

Redis的IO多路复用以及Select,Epoll的演进

少年阿斌 2023-03-28 原文
什么是阻塞,非阻塞,异步同步,select,poll,epoll?今天我们用一遍文章解开这多年的迷惑。

首先我们想要通过网络接收消息,是这样的一个步骤。

  1. 用户空间向内核空间请求网络数据
  2. 内核空间把网卡数据读取到内核缓冲区
  3. 将内核缓冲区的数据复制到用户缓冲区
根据我们请求数据的情况不同,以及内核缓冲区到用户缓冲区的不同,分为了阻塞,非阻塞,异步同步的区别。

在《UNIX网络编程》一书中,总结归纳了5种I0模型:

  • 阻塞 I0 ( Blocking I0)
  • 非阻塞 I0 (Nonblocking  I0)
  • I0多路复用(I0 Multiplexing)
  • 信号驱动I0 (Signal Driven  I0 )
  • 异步I0 (Asynchronous  I0)

阻塞IO

  1. 用户应用请求内核是否有新的网络数据
  2. 如果没有数据,就阻塞直到有数据到来
  3. 等待内核将数据拷贝到用户空间
  4. 用户应用处理数据
以上可以看出来,根据等待数据的方式不同,分为阻塞和非阻塞。

阻塞IO在请求内核数据的时候,没有数据就会一直阻塞直到获取数据。

非阻塞IO

  1. 用户应用请求内核是否有新的网络数据
  2. 如果没有数据,内核直接返回没数据,用户应用可以隔一段时间再来请求。
  3. 等待内核将数据拷贝到用户空间
  4. 用户应用处理数据
非阻塞IO在等待内核数据的时候,没有数据就会得到没数据的结果,应用可以进行其他动作。

同步IO

同步IO的主要看内核数据到用户空间的过程是同步进行的就是同步IO

异步IO

异步IO首先是非阻塞IO,区别在于成功标志的时机。异步IO连内核到用户态的数据拷贝都是异步的,直到数据拷贝完成,才会回调一个信号,通知一切已经准备完成。用户应用此时就可以直接处理结果了。

总结

阻塞非阻塞指的是在获取结果上是否会阻塞等待结果完成

同步异步指的是是否会参与IO读写,或者是等待读写成功的回调

redis的IO多路复用

如果是阻塞IO也就是BIO,那么在一个fd(文件描述符)没有数据的时候,就是阻塞一直等待,如果同时有多个fd,对于单线程来说,只能一直等第一个有数据,然后再接着处理第二个,效率很慢。

就像顾客点餐,要一直等到第一个人点完餐,后面的人才有机会。BIO也有个解决办法,一般是增加多线程,每个线程都维护一个fd,就相当于为每个顾客都添加一个点餐台。在fd足够多的情况下,会有大量的线程被创建,线程可是有上限的,开销也大(更多线程需要更多的内存空间)。

如果是非阻塞IO也就是NIO,会有顾客没点完餐,然后造成CPU一直在询问一直空转的情况。

因此引入了IO多路复用模型:利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源

文件描述符( File Descriptor) :简称FD,是一个从0开始递增的无符号整数,用来关联Linux中的一一个文件。在Linux

中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket),

这时候每来一个顾客(FD​),我们就会给他一个开关(注册进监听事件​),一个服务员(一个线程​)等待开关亮起(阻塞等待事件​)。有顾客完成,就会按下开关,一定的频率下开关会亮起(事件通知​),服务员会选取按下开关的一批人,给他们点餐(批量处理事件)。

IO多路复用的实现有select,poll,epoll,我们来看看他们的优缺点。

select

select是Linux中最早的I/O多路复用实现方案,并且windows操作系统上只支持select。这就是为啥window发挥不出redis的最大性能的一个原因。

select函数执行流程

  1. 用户空间创建fd_set,把需要监听的位置置1,比如 1,2,5
  2. 用户空间拷贝fd_set(注册的事件集合)到内核空间
  3. 内核遍历所有fd文件,并将当前进程挂到每个fd的等待队列中,当某个fd文件设备收到消息后,会唤醒设备等待队列上睡眠的进程,那么当前进程就会被唤醒
  4. 内核如果遍历完所有的fd没有I/O事件,则当前进程进入睡眠,当有某个fd文件有I/O事件或当前进程睡眠超时后,当前进程重新唤醒再次遍历所有fd文件
  5. 内核有事件产生,会把fd_set中有事件的位置保留为1,没有事件的位置擦除为0.
  6. 内核拷贝fd_set给用户空间
  7. 用户空间线程被唤醒,遍历fd_set为1的位置,确认是哪些fd有就绪事件,然后开始处理
  8. 用户空间处理完事件,再一次将要监听的fd_set设置为1,重复之前的监听动作
根据上面可以很清楚的看出整个执行流程在用户空间和内核空间的切换。

select函数的缺点

  • 单个进程所打开的FD是有限制的,通过 FD_SETSIZE 设置,默认1024
  • 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
  • 每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除
  • select函数在每次调用之前都要对参数进行重新设定,这样做比较麻烦,而且会降低性能
  • 进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次

poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的

poll运行流程

①创建pollfd数组, 向其中添加关注的fd信息,数组大小自定义

②调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限

③内核遍历fd,判断是否就绪

④数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n

⑤用户进程判断n是否大于0

⑥大于0则遍历pollfd数组,找到就绪的fd

与select对比

  • select模式中的fd_ set大小固定为1024,而pollfd在内核中采用链表,理论上无上限.
  • 监听FD越多,每次遍历消耗时间也越久,性能反而会下降
poll还是没有解决需要遍历判断fd事件的方式,只是增加了监听数量,在fd很多的情况下,性能下降的更加严重

epoll

epoll可以理解为event pool,不同与select、poll的轮询机制,epoll采用的是事件驱动机制,每个fd上有注册有回调函数,当网卡接收到数据时会回调该函数,同时将该fd的引用放入rdlist就绪列表中。

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

他主要有三个函数,epoll的执行流程

  1. 调用epoll_create创建一个eventpoll结构体,这个结构体有一个监听事件红黑色,和一个就绪链表(这个链表只会存放就绪fd,避免我们无效的遍历所有fd)

  1. 调用epoll_ctl向eventpoll中注册一个监听的fd,并且注册上fd对应事件的回调函数。

  1. 调用epoll_wait开始阻塞等待事件到来
  2. 内核将监听到的事件添加一份到就绪队列list_head

  1. 内核唤醒用户线程,并将就绪链表拷贝到用户空间

  1. 用户应用只需要关心这些就绪的fd事件,直接取出结构体里关联的回调函数进行回调即可处理事件。
对应的redis的server执行流程

  1. 调用epoll_create创建一个eventpoll结构体
  2. 调用epoll_ctl向eventpoll中注册一个监听连接的serverSocket,并关联上处理accept事件的函数
  3. 调用epoll_wait阻塞等待fd事件(等待客户端连接)
  4. 用户程序被唤醒,事件到来(现在只有连接事件)。根据生成的客户端的FD,调用epoll_ctl注册一个监听,并且关联上处理read事件的函数和处理write事件的函数。
  5. 继续调用epoll_wait阻塞等待fd事件(等待客户端连接或客户端命令执行请求)
  6. 用户程序被唤醒,事件到来(连接事件或者命令执行请求),假设是客户端执行请求事件,根据客户端的fd对应的read事件直接调用绑定的回调函数来处理,将结果再写回到fd缓存中。
  7. 继续调用epoll_wait等待accept,read,write事件。
epoll优点

  • EPOLL支持的最大文件描述符上限是整个系统最大可打开的文件数目, 1G内存理论上最大创建10万个文件描述符
  • 每个文件描述符上都有一个callback函数,当socket有事件发生时会回调这个函数将该fd的引用添加到就绪列表中,select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可
  • select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多
读事件很好理解,有一个读事件就立马处理请求,怎么理解写事件?

当socket 写缓冲区已满,假如设置了非阻塞I/O,应用程序调用send会返回EAGAIN,告诉应用程序写缓冲区已满,下次再来尝试调用,这时候就有一个尝试的时机问题,应用程序怎么知道socket 缓冲区可写呢?如果频繁调用send,会浪费CPU。这时候,epoll就排上用场了,对socket 设置写事件,并添加到 epoll中,应用程序调用epoll_wait,当该socket 的写缓冲有空余时,就返回对应的写事件,应用程序这时候就可以调用send,发送数据。

所以写事件是用来告诉程序,写缓冲是空余的。一般情况下fd都是有写事件的。但是在写缓冲区满了的时候,就会频繁触发写事件。所以我们可以一开始不监听写事件,直到发现数据量可能大于缓冲区,再监听写事件

参考:高效处理写事件

参考

select poll epoll

黑马多路复用视频

有关Redis的IO多路复用以及Select,Epoll的演进的更多相关文章

  1. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  2. ruby-on-rails - rspec should have_select ('cars' , :options => ['volvo' , 'saab' ] 不工作 - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion在首页我有:汽车:VolvoSaabMercedesAudistatic_pages_spec.rb中的测试代码:it"shouldhavetherightselect"dovisithome_pathit{shouldhave_select('cars',:options=>['volvo','saab','mercedes','audi'])}end响应是rspec./spec/request

  3. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  4. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  5. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  6. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  7. 阿里云国际版免费试用:如何注册以及注意事项 - 2

    作为新的阿里云用户,您可以50免费试用多种优惠,价值高达1,700美元(或8,500美元)。这将让您了解和体验阿里云平台上提供的一系列产品和服务。如果您以个人身份注册免费试用,您将获得价值1,700美元的优惠。但是,如果您是注册公司,您可以选择企业免费试用,提交基本信息通过企业实名注册验证,即可开始价值$8,500的免费试用!本教程介绍了如何设置您的帐户并使用您的免费试用版。​关于免费试用在我们开始此试用之前,您还必须遵守以下条款和条件才能访问您的免费试用:只有在一年内创建的账户才有资格获得阿里云免费试用。通过此免费试用优惠,用户可以免费试用免费试用活动页面上列出的每种产品一次。如果您有多个帐

  8. ruby-on-rails - 事件记录 : Select max of limit - 2

    我正在尝试将以下SQL查询转换为ActiveRecord,它正在融化我的大脑。deletefromtablewhereid有什么想法吗?我想做的是限制表中的行数。所以,我想删除少于最近10个条目的所有内容。编辑:通过结合以下几个答案找到了解决方案。Temperature.where('id这给我留下了最新的10个条目。 最佳答案 从您的SQL来看,您似乎想要从表中删除前10条记录。我相信到目前为止的大多数答案都会如此。这里有两个额外的选择:基于MurifoX的版本:Table.where(:id=>Table.order(:id).

  9. ruby - 为什么不能使用类IO的实例方法noecho? - 2

    print"Enteryourpassword:"pass=STDIN.noecho(&:gets)puts"Yourpasswordis#{pass}!"输出:Enteryourpassword:input.rb:2:in`':undefinedmethod`noecho'for#>(NoMethodError) 最佳答案 一开始require'io/console'后来的Ruby1.9.3 关于ruby-为什么不能使用类IO的实例方法noecho?,我们在StackOverflow上

  10. ruby-on-rails - rails 上的 ruby : radio buttons for collection select - 2

    我有一个集合选择:此方法的单选按钮是什么?谢谢 最佳答案 Rails3中没有这样的助手。在Rails4中,它是collection_radio_buttons. 关于ruby-on-rails-rails上的ruby:radiobuttonsforcollectionselect,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/18525986/

随机推荐