I/O复用的出现,使得程序能够同时监听多个文件描述符,并在提高程序性能方面发挥了重要作用,其中I/O复用使用最多的情景是TCP服务器要同时处理监听socket和连接socket。
虽然I/O复用具有监听多个文件描述符的功能,但是其本质上是阻塞的,因为当多个文件描述符就绪时,程序自身只能按顺序依次处理每一个文件描述符,这看起来像是程序在串行执行,并不能实现并发(实现并发只能依靠多进程和多线程编程)。
在Linux系统下,实现I/O复用的系统调用主要是select、poll、epoll。这里我们只介绍poll和epoll。
首先来看一下poll。
poll
poll系统调用是在指定的时间内,轮询一定数量的文件描述符,来测试其中有没有就绪的文件描述符,这一点基本与select一样。poll函数原型如下:
int poll(struct pollfd *fds,nfds_t nfds,int timeout) //需要引用<poll.h>头文件
下面我们来分析一下每一个参数的作用
- 参数fds是一个pollfd结构类型的数组,它指定所有用户关注的文件描述符上发生的可读、可写、和异常等事件。pollfd结构体定义如下:
struct pollfd
{
int fd; //用户关注的文件描述符
short events; //用户关注的事件
short revents; //由内核修改,表示发生的事件集合
}
- 参数nfds表示数组的长度,元素的个数,或者用户关注的文件描述符个数
- 参数timeout表示超时时间
- 返回值为int,返回-1表示出错,0表示超时,>0则表示就绪文件描述符的个数
相比于select系统调用,poll使得:
- 用户关注的事件类型更多
- 内核修改的文件描述符和用户关注的文件描述符分开表示,并且每次调用不需要重新设置
- 文件描述符不再是按位表示,而是采用int类型,所以用户关注的文件描述符取值范围比以前更大,文件描述符用户可以自己设置。
epoll
首先epoll是Linux系统特有的I/O复用函数,与select和poll有着很大的差异。
- epoll使用一组函数来监听用户关注的文件描述符
- epoll将用户关注的文件描述符存放在内核里的一个事件表中,每次调用不需要重复传入文件描述符集或事件集
- epoll需要使用额外的文件描述符来唯一标识内核中的事件表
epoll的一组函数:
int epoll_create(int size); //用来创建内核事件表
int epoll_ctl(int epollfd,int op,int fd,struct epoll_event *event);
//设置(添加,修改,删除)内核事件表的文件描述符的事件
epoll_wait(int epollfd,struct epoll_event *event,int maxevents,int timeout);
epoll和poll的区别:
- 返回的文件描述符不同。poll返回所有用户关注的文件描述符(就绪的和未就绪的),epoll只返回就绪的文件描述符,所以poll用户程序检测就绪文件描述符的时间复杂度为O(n),epoll则为O(1);
- 函数数量不同。poll系统调用只有poll一个函数,epoll系统调用则是一组函数,分别是:
函数 | 功能 |
---|---|
epoll_create | 创建文件描述符 |
epoll_ctl | 操作epoll内核事件表 |
epoll_wait | 检测文件描述符上的事件 |
- 函数参数不同。poll将用户关注的事件类型和内核修改的事件类型分离开表示,而epoll则是由内核事件表维护用户关注的文件描述符上的事件类型;
- 调用时文件描述符的拷贝不同。poll每次调用都需要将用户空间的数据拷贝到内核空间,返回时又将内核空间的数据再拷贝到用户空间,而epoll只会调用epoll_ctl时拷贝一次,epoll_wait调用时只从内核空间向用户拷贝就绪的文件描述符;
- 内核实现方式不同。poll采用轮询的方式检测就绪事件,epoll则采用回调方式;
- 支持的工作模式不同。poll只能在LT模式下工作,epoll则支持更高效的ET模式。