目录
1、多路转接IO
2、select
流程原理:
(1)用户定义描述符集合(可读的,可写的,异常的)并且进行初始化,将需要监控指定事件的描述符添加到指定的时间描述符集合中
(2)将集合拷贝到内核中,对集合中的描述符进行轮询遍历判断是否有描述符就绪了事件(等待超时或有描述符就绪了,监控调用返回,返回之前将集合中没有就绪对应事件的描述符从集合中移除出去)
(3)因为select模型监控返回时,从集合中移除了没有就绪的描述符,因此,进程只需要判断哪个描述符还在集合中就能知道哪个描述符就绪了哪个事件,进而进行对应的操作
select优缺点分析:
优点:
缺点:
3、poll
流程原理:
(1)用户定义描述符事件结构体数组,将需要监控的描述符组织事件信息进行添加
(2)发起监控调用开始监控
(3)监控调用返回后,遍历所监控的描述符事件结构体数组,通过每个节点中的revents成员确定,当前节点的描述符就绪了什么事件进而进行处理
poll优缺点分析:
优点:
缺点:
4、epoll
流程原理:
(1)创建epoll句柄
(2)向内核的epoll中添加要监控的描述符事件
(3)发起调用开始监控
(4)epoll_wait返回后只需要根据返回值遍历events数组就可以逐个对就绪的描述符进行操作
epoll的工作方式
水平触发(LT)
边缘触发(ET)
epoll优缺点分析:
优点:
缺点:
多路转接IO:也叫多路复用IO,是一种模型技术——针对大量的描述符进行就绪事件监控,判断哪个描述符就绪了指定事件,让进程能够针对就绪的描述符进行操作,进而提高进程对描述符的处理效率,并且可以对某个描述符IO进行超时控制
应用场景:只有对描述符进行监控的需求或想要对描述的IO进行超时控制的需求都可以使用多路转接模型,常被用于高并发服务器中的事件触发模型
适用于有大量描述符需要监控,但是同一时间只有少量活跃的场景,或者就是单个描述符的IO超时控制的场景
多路转接模型与线程池:
多路转接模型用于实现并发服务器——针对就绪轮询处理,缺点:只适用于大量监控,少量活跃的场景
线程池用于实现并发服务器——基于操作系统的调度轮询,缺点:有可能线程池中的线程处于空等待状态
因此,在并发服务器中,两者搭配使用(多路转接模型进行事件监控,有描述符就绪抛入线程池中进行处理)
实现:select、poll、epoll
select模型:描述符集合fd_set结构体中只有一个元素(数组),被当做位图使用,位图的大小取决于宏__FD_SETSIZE,默认1024
fd_set rfds;
void FD_ZERO(fd_set *set);—清空集合 例:FD_ZERO(&rfds);
void FD_SET(int fd, fd_set *set);—添加描述符集合(设置set中的fd位) 例:FD_SET(0, &rfds);
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数nfds是需要监控的最大的文件描述符值+1
rdset,wrset,exset分别对应需要监控的可读文件描述符集合,可写文件描述符集合,异常文件描述符集合,不监控则传空
timeout为监控的等待超时控制,timeout取值为0表示不阻塞,取值为NULL表示永久阻塞,是一个timeval结构体
struct timeval{
time_t tv_sec;
time_t tv_usec;
};
返回值:出错返回-1,没有就绪的描述符返回0,成功返回就绪的描述符个数(大于0)
int FD_ISSET(int fd, fd_set *set);—判断fd描述符是否在set集合中
void FD_CLR(int fd, fd_set *set);—从set集合中移除fd描述符
遵循posix标准,跨平台移植性好
struct pollfd{
int fd; —需要监控的文件描述符
short events; —要监控的事件,POLLIN(可读)/POLLOUT(可写)
short revents; —实际就绪的事件
};
events和revents取值:
事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
POLLIN | 数据(普通数据和优先数据)可读 | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级带数据可读(Linux不支持) | 是 | 是 |
POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
POLLOUT | 数据(普通数据和优先数据)可写 | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭或对方关闭了写操作 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起,比如管道写端被关闭,读端描述符将收到POLLHUP事件 | 否 | 是 |
POLLNVAL | 文件描述符没有打开 | 否 | 是 |
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:描述符事件结构体数组首地址
nfds:数组中有效事件个数
timeout:超时时间,单位是毫秒
返回值:成功返回就绪的事件个数,失败返回-1
int epoll_create(int size);
size必须大于0,在Linux2.6.8后是被忽略的
返回值:成功返回epoll操作句柄,失败返回-1
注意:使用完之后,必须调用close()关闭
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create返回的操作句柄
op取值:
EPOLL_ CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除fd
fd:要监控的描述符
event:针对要监控的描述符所组织的事件结构
struct epoll_event{
uint32_t events; —要监控的事件以及返回后的实际就绪事件
union{
int fd; —要监控的描述符
void *ptr;
}data;
};
返回值:成功返回0,失败返回-1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:epoll_create返回的操作句柄
events:是一个epoll_event事件结构体数组首地址,用于获取就绪描述符事件
maxevents:events数组的节点数量
timeout:超时时间,单位毫秒(为0则立即返回,为-1则永久阻塞)
返回值:失败返回-1,超时返回0,成功返回就绪的事件个数
epoll_wait:是一个异步阻塞接口,监控任务交给系统完成,系统针对每个描述符的就绪事件作出回调,回调处理中,是将就绪的描述符对应的事件信息向双向链表中拷贝了一份,并修改events成员为实际就绪的事件
对于当前进程来说,调用了epoll_wait接口之后只是判断链表是否为空就能确定是否有就绪,如果不为空则将这个链表的信息拷贝到epoll_wait传入的数组中返回给用户
select和poll默认只有水平触发
可读事件:只要缓冲区中有数据就会触发事件
可写事件:只要缓冲区中有空闲就会触发事件
可读事件:只有新收到数据才会触发一次事件
可写事件:只有从不可写变为可写的时候才会触发一次事件
边缘触发因为只有新数据到来的时候才会触发事件,因此我们通常在一次事件触发中将所有的数据全部获取进行处理
想要一次性将缓冲区的数据全部取出只能通过循环多次读取缓冲区,直到某一次读取缓冲区数据时读不到数据为止,但是如果读取数据缓冲区的时候缓冲区没有数据了会造成阻塞,因此边缘触发要借助非阻塞操作完成,关于非阻塞操作,请参考https://blog.csdn.net/m0_46657980/article/details/115560584
跨平台移植性差