1. 同步阻塞IO
同步阻塞io在数据在数据拷贝到两个阶段都是阻塞的,即把socket的数据拷贝到内核缓冲区和把内核缓冲区的数据拷贝到用户态到应用程序缓冲区都是阻塞的。用户线程在这个期间不能处理其他任务。
优点:简单易用
缺点:为每一次io请求都要分配一个线程,如果获取不到数据会一直阻塞
2. 同步非阻塞
同步非阻塞不会像同步阻塞那样两个阶段都阻塞,它在第一个阶段中向内核请求数据时如果此时内核还没准备好数据,那么它会直接返回一个错误,不会一直等待下次。等待一定时间后会继续发起io请求。
优点:允许单个线程处理多个io请求,线程可以执行其他任务
缺点:频繁的轮询请求会消耗cpu资源
3. IO多路复用
io多路复用本质上和同步非阻塞模式一样,它解决了同步非阻塞中用户线程不断轮询请求而消耗cpu资源的问题。io多路复用增加了一个select / poll / eoll 系统调用,由内核来实现请求线程本来该做的轮询操作。具体来说,执行系统调用时,会将已经建立连接的socket放在一个文件描述符的集合中,然后把这个fd集合拷贝到内核态,让内核来检查是否有io事件已经准备好了。再把检查好的fd集合拷贝到用户态到缓冲区中。
3.1 select实现方式
它采用位数组BitsMap来存储已建立连接到socket请求,构成fd集合,把fd集合拷贝到内核态,内核态遍历整个fd集合,若数据准备就绪,则对应标志位设置为1。遍历完成后,再把fd集合拷贝到用户缓冲区。
缺点:监听的 socket 集合 数量受位数组大小的限制,每次拷贝要拷贝全部fd集合,检查时要从头到尾遍历所有的 fd 集合
3.2 poll的实现方式
poll针对select监听fd集合的数量限制进行了改进,采用了结构体 + 链表的形式来组织fd集合,与select没有太大区别
3.3 epoll的实现方式
● select和poll的实现存在以下问题:
a. 每次io请求需要把整个fd集合拷贝进内核态或者从内核态拷贝到用户态
b. 要遍历全部fd集合才能得到需要处理的数据集合
select 需要遍历所有注册的I/O事件,找出准备好的的I/O事件。 而 epoll 则是由内核主动通知哪些I/O事件需要处理,不需要用户线程主动去反复查询,因此大大提高了事件处理的效率。
它的实现主要通过在内核维护一个红黑树和就绪链表,红黑树用于记录所有待处理的fd集合,而就绪链表用于记录准备好的就绪事件。当有fd请求需要监听是,调用epoll_ctl ( )来添加监听,不要传递整个 fd 集合,传递要监听的 fd 即可 。当 epoll_wait() 调用发生并且检测到一个或多个事件就绪时,内核将这些就绪事件的数据从内核的事件列表拷贝到用户提供的 events 数组中。
3.3.1事件触发模式
● 水平触发(LT):默认工作模式,表示当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件。
● 边缘触发(ET): 当epoll_wait检测到事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。边缘触发只在状态由未就绪变为就绪时只通知一次。
LT模式和ET模式各有优缺点,无所谓孰优孰劣。使用 LT 模式,我们可以自由决定每次收取多少字节(对于普通 socket)或何时接收连接(对于侦听 socket),但是可能会导致多次触发;使用ET模式,我们必须每次都要将数据收完(对于普通socket)或必须理解调用accept接收连接(对于侦听socket),其优点是触发次数少。
参考:参考1
参考:参考2