主要用于进行大量的IO就绪事件监控,能够让我们的进程只针对就绪了指定事件的IO进行IO操作
1.避免对没有就绪的描述符进行操作而导致的阻塞
2.避免对大量没有就绪的描述符进行操作带来的效率降低
就绪事件:IO事件的就绪
可读事件:一个描述符当前是否有数据可读
可写事件:一个描述符当前是否可以写入数据
异常事件:一个描述符是否发生了某些异常
只对就绪的描述符进行IO操作有什么好处呢?-----避免阻塞,并且提高效率
1.在默认的socket中,例如tcp一个服务端只能与一个客户端的socket通信一次,因为我们不知道哪个为客户端新建的socket有数据到来或者监听socket有新连接,有可能就会对没有新连接到来的监听socket进行accept操作而阻塞或者对没有数据到来的普通socket进行recv操作
2.在tcp服务端中,将所有的socket设置为非阻塞,若没有数据到来,则立即报错返回,进行下一个描述符的操作,这种操作中,对没有就绪事件的描述符进行操作,降低了处理效率
如何实现多路转接IO:操作系统提供了三种模型
1.select模型
2.poll模型
3.epoll模型
1.用户定义想要监控的事件的描述符集合(就绪事件有三种,但是并不是每一个描述符都要监控所有事件),例如,定义可读事件的描述符集合,将哪些描述符添加这个集合中,就表示要对这个描述符监控可读事件,因此是想要监控什么事件就定义什么集合
集合:fd_set结构体,结构体中只有一个成员,就是一个数组-----作为位图使用,向集合中添加一个描述符,描述符就是一个数字,添加描述符其实就是这个数字对应的比特位置1,表示这个描述符被添加到集合中.这个数组中有多少个比特位或者说select最多能监控多少描述符,取决于宏_FD_SETSIZE,默认是1024
fd_set rfds,wfds,efds(可读,可写,异常)
void FD_ZERO(fd_set *set);–清空指定的描述符集合
void FD_SET(int fd,fd_set *set);–将fd描述符添加到set集合中
2.发起调用,将集合中数据拷贝到内核中进行监控,监控采用轮询遍历方式进行
int select(int nfds,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
nfds:所有集合中最大的那个描述符数值+1,为了减少内核中遍历次数
readfds:可读事件集合
writefds:可写事件集合
exceptfds:异常事件集合
timeout:select默认是一个阻塞操作(struct timeval{tv_sec;tv_usec}),若timeout=NULL表示永久阻塞直到有描述符就绪在返回;若timeout中数据为0,则表示非阻塞,没有就绪则立即返回;若timeout有数据,若指定时间内没有描述符就绪则超时返回
返回值:返回值小于0,表示监控出错,返回值等于0,表示没有描述符就绪;返回值大于0,表示就绪的描述符个数
select会在返回之前干一件事情:将所有集合中没有就绪的描述符都给从集合中移除出去(调用返回后,集合中保存的都是就绪的描述符)
3.程序员遍历判断哪个描述符还在哪个集合中,确定哪个描述符就绪了哪个事件,进而进行相应操作
其他操作:
void FD_CLR(int fd,fd_set set)----从set集合中移除fd描述符
int FD_ISSET(int fd,fd_set set)----判断fd描述符是否在set集合中
缺点:
1.select所能够监控的描述符数量有最大上限,取决于宏_FD_SETSIZE,默认为1024
2.select进行监控的原理,是在内核中进行轮询遍历判断,性能会随着描述符的增多而下降
3.select返回时移除整个集合中未就绪的描述符,每次监控都需要重新添加描述符,重新拷贝到内核
4.select只能返回就绪的描述符集合,无法直接返回就绪的描述符,需要用户进行遍历判断哪个描述符还在集合中才能确定是否就绪
优点:
1.select遵循posix标准,可以跨平台移植
2.select的超时等待时间设置,可以精细到微秒
多线程/进程tcp服务器也能实现并发并行与多路转发模型实现并发有什么区别?
多路转接只是轮询处理,监控的各个描述符之间的均衡关系需要程序用户态处理
多线程/进程,他们的均衡态是操作系统调度实现的
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
fds:要监控的描述符事件结构体
struct pollfd{
int fd;//要监控的描述符
short events;//,描述符想要监控的事件(POLLIN-可读/POLLOUT-可写)
short revents;//实际就绪的事件
nfds:实际第一个参数描述符事件结构体数量
timeout:超时等待时间-毫秒
返回值:返回值大于0表示就绪的描述符个数,返回值等于0表示监控超时;返回值小于0表示监控出错
1.定义描述符事件结构体数组,将需要监控的描述符以及对应的事件信息填充到数组中.
例如:struct pollfd fds[10]; fds[0].fd=0; fds[0].events=POLLIN;//监控可读事件
2.发起监控调用poll,将数组中数据拷贝到内核中进行轮询遍历监控,有描述符就绪或者等待时间超时后返回,返回时将这个描述符实际就绪的事件填充到对应的结构体revents中,(如果描述符没有就绪,则revents中数据为0)
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
3.监控调用返回后,程序员在程序中遍历数组中每个节点的revents,确定当前节点的描述符就绪了什么事件,进而进行对应操作
优点:
1.poll通过描述符事件结构体的方式简化了select的三种描述符集合的操作流程
2.poll所能监控的描述符,没有了最大数量限制
3.poll每次监控不需要重新定义事件结构体
缺点:
1.监控原理是在内核中进行轮询判断,会随着描述符增加而性能下降
2.无法跨平台移植
3.每次监控调用返回后也需要程序员在程序中进行遍历判断才能知道哪个描述符就绪了哪个事件
1.在内核中创建eventpoll结构体,返回一个描述符作为代码中的操作句柄
int epoll_create(int size);
size:要监控的描述符最大数量,但是在Linux2.6.8之后被忽略,只要大于0就行
2.对需要监控的描述符组织事件结构体,将描述符以及对应事件结构添加到内核的eventpoll结构体中
int epoll_ctl(int epfd,int op,int fd,struct epoll_event * event);
epfd:epoll操作句柄
op:EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DEL;
fd:要监控的描述符
event:监控描述符对应的事件结构体信息
struct epoll_event{
uint32_t events;//表示要监控的事件,以及监控调用返回后实际就绪的事件-----EPOLLIN/EPOLLOUT
union epoll_data {int fd;//描述符 void *ptr; }data;
}
每一个需要监控的描述符都会有一个对应的事件结构,当描述符就绪了监控的事件后,就会将这个事件结构体返回给程序员
3.开始监控,当有描述符就绪或者等待超时后监控调用返回
int epoll_wait(int epfd,struct epoll_event *evs,int maxevents,int timeout);
epfd:epoll的操作句柄,通过这个句柄找到内核中指定的eventpoll结构
evs:epoll_event描述符的事件结构体数组的首地址,用于获取就绪的描述符对应的事件结构体
maxevents:evs数组的节点数量,主要是为了防止就绪太多,向evs中放置的时候越界访问
timeout:超时等待时间—单位:毫秒
返回值:返回值大于0表示就绪的描述符个数;返回值等于0表示等待超时,小于0表示监控出错
异步操作:完成流程不固定,实际功能并不由自身完成
epoll_waitt异步阻塞操作:只是发起监控请求,实际监控过程由系统完成----异步操作
而操作系统进行监控就是,当描述符就绪了指定事件,则将描述符对应事件信息放到双向链表中(系统对每个描述符就绪事件做的回调函数做的事情)
epoll_wait调用发起后,调用并没有立即返回,而是等待双向链表不为空后或者超时返回—阻塞操作
IO事件就绪
可读事件:接收缓冲区中的数据大小大于低水位标记
可写事件:发送缓冲区的剩余空间大小大于低水位标记
低水位标记:就是一个基准值,通常默认是一个字节
水平触发:默认的触发模式
可读事件:接收缓冲区中的数据大小大于低水位标记
可写事件:发送缓冲区的剩余空间大小大于低水位标记
边缘触发:需要在epoll_event结构体中的events成员中设置EPOLLET
可读事件:(不关注接收缓冲区是否有数据)每次有新数据到来的时候,才会触发一次事件,因此每次新数据到来最好能够一次性将所有数据读出,否则epoll的边缘触发不会触发第二次事件,只有等下次新数据到来才能触发,因为我们也不知道缓冲区有多少数据,因此只能循环读取才能取完所有数据
但是recv在读取数据的时候1若是读取到没有数据的时候,recv就阻塞了
这时候,就是非阻塞IO用武之地,两种方法
1.recv可以设置flag为MSG_DONTWAIT;直接将描述符的属性设置为非阻塞
2.fcntl(int fd,int cmd, …arg) cmd:F_SETFL/F_GETFL arg:O_NONBLOCK–非阻塞属性可写事件:剩余空间只有从无到有的时候才会触发事件
为什么要有边缘触发?水平触发和边缘触发哪个好?
边缘触发:是有新数据到来就会触发
假设现在要接收一条数据,但是发现缓冲区中的数据不完整,如果读取数据出来,就需要额外维护,等到下一条数据到来的时候,进行读取,补全上一条数据,若是数据不完整,不把数据读出来,则水平触发会一直触发事件,这时候边缘会比较好,因为边缘触发是在有新数据到来的时候才会触发事件(常用于一种一直触发事件,但是又不是每次都要进行操作的情况)
多路转接模型进行服务器并发处理与多线程/多进程并发并行处理有什么区别?
多路转接模型进行服务器并发处理:指的是在单执行流中进行轮询处理就绪的描述符
若就绪的描述符较多,则很难做到负载均衡,做法就是做出规定每个描述符只能读取指定数量的数据,读取了就进行下一个描述符(用户态完成的负载均衡)
多路转接模型仅适用于:有大量描述符需要监控,但是同一时间只有少量活跃的场景
多线程/多进程多执行流进行并发/并行:指的是操作西永通过轮询调度执行流实现每个执行流中描述符的处理,系统内核层面的负载均衡,不需要用户态做过多处理
基于两者特点:一起使用最佳
多路转接模型监控大量描述符,哪个描述符有就绪事件了,再去创建执行流进行处理,防止直接为描述符创建执行流,空耗资源
优点:
1.监控的描述符没有数量上限
2.所有的描述符事件信息只需要向内核拷贝一次
3.监控采用异步阻塞,性能不会随着描述符增多而下降
4,直接向程序员返回就绪的描述符事件信息,可以让程序员中直接对就绪的描述符进行操作,没有空遍历
缺点:
1.无法跨平台
2.监控的超时时间最多精细到毫秒
udp能不能使用epoll----可以,只要有描述符想要监控IO事件,都可以使用多路转接模型
在特别少量描述符需要监控的时候(比如udp服务器,只有一个描述符需要监控,或者tcp单独描述符监控的时候),适用select比较好,因为不需要在内核中做过多的操作,并且轮询遍历也比较简单,性能不会下降很多