IO多路复用技术
原理:对大量描述符进行事件(可读/可写/异常)监控
作用:替进程监控大量描述符,告诉进程什么时候发生了什么事件,进程可以轮询针对发生了某个相应的事件描述符进行相应的操作。
适用场景:对大量描述符进行监控,但是同一时间只有少数描述符活跃的场景
IO多路转接(服务端高并发)三种模型:
select模型
函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
fd_set 是一个文件描述符集,大小由FD_SETSIZE决定,一般是1024
nfds 被监听的文件描述符的总数,一般为所有描述符的最大值+1;提高内核中监控效率
readfds、writefds、exceptfds 可读、可写、异常事件监控集合,关心哪种事件就将其添加到哪个集合中
timeout select默认是一个阻塞监控,timeout为超时等待时间,若为空则一直阻塞直到有描述符就绪或者出错
返回值:>0 就绪描述符个数 =0 等待超时,无描述符就绪 <0 监控出错(描述符被关闭)
使用之前需要先清除
原理及流程:
①用户将自己定义的相应事件的描述符添加到fd_set集合中;
②然后将自己关心的事件描述符添加到相应的集合中去
③select将集合拷贝到内核中进行轮询遍历,判断是否有描述符就绪了相应的事件
④若集合中有描述符就绪,则继续轮询遍历完集合,然后将没有就绪的描述符从集合中移除(此时集合中只剩就绪的描述符)
⑤进程通过轮询遍历所有的描述符是否在集合中,找出就绪的描述符(若描述符在集合中,则就是就绪的描述符),然后进行相应事件的操作
优点:遵循posix标准,支持跨平台;监控时间可精确到微秒
缺点:
①能够监控的描述符有限,因为监控集合上限为1024
②需要在内核中描述符进行轮询遍历,效率会随着描述描述符的增多而降低
③只给用户返回了一个就绪集合,没有告诉用户具体哪个描述符就绪了,需要用户进行二次轮询判断,效率会随着描述符的增多而降低的
④每次都需要将描述符拷贝内核中,占用内存
⑤UI修改将空集合,每次监控需要用户重新添加描述符
poll模型
原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
poll采用事件结构的方式对描述符进行相应事件的监控
fds 描述符事件结构数组
nfds 要监控的事件的个数
timeout 超时等待时间(毫秒)
原理及流程:
①用户定义一个描述符数组,然后将自己关心的事件描述符添加到数组中
②将pollfd事件数组拷贝到内核中进行轮询监控,判断描述符时候就绪了关心的事件
③将描述符实际就绪的事件信息添加到revents中
④当poll返回时,用户轮询遍历pollfd数组,判断每一个revents是否是自己关心的事件,进而对其进行相应的操作
优点:
①可监控描述符数量无上限
②poll采用事件结构形式对事件进行监控,简化了select三步集合操作的流程
缺点:
①不能跨平台,只能在Linux下使用
②与select的缺点类似,由于与select相比无多大改变,所以基本被淘汰
epoll模型(Linux下性能最高的到并发模型)
// 创建epoll
int epoll_create(int size) 创建epoll(在内核中创建eventpoll结构体),size决定最多监控多少描述符(Linux2.6.8之后被忽略,大于0即可)
返回一个文件描述符,作为epoll的操作句柄
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll采用事件结构方式对描述符进行事件监控
用户定义struct epoll_event描述事件结构信息;将事件信息可以拷贝到内核中添加到eventpoll结构体的红黑树中
epfd:epoll句柄
op:对内核eventpoll进行操作
EPOLL_CTL_ADD :向红黑树中添加描述符的监控事件结构信息event
EPOLL_CTL_MOD :从红黑树中删除描述符fd的监控时间结构信息event
EPOLL_CTL_DEL :修改描述符fd在红黑树中的对应事件结构信息event
fd:用户需要监控的描述符
event:描述符对应的事件结构信息
struct epoll_event {
uint32_t events; 用户对描述符进行监控的事件(EPOLLIN/EPOLLOUT)
union {
int fd; // void* ptr;
}data
}
// 开始监控
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
epfd:epoll句柄
events: epoll_event操作结构信息数组
maxevents 操作结构信息数组的节点数量
timeout 等待超时时间(毫秒)
返回值: <0 监控出错 ==0 监控超时 >0 返回就绪描述符事件个数
epoll_wait会将就绪的描述符对应事件结构信息拷贝到events结构数组中,相当于告诉了用户具体哪个描述符据徐。用户可以直接从epoll_event结构体数组中取出信息,对描述符直接进行相应事件操作
概念:epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
epoll监控流程:
①epoll对描述符的监控是一个异步阻塞操作,epoll_wait发起调用,让操作系统对描述符进行相应事件进行监控
②操作系统对每个要监控的描述符都定义一个就绪事件回调函数,当描述符对应的某个时间就绪的时候,触发事件,调用回调函数,将描述符事件结构信息指针添加到eventpoll的双向链表中
③此时epoll_wait并没有返回,而是每隔一段时间就看一下eventpoll中双向链表是否为空,进而判断有无描述符就绪,若链表为空则无描述符就绪,则挂起等待一会儿重新查看;若链表不为空则有描述符就绪,就将该描述符对应的事件结构信息拷贝到epoll_wait传入到事件结构数组中,然后调用返回。
epoll的事件触发方式:默认为水平触发
水平触发:只要接收缓冲区数据、发送缓冲区空闲空间大小大小大于低水位(1kb)时,事件就会一直触发
可读事件就绪:接收缓冲区数据大于低水位
可写事件就绪:发送缓冲区空闲空间大于低水位
边缘触发:只有当有新数据到来时可读事件才会被触发一次,需要用户在本次触发中将缓冲区中的数据尽量一次性读完(循环读取,直到缓冲区中没有数据为止,但是程序会卡死在recv处),为了避免一次性读取数据造成程序卡死,需要用户手动将描述符设置为非阻塞。
可读事件就绪:接收缓冲区中,只有当有新数据到来时,事件才会被触发一次
可写事件就绪:发送缓冲区中,只有当空闲空间大小变为大于0时才会触发一次
设置非阻塞方法:
int fcntl(int fd, int cmd, … /* arg */ );
fd :要设置的描述符
cmd :对描述符要进行的操作
优点:
①监控描述符无上限
②epoll_wait使用异步非阻塞操作在内核中完成事件监控;事件监控是操作系统通过事件回调的方式将就绪描述符事件信息添加到双向链表中;而epoll_wait只是每隔一段事件看一下双向链表中是否为空判断是否有就绪描述符,性能不会因为描述符的增多而降低
③epoll直接通过epoll_wait传入的事件结构数组向用户返回具体就绪事件信息,可以直接告诉用户哪些描述符就绪,不需要用户进行空遍历查找
缺点:无法跨平台,相对于selec监控时间无法精确到毫秒
epoll的惊群问题:当多个进程/线程同时阻塞在epoll_wait等待同一个事件时,如果这个事件发生了,此时就会唤醒所有的进程,但是最终只有一个进程/线程处理了该事件,其他进程/线程就会进入休眠状态,造成资源浪费的现象
产生原因:某一事件发生时,同一时刻大量进程被唤醒解决方法:个子进程在epoll_wait()之前先去申请锁,申请到则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量