讲讲IO多路复用机制和epoll模型

大家都知道Redis的速度非常快,而且每秒能支撑上万的请求。

得益于三点:

  1. 最关键的一点Redis是纯内存操作
  2. 它的数据结构非常高效
  3. 它采用了多路复用机制 使用的是epoll模型

那么我们今天就来讲讲这个 多路复用机制和 epoll 模型。

那么 它俩到底是什么?

IO多路复用 是网络模型中的一种,而 epoll 是它的一种实现

接下来我来展开说说。

网络模型

讲到网络模型 就不得不提到一本神作:《Unix网络编程》

作者将LinuxIO模型分为五类:

阻塞IO,非阻塞IO,IO多路复用,信号驱动,异步IO

为了让后续的讲解能正常进行或者说更好理解,我在这里提一嘴:

应用是没法直接去硬件拿数据的,必须通过内核与硬件进行交互。

而为了避免用户应用导致冲突甚至内存崩溃,用户应用与内核是分离的,进程的寻址空间划分为了两部分:内核空间和用户空间

  • 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后才能写入设备
  • 读数据时,要从设备读取数据到内核缓冲区,然后再拷贝到用户缓冲区。

不管是读取还是写入,无非都是从用户应用程序发送一个请求到内核。

在内核中,可以分为两个阶段:

  • 第一阶段是内核等待数据;
  • 第二阶段是数据就绪后,将数据拷贝到用户空间。

而我们讲的IO模型,就是为了优化这个读取和写入而进行的。

阻塞IO

顾名思义,就是这两阶段都必须阻塞等待。

比如我使用recvfrom操作,去找你拿信息,你没有,那我就等着。等到你有。

非阻塞IO

非阻塞IO的 recvfrom操作 会立即返回结果。也就是 第一阶段,你要是没有 我就直接返回没有 不会像阻塞IO模型一样阻塞在那边。

但是! 它会反复去询问,现在有了吗?现在有了吗? 这样。

这其实很鸡肋对吧,因为看着你确实没阻塞住,但是你还得一直问,不仅是瞎忙活,还浪费了CPU的性能去重复发请求。所以可能还不如阻塞IO。

所以

无论是阻塞IO还是非阻塞IO,用户应用在一阶段,都需要去调用recvfrom来获取数据,差别其实就在于无数据时的处理方案:

  • 如果调用recvfrom时,切好没有数据,阻塞IO会使进程阻塞,非阻塞IO使CPU空转,都不能充分发挥CPU作用。

  • 如果调用recvfrom时,切好数据,则用户进程可以直接进入第二阶段,读取并处理数据

这就会有个问题,服务器端处理客户端Socker请求时,在单线程情况下,只能依次处理每一个Socket,如果正在处理的Socket切好未就绪(数据不可读或不可写),线程就阻塞住了,其他所有客户端Socket就只能等着,即使他们的数据已经就绪了。而第二阶段实际上是不可避免的, 只能优化第一阶段。

这可以结合场景来理解:

顾客排队点餐,分两步:

  1. 顾客想想吃什么(等待数据)

  2. 顾客想好了,开始点餐(读取数据)

想要提高效率有两种办法:

  1. 加更多服务员(多线程嘛)

  2. 不排队,谁想好了吃什么(线程就绪了),他就叫服务员过来(用户应用就去读数据)

IO多路复用

既然一个一个来很慢,那我就监听所有人,也就是方法2。

就像现在我们去餐厅吃饭,都是坐在座位上先看菜单,选好了就叫服务员来。这也就是IO多路复用的基本思想。

利用单个线程来同时监听多个文件描述符 FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。(也能回答 IO多路复用是什么)

IO多路复用实际上是种思想嘛,他有

三种实现:

  • select :它只能支持1024个连接

  • poll :它可以自定义

  • epoll :它是最高效的实现方式,分为两种事件通知机制:TL 水平触发(默认)和 ET 边缘触发

那么问题来了:用户进程要怎么知道内核中数据是否就绪呢?

这个要说说FD:

文件描述符 FD

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

它们全都是文件。而文件有一个文件描述符(File Descriptor),简称FD。

FD是一个从0 开始递增的无符号整数,用来关联Linux中的一个文件。

那现在用户进程,需要1 2 5 这三个文件。

当用户进程将这个需求传递给内核时,它会创建一个类似于表或数组的数据结构,表示需要的文件,例如: 2、5、7对应的位置为1,其余位置为0,形如:

0 0 1 0 0 1 0 1 0 0 0 …

在内核中,当这个表中的某个文件 比如 5准备好后,内核会将表更新为:

0 0 1 0 0 0 0 1 0 0 0 …

然后将这个表或数组传回给用户进程。

select和poll这两种实现方式就是:

用户进程逐个比对所接收到的表,若发现某个文件的状态发生变化,就意味着它已准备好,用户进程就可以进入第二阶段进行读取等操作。

还是结合点餐场景,有顾客想好吃什么了,它就按座位上的灯,服务员看到灯亮了,他就去找这个顾客,但是他不知道具体是谁按的,所以就得逐个问,是你嘛?是你嘛?直到找到对应的用户。这个是最早期的IO多路复用实现方案,也就是 select 和 poll 的实现方案。

这个通知机制不太好。所以后来linux就升级了一下,epoll 这种就是 每桌有个桌号,我灯亮了可以看到桌号 就不用一个个问了。

其实也就是 他返回的时候 我直接告诉你是谁就得了,你还比对个啥,对吧。效率一下就上来了。

那么为什么select只能支持1024个连接呢?

就是因为这个数组,他把数组大小写死了,是个常量 1024,那只有1024那么大,不就只能支持1024个连接了,在实际中甚至还更低。

再比如poll实现,他是可以自定义大小,连接数上来了。但是我们前面说到了,他是挨个比对的,那太大了能行吗。大小为10000000的数组 你挨个过去 得比对到什么时候。

而epoll的方式, 就哪个好了通知哪个,把CPU的运用率拉满了。

这也能回答一个问题,Redis为什么选择单线程。

就是因为他在内存中操作,内存操作速度太快了。甚至可以到微妙级别,配合上 epoll,单线程就够了。它的性能瓶颈永远是网络延迟而不是执行速度。还不用考虑上下文切换的性能开销,还有多线程的线程安全问题。

那到这就结束了。当然,这里面还有非常非常多细致的点。

后续我有空了会再整理发文。如果觉得对你有帮助的话,帮忙点赞评论支持一下。

谢谢观看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值