在读redis源代码的过程中,我一直在考虑一个问题,就是“为什么单线程的redis能做到如此高效?”。为了弄清楚这个问题,我查阅了一些资料,大概搞清楚了epoll等I/O模型的发展及其原理,以下是一个记录整理。
##I/O模型
###操作系统与网络I/O
上图来自维基百科,是一个基本的计算机结构。计算机主要完成两个工作,运算和I/O。因为CPU处理效率以及各种设备的I/O效率千差万别,所以协调I/O提高效率是操作系统的一个重要任务。各种I/O模型应运而生。
####同步和异步
计算机任务执行的实体是进程,进程和I/O的关系需要理清。根据执行I/O时进程的状态,可以把I/O操作分为同步和异步两种方式。
* 同步IO操作:引起进程的阻塞直到IO操作完成
* 异步IO操作:IO操作不会引起进程阻塞
其中同步和异步指进程和IO的关系,阻塞和非阻塞指进程的状态。结合操作系统的进程管理,可以认为最优的状态是:a)进程完全不被影响,继续执行直到I/O完成再处理 b)进程完全挂起,把资源交给其他进程,I/O完成后再醒来继续执行。
###网络IO模型
####阻塞IO
最基础的方式,问题在于进程阻塞于一个IO就不能响应其他请求,效率很低。
####非阻塞IO
使用轮询的方式,会占用大量的计算资源,应该只有特殊情况才会使用到。
####IO复用
关键在于复用,一个进程可以一次性等待多个IO。
####信号驱动IO
使用范围很小,在TCP下信号产生的过于频繁难以区分含义,所以只在使用UDP协议的程序中应用。“作者能找到的实际使用信号驱动的I/O程序是基于UDP的NTP服务器程序”。与异步IO的理念区别仅仅在于是否由操作系统自动拷贝数据到内核空间,我不明白为什么不直接发展成异步IO,反而要加入这个模型。
####异步IO
整个过程进程都是非阻塞的。
前4种都是同步的,第5种是异步
##socket
###socket基本概念
socket工作在会话层以上,通过绑定并监听指定端口,接收数据。
###socket示例
最简单的accept()方式是阻塞的,在建立连接之前程序挂起。可以使用while循环accept,不过这种方式read()也是阻塞的,所以在第一个连接结束之前,第二个连接无法被处理。
写代码中的两个有趣的点:
* listen函数的backlog有一个magic number 511
* close()之后有一个time_wait的过程,这时候如果再绑定相同端口会失败,可以使用setsockopt()
##改进
###使用多进程改进
在while循环中,每次accept就fork出一个新进程,这样就能同时处理多个连接。这里需要处理父进程与子进程的关系,引用计数,信号处理等问题。而且每次创建销毁进程消耗较大。
###使用select改进
在while循环中,使用select监听一个fd集合,当其中的fd可读/写/异常时从阻塞恢复。可以同时监听多个fd。当然读写也会阻塞,所以要配合多进程/多线程使用。因为不需要每个连接就生成一个新进程,比fork的方式要更优化。
存在的问题是:
* fd数量有限
* 每次都循环检查所有fd效率低
* 用户态和内核态内存拷贝效率低
###使用epoll改进
epoll针对select的问题做了优化:
* 上限是最大可以打开文件的数目
* 只关注“活跃”的链接不用循环检查
* 使用内存共享避免拷贝
使用epoll 主要调用3个API :
int epoll_create(int size); //2.6.8之后size参数被忽略,参考
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
另外特别需要注意的是水平触发(LT)和边缘触发(ET)的概念,以及events的处理(出错,client关闭等等)。
以上几种方式我都简单实现了,代码见【20】。
###性能对比测试
这篇论文【18】 对select/poll/epoll的性能做了对比测试。可以看出在所有I/O都活跃的情况下,select和epoll的性能相近。但存在大量空闲连接时,epoll的性能就明显高于select了,这与epoll的改进思路是相符合的。
在查找资料的过程中,我发现大部分资料都语焉不详或者思路不清晰。所以把IO和socket作为前置知识,结合了一些代码示例。希望应该能更好的理清思路,理解I/O模型为什么要这样设计和实际应用中为什么要这样改进。写的东西不多,干货都在参考文章中。
参考
【1】Unix 五种基本I/O模型的区别 - 语行 - 博客园
【2】Operating Systems: I/O Systems
【3】OSI model - Wikipedia, the free encyclopedia
【4】简单理解Socket - dolphinX - 博客园
【5】Linux Howtos: C/C++ -> Sockets Tutorial
【6】linux文件设备与I/O:read/write函数 与 阻塞 Block_面包坊_百度空间
【7】socket编程-listen函数之backlog_飞翔的鱼在北京_新浪博客
【8】[C/C++] 解決Socket連續Bind同一個Port的問題 | 不務正業紀實
【9】linux 多进程 缺点 - 网摘记录 - ITeye技术网站
【10】The GNU C Library: Server Example
【11】Linux Epoll介绍和程序实例 - sparkliang的专栏 - 博客频道 - CSDN.NET
【12】Linux下select, poll和epoll IO模型的详解 - tianmo2010的专栏 - 博客频道 - CSDN.NET
【13】UNIX网络编程--I/O复用:select函数和poll函数讲解(六) - 鱼思故渊的专栏 - 博客频道 - CSDN.NET
【14】epoll 或者 kqueue 的原理是什么? - 知乎 Epoll detailed
【15】How to use epoll? A complete example in C - Banu Blog
【16】epoll(4): I/O event notification facility - Linux man page
【17】epoll_create(2): open epoll file descriptor - Linux man page
【18】https://www.kernel.org/doc/ols/2004/ols2004v1-pages-215-226.pdf
【19】networking - Caveats of select/poll vs. epoll reactors in Twisted - Stack Overflow
【21】unix下的I/O------阻塞,非阻塞,同步,异步 - 51CTO.COM