史上最全IO模型详解

操作系统

在介绍IO模型之前,先要介绍内核态和用户态。在操作系统中,因为有些操作指令是不能开放给用户的,比如读取磁盘,清除内存,读写网卡数据。这样用户态程序不能随便操作内核地址,即使用户程序崩溃了,对于操作系统也不会有影响。

IO的分类

IO过程一般分为 用户进程《–》内核空间,内核空间《–》设备空间(磁盘或者网卡)

网络IO和磁盘IO

  • 网络IO:把网络数据读到内核缓冲区,然后从内核缓冲区复制到进程空间
  • 磁盘IO: 把数据从磁盘读到内核缓冲区,然后从内核复制到进程空间

同步IO和异步IO

  • 同步IO:A调用B的数据,在B没有准备好数据前不会通知A,并且B不会做其他事情
  • 异步IO:A调用B的数据,B会告诉A已经收到请求,然后B在准备A的数据过程中可能会做其他事,处理完后通过回调的方式通知A。
    同步和异步的区别就是被调用的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。

阻塞IO和非阻塞IO

  • 阻塞IO:A调用B,在此期间A一直等B的结果,不去处理其他事情
  • 非阻塞IO:A调用B,A不会一直等B的结果,回去干别的事。
    两者的区别主要是在调用方是否在一直等待。

同步和异步强调的是被调用方(B–操作系统),阻塞和非阻塞强调的是调用方(A–应用程序)

操作系统的IO模型

阻塞IO模型

一个线程处理所有请求

非阻塞IO模型

多线程版本阻塞IO模型

复用IO模型

如果每个请求都要一个线程处理,那么在高并发情况下,会造成资源的浪费服务器的崩溃。
IO服用模型的思路就是一个线程监控多个网络请求,每一个网络请求以文件描述符fd 标识。
这样一个线程就可以管理很多个请求,当某个数据准备好之后再用别的线程处理。
IO复用模型
IO复用模型提供了select、poll、epoll三个函数监控fd的操作。
select/poll:select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。

epoll:第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
在这里插入图片描述
epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)
边缘触发:使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;只通知一次
水平触发:使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;只要你没处理,会一直通知你

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符准备就绪,能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写(一个个的处理),也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

小结:

某教室有10名学生和1名老师,这些学生上课会不停的提问,所以一个老师处理不了这么多的问题。那么学校为每个学生都配一名老师,也就是这个教室目前有10名老师。此后,只要有新的转校生,那么就会为这个学生专门分配一个老师,因为转校生也喜欢提问题。如果把以上例子中的学生比作客户端,那么老师就是负责进行数据交换的服务端。则该例子可以比作是多进程的方式。

后来有一天,来了一位具有超能力的老师,这位老师回答问题非常迅速,并且可以应对所有的问题。而这位老师采用的方式是学生提问前必须先举手,确认举手学生后在回答问题。则现在的情况就是IO复用。并且select、poll老师采用的是轮询举手的同学方式回答问题,而epoll老师采用的是不用让同学一直举手的方式,他准备好答案会找到相关的同学返回答案。

信号驱动IO模型

我们说信号驱动IO模型是一种异步非阻塞IO模型,指的是用户线程去内核空间请求数据时,直接注册一个信号处理函数,然后用户线程返回(异步),而内核空间接收到请求后,开始处理(此时并不会阻塞,内核空间可以同时接收多个请求,注册多个信号处理函数);

但是,等到内核空间读取到数据之后,应用线程需要将数据从内核空间拷贝到用户空间,**此时是用户线程是阻塞的;**也就是说:应用程序将数据从内核态拷贝到用户态的过程是阻塞等待的,这是和异步IO的本质区别;

异步IO模型

在以上4种IO模型中,每次要去读取数据时都是事先发送请求询问内核是否有可读数据,然后再发起真正的读取数据请求;

在异步IO模型中,应用只需要向内核发送一个请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间(而信号驱动是告诉应用程序何时可以开始拷贝数据),异步IO模型真正的做到了完完全全的非阻塞;
所以异步最大的区别就是:当数据准备好以后,内核能自主的把数据复制到用户程序

Reactor模型

虽然epoll模型已经可以实现一个线程管理多个IO请求,但是当epoll_wait返回大量的就绪fd的时候,就需要等待这些时间处理完成,才能处理新的时间,这样容易让新的事件超时,因此提出了Reactor模型。
主从Reactor模型,主Reactor只会响应客户端连接的事件,即accept事件,剩下的交给从Reactor进行处理,进行了解耦。主Reactor将epoll进行封装,管理就绪的事件,然后交给从线程管理。

Redis的IO多路复用模型

Redis基于Reactor模型开发自己的网络事件处理器,这个处理器被称为文件事件处理器 。通过文件事件分派处理器将套接字请求调用相应的事件处理器进行处理。事件处理器分为:

  • 连接应答处理器:对连接服务器的各个客户端进行响应
  • 命令请求处理器:接受客户端传来的命令请求
  • 命令回复处理器:向客户端返回命令的执行结果
  • 复制处理器:当主服务器和从服务器进行复制操作时, 主从服务器都需要关联此处理器

Netty多路复用模型

多主多从的Reactor的模型

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值