IOIO是什么?
我们都知道unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket,还是FIFO、管道、终端,对我们来说,一切都是文件,一切都是流。在信息 交换的过程中,我们都是对这些流进行数据的收发操作,简称为I/O操作(input and output),往流中读出数据,系统调用read,写入数据,系统调用write。
io源有哪些磁盘IO发送一条磁盘IO的指令, 指令一般是通知磁盘开始扇区位置,然后给出需要从这个初始扇区往后读取的连续扇区个数,同时给出动作是读,还是写。磁盘收到这条指令,就会按照指令的要求,读或者写数据。控制器发出的这种指令+数据,就是一次IO,读或者写。
磁盘IO的并发一个磁盘同一时刻只能执行一条指令, 因此单磁盘并发度为0
内存IO就是从内存中读写数据, 速度非常快, 通常不会成为性能瓶颈, 一般不考虑
设备IO从一个外接设备写入或者读取数据, 设备IO需要考虑设备是否是个互斥资源. 互斥资源的IO某一时刻只能被一个线程占用.
网络IO网络IO其实也属于设备IO的一种, 但是通常单独讨论. 网络IO也就是对网卡的读写, 也就是发送请求和接受请求在网卡上数据读写的IO. 主要就是利用socket套接字发生和接受数据.
数据拷贝不同的IO源, 所遵循的数据拷贝都是一致的.
DMA控制器DMA(Direct Memory Access,直接存储器访问) : 它是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。
传统IO操作读操作
拷贝两次, 上下文切换两次
(1) 用户进程通过read()函数向内核发起读取的调用 上下文从用户切换到内核(2) cpu利用 DMA 控制器从内存或磁盘拷贝到读缓存区 拷贝一次(3) cpu将缓存区的数据拷贝到用户进程的缓存区 拷贝一次(4) 上下文从用户切换到内核 read()函数返回写操作拷贝两次 上下文切换两次
(1) 用户进程调用write()函数, 向内核发起系统调用 上下文用户切换到内核(2) cpu将数据从用户缓存区拷贝到内核缓存区(这说明传统模式下用户进程没有权利去直接拷贝数据, 必须交给内核来完成) 拷贝一次(3) CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到内存或者磁盘 拷贝一次(4) 上下文从内核切换到用户 write()函数返回零拷贝于这种数据传输方式来说,应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输.也就是用户进程可以直接对磁盘或者内存进行读写. 这样数据就不需要拷贝了, 但是当然传统方式的一些好处必然也就需要舍弃
Linux 中提供类似零拷贝的系统调用主要有 mmap(),sendfile() 以及 splice()。
mmap() (读一次拷贝, 写不变)一次拷贝发生在DMA会从磁盘或者内存将数据读入共享缓存区. 用户进程就可以直接使用共享缓存区中的数据.
和传统的区别就是read操作变成了mmap,之后用户空间会和内核态共享同一块内核缓存区,读入的数据都在这个内核缓存区里面。写入的话还是和原来一样。
sendfile() (读一次拷贝, 写两次次拷贝)该方法适用于读取的数据可以直接写入到别的IO源里, 拷贝操纵直接在内核空间中完成, 用户进程不需要参与, 减少了上下文切换
读的一次拷贝发生在DMA会从磁盘或者内存将数据读入内核缓存区
写的两次拷贝
(1) 内存缓存区拷贝到socket缓存区(2) socket缓存区内容复制到网卡splice() (读一次拷贝, 写一次拷贝)该方法适用于读取的数据可以直接写入到别的IO源里, 内存缓存和socket缓存间直接建立通道, 无需复制操作, 直接就可以互相访问.
读写就绪状态(1) 读就绪状态
内核缓冲区中数据字节数大于等于用户进程请求读的字节数,此时系统可以将内核缓冲区的数据搬到用户缓冲区.
(2) 写就绪状态
内核缓冲区中剩余字节空间数(空闲空间)大于等于用户进程请求写的字节数,此时系统可以将用户缓冲区的数据搬往内核缓冲区.
也就是说一个读或写的过程,首先要经历一个读/写的就绪状态,读/写就绪后,才进行"真正"的IO,读就绪后,系统才能将内核缓冲区的数据搬到用户缓冲区;
写就绪后,系统才能将用户缓冲区的数据搬往内核缓冲区.
网络IO网络IO作为java服务重点关注的情况, 无论是请求/相应, 还是数据库操作都通过网络IO完成
网络IO的特点其它IO源在读取的时候, 基本不会有数据不存在的情况. 但是网络IO对于操作系统来说, 读写的都是网卡的内容, 网卡的内容能否被读取, 取决于是否有新的数据通过网络写入. 因此用户进程并不知道何时才能从网络IO获取数据, 因此就需要使用IO模型.
网络IO收取网络包的过程www.easemob.com/news/5544
www.yuque.com/henyoumo/ik…
(1) 网卡收到数据后, 网络驱动会通过DMA把网卡上收到的数据写到内存里, 并想cpu发送一个软中断, 通知cpu有数据到达(2) 内核具有一个线程ksoftirqd专门用来处理软中断的请求, ksoftirqd不断循环, 判断是否有软中断请求需要处理. 不断用poll()的方式轮询.(3) ksoftirqd发现由网卡的中断请求后, 将数据交到各级协议栈处理.(4) 协议栈负责将不同协议的数据都处理完毕(比如收到了完整的多个TCP数据报文), 变成可用的数据结合socket放到socket队列中去. 代表socket的数据就绪了.(5) 用户进程自己维护一个不停循环的线程(或者由用户进程的网络框架维护), 不停的去访问内核空间看有没有就绪的socket数据.IO模型IO模型适用于所有和IO源交互的情况, 但是对于网络IO来说, IO交互的等待时间可能无限长.
(1) IO模型主要用来讨论数据未就绪的情况, 如果数据已经就绪了, 啥模型都能直接读取数据(2) IO模型只讨论应用程序触发获取数据的操作之后的情况, 至于应用程序何时触发, 和IO模型无关阻塞IO(BIO)(1) 应用程序的一个线程获取数据的操作, 此时内核数据未就绪(2) 线程一直等待(3) 内核数据就绪, 唤醒线程读取内核数据到用户进程空间中(4) 线程的读取数据操作完成阻塞IO一个线程只能用来获取一个socket套接字的数据.
非阻塞IO(BIO)(1) 应用程序的一个线程获取数据的操作, 此时内核数据未就绪, 返回一个错误信息(2) 线程得到返回后, 可以执行别的操作, 之后会再次来尝试获取数据(3) 线程不断轮询, 直到内核数据就绪, 就将内核数据拷贝到用户空间(4) 线程的读取数据操作完成非阻塞IO, 如果你在线程中维护多个socket连接的信息, 是可以实现和select()差不多的效果.
IO多路复用IO多路复用是一种同步IO模型,一个线程监听多个IO事件,当有IO事件就绪时,就会通知线程去执行相应的读写操作,没有就绪事件时,就会阻塞交出cpu。
多路是指网络链接,复用指的是复用同一线程。
因为IO多路复用不止适用于套接字, 适用于所有文件描述符fd, 因此介绍时以fd来介绍
select()用户线程维护一个数组, 记录所有感兴趣的fd(在socket中就是已经建立连接的所有套接字). 数组的大小有限制, 在32位系统中,最大值为1024个,而在64位系统中,最大值为2048个
(1) 线程不断调用select()方法, 将数组从用户空间拷贝到内核空间, 内核空间会按照数组检查一遍fd是否发生了IO事件(就是socket读队列有没有数据), 如果有, 就使该fd为就绪状态(此时不拷贝数据)
(2) select()方法返回, 进程遍历一遍该数组, 看看哪些fd是就绪状态, 如果就绪了, 就调用fd的对应方法, 将数据从内核空间拷贝到进程空间中.
poll()进程维护一个链表, 因为是链表, 所以没有长度的限制.
其它的操作过程和select()方法一样. 性能没啥提升
epoll()epoll就是对select和poll的改进了。它的核心思想是基于事件驱动来实现的,相当于提前建立好相应的数据结构 回调函数的使用, 使得不需要轮询, 而是只返回就绪的fd.
epoll操作实际上对应着有三个函数:epoll_create,epoll_ctr,epoll_wait.
epoll_createepoll_create相当于在内核中创建一个存放fd的数据结构。在select和poll方法中,内核都没有为fd准备存放其的数据结构,只是简单粗暴地把数组或者链表复制进来;而epoll则不一样,epoll_create会在内核建立一颗专门用来存放fd结点的红黑树,后续如果有新增的fd结点,都会注册到这个epoll红黑树上。
epoll_ctrselect和poll会一次性将监听的所有fd都复制到内核中,而epoll不一样,当需要添加一个新的fd时,会调用epoll_ctr,给这个fd注册一个回调函数,然后将该fd结点注册到内核中的红黑树中。当该fd对应的设备活跃时,会调用该fd上的回调函数,将该结点存放在一个就绪链表中。这也解决了在内核空间和用户空间之间进行来回复制的问题。
epoll_waitepoll_wait方法就是进程获取就绪fd的时候调用, 其实直接就是从就绪链表中取结点
epoll的工作流程就算是epoll模型, 也需要线程去主动去获取数据, 即调用epoll_wait()方法, 此时就绪链表如果有数据, 那就直接返回, 如果没有数据, 线程就会进入阻塞状态, 然后当有数据后, 就会唤醒该线程, 获得数据的线程就会从epoll_wait()方法继续向后执行
何时选择select(), poll() 或者epoll()并不是所有的情况中epoll都是最好的,比如当fd数量比较小的时候,epoll不见得就一定比select和poll好
AIO 异步IO异步IO肯定不是阻塞的了, 异步乍一看和epoll回调类似, 但是epoll其实是等数据就绪了之后, 唤醒之前尝试获取数据的线程, 之前的线程在被唤醒前是一直阻塞的
(1) 用户线程向内核空间发起一次读取数据的调用.(2) 如果数据就绪, 直接读取, 将数据拷贝到用户空间(3) 如果未就绪就直接返回, 然后该线程销毁(4) 内核已经知道了用户进程想要哪些数据, 等到内核数据准备好之后, 内核主动将数据拷贝到用户空间, 内核就去主动调用用户提供的回调函数来处理数据.jdk的IO演变历程(1) 一个是jdk 1.4,这个版本之前java仅支持传统的bio,之后支持nio;(2) jdk 1.7,这个版本之后,有了aio。(3) 编程语言层面上的io操作,其实调用的是操作系统内核的read/write 接口(对底层硬件设备的读写),所以本质上还得依赖于操作系统内核,如果操作系统不支持aio,即使编程语言层面上有aio接口,也没用,这也是为什么有了aio,但是目前大多数应用实际使用的还是nio,由于应用大多部署在linux服务器,而linux操作系统内核尚未实现aio(windows 实现了aio).
作者:用自己的话说链接:https://juejin.cn/post/6954732511268175886