Input/Output
本文作为一篇学习笔记,旨在了解IO阻塞原理,以及Linux系统相关知识。参考了部分博文。
同步、异步、阻塞、非阻塞
先说下我理解的同步、异步、阻塞、非阻塞。
同步和异步:是否需要函数的返回值。同步需要等待函数的结果,此时线程会休眠或者轮询,这些都是同步。异步是函数直接返回,结果通过回调处理。
阻塞和非阻塞:线程挂起是阻塞,线程没有挂起非阻塞。
根据此文IO语义环境来看,同步和异步是针对应用程序与内核的交互而言。 同步过程中进程触发IO操作并等待(阻塞)或轮询查看IO操作(非阻塞)是否完成。完成后数据在内核态,可进行IO读写。
异步过程中进程触发IO操作后,直接返回,IO交给内核处理,完成后内核通知进程,此时数据已存在用户态中。
更直观的理解阻塞和同步
网络上数据到用户程序,可分为两个阶段:
-
第一阶段:硬件网卡到内核态;
-
第二阶段:内核态到用户态。
阻塞IO和非阻塞IO的区别在于第一阶段,发起IO请求的进程是否会被阻塞。如果阻塞直到IO完成就是阻塞IO,否则非阻塞IO。 以上任何一个阶段阻塞,称为同步IO,否则异步IO。
从上图中可看出,IO阻塞原因在我理解是这样:
IO阻塞发生在内核等待数据的时候,或者内核空间拷贝数据到用户空间的时候阻塞。
Java IO和Linux IO
Java IO
-
IO:同步阻塞IO,面向流;像InputStream字节流通道,不能存储,只能读一次。
-
NIO:同步非阻塞IO,面向缓冲区。Java中的NIO 不是Linux IO模型中的NIO, 而是多路复用IO。
Linux IO
BIO:同步阻塞IO
内核操作彻底完成后,才返回到用户空间,执行用户的操作。
NIO:同步非阻塞IO
-
在内核缓冲区没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息(非阻塞);
-
在内核缓冲区有数据的情况下,是阻塞的,直到数据从内核缓存复制到用户进程缓冲(同步)。
NIO优点:每次发起的IO系统调用,在内核等待数据的过程中可以直接返回。用户线程不会阻塞。
NIO缺点:需要不断的重复发起IO系统调用,不断的询问内核,将占用大量cpu时间。
AIO:异步IO, 是非阻塞的
内核IO处理完成后采用回调来通知用户程序。目前只有window系统下通过IOCP实现了真正的异步。linux下都是同步IO。
IO多路复用:
即经典的reactor设计模式,java中的selector和linux中的epoll都是这种模型。
多路复用基本原理就是:不需要用户扫描连接,由kernel给出哪些连接有数据,然后应用从有数据的连接读取数据。
IO模型对应关系
select、poll、epoll区别
文件描述符fd
linux内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个fd。
对socket的读写也会有相应的描述符,称为socketfd, 描述符就是一个数字,它指向内核中的一个结构体(文件路径、数据区等一些属性), 通过fd就知道要操作哪个流。
select、poll
select和poll本质上没啥区别,就是文件描述符数量的限制,select根据不同的系统,文件描述符限制为1024或者2048,poll没有数量限制。
select、poll都是把文件描述符集合保存在用户态,每次把集合传入内核态,内核态返回ready的文件描述符。 涉及用户态内核态复制fd集合。还需要遍历所有fd来获取已就绪的socket。
epoll
和epoll相关的三个函数:
-
epoll_create函数: 创建一个epoll对象,用于监控和管理fd; 对应JDK NIO代码中的Selector.open()。
-
epoll_ctl函数: 往红黑树中添加、删除、更新管理的fd; 对应JDK NIO代码中的socketChannel.register(selector, ops)。
-
epoll_await函数: 等待就绪的fd; 对应JDK NIO代码中的
selector.select()
。
1. 当用户进程调用了select,那么整个进程会被block;
2. 而同时,kernel会监视所有select负责的socket;
3. 当任何一个socket中的数据准备好了,select就会返回;
4. 这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
epoll 对象核心的两个数据结构:红黑树(存储所有注册的fd)和链表(存储就绪的fd)。
红黑树是为了应对用户的增删改需求,时间复杂度O(logn); 就绪链表用于存放就绪fd事件。epoll对象只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组;
简单点说:epoll是通过epoll_create、epoll_ctl、epoll_await三个系统调用完成的,每当接入一个文件描述符,通过ctl添加到内核维护的红黑树中,并且注册回调函数,内核在检测到某fd可读可写时调用回调函数,该回调函数将fd放到就绪链表中。通过await获取链表中准备好数据的fd,程序去处理。
epoll支持水平触发LT 和边缘触发ET,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。由于只通知一次,所以比水平触发效率高。
redis是水平触发模式。 水平触发模式下,如果数据未处理完,内核会继续通知。
总结:
IO多路复用优势不是对于单个连接能处理的更快,而在于它可以同时处理多个connection。