什么是阻塞,非阻塞,异步同步,select,poll,epoll?今天我们用一遍文章解开这多年的迷惑。
首先我们想要通过网络接收消息,是这样的一个步骤。
根据我们请求数据的情况不同,以及内核缓冲区到用户缓冲区的不同,分为了阻塞,非阻塞,异步同步的区别。
在《UNIX网络编程》一书中,总结归纳了5种I0模型:
以上可以看出来,根据等待数据的方式不同,分为阻塞和非阻塞。
阻塞IO在请求内核数据的时候,没有数据就会一直阻塞直到获取数据。
非阻塞IO在等待内核数据的时候,没有数据就会得到没数据的结果,应用可以进行其他动作。
同步IO的主要看内核数据到用户空间的过程是同步进行的就是同步IO
异步IO首先是非阻塞IO,区别在于成功标志的时机。异步IO连内核到用户态的数据拷贝都是异步的,直到数据拷贝完成,才会回调一个信号,通知一切已经准备完成。用户应用此时就可以直接处理结果了。
阻塞非阻塞指的是在获取结果上是否会阻塞等待结果完成
同步异步指的是是否会参与IO读写,或者是等待读写成功的回调
如果是阻塞IO也就是BIO,那么在一个fd(文件描述符)没有数据的时候,就是阻塞一直等待,如果同时有多个fd,对于单线程来说,只能一直等第一个有数据,然后再接着处理第二个,效率很慢。
就像顾客点餐,要一直等到第一个人点完餐,后面的人才有机会。BIO也有个解决办法,一般是增加多线程,每个线程都维护一个fd,就相当于为每个顾客都添加一个点餐台。在fd足够多的情况下,会有大量的线程被创建,线程可是有上限的,开销也大(更多线程需要更多的内存空间)。
如果是非阻塞IO也就是NIO,会有顾客没点完餐,然后造成CPU一直在询问一直空转的情况。
因此引入了IO多路复用模型:利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源
文件描述符( File Descriptor) :简称FD,是一个从0开始递增的无符号整数,用来关联Linux中的一一个文件。在Linux
中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket),
这时候每来一个顾客(FD
),我们就会给他一个开关(注册进监听事件
),一个服务员(一个线程
)等待开关亮起(阻塞等待事件
)。有顾客完成,就会按下开关,一定的频率下开关会亮起(事件通知
),服务员会选取按下开关的一批人,给他们点餐(批量处理事件
)。
IO多路复用的实现有select,poll,epoll,我们来看看他们的优缺点。
select是Linux中最早的I/O多路复用实现方案,并且windows操作系统上只支持select。这就是为啥window发挥不出redis的最大性能的一个原因。
select函数执行流程
用户
空间创建fd_set,把需要监听的位置置1,比如 1,2,5用户
空间拷贝fd_set(注册的事件集合)到内核空间内核
遍历所有fd文件,并将当前进程挂到每个fd的等待队列中,当某个fd文件设备收到消息后,会唤醒设备等待队列上睡眠的进程,那么当前进程就会被唤醒内核
如果遍历完所有的fd没有I/O事件,则当前进程进入睡眠,当有某个fd文件有I/O事件或当前进程睡眠超时后,当前进程重新唤醒再次遍历所有fd文件内核
有事件产生,会把fd_set中有事件的位置保留为1,没有事件的位置擦除为0.内核
拷贝fd_set给用户空间用户
空间线程被唤醒,遍历fd_set为1的位置,确认是哪些fd有就绪事件,然后开始处理用户
空间处理完事件,再一次将要监听的fd_set设置为1,重复之前的监听动作根据上面可以很清楚的看出整个执行流程在用户空间和内核空间的切换。
select函数的缺点
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的
poll运行流程
①创建pollfd数组, 向其中添加关注的fd信息,数组大小自定义
②调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
③内核遍历fd,判断是否就绪
④数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
⑤用户进程判断n是否大于0
⑥大于0则遍历pollfd数组,找到就绪的fd
与select对比
poll还是没有解决需要遍历判断fd事件的方式,只是增加了监听数量,在fd很多的情况下,性能下降的更加严重
epoll可以理解为event pool,不同与select、poll的轮询机制,epoll采用的是事件驱动机制,每个fd上有注册有回调函数,当网卡接收到数据时会回调该函数,同时将该fd的引用放入rdlist就绪列表中。
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
他主要有三个函数,epoll的执行流程
对应的redis的server执行流程
epoll优点
读事件很好理解,有一个读事件就立马处理请求,怎么理解写事件?
当socket 写缓冲区已满,假如设置了非阻塞I/O,应用程序调用send会返回EAGAIN,告诉应用程序写缓冲区已满,下次再来尝试调用,这时候就有一个尝试的时机问题,应用程序怎么知道socket 缓冲区可写呢?如果频繁调用send,会浪费CPU。这时候,epoll就排上用场了,对socket 设置写事件,并添加到 epoll中,应用程序调用epoll_wait,当该socket 的写缓冲有空余时,就返回对应的写事件,应用程序这时候就可以调用send,发送数据。
所以写事件是用来告诉程序,写缓冲是空余的。一般情况下fd都是有写事件的。但是在写缓冲区满了的时候,就会频繁触发写事件。所以我们可以一开始不监听写事件,直到发现数据量可能大于缓冲区,再监听写事件
参考:高效处理写事件
select poll epoll
黑马多路复用视频