IO模型解惑

本文基于《构建高性能网站》整理。之前对于各种IO模型的理解不是很清晰,发现这本书里整理得比较好,这里记录下相关要点。

IO操作根据设备类型一般分为内存IO,网络IO,和磁盘IO。其中内存IO的速度大大快于后两者,计算机的性能瓶颈一般不在于内存IO. 尽管网络IO可通过购买独享带宽和高速网卡来提升速度,可以使用RAID磁盘阵列来提升磁盘IO的速度,但是由于IO操作都是由系统内核调用来完成,而系统调用是通过cpu来调度的,而cpu的速度远远快于IO操作,导致会浪费cpu的宝贵时间来等待慢速的IO操作。为了让cpu和慢速的IO设备更好的协调工作,减少CPU在IO调用上的消耗,逐渐发展出各种IO模型。

同步阻塞I/O

阻塞是指当前发起IO操作的进程被阻塞。同步阻塞IO是指当进程调用某些IO操作的系统调用或者库函数时,比如accept(),send(),revc()等,进程会暂停下来,等待IO操作结束后再继续运行。

这种IO模型比较简单,可以和多进程结合起来有效的利用CPU资源,但这样的代价是需要多进程的内存开销。

 

同步非阻塞I/O

同步阻塞IO中,进程的等待时间可能包括两个部分,一个是等待数据就绪,比如等待数据可以读和可以写;另一个是等待数据的复制,当数据准备好后,读写操作的耗时。

同步非阻塞IO的调用的区别在于,不会去等待数据的就绪,如果数据不可读或者不可写,相关系统调用会立即告诉进程(立即返回)。 比如使用非阻塞recv()接受网络数据后,函数就及时返回,告诉进程没有数据可读了。

其好处是如果结合反复轮询来尝试数据是否就绪,那么在一个进程里可以同时处理多个IO操作。问题在于需要进程来轮询查看数据是否就绪,进程处于忙碌等待状态。

非阻塞IO一般只针对网络IO有效,当我们在socket的选项设置中使用O_NONBLOCK时,这个socket的send()或者recv()就会采用非阻塞方式。 对于磁盘IO非阻塞IO并不产生效果。

多路I/O就绪通知

多路IO就绪通知模型下,允许进程通过一种方法来同时监视所有文件描述符,并且可以快速获得所有就绪的文件描述符,然后只针对这些描述符来进行数据访问。它提供了对大量文件描述符就绪检查的高性能方案。

要注意的是,IO就绪模型只是解决了快速获得就绪的文件描述符的问题,在得知数据就绪后,就数据访问本身而言,还是需要选择阻塞或者非阻塞的访问方式。

由于平台和历史的原因,多路IO就绪通知有多种不同的实现,性能也存在一定的差异。

  • select

select最早出现于1983年4.2BSD中,通过一个select()系统调用来监视包含多个文件描述符的数组,当select()返回后,这个数组中就绪的文件描述符会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

其缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在linux上一般为1024,不过可以通过修改宏定义或者重新编译内核的方式提升这一限制。所以假如使用的select()的服务器已经维持了1024个连接,后续的请求可能会被拒绝。

另外,select()维护着的存储大量文件描述符的数据结构,随着文件描述符数量的增长,其复制开销也线性增长。

另一方面,由于网络延迟使得大量tcp连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,会浪费一定的开销。

  • poll

poll在1986年诞生于System V Release3.显然unix不愿意直接沿用BSD的select,而是重新实现了一遍。poll和select在本质上没有太多区别,但是poll没有最大文件描述符数量的限制。

poll和select的一个共同缺点是,包含大量文件描述符的数组在用户态和内核的地址空间被整体复制,而不论这些文件描述符是否就绪,其开销随文件描述符数量的增加而线性增长。

另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()或者poll()时会再次报告这些文件描述符, 所以他们一般不会丢失就绪的消息,这种方式成为水平触发(Level Triggered)。

  • SIGIO

linux2.4上提供了SIGIO, 它通过实时信号(Real Time Signal)来实现select/poll的通知方法。不同在于,select/poll告诉我们哪些文件描述符是就绪的,直到我们读写它之前,每次select/poll都会告诉我们;而SIGIO则仅仅告诉我们哪些文件描述符刚刚变为就绪状态,它之说一遍,如果我们没有采取行动,SIGIO不会再次告知,这种方式被称为边缘触发(Edge Triggered). SIGIO几乎是linux2.4下性能最好的多路IO就绪通知方法。

SIGIO的缺点在于,代表事件信号由内核中的事件队列来维护,信号按照顺序执行通知,这可能导致一个信号到达时,该事件已经过期,其所描述的文件描述符已经被关闭。另一方面,事件队列的长度是有限制的,有可能被事件装满,很容易发生事件丢失,所以需要采用其他方法来弥补损失。

  • /dev/poll

sun在Salaris中提供了新的实现方法,它使用虚拟的/dev/poll设备,我们可以将需要监视的文件描述符数组写入这个设备,然后调用loctl()来等待时间通知。当loctl()返回就绪的文件描述符后,我们可以从/dev/poll中读取所有就绪的文件描述符数组,这点类似SIGIO节省了扫描所有文件描述符的开销。

在linux下有很多方法可以实现类似/dev/poll的设备,但都没有提供直接的内核支持,这些方法在服务器负载较大时性能不够稳定。

  • /dev/epoll

名为/dev/epoll的设备以补丁的形式出现在linux2.4上,它提供了类似/dev/poll的功能,而且增加了内存映射(mmap)技术,在一定程度上提高了性能。

但是/dev/epoll仍然只是一个补丁,linux2.4并没有将它的实现加入内核。

  • epoll

linux2.6出现了由内核直接支持的实现方法epoll,它几乎具备了之前所说的一切优点,被公认为是linux2.6下性能最好的多路IO就绪通知方法。

epoll可以同时支持水平触发和边缘触发,理论上边缘触发的性能会高一些,但是代码实现相当复杂,因为任何意外的丢失事件都会造成请求处理错误。在默认情况下epoll采用水平触发,如果使用边缘触发,则需要在事件注册时增加EPOLLET选项。

在Lighttpd中的epoll模型代码中注释掉了EPOLLET,没有使用边缘触发方式:

ee.events |= EPOLLIN|EPOLLOUT /*|EPOLLET*/;

而在nginx的epoll模型代码里使用了边缘触发:

ee.events = EPOLLIN|EPOLLOUT|EPOLLET;

 

在epoll中同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时并不是返回实际的描述符,而是一个代表就绪描述符数量的值,需要取epoll指定的一个数组中依次取的相应数量的文件描述符。这里也使用了内存映射技术,这样便彻底省掉了在系统调用时复制这些文件描述符的开销。

另一个本质的改进在于epoll采用基于时间的通知方式。在select/poll中,进程只有在调用一定的方法后,内核才将所有的文件描述符进行扫描。而epoll事先通过epoll_ctl()来注册每一个文件描述符,一旦某个文件描述符就绪,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时就会获得通知。

  • kqueue

FreeBSD中实现了kqueue,kqueue像epoll一样可设置水平触发或者边缘触发,同时kqueue可以用来监视磁盘文件和目录,但是它的api在很多平台上不支持,而且文档匮乏。kqueue和epoll的性能非常接近。

原创文章,作者:kepupublish,如若转载,请注明出处:https://blog.ytso.com/98837.html

(0)
上一篇 2021年8月21日
下一篇 2021年8月21日

相关推荐

发表回复

登录后才能评论