IO模型
阻塞IO
同步IO
要等待两个阶段
1.DMA从硬盘考贝数据到内存缓存区,然后通知CPU数据准备好了
2.CPU把内存缓冲区的数据 考到用户缓冲区,然后用户进程开始漫长的拷贝数据阶段,同步操作
用户read操作后,就会一直阻塞等待数据
非阻塞IO
1.用户进程发起read命令,cpu给dma发取读取命令,如果dma没有把数据准备好,内核会立刻给用户进程返回一个error
2.用户进程角度,虽然用户进程不会被阻塞,但是它会一直轮询,直到数据准备好了,停止轮询,开始漫长的数据拷贝,这是同步操作
用户read后,不会阻塞,但是一直轮询
同步IO
同步IO就是指 必须一直等待IO传输数据完成。一直阻塞在这里。
在同步文件IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行。
多路复用
也叫事件驱动IO,select epoll
一个进程,可以处理多条网络IO
基本原理是 内核空间 有一个内核线程,然后不断轮询 ,然后一旦这个socket有网络事件产生,就可以把他拷贝给用户进程
select
- select会把用户进程的所有socket文件描述符放在一个集合里,然后拷贝到内核空间,然后有一个内核线程会轮询的方式,询问网络事件,然后遍历文件描述符集合,把他们设置可读或者可写,然后拷贝到用户进程,然后用户进程再遍历集合,对网络事件处理,
- 所以有两次拷贝,两次遍历
- select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为
1024
,只能监听 0~1023 的文件描述符
poll
方式和上面一样,只不过存储文件描述符,是以链表的形式,解决了限制问题,但是还是受系统限制
两者每次都要遍历socket集合,寻找可读可写的socket ,on复杂度
进程调用select的时候,进程会被阻塞,当返回socket的时候,就可以继续操作了
所以说 没有网络连接的时候,select poll还是会被阻塞。
它是对通知事件进行阻塞,而不是IO调用阻塞,即不是被数据准备阻塞,socket有读写状态时,多路复用器,就会通知线程进行读写
epoll
解决select poll 的效率问题,因为他们都要遍历 拷贝,并发上来后就很低效。
1.内核使用红黑树来跟踪每个socket文件描述符的状态, 而且每次只用通过epoll_ctl(),传入一个文件描述符,而不用拷贝整个文件描述符集合,大大提高了效率,解决了拷贝问题。
2.使用了事件驱动的机制,内核维护了一个就绪链表,当文件描述符有事件发生,就加入到就绪链表
3.当用户进程调用epoll_wait的时候,就会把就绪链表拷贝到用户空间,返回的是就绪事件的个数,这样就不用再去遍历了,直接一个个取出来。
其实写个 epoll 的例子就什么都明白了,调用 epoll_wait
时阻塞,之后调用 read
或 write
时是非阻塞(因为对文件描述符调用了 fcntl
将其设为非阻塞模式)。
边缘触发
使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完
水平触发
使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read
和 write
)返回错误,错误类型为 EAGAIN
或 EWOULDBLOCK
。
一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
另外,使用 I/O 多路复用时,最好搭配非阻塞 I/O 一起使用,Linux 手册关于 select 的内容中有如下说明:
在Linux下,select() 可能会将一个 socket 文件描述符报告为 “准备读取”,而后续的读取块却没有。例如,当数据已经到达,但经检查后发现有错误的校验和而被丢弃时,就会发生这种情况。也有可能在其他情况下,文件描述符被错误地报告为就绪。因此,在不应该阻塞的 socket 上使用 O_NONBLOCK 可能更安全。
简单点理解,就是多路复用 API 返回的事件并不一定可读写的,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况。
- epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。
- epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。
epoll 解决了 拷贝集合问题 ,还有遍历的问题。
多路复用 只是能处理更多的连接,但是不能解决阻塞问题,所以一般配合非阻塞IO
信号驱动IO
在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函
数,然后用户线程会继续执行其他操作,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到
信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。
这个最终还是同步操作,因为要和cpu进行IO数据传输,数据准备过程,它可以干别的事情,传输还是要靠自己完成
异步IO
异步io 才是完全不需要等待数据准备,也不需要和cpu进行数据传输,也就是说IO的两个阶段,用户线程也不参与。 非阻塞IO,虽然名义上 立马返回,但是它会一直轮询问数据准备好了没有, io的所有操作全部由内核完成,当内核完成了数据的准备,和数据的传输,会给用户一个信号,表示数据可以使用了
非阻塞IO是立刻返回,还要一直轮询问数据是否准备好。最终IO操作还是同步的
而异步IO不用管数据是否准备好了,而是立刻返回干别的事情,
其实 阻塞非阻塞IO,多路复用最终都是同步IO