IO多路复用以及epoll详解

同步与异步

简介

同步与异步主要是从消息通知机制角度来说的,也就是调用结果通知的方式。

同步: 当一个同步调用发出去后,调用者要一直等待调用结果的通知后,才能进行后续的执行。
异步:当一个异步调用发出去后,调用者可以直接返回,不需要等待结果的返回。
异步调用,要想获得结果,一般有两种方式:

  1. 主动轮询异步调用的结果;
  2. 被调用方通过callback来通知调用方调用结果。

异步的概念和同步相对。当一个同步调用发出后,调用者要一直等待返回消息(结果)通知后,才能进行后续的执行;当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

生活中的例子

同步买奶茶:小明点单交钱,然后等着拿奶茶;

异步买奶茶:小明点单交钱,店员给小明一个小票,等小明奶茶做好了,再来取。

异步买奶茶: 小明要想知道奶茶是否做好了,有两种方式:

  1. 小明主动去问店员,一会就去问一下:“奶茶做好了吗?”…直到奶茶做好。这叫轮训。
  2. 等奶茶做好了,店员喊一声:“小明,奶茶好了!”,然后小明去取奶茶。这叫回调。

阻塞与非阻塞

阻塞与非阻塞的重点在于进/线程等待消息时候的行为,也就是在等待消息的时候,当前进/线程是挂起状态,还是非挂起状态。

阻塞调用在发出去后,在调用结果返回之前,当前进/线程会被挂起,直到在有结果之后返回,当前进/线程才会被激活。

非阻塞调用在发出去后,不会阻塞当前进/线程,而会立即返回。

阻塞买奶茶:小明点单交钱,干等着拿奶茶,什么事都不做;
非阻塞买奶茶:小明点单交钱,等着拿奶茶,等的过程中,时不时刷刷微博、朋友圈。

同步与阻塞

对于同步调用来说,很多时候当前线程可能还是激活的,只是从逻辑上当前函数没有返回而已,此时,这个线程可能也会处理其他的消息。就是说这个线程还是可以干活的:

(a) 如果这个线程在等待当前函数返回时,仍在执行其他消息处理,那这种情况就叫做同步非阻塞;

(b) 如果这个线程在等待当前函数返回时,没有执行其他消息处理,而是处于挂起等待状态,那这种情况就叫做同步阻塞;

对于阻塞调用来说,则当前线程就会被挂起等待当前函数返回。

同步异步与阻塞非阻塞

通过上面的分析,我们可以得知:

  1. 同步与异步,重点在于消息通知的方式;
  2. 阻塞与非阻塞,重点在于等消息时候的行为。

阻塞 / 非阻塞和同步 / 异步,其实就是两个不同角度的 I/O 划分方式。它们描述的对象也不同,阻塞 / 非阻塞针对的是 I/O 调用者(即应用程序),而同步 / 异步针对的是 I/O 执行者(即系统)。

所以,就有了下面4种组合方式:

  1. 同步阻塞:小明在柜台干等着拿奶茶;
  2. 同步非阻塞:小明在柜台边刷微博,还需要去做另外一个事情,即注意奶茶好了没有;
  3. 异步阻塞:小明拿着小票啥都不干,一直等着店员通知他拿奶茶;
  4. 异步非阻塞:小明拿着小票,刷着微博,等着店员通知他拿奶茶。

由此对应在linux IO模型上,就有了以下的模型:

同步阻塞IO(blocking IO)

这个效率是最低的。拿上面的例子来说,就是你干等着拿奶茶,什么别的事都不做。

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

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

同步非阻塞IO(nonblocking IO)

效率也是低下的。想象一下,你一边刷着微博,一边还需要经常抬头看奶茶好了没有,如果把2个动作看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的。

很多人会写阻塞的read/write 操作,如果对fd设置O_NONBLOCK 标志位,这样就可以将同步操作变成非阻塞的了。

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

nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。流程如图所示:

img

IO 多路复用( IO multiplexing)

由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。如果轮询不是进程的用户态,而是有人帮忙就好了。那么这就是所谓的 “IO 多路复用”。I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流。

IO多路复用在阻塞到select阶段时,用户进程是主动等待并调用select函数获取数据就绪状态消息,并且其进程状态为阻塞。所以,把IO多路复用归为同步阻塞模式

异步非阻塞IO(asynchronous IO)

看上面的例子就很清楚了,效率最高了。因为刷微博是你(等待者)的事情,而店员你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换

用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知IO两个阶段,进程都是非阻塞的

IO多路复用

I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流。采用非阻塞的模式,当一个连接过来时,我们不阻塞住,这样一个进程可以同时处理多个连接了。

(1)select==>时间复杂度O(n)

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(2)poll==>时间复杂度O(n)

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

(3)epoll==>时间复杂度O(1)

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

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

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,在BSD上面对应的是kqueue, win下对应的iocp。而select则应该是POSIX所规定,一般操作系统均有实现。

epoll

两种 I/O 事件通知的方式:水平触发和边缘触发,它们常用在套接字接口的文件描述符中。

  • 水平触发LT:只要文件描述符可以非阻塞地执行 I/O ,就会触发通知。也就是说,应用程序可以随时检查文件描述符的状态,然后再根据状态,进行 I/O 操作。
  • 边缘触发ET:只有在文件描述符的状态发生改变(也就是 I/O 请求达到)时,才发送一次通知。这时候,应用程序需要尽可能多地执行 I/O,直到无法继续读写,才可以停止。如果 I/O 没执行完,或者因为某种原因没来得及处理,那么这次通知也就丢失了。

简单说epoll和select/poll最大区别是:epoll内部使用了mmap共享了用户和内核的部分空间,避免了数据的来回拷贝;epoll基于事件驱动,epoll_ctl注册事件并注册callback回调函数,epoll_wait只返回发生的事件避免了像select和poll对事件的整个轮寻操作。

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

以nginx为例,每进来一个request,会有一个worker进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向后端服务器转发request,并等待请求返回。那么,这个处理的worker不会这么傻等着,他会在发送完请求后,注册一个事件:“如果upstream返回了,告诉我一声,我再接着干”,于是他就继续去做监听新的请求。这就是异步。此时,如果再有request 进来,他就可以很快再按这种方式处理。这就是非阻塞IO多路复用。而一旦上游服务器返回了,就会触发这个事件,worker才会来接手,这个request才会接着往下走。这就是异步回调

总结

  • 同步与异步,重点在于消息通知的方式,同步就一直等消息通知,而异步则是调用者可以直接返回,可以通过回调的方式来通知调用者
  • 阻塞与非阻塞,重点在于等消息时候的行为,阻塞就是进程会挂起,不做任何事情了;而非阻塞调用在发出去后,就不等结果,直接返回去做其他事情
  • IO多路复用的意思是指在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流。而所谓的socket就是一个会话,如TCP三次握手完之后,就建立了一个会话,linux就使用FD文件描述符来管理。单个线程指的是一个应用程序可以通过一个线程来管理多个socket会话。比如一个进程接受了10000个连接,这个就叫做I/O复用,就是多个I/O可以复用一个进程。
  • I/O多路复用目前有select、poll、epoll等方式实现,当连接有I/O流事件产生的时候,就会去唤醒进程去处理,但是进程并不知道是哪个连接产生的I/O流事件,就需要全部轮询一次,这个就是select以及poll的工作原理,而select与poll的最主要的区别是select在32位的系统只支持1024个socket,但poll没有限制。
  • epoll当连接有I/O流事件产生的时候,epoll就会去告诉进程哪个连接有I/O流事件产生,然后进程就去处理这个进程。就是说epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据。
  • I/O多路复用我个人认为可以理解为看门狗,一条狗负责一个进程的全部监听,有数据来了狗就负责“汪汪”一下,告诉应用进程有数据来了,但哪个socket有数据呢,那不好意义,小狗狗是不清楚的,所以应用程序 还需要自己再对fd列表循环一次。而epoll高级很多,类似狗会说人话,告诉进程哪个socket有数据流。
  • 通知进程有数据来了之后,那进程需要自己发起请求,去取数据,这一点来看,本质上还是同步IO。

参考资料

通俗讲解 异步,非阻塞和 IO 复用:https://www.zybuluo.com/phper/note/595507

聊聊同步、异步、阻塞与非阻塞: https://www.jianshu.com/p/aed6067eeac9

聊聊Linux 五种IO模型https://www.jianshu.com/p/486b0965c296

select、poll、epoll之间的区别:https://www.cnblogs.com/aspirant/p/9166944.html

epoll的本质:https://zhuanlan.zhihu.com/p/63179839

  • 本文作者: wumingx
  • 本文链接: https://www.wumingx.com/performance/io-epoll.html
  • 本文主题: IO多路复用以及epoll详解
  • 版权声明: 本站所有文章除特别声明外,转载请注明出处!如有侵权,请联系我删除。
0%