高级IO——主要讲并发模型

一、5种IO:阻塞IO、非阻塞IO、信号驱动IO、多路转接IO、异步IO
【面试】什么是阻塞和非阻塞,它们之间的区别是什么?---->它们关注的是是否等待
阻塞:为了完成功能发起调用,但是如果当前不具备完成条件,则等待;
非阻塞:为了完成功能发起调用,但是如果当前设备不具备完成条件,则报错返回;
区别:不具备完成条件的情况下,发起调用是否立即返回。
【面试】什么是同步和异步,它们之间有什么区别?---->它们关注的是如何完成这个操作
同步:为了完成功能发起调用,但是如果不具备当前条件,则等待,直到功能完成;
异步:为了完成功能发起调用,但是如果当前设备不具备完成条件,则立即返回,将需要完成的功能交给操作系统当操作系统完成之后则通过一些其他的方式(信号通知)告诉我们功能完成;
区别:要完成某个功能,在条件不具备的情况下,一个是一直等待,一个是立即返回。
注意:同步与异步中的同步和同步与互斥中的同步时两个完全不同的概念。
1.阻塞
在这里插入图片描述
2.非阻塞
在这里插入图片描述
3.信号驱动
在这里插入图片描述
4.异步
在这里插入图片描述
典型的异步IO:Linux下的AIO(在大量磁盘读写时用异步IO)
5.多路转接
让别人来替我们监控整个等待过程,看现在哪一个就绪好了,直接完成相应的操作让多路转接模型替用户监控多个描述符的等待过程,如果哪个描述符就绪好了,则完成监控过程返回并且通知我们哪些描述符已经就绪,接下来用户直接读取数据即可。
二、非阻塞IO设置
fcntl F_SETFL_GETFL
三、多路转接模型
select
poll(选学)
epoll
1.select
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
select建立监控集合,对描述监控指定事件(可写、可读、异常),当监控集合中的某个描述符就绪了,则返回并且告诉我们有几个描述符就绪了,将监控集合中没有就绪的描述符从集合中移除。
怎么判断就绪:缓冲区中有一个低水位标记,可读就是当接收缓冲区中的数据大小达到低水位标记大小则可读可写,此时操作空间大小就是当发送缓冲区剩余空间大小,大于低水位标记则可写。
(1)select对描述符进行监控,分了三种监控,分别是可写事件、可读事件、异常事件;如果想要对描述符进行某个事件的监控,则需要将描述符添加到指定集合中。这个集合就是一个位图,位图大小默认是1024,添加指定描述符到集合中,就是置描述符数据所对应的比特位为1。
(2)fd_set这个结构体实际就是一个位图:long int _fd_mask fds_bits[1024/sizeof(long int)]=4个字节。
(3)select在tcp服务端程序中的应用的过程(高并发处理)
在tcp服务端程序中,因为我们并不知道何时该接收数据,或者何时accept获取新连接,因此在没有数据到来/没有请求到来时调用recv/accept就会阻塞,导致服务端程序要不然只能连续一个客户的数据,要不然就每个客户端只能处理一次;假如我们知道客户端数据/连接请求什么时候来,我们在合适的时候调用recv/accept则不会造成阻塞,并且可以实现谁的数据到来处理谁的数据,达到高并发服务端程序的编写目的。
*select在tcp服务端程序中应用过程的图解:
在这里插入图片描述
(4)select的整体过程
集合其实是一个位图,添加描述符就是修改位图比特位;select创建三个描述符集合,分别监控可读事件、可写事件、异常事件,向这三个集合中添加描述符,对描述符不同的状态分别添加到对应的集合中去。将集合中的数据拷贝到内核中,进行阻塞监控(间隔事件、轮询遍历、判断是否有描述符就绪)。如果编译没有描述符就绪,则继续休眠或判断是否超时,若超时则返回0;如果有描述符就绪,将集合中没有就绪的描述符全部从集合中移除。由于移除,所以我们对集合进行了修改,所以每次都需要清空集合重新添加描述符。
其中:
就绪概念:描述符对应缓冲区中数据大小(空闲空间大小)是否大于低水位标记,大于就是就绪状态。
判断就绪的描述符在哪:从0开始遍历到max-fd的描述符,对就绪描述符进行相关操作。
(5)select优缺点
优点:
a.跨平台;
b.超时时间的控制比较精准;
缺点:
a.能够将恐的描述符有一个最大上限(因为位图最大取决于FD-SETSIZE;
b.因为select判断集合中描述符就绪后会修改集合中的内容,因此需要每次重新添加描述符(编程太麻烦);
c.因为select不会告诉我们具体是哪个描述符就绪了,需要我们从头开始遍历描述符(随着描述符增多,效率降低)
d.因为select每次都需要将集合数据拷贝到内核中,并且在内核轮询间遍历实现监控,因此性能随着描述符增多而降低;
2.poll原理
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
相较于select优点:
a.描述符无上限;
b.监控集合只有1个,每个节点可以关注不同事件;
3.epoll模型
epoll是linux下使用读最高、性能最高的多路转接模型。
(1)epoll工作流程:
1)创建poll;在内核中创建epollevent结构(rdllist——就绪事件双向链表;rdr——红黑树事件集合)
2)向事件集合中添加事件节点(每个事件只需要向内核添加一次即可)
3)epoll在内核使用信号驱动事件回调方式实现监控(当内核检测到集合中的描述符就绪,则直接调用回调函数,将就绪事件节点指针,添加到就绪事件链表中;epoll每隔一段时间就看一下双向链表是否为空,来判断是否有就绪事件,如果有的话则将就绪事件拷贝到用户态供用户操作)。
4)开始监控后,用户直接拿到的就是就绪事件,直接进行相应操作。
(2)工作流程的流程图
在这里插入图片描述
(3)epoll优缺点
优点:
a.描述符无上限;
b.每个事件只需要向内核拷贝一次;
c.epoll在内核使用事件回调将就绪事件添加到双向链表,每隔一会判断双向链表是否为空来判断是否有描述符就绪,性能不会随着描述符增多而降低;(因为epoll在进行监控的时候使用的是回调函数,这个回调函数进行实时监控,当发现有就绪的事件时就已经将就绪的事件添加到就绪事件双向链表中,就不需要等发现了就绪事件然后通知我们然后才进去寻找就绪事件在哪。这样就与我们的描述符多少没有关系了)
d.epoll直接将就绪的事件拷贝给用户,用户拿到就绪事件即可操作,不用遍历位图寻找就绪事件,性能提高了。
缺点:相较于select不能跨平台;
(4)epoll的两种事件触发方式
边缘触发:每条新数据的到来,就会触发就绪事件,并且不管缓冲区数据是否大于低水位标记;
水平触发:只要缓冲区数据大于低水位标记,就会触发就绪事件;
对于epoll有两种触发模式可选,默认为水平触发,但是select只有水平触发模式;一旦epoll的触发模式被设置成了边缘触发模式,将要求用户每次都将缓冲区的数据读完进行处理,因为对于不触发就绪,socket按照程序逻辑就不会进行多次处理;对于这个将数据读完,如果一次性读完必须得分几次才能读完(因为空间大小限定),这样就会造成阻塞,但是在并发模型中不能存在阻塞,因此需要将描述符设置为非阻塞状态。
3.多路转接使用场景:应用于大量连接,但是同一时间只有少量连接活跃的场景,因为多路连接只有并发处理请求。
【面试】epoll的惊群问题是什么?怎么处理?
惊群的问题:
我们在网络编程中经常用到多进程和多线程模型,大概的思路是父进程创建socket、bind、listen后,通过fork创建多个子进程,每个子进程继承了父进程的socket,调用accept开始监听等待网络连接。这个时候有多个进程同时等待网络的连接事件,当这个事件发生时,这些进程被同时唤醒,就是”惊群“。我们知道进程被唤醒,需要进行内核重新调度,这样每个进程同时区响应这个事件,而最终只有一个进程能处理事件成功,其他进程在处理事件失败之后会重新休眠或其他。简而言之,惊群就是当多个进程和线程同时阻塞等待同一个事件时,如果这个事件发生,会唤醒所有的进程,但是最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,这种性能浪费就是惊群。
解决办法:
Linux中使用mutex互斥锁解决这个问题,具体措施使用全局互斥锁,每个子进程在epoll_wait()之前先去申请锁,申请到锁则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置的7/8时,则不会再尝试区申请锁)来均衡各个进程的任务量。

猜你喜欢

转载自blog.csdn.net/ZhuiZhuDream5/article/details/88376700