vlambda博客
学习文章列表

「IO 模型」聊聊五种 IO 模型之女友篇1

做一个比较注重七寸认知的技术人!

Hello,大家好!我是七寸君。

上篇文章,解释了五中 IO 模型的概念理解,特此更正一下文章最后一张图关于 IO 多路复用定位成异步阻塞模型是不正确的,而应该是同步阻塞模型。

接下来,本篇文章以一个生动形象的例子“周末和女友去逛街,中午饿了,我们准备去吃饭,需要排队”来解释说明五中 IO 模型。

01



同步阻塞 IO

场景描述

我和女友点完餐后,不知道什么时候能做好,只好坐在餐厅里面等,女友本想还和我一起逛街的,但是不知道饭能什么时候做好,只好和我一起在餐厅等,而不能去逛街,直到吃完饭才能去逛街,中间等待做饭的时间浪费掉了。这就是典型的阻塞

网络模型

同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在 Linux 中,默认情况下所有的 Socket 都是 blocking。它符合人们最常见的思考逻辑。阻塞就是进程 "被" 休息, CPU处理其它进程去了

在这个 IO 模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞,不能处理别的网络IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。在调用 recv()/recvfrom() 函数时,发生在内核中等待数据和复制数据的过程,大致如下图:

「IO 模型」聊聊五种 IO 模型之女友篇1

流程描述

当用户进程调用了 recv()/recvfrom() 这个系统调用,kernel 就开始了IO的第一个阶段:备数据对于网络 IO 来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的 UDP 包。这个时候 kernel 就要等待足够的数据到来。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。第二个阶段:数据拷贝当 kernel 一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存,然后 kernel 返回结果,用户进程才解除 block 的状态,重新运行起来。

所以,blocking IO 的特点就是在 IO 执行的两个阶段都被 block 了。

优点:

> 1. 能够及时返回数据,无延迟;

> 2. 对内核开发者来说这是省事了;

缺点:

> 1. 对用户来说处于等待就要付出性能的代价了;


02



同步非阻塞 IO

场景描述

我女友不甘心白白在这等,又想去逛商场,又担心饭好了。所以我们逛一会,回来询问服务员饭好了没有,来来回回好多次,饭都还没吃都快累死了啦。这就是非阻塞需要不断的询问,是否准备好了。

网络模型

同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式在这种模型中,设备是以非阻塞的形式打开的。这意味着 IO 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或 EWOULDBLOCK)。

在网络 IO 时候,非阻塞 IO 也会进行recvform系统调用,检查数据是否准备好,与阻塞 IO 不一样,非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 '被' CPU光顾。

也就是说非阻塞的 recvform 系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起 recvform 系统调用重复上面的过程,循环往复的进行 recvform 系统调用。这个过程通常被称之为轮询轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态

在 Linux 下,可以通过设置 Socket 使其变为 non-blocking。当对一个non-blocking Socket 执行读操作时,流程如图所示:

「IO 模型」聊聊五种 IO 模型之女友篇1

流程描述

当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 error。从用户进程角度讲,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,non-blocking IO 的特点是用户进程需要不断的主动询问kernel数据好了没有。

同步非阻塞方式相比同步阻塞方式:

> 优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。

> 缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次 read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

03



IO 多路复用

场景描述

与第二个方案差不多,餐厅安装了电子屏幕用来显示点餐的状态,这样我和女友逛街一会,回来就不用去询问服务员了,直接看电子屏幕就可以了。这样每个人的餐是否好了,都直接看电子屏幕就可以了,这就是典型的IO多路复用。

网络模型

由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的 CPU 时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。如果轮询不是进程的用户态,而是有人帮忙就好了那么这就是所谓的 “IO 多路复用”。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。

IO 多路复用有两个特别的系统调用 select、poll、epoll 函数select 调用是内核级别的,select 轮询相对非阻塞的轮询的区别在于---前者可以等待多个socket,能实现同时对多个 IO 端口进行监听,当其中任何一个 socket 的数据准好了,就能返回进行可读,然后进程再进行 recvform 系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。select 或poll 调用之后,会阻塞进程,与blocking IO阻塞不同在于,此时的 select 不是等到 socket 数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。也可以理解为"非阻塞"吧

I/O复用模型会用到 select、poll、epoll 函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。

对于多路复用,也就是轮询多个 Socket。多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了,当然也可以针对不同的编号。具体流程,如下图所示:

流程描述

IO multiplexing 就是我们说的 select,poll,epoll,有些地方也称这种 IO方式为event driven IO。select/epoll 的好处就在于单个process就可以同时处理多个网络连接的IO它的基本原理就是 select,poll,epoll 这个function 会不断的轮询所负责的所有 Socket,当某个 Socket 有数据到达了,就通知用户进程。

当用户进程调用了 select,那么整个进程会被 block,而同时,kernel会“监视”所有 select 负责的 socket,当任何一个socket中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。

多路复用的特点是通过一种机制一个进程能同时等待多个 IO 文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select、poll、epoll 函数就可以返回。对于监视的方式,又可以分为  select、poll、epoll 三种方式。

上面的图和 blocking IO 的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个 system call (select 和 recvfrom),而 blocking IO 只调用了一个 system call (recvfrom)但是,用 select 的优势在于它可以同时处理多个 connection。

所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。(select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接)

在 IO multiplexing Model 中,实际上,对于每一个 socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的 process 其实是一直被block的。只不过 process 是被 select 这个函数 block,而不是被socket IO给block所以 IO 多路复用是阻塞在 select、epoll 这样的系统调用之上,而没有阻塞在真正的 I/O系 统调用如 recvfrom 之上。

在 I/O 编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者 I/O 多路复用技术进行处理。I/O 多路复用技术,通过把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源,I/O 多路复用的主要应用场景如下:

> 1. 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字;

> 2. 服务器需要同时处理多种网络协议的套接字;

了解了前面三种 IO 模式,在用户进程进行系统调用的时候,他们在等待数据到来的时候,处理的方式不一样,直接等待,轮询,select、poll轮询,两个阶段过程:

> 第一个阶段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。

> 第二个阶段都是阻塞的。

从整个 IO 过程来看,他们都是顺序执行的,因此可以归为同步模型(synchronous),都是进程主动等待且向内核检查状态【此句很重要!

高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式。要理解这一点,首先要扯到并发和并行的区别。比如:去某部门办事需要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度。也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求),而并行数是可以同时工作的物理资源数量(如 CPU 核数)。通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大。而同步非阻塞方式可以把多个 IO 请求丢到后台去,这就可以在一个进程里服务大量的并发 IO 请求。

注意:IO 多路复用是同步阻塞模型还是异步阻塞模型,此处仍然不太清楚的,强烈建议大家在细究 中讲同步与异步的根本性区别,同步是需要主动等待消息通知,而异步则是被动接收消息通知,通过回调、通知、状态等方式来被动获取消息。IO 多路复用在阻塞到 select 阶段时,用户进程是主动等待并调用 select 函数获取数据就绪状态消息,并且其进程状态为阻塞。所以,把IO多路复用归为同步阻塞模式。

- EOF -

推荐阅读   点击标题可跳转


关注「七寸知架构」加星标,不错过技术那些事儿

        
          
          
        
❤️ 您的 点赞 在看 就是最大的支持❤️