IO模型碎碎念

IO相关概念

同步和异步

同步:发出一个调用后,在没有得到结果之前,该调用就不会返回。当调用返回时,就能得到返回结果。(调用者主动等待结果)

异步:发出一个调用后,这个调用就返回了,没有返回结果。(调用者被动等待结果,会得到通知)

阻塞与非阻塞

阻塞:某个请求发出后,如果不满足请求需要的条件,那么就会一只等待,直到满足条件。

非阻塞:某个请求发出后,如果不满足请求需要的条件,则立即返回一个标志信息告知条件不满足,而不会一直等待。一般需要通过循环判断请求条件是否满足来获取请求结果。

同步/异步关注的是**消息通信机制,程序之间的协作关系。 阻塞/非阻塞关注的是**程序在等待调用结果(消息,返回值)时的状态,程序执行状态。

同步IO和异步IO

同步IO:用户进程触发IO操作后,等待或者轮询的去查看IO操作是否完成。

异步IO:用户进程触发IO操作以后,可以干别的事,IO操作完成以后自动通知当前线程。(类似回调)

阻塞IO与非阻塞IO

阻塞IO:如果数据没有就绪,会一直等待,直到数据就绪。

非阻塞IO:如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。

Linux下的五种IO模型

IO过程简介

image

网络IO的本质是Socket的读取,对于一次IO访问(以read为例),数据会先被拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间中。 所以说,当一个read操作发生时,它会经历两个阶段:

第一阶段:等待数据准备好

第二阶段:将数据从内核拷贝到用户进程中

对于socket流而言:

第一步:通常涉及等待网络上的数据分组到达,然后复制到内核的某个缓冲区。

第二步:把数据从内核缓冲区复制到应用进程缓冲区。

当然,如果内核空间的缓冲区中已经有数据了,那么就可以省略第一步。至于为什么不能直接让磁盘控制器把数据送到应用程序的地址空间中呢?最简单的一个原因就是应用程序不能直接操作底层硬件。


Linux网络IO模型大致分为以下五种:

  1. 阻塞IO
  2. 非阻塞IO
  3. I/O多路复用(异步阻塞 I/O)
  4. 信号驱动I/O(SIGIO)
  5. 异步I/O

前四种都属于同步IO,最后一种是异步IO。

阻塞IO

应用程序调用一个IO函数,如下图中的recvfrom()函数,此时内核数据没有准备好,导致应用程序阻塞,等待数据准备好。 如果数据准备好了,会将数据从内核拷贝到用户空间,IO函数返回成功指示。应用程序解除阻塞,处理数据。

image

场景描述:好比在餐厅吃饭,但今天是情人节人太多,需要排队叫号才能吃饭,吃完才能去逛商场,中间排队叫号等待的时间浪费掉了。这就是阻塞。

非阻塞IO

进程反复调用IO函数,每次调用都会立即返回(如图中前三次调用都立即返回EWOULDBLOCK,表示数据还没有准备好),这个时候不会阻塞用户进程(可以做其他的事情);直到某一次调用时,数据准备好了,于是进行数据拷贝,在这个过程中用户进程是阻塞状态。

对于应用程序而言,虽然这个IO操作很快就返回了,但是它并不知道这个IO操作是否真的成功了。 为了知道IO操作是否成功,一般有两种策略:

一是需要应用程序主动地循环地去问,直到数据准备好为止,在这个不断问的过程中 (轮询),会大量的占用CPU的时间;

二是采用IO通知机制,比如:IO多路复用或信号驱动IO。

image

场景描述 :还是刚刚的例子,觉得等待太漫长了,于是决定去逛商场,但是逛一会就回来问问服务员有没有被叫号,来来回回好几次。这就是非阻塞。

IO多路复用

当用户进程调用了select,那么整个进程会阻塞,而同时select负责监听Socket,当任何一个Socket中的数据准备好了,select就会返回。这个时候用户进程再调用recvfrom(),将数据从内核拷贝到用户进程。如图。

优势

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

IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的IO系统调用如recvfrom()之上。

image

场景描述:发现关注微信号可以看到叫号的状态,所以在逛商场的时候,就不用经常回去问服务员了,而是看下微信就可以了。(不仅仅是我们不用问服务员,其他所有人都可以不用问服务员)。这就是IO多路复用。

信号驱动IO(SIGIO)

首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行不会阻塞。当数据准备好时,进程会收到一个SIGIO信号,随后就可以在信号处理函数中调用IO操作(recvfrom()函数)处理数据。

这种IO模型的优点是当进程调用recvfrom()时,数据必定已经可以读取了,因为当内核发送SIGIO信号时,数据已经准备就绪了。

image

场景描述:如果这个商家没有微信公众号可以查看状态,那么我们可以留一个电话号码,告诉服务员如果叫到号了就打电话通知我们,没打我们就继续逛商场,打了我们就可以直接过去吃饭了。这就是信号驱动IO,打电话就相当于收到信号。

异步IO

用户进程告知内核启动某个操作(图中aio_read操作,立即返回),并让内核在整个IO操作完成后通知我们,用户进程不阻塞;在数据拷贝的时候用户进程也不会阻塞。

前面的IO模型都是需要我们在 读写事件就绪后(数据准备好以后) 自己负责进行读写,而异步IO则不需要,是内核替我们做了这些事,最后只需要当IO完成时才通知我们。注意它和信号驱动IO不同,信号驱动IO是通知我们何时可以进行IO,而异步IO是通知我们何时完成了IO。

image

场景描述:突然我们不想在饭店吃饭了,打算回家休息。于是我们叫了饭店的外卖,等饭好了会自动送到家里。这就是异步。

5个IO模型的比较

image