你了解IO相关知识点吗?
IOIO是什么?
我们都知道unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket,还是FIFO、管道、终端,对我们来说,一切都是文件,一切都是流。在信息交换的过程中,我们都是对这些流进行数据的收发操作,简称为IO操作(inputandoutput),往流中读出数据,系统调用read,写入数据,系统调用write。IO源有哪些磁盘IO
发送一条磁盘IO的指令,指令一般是通知磁盘开始扇区位置,然后给出需要从这个初始扇区往后读取的连续扇区个数,同时给出动作是读,还是写。磁盘收到这条指令,就会按照指令的要求,读或者写数据。控制器发出的这种指令数据,就是一次IO,读或者写。磁盘IO的并发
一个磁盘同一时刻只能执行一条指令,因此单磁盘并发度为0内存IO
就是从内存中读写数据,速度非常快,通常不会成为性能瓶颈,一般不考虑设备IO
从一个外接设备写入或者读取数据,设备IO需要考虑设备是否是个互斥资源。互斥资源的IO某一时刻只能被一个线程占用。网络IO
网络IO其实也属于设备IO的一种,但是通常单独讨论。网络IO也就是对网卡的读写,也就是发送请求和接受请求在网卡上数据读写的IO。主要就是利用socket套接字发生和接受数据。数据拷贝
不同的IO源,所遵循的数据拷贝都是一致的。DMA控制器
DMA(DirectMemoryAccess,直接存储器访问):它是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU的大量中断负载。
传统IO操作读操作
拷贝两次,上下文切换两次(1)用户进程通过read()函数向内核发起读取的调用上下文从用户切换到内核(2)cpu利用DMA控制器从内存或磁盘拷贝到读缓存区拷贝一次(3)cpu将缓存区的数据拷贝到用户进程的缓存区拷贝一次(4)上下文从用户切换到内核read()函数返回写操作
拷贝两次上下文切换两次(1)用户进程调用write()函数,向内核发起系统调用上下文用户切换到内核(2)cpu将数据从用户缓存区拷贝到内核缓存区(这说明传统模式下用户进程没有权利去直接拷贝数据,必须交给内核来完成)拷贝一次(3)CPU利用DMA控制器将数据从网络缓冲区(socketbuffer)拷贝到内存或者磁盘拷贝一次(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。comnews5544
www。yuque。comhenyoumoik(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操作实际上对应着有三个函数:epollcreate,epollctr,epollwait。epollcreate
epollcreate相当于在内核中创建一个存放fd的数据结构。在select和poll方法中,内核都没有为fd准备存放其的数据结构,只是简单粗暴地把数组或者链表复制进来;而epoll则不一样,epollcreate会在内核建立一颗专门用来存放fd结点的红黑树,后续如果有新增的fd结点,都会注册到这个epoll红黑树上。epollctr
select和poll会一次性将监听的所有fd都复制到内核中,而epoll不一样,当需要添加一个新的fd时,会调用epollctr,给这个fd注册一个回调函数,然后将该fd结点注册到内核中的红黑树中。当该fd对应的设备活跃时,会调用该fd上的回调函数,将该结点存放在一个就绪链表中。这也解决了在内核空间和用户空间之间进行来回复制的问题。epollwait
epollwait方法就是进程获取就绪fd的时候调用,其实直接就是从就绪链表中取结点epoll的工作流程
就算是epoll模型,也需要线程去主动去获取数据,即调用epollwait()方法,此时就绪链表如果有数据,那就直接返回,如果没有数据,线程就会进入阻塞状态,然后当有数据后,就会唤醒该线程,获得数据的线程就会从epollwait()方法继续向后执行何时选择select(),poll()或者epoll()
并不是所有的情况中epoll都是最好的,比如当fd数量比较小的时候,epoll不见得就一定比select和poll好AIO异步IO
异步IO肯定不是阻塞的了,异步乍一看和epoll回调类似,但是epoll其实是等数据就绪了之后,唤醒之前尝试获取数据的线程,之前的线程在被唤醒前是一直阻塞的(1)用户线程向内核空间发起一次读取数据的调用。(2)如果数据就绪,直接读取,将数据拷贝到用户空间(3)如果未就绪就直接返回,然后该线程销毁(4)内核已经知道了用户进程想要哪些数据,等到内核数据准备好之后,内核主动将数据拷贝到用户空间,内核就去主动调用用户提供的回调函数来处理数据。jdk的IO演变历程(1)一个是jdk1。4,这个版本之前java仅支持传统的bio,之后支持nio;(2)jdk1。7,这个版本之后,有了aio。(3)编程语言层面上的io操作,其实调用的是操作系统内核的readwrite接口(对底层硬件设备的读写),所以本质上还得依赖于操作系统内核,如果操作系统不支持aio,即使编程语言层面上有aio接口,也没用,这也是为什么有了aio,但是目前大多数应用实际使用的还是nio,由于应用大多部署在linux服务器,而linux操作系统内核尚未实现aio(windows实现了aio)。
作者:用自己的话说
链接:https:juejin。cnpost6954732511268175886