在读redis源代码的过程中,我一直在考虑一个问题,就是“为什么单线程的redis能做到如此高效?”。为了弄清楚这个问题,我查阅了一些资料,大概搞清楚了epoll等I/O模型的发展及其原理,以下是一个记录整理。

##I/O模型

###操作系统与网络I/O

多种I/O模型及其对socket效率的改进_socket

上图来自维基百科,是一个基本的计算机结构。计算机主要完成两个工作,运算和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就不能响应其他请求,效率很低。

多种I/O模型及其对socket效率的改进_socket_02

####非阻塞IO

使用轮询的方式,会占用大量的计算资源,应该只有特殊情况才会使用到。

多种I/O模型及其对socket效率的改进_IO模型_03

####IO复用

关键在于复用,一个进程可以一次性等待多个IO。 

多种I/O模型及其对socket效率的改进_IO模型_04   

####信号驱动IO

使用范围很小,在TCP下信号产生的过于频繁难以区分含义,所以只在使用UDP协议的程序中应用。“作者能找到的实际使用信号驱动的I/O程序是基于UDP的NTP服务器程序”。与异步IO的理念区别仅仅在于是否由操作系统自动拷贝数据到内核空间,我不明白为什么不直接发展成异步IO,反而要加入这个模型。

多种I/O模型及其对socket效率的改进_IO模型_05

####异步IO

整个过程进程都是非阻塞的。

多种I/O模型及其对socket效率的改进_socket_06

前4种都是同步的,第5种是异步


##socket

###socket基本概念

socket工作在会话层以上,通过绑定并监听指定端口,接收数据。

多种I/O模型及其对socket效率的改进_socket_07

###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

【20】mickhan/socket_demo

【21】unix下的I/O------阻塞,非阻塞,同步,异步 - 51CTO.COM