在之前的同步异步文章中埋了个坑,今天就来填一下吧。当CPU发起IO调用,需要读取文件里的东西时,由于IO处理的速度没有CPU那么快,因此,通常都是CPU在等待IO处理完毕返回数据。当IO处理中到CPU拿到文件数据,这里存在几种IO模型,分别是read,select,poll,epoll,kqueue。在此先说一下fd,系统在读写文件的时候,需要文件描述符(file descriptor简称fd),当调用系统内核来进行文件操作时,内核会返回一个文件描述符,若要知道IO是否处理完毕,只需查看fd中的事件状态即可,即程序调用内核时就是查看这个状态,来判断是否完成操作的。下面就简单介绍下这几种IO模型。

read

这是原始的轮询模式,当CPU发起IO调用时,处于等待状态,然后IO就去处理,然后就重复的检查fd的事件状态,当事件状态为处理完毕后,读出完整数据。这种做法比较原始以及性能较差。

select

select是在read上的改进,有一个数组负责存着一堆文件描述符,然后遍历数组中的文件描述符来检查当中的事件状态,当某个文件已经处理完毕之后,就进行一次read操作来读取完整数据,然后交给应用。

select的缺点有两个。①是存放文件描述符的数组大小有限,一个进程处理的文件描述符(fd)数有限(cat /proc/sys/fs/file-max).32位机默认是1024个。64位机默认是2048.②随着fd的增多,会造成性能线性下降的问题(主因线性遍历)。

poll

poll则是在select的基础上进行改良,采用链表的数据结构来存储文件描述符,以解决select的数组大小限制问题。但是还是需要线性遍历,因此select的第二个缺点还是无法避免。

epoll

epoll只能在Linux使用。epoll是目前性能强劲的io复用模型,epoll的原理是每当操作文件时,都有一个回调函数与fd相对应,当io操作完毕后,就会调用这个回调函数来通知应用获取数据。

epoll中由三个函数,分别是epoll_create,epoll_ctl,epoll_wait。

1
2
3
4
#include <sys/epoll.h>
int epoll_create(int 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);

epoll_create用于创建epoll句柄,然后epoll来监听管理一定量的文件描述符。当然这个句柄本身就占一个fd。

epoll_ctl用于控制管理一个epoll句柄下面N个的fd,比如可以对一个epoll句柄下的某个fd进行增删该事件监听,这里监听的事件指的是epoll观察指定fd的状态,比如某个fd需要读写数据等。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
};
typedef union epoll_data
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

epoll_wait,根据触发方式(水平触发或者边缘触发),来收集刚刚epoll_ctl中已注册并且触发的事件(就绪链表),比如收集了fd为2,3的已经触发的事件,然后把这些事件赋值给一个数组(参数二中的events数组),如果没有超时(timeout),epoll_wait就会返回 触发事件的数目 ,然后去指定事件数组里拿相应数目的文件描述符(epoll_event储存了许多信息,里面的data就包含文件描述符)即可,这样就可以达到不复制文件描述符省去巨大的开销。同时也epoll只需拿到触发事件的文件描述符及其结果,而不像select和poll一样把不活跃的fd都一同获取了。

虽然说epoll_wait也是像select和poll一样需要轮询,不过它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空。另外epoll使用了mmap(内存映射技术),加速与内核与用户空间的消息传递,避免不必要的内存拷贝。

kqueue

kqueue与epoll一样都是基于事件的方式来处理IO复用的问题,只是kqueue是应用在BSD系统上,在此就不在赘述。

水平触发 & 边缘触发

LT:水平触发,支持阻塞和非阻塞io,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。select和poll只支持水平触发。

ET:边缘触发,仅支持非阻塞io,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。epoll支持边缘触发。

总结

read属于原始的IO模型,而select,poll,epoll,kqueue都是属于IO复用模型,可以管理一定量的文件描述符。epoll基于事件的处理方式在处理大量的连接时仍保持这不错的性能,有效的提高了IO效率。

参考

http://blog.csdn.net/yusiguyuan/article/details/15027821

http://blog.csdn.net/xiajun07061225/article/details/9250579

http://www.hulkdev.com/posts/epoll-io

http://blog.csdn.net/jay900323/article/details/18141217/

http://blog.csdn.net/tianmohust/article/details/6677985/

http://www.cnblogs.com/Anker/p/3263780.html