聊聊同步、异步、阻塞、非阻塞以及IO模型

前言

在使用Netty改造手写RPC框架的时候,需要给大家介绍一些相关的知识,这样很多东西大家就可以看明白了,手写RPC是一个支线任务,后续重点仍然是Kubernetes相关内容。

阻塞与非阻塞 同步与异步

阻塞与非阻塞

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式。当数据没有准备的时候,阻塞需要等待调用结果返回之前,进程会被挂起,函数只有在得到结果之后才会返回。非阻塞和阻塞的概念相对,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

同步与异步

同步指的是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。异步的概念和同步相对,当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

举个例子来说,对于我们经常使用B/S架构来说,同步和异步指的是从客户端发起访问数据的请求,阻塞和非阻塞指的是服务端进程访问数据,进程是否需要等待。这两者存在本质的区别,它们的修饰对象是不同的。

阻塞和非阻塞是指进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪。

同步和异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞,异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。

举个例子

妈妈让我去厨房烧一锅水,准备下饺子。

阻塞:水只要没烧开,我就干瞪眼看着这个锅,沧海桑田,日新月异,我自岿然不动,厨房就是我的家,烧水是我的宿命;

非阻塞:我先去我屋子里打把王者,但是每过一分钟,我都要去厨房瞅一眼,生怕时间长了,水烧干了就坏了,这样导致我游戏也心思打,果不然,又掉段了;

同步:不管是每分钟过来看一眼锅,还是寸步不离的一直看着锅,只要我不去看,我就不知道水烧好没有,浪费时间啊,一寸光阴一寸金;

异步:我在淘宝买了一个电水壶,只要水开了,它就发出响声,嗨呀,可以安心打王者喽,打完可以吃饺子喽;

总结:

阻塞/非阻塞:我在等你干活的时候我在干啥?

阻塞:啥也不干,死等;

非阻塞:可以干别的,但也要时不时问问你的进度;

同步/异步:你干完了,怎么让我知道呢?

同步:我只要不问,你就不告诉我;

异步:你干完了,直接喊我过来就行;

IO模型

网络IO的本质是Socket的读取,Socket在Linux系统被抽象为流,IO可以理解为对流的操作。Linux标准文件访问方式如下:

img
img

当发起一个read操作的时候,会经历2个阶段:

  1. 等待数据准备;
  2. 将数据从内核拷贝到进程中;

对于socket流也会经历两个阶段:

  1. 将磁盘或者其他设备到达以后的信息,拷贝到内核的缓存区中;
  2. 将内核的缓存区的数据复制到应用进程缓存中;

网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者,接下来我们介绍下IO模型:

同步阻塞IO(blocking IO)

同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在Linux中,默认情况下所有的socket都是blocking。阻塞就是进程休息, CPU处理其它进程去了。

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

img
img
  1. 应用进程向内核发起recfrom读取数据;

  2. 准备数据报(应用进程阻塞);

  3. 将数据从内核负责到应用空间;

  4. 复制完成后,返回成功提示;

特点

同步阻塞 IO 整个过程都是阻塞的,对于用户可以及时返回数据,无延迟,对于开发者来说简单省事,对于系统来说无法应对高并发访问,以及用户在等待期间也无法进行其他任何操作。

同步非阻塞IO(nonblocking IO)

同步非阻塞就是采用轮询的方式,定时去查看数据是否准备完成。在这种模型中,进程是以非阻塞的形式打开的。IO 操作不会立即完成,如果该缓冲区没有数据的话,就会直接返回一个EWOULDBLOCK错误,不会让应用一直等待中。

非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不一样,非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会被CPU访问。也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。

img
img
  1. 应用进程向内核发起recvfrom读取数据;

  2. 没有数据报准备好,即刻返回EWOULDBLOCK错误码;

  3. 应用进程向内核发起recvfrom读取数据;

  4. 已有数据包准备好就进行一下步骤,否则还是返回错误码;

  5. 将数据从内核拷贝到用户空间;

  6. 完成后,返回成功提示;

特点

同步非阻塞方式相比同步阻塞方式,在等待任务期间进程可以处理其他事情,缺点的话就是因为采用定时轮询的方式,导致系统整体的吞吐量降低。

IO多路复用( IO multiplexing)

同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,当并发情况下服务器很可能一瞬间会收到几十上百万的请求,这种情况下同步非阻塞IO需要创建几十上百万的线程去读取数据,同时又因为应用线程是不知道什么时候会有数据读取,为了保证消息能及时读取到,那么这些线程自己必须不断的向内核发送recvfrom 请求来读取数据。这么多的线程不断调用recvfrom 请求数据,明细是对线程资源的浪费。

于是有人就想到了由一个线程循环查询多个任务的完成状态(fd文件描述符),只要有任何一个任务完成,就去处理它。这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO多路复用。

img
img
  1. 应用进程向内核发起select调用;

  2. kernel会监听所有select负责的socket;

  3. 任何一个socket中的数据准备好了,select就会返回;

  4. 应用进程再调用recvfrom操作,将数据从内核拷贝到用户空间;

  5. 完成后,返回成功提示;

特点

IO多路复用与同步非阻塞相比,应用线程通过调select/poll之后,阻塞住,进入到内核态后由内核线程来轮询这个应用线程所关注的所有文件描述符对应的缓冲区是否有数据准备就绪,只要有一个缓冲区数据准备就绪,就可以进行数据拷贝然后返回给用户线程,这种方式就减少了用户线程的不断轮询以及避免在每次轮询时所产生的两次上下文切换过程。

此外就是IO多路复用模型可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(这里并不是全部数据可读或可写),才真正调用I/O操作函数。

此外还需要注意的是,IO多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了。

信号驱动IO(signal-driven IO)

IO多路复用解决了一个线程或者多个线程可以监控多个文件描述符的问题,但是select是采用轮询的方式来监控多个文件描述符的,通过不断的轮询文件描述符的可读状态来知道是否有可读的数据,这样无脑的轮询就显得有点浪费,因为大部分情况下的轮询都是无效的,于是乎有人就想,能不能不要总是去轮询数据是否准备就绪,能不能发出请求后,等数据准备好了在通知我,所以这样就出现了信号驱动IO。

信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号,通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求。因为信号驱动IO的模型下,应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个文件描述符。

image.png
image.png
  1. 应用进程开启套接口信号驱动IO功能,通过系统调用sigaction执行一个信号处理函数,请求即刻返回;

  2. 当数据准备就绪时,就生成对应进程的SIGIO信号,通过信号回调通知应用进程;

  3. 应用进程再调用recvfrom操作,将数据从内核拷贝到用户空间;

  4. 完成后,返回成功提示;

特点

信号驱动IO相比于IO多路复用,在通过这种建立信号关联的方式,实现了发出请求后只需要等待数据就绪的通知即可,这样就可以避免大量无效的数据状态轮询操作。

异步非阻塞 IO(asynchronous IO)

不管是IO多路复用还是信号驱动,我们要读取数据的时候,总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。这个时候我们会有一个疑问,为什么在读数据之前总要有个数据就绪的状态,可不可以应用进程只需要向内核发送一个read 请求,告诉内核要读取数据后,就立即返回。当内核数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,所以这样就出现了异步非阻塞 IO模型。

异步非阻塞IO模型应用进程发起aio_read操作之后,立刻就可以开始去做其它的事。后续的操作有内核接管,当内核收到一个asynchronous read之后,它会立刻返回,不会对用户进程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成以后,内核会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程。

image.png
image.png
  1. 应用进程发起aio_read操作,立即返回;

  2. 内核等待数据准备完成,然后将数据拷贝到用户内存;

  3. 内核会给用户进程发送一个signal信号;

  4. 收到信号,返回成功提示;

特点

异步非阻塞 IO相比于信号驱动IO,信号驱动IO模型只是由内核通知我们可以开始下一个IO操作,而异步非阻塞 IO模型是由内核通知我们操作什么时候完成。

五种IO模型总结

阻塞IO和非阻塞IO区别

调用阻塞IO会一直阻塞住对应的进程直到操作完成,而非阻塞IO在内核还准备数据的情况下会立刻返回。

同步IO和异步IO区别

两者的区别就在于同步IO做IO操作的时候会将进程阻塞,也就是应用进程调用recvfrom操作,recvfrom会将数据从内核拷贝到用户内存中,在这段时间内,进程是被阻塞的。

img
img

举个例子

小王去买火车票,三天后买到一张退票。参演人员(老李,黄牛,售票员,快递员),往返车站耗费1小时。

同步阻塞 IO

小王去火车站买票,排队三天买到一张退票。整个三天小王无法做其他事情,只能做买票的一件事情。

同步非阻塞 IO

小王去火车站买票,隔一天去火车站问有没有退票,三天后买到一张票。整个过程中小王需要往返3次,往返消耗3小时,这个期间小王可以做其他事情。

IO多路复用
select/poll

小王去火车站买票,委托黄牛购买,然后每隔12小时打电话询问黄牛,黄牛三天买到票,然后小王去火车站交钱领票。整个小王需要往返2次,往返消耗2小时,黄牛需要手续费100,打电话6次,这里的黄牛就是select/poll,多路指的就是一个黄牛可以服务多个人。

epoll

小王去火车站买票,委托黄牛购买,黄牛买到后即通知小王去领,然后小王去火车站交钱领票。整个过程小王需要往返2次,往返消耗2小时,黄牛需要手续费100,无需打电话。

信号驱动IO

小王去火车站买票,售票员留下电话,有票后,售票员电话通知小王,然后小王去火车站交钱领票。整个过程小王需要往返2次,往返消耗2小时,无手续费,无需打电话。

异步非阻塞 IO

小王去火车站买票,给售票员留下电话,有票后,售票员电话通知小王并快递送票上门。整个过程小王需要往返1次,往返消耗1小时,无手续费,无需打电话。

参考

IO 多路复用是什么意思

100%弄明白5种IO模型

聊聊Linux 五种IO模型

结束

欢迎大家点点关注,点点赞!