多路I/O复用:内核监听多个文件描述符的属性(读写缓冲区)变化,如果某个文件描述符的读缓冲区变化了,这个时候就是可以读了,将这个事件告知应用层
一、Select
1.函数API
select函数的API:
//监听多个文件描述符的属性变化(读、写、异常)
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
FD_ZERO(int fd, fd_set* fds);
FD_SET(int fd, fd_set* fds);
FD_ISSET(int fd, fd_set* fds);
FD_CLR(int fd, fd_set* fds);
2.select的使用
1)运行机制
select()的机制中提供一种fd_set的数据结构,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。
select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
2)使用步骤
- 使用FDZERO初始化一个fd_set对象。
- 调用FD_SET,把套接字加入到fd_set集合中。
- 调用select函数,等待返回。
- 完成后返回所有的fd_set集合中设置的套接字句柄的总数,对每个集合进行了相应的更新。
- 根据select函数的返回值,联合FD_ISSET对fd_set进行检查。
- 知道了集合中待解决的IO操作后,对IO进行处理。 返回1,继续进行select的调用处理。
3.优缺点
- 优点:跨平台
- 缺点:
- 文件描述符1024的限制,由于FD_SETSIZE的限制
- 只返回文件描述符的个数,具体的哪个变化需要遍历
- 每次都需将需要监听的文件描述符集合由应用层拷贝到内核
- 大量并发,少了活跃,select效率低
二、Poll
1.介绍
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理。
但是poll没有最大文件描述符数量的限制,select为每个条件构造一个描述符集,而poll则是构造一个pollfd结构体数组,每个数组元素为一个结构体指针,指定一个描述符标号及其所关心的条件。
2.函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
typedef struct pollfd {
int fd; // 需要被检测或选择的文件描述符
short events; // 对文件描述符fd上感兴趣的事件
short revents; // 文件描述符fd上当前实际发生的事件
} pollfd_t;
三、Epoll
Epoll 在 Select 和 Poll 的基础上引入了 eventpoll 作为中间层,使用了先进的数据结构,是一种高效的多路复用技术。
1.特点
- 没有文件描述符1024的限制
- 以后每次监听都不需再次将需要监听的文件描述符拷贝到内核
- 返回的是已发生变化的文件描述符,不需要遍历树
2.步骤
- 调用 epoll_create 建立一个 epoll 对象(在 epoll 文件系统中给这个句柄分配资源)
- 调用 epoll_ctl 向 epoll 对象中添加连接的套接字
- 调用 epoll_wait 收集发生事件的连接
这样只需要在进程启动的时候建立一个 epoll 对象,并在需要的时候向它添加或者删除连接就可以了,因此,在实际收集的时候,epoll_wait 的效率会非常高,因为调用的时候只是传递了发生 IO 事件的连接。
3.工作模式
epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
- 水平触发:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait() 时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。
- 边缘触发:当被监控的文件描述符上有可读写事件发生时,epoll_wait() 会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
四、总结
图片来源《Linux 高性能服务器编程》