East-why-coroutine.md

Pre

协程这篇博客中我们讲解了何为协程,粗略的理解就是一个可以由我们自己控制换入换出执行的函数,切入点和切回点可以由我们自己控制,为什么我们的服务器中要使用协程呢?

我们先来复习下几种基本的网络IO模型:

1.阻塞式网络IO模型

socket的recv请求可以分为两个阶段,一个是等待网络数据就绪阶段,另一个是复制数据阶段。

阻塞式网络IO模型

这种模型下我们写代码非常简单,因为当函数返回时我们就知道结果了,接着去处理业务就可以了,缺点也非常明显,因为无法接着处理后续逻辑,我们的线程只能挂起,如果是一个单线程的程序,那么在这里就只能一起等,我们的程序就”卡”住了。

2.非阻塞式网络IO模型

非阻塞式IO模型不同的点是,当我们期望从缓冲区读取数据时,若没有数据就会立即返回,返回一个错误码(EWOULDBLOCK或者是EAGAIN),表面此时数据尚未准备好。这也就意味着我们仍需要调用这个函数,需要轮询结果,如图所示,当有数据后,会执行第二阶段,将数据从内核空间复制到用户空间,我们就可以使用这些数据了。
非阻塞式网络IO模型

与阻塞式IO相比,我们的确不需要一直阻塞在第一阶段-等待数据了,因为网络IO的延迟与执行几个函数相比不是一个数量级的,这算不算一个很大的提升呢? 我们上面提到,我们需要一直轮询结果,所以我们需要频繁调用这些函数,如果一直没有数据过来,那么就会造成无意义的CPU空耗。

3.IO多路复用

IO多路复用

IO多路复用就是解决上述问题的,这里拿select举例,在第一阶段也是等待数据就绪,但是这里可以等待多个socket对象,而不再只针对一个,一旦其中有一个socket有事件触发,就会给我们返回结果,然后我们在针对触发的socket处理对应的数据。
我们代码中使用的epoll是select和poll之后的一个更强有力的IO多路复用接口,这三者的区别是:

select:
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
一般来说这个数目和系统内存关系很大,1024或者是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

epoll:

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便可以收到通知。

epoll为什么要有EPOLLET触发模式?

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

epoll的优点:

1、没有最大并发连接的限制,能打开的FD的上限远大于1024/2048;

2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的fd才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

为什么引入协程?

在了解完三种IO模型后,我们假设我们的服务器程序就是一个Http服务器程序, 客户端现在有两种方式去访问这个服务器。我们假设现在执行一个Get method。

  1. 使用阻塞式IO,当我们建立连接后,等待socket可写,一般连接后socket缓冲区是可写的,一个Get方法所占字节数也不多,所以可以直接发送出去, 然后我们阻塞调用recv,等待数据返回后,我们交给上层的Http协议处理数据,如果数据没有传输完成(还没有收到完整的回复),我们继续调用recv等待数据返回

  2. 使用IO复用,我们将对应的socket通过epoll_ctl指定可读事件添加到epoll fd中,然后调用epoll_wait函数等待连接建立完成,一旦连接建立完成后,socket的发送缓冲区是空的也就是可写的,epoll_wait对应的fd可写事件触发,我们发送对应的请求,然后再次调用epoll_ctl将监听的事件改为可读事件,然后继续调用epoll_wait, 等待数据返回后,交给协议层判断数据是否传输完成,如果没有则继续等待epoll_wait返回对应的可读事件重复执行测过程。

可以看出来,IO复用的逻辑要复杂的多,一个socket的操作要经历多次事件循环(通常epoll_wait在一个循环中持续调用),而每次执行epoll_wait后,触发的事件也是随机的,所以我们没有办法将相关的数据存储在当前栈帧上,而且从这个例子中可以看出,一次调用可能没有办法获取完整的数据,意味着我们要保存上下文,这里就可以联想到协程了,我们可以将上下文保存在协程中,进而想到这一次IO操作也放在协程中处理。

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2015-2025 Xudong0722
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信