I/O多路复用技术
概述
为了了解i/o多路复用技术,本文将由以下思路对i/o多路复用技术进行阐述:
- 什么是i/o多路复用
- 为什么需要i/o多路复用
- i/o多路复用有哪些
- i/o多路复用是如何实现的
本文仅代表作者一些浅显的观点,如有问题欢迎指正,阅读本文之前建议了解下5种i/o模型,以及5种i/o模型大概的流程。
什么是I/O多路复用技术
I/O复用技术是5种I/O中的一种同步I/O模型,其通过将多个i/o操作放入同一进程或者线程中进行处理,实现仅使用少量进程或者线程就可以同步处理大量i/o操作的功能。
为什么使用I/O复用技术
提到为什么使用i/o多路复用技术时就不得不提到其他i/o模型的缺陷。对于其他之前出现i/o模型对于每一个i/o操作都需要生成一个对应的进程或者线程进行处理,在并发量大的情况下所耗费的资源是非常大的,只适用于并发量小的情况。而在网络编程中,通常并发量都很大,之前的i/o模型应对这种特殊情况表现较差,为了应对这种并发量大的情况i/o多路复用技术则应运而生。
I/O复用技术有哪些
现有的i/o复用技术主要包括了一下三种:
- select:
- 首先,select将在用户态将需要检测的描述符放入fd队列中,并将整个fd队列拷贝到内核态中同时阻塞select进程;
- 其次,在内核态中在给定时间内不断的监听是否有描述符所对应的数据到达,如果有数据到达或是时间耗尽则将fd队列拷贝回用户态并唤醒select进程;
- 最后,select进程遍历整个fd队列查看是否有某个描述符需要进行i/o;
- poll:
poll与select非常类似,只是在描述fd队列的方式不同,在select中默认fd队列的大小为1024,而poll描述fd队列使用了链表的储存方式,没有最大文件描述符数量的限制。 - epoll
为了解决select与poll中,每次判断描述符都需要将所有描述符都从内核态拷贝回用户态并进行一次遍历,这过程中所耗费的资源较多,因此为了更快的查找到需要进行操作的描述符出现了epoll这种方式。- 首先,通过epoll_create在内核态中开辟一个句柄,这个句柄占据1个fd大小的空间;
- 其次,通过epoll_ctl将文件描述符按顺序放入epoll_create所生产的句柄之后,通常来说使用的是红黑树的存储结构,查找所需要的时间复杂读为O(log n);
- 最后,在所用文件描述符都放入内核态中后,调用epoll_wait来等待描述符i/o事件的到来,如果某一个fd对应的数据准备完毕,只需要将对应的fd拷贝进用户态而不需要将整个fd集合都拷贝如内核态,节省了拷贝与遍历集合的时间。
更直观的流程图可见参考文章
I/O复用技术是如何实现的
select是如何实现的
select中通过fd_set作为文件描述符,其实现使用了long类型的数组,其结构可以理解为:
typedef struct{
long int fds_bits[32];
}fd_set;
其中每一位可以代表一个文件描述符,所以fd_set最多表示1024个文件描述符,因此创建一个select进程最多可以同时监听1024个socket,这限制了select在i/o量更大情况下的性能。
可通过以下四个宏进行设置:
- void FD_ZERO(fd_set *fdset); //清空集合
- void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
- void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
- int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
#include <sys/select.h>
int select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
可以看出select中文件描述符可以分为三类:readfds、writefds、exceptfds;
timeout则代表内核需要等待多少时间后唤醒该进程,其输入可以分为一下三类:
- NULL:代表除非有描述符准备好I/O时才返回;
- 0:检查描述字后立即返回,一般在轮询的方法中使用;
- 正数:等待一段时间,如果在时间内有描述符准备好I/O时则返回;
poll是如何实现的
poll中使用pollfd作为文件描述符
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
} ;
events域是监视该文件描述符的事件掩码,由用户来设置这个域。
revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。
# include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
从poll的输入中可以看出其与select的区别,select文件描述符分为了readfds、writefds、exceptfds三类,而poll直接进行了统一,其具体等待什么样的时间则通过pollfd中evenets来表示。同时poll模型里面通过使用链表的形式来保存自己监控的fd信息,没有存储数据的上线,解决了select中最多是能放下1024个文件描述符的问题。
epoll是如何实现的
epoll中文件描述符的定义如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events描述事件类型,其中epoll事件类型有以下几种:
- EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
- EPOLLOUT:表示对应的文件描述符可以写
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
- EPOLLERR:表示对应的文件描述符发生错误
- EPOLLHUP:表示对应的文件描述符被挂断
- EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
相比于poll,epoll描述的事件类型增加了ONESHOT这种方式。
- epoll_creart()
创建一个指示epoll内核事件表的文件描述符
#include <sys/epoll.h>
int epoll_create(int size)
- epoll_ctl()
操作内核事件表监控的文件描述符上的事件:注册、修改、删除
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
- epfd:为epoll_creat的句柄
- op:表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd);
EPOLL_CTL_MOD (修改已经注册的fd的监听事件);
EPOLL_CTL_DEL (从epfd删除一个fd); - event:告诉内核需要监听的事件
- epoll_wait()
等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
-
events:用来存内核得到事件的集合
-
maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size
-
timeout:是超时时间
- -1:阻塞
- 0:立即返回,非阻塞
- >0:指定毫秒
-
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
epoll事件
水平触发(level-triggered)
当存储被监控socke描述符的队列skb(SocketBuffer)非空时,服务器不断的从epoll_wait状态下苏醒,发出读事件,需要不断的将数据从内核中取出,直到内核中数据都被取出才停止发出读事件;
// 当存储被监控socke描述符的队列不满时,就不断
边缘触发(edge-triggered)
当存储被监控socke描述符的队列skb(SocketBuffer)中有需要被读地数据时,服务器只会从epoll_wait状态下苏醒一次,需要一次性将队列中所有需要被取出的数据从内核中取出。
其他
epoll与select有哪些区别
参考文章: