IO模型
2020-03-16 14:05:58 2 举报
AI智能生成
IO模型
作者其他创作
大纲/内容
Socket套接字
和File差不多,代表一个数据源,打开一个对应的FD
<font color="#c41230">Server</font>Socket
常用方法
ServerSocket()
构造函数,新建一个服务端Socket对象,可以指定到<font color="#c41230">本地</font>端口号
bind()
将服务端Socket绑定到特定IP地址和端口号
accept()
开始接受到此套接字的客户端Socket连接(connect)
完成TCP三次握手,建立物理链路
getChannel()
返回与此套接字关联的唯一ServerSocketChannel对象
Socket<br>
常用方法
Socket()
构造函数,新建一个服务端Socket对象,可以指定IP和端口号
connect()
此套接字连接到服务器
getChannel()
返回与此数据报套接字关联的唯一SocketChannel对象
getInputStream()
getOutputStream()
NIO
Channel(通道)
特点<br>
Channel是全双工的,比流更好地映射底层操作系统的API,底层操作系统的通道都是全双工<br>
自动flush到文件
常用方法
close()
关闭此通道
isOpen()
判断此通道是否处于打开状态
常用channel
网络Socket数据读写
SelectableChannel
内部已经关联了文件源(socket)<br>
常用方法
configureBlocking(false)
设置此通道的阻塞模式
register(selector, SelectionKey.OP_ACCEPT)
注册此通道到指定选择器
TCP的数据读写
ServerSocketChannel
bind()
将服务端Socket绑定到特定IP地址和端口号
open()
新建(打开)服务器Socket通道
accept()
开始接受到此通道客户端Socket连接(connect)
socket()<br>
获取与此通道关联的服务端Socket
SocketChannel
connect() <br>
连接此Socket通道到ServerSocket
open()
新建(打开)客户端Socket通道
socket()
获取与此通道关联的客户端Socket
read()
将字节序列从此通道读入Buffer缓冲区
write()
将字节序列从Buffer缓冲区中写入此通道
UDP的数据读写
DatagramChannel
本地文件的数据读写
FileChannel
open()<br>
新建(打开)一个文件通道对象
read()
将字节序列从此通道读入Buffer缓冲区
write()
将字节序列从Buffer缓冲区中写入此通道
transferTo()
零拷贝,调用这个方法将会引起sendfile系统调用
map()
直接内存映射,底层基于基于mmap()函数实现
对于大文件而言,内存映射比普通IO流要快,小文件则未必。被映射的文件不能超过2G的大小
Selector(多路复用器)
Selector 一般称为选择器(或多路复用器),是<font color="#c41230">有差别</font>的Channel管理器<br>
Selector屏蔽了底层的系统调用select()、poll()、epoll()<br>
常用方法
open()<br>
新建(打开)一个选择器对象
select()<br>
轮询该Selector关心事件的一组channel,返回活跃channel的一组键集
这个方法会阻塞, 直到注册在Selector中的Channel发生该Selector关心的事件<br>
keys() <br>
返回此选择器上的所有键集
selectedKeys()
返回此选择器的已选择键集
SelectionKey <br>
表示SelectableChannel在Selector中的注册标记
常用属性
SelectionKey.OP_ACCEPT<br>
服务器监听到了客户连接
SelectionKey.OP_CONNECT
客户端与服务器的连接已经建立成功<br>
SelectionKey.OP_READ
通道中已经有了可读的数据
SelectionKey.OP_WRITE<br>
已经可以向通道写数据了
常用方法
channel()
从SelectionKey获取对应通道ServerSocketChannel
isAcceptable()<br>
测试此键的通道是否已准备好接受新的套接字连接
isConnectable()
测试此键的通道是否已完成其套接字连接操作
isReadable()
测试此键的通道是否已准备好进行读取
isWritable()
测试此键的通道是否已准备好进行写入
Buffer(IO缓存区)
特点
Buffer作为最小单位用于和NIO Channel交互,我们从Channel中读取数据到buffer里,从Buffer把数据写入到Channel
缓冲区在创建时就被分配内存,这块内存区域一直被重用,可以减少动态分配和回收内存的次数<br>
buffer的本质是内存<font color="#c41230">字节数组</font><br>
常用属性
容 量(Capacity )<br>
缓冲区可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
限 制(Limit )
缓冲区写状态,还原limit的值到capacity等待新数据的写入<br>
缓冲区读状态,flip翻转(变成读状态)时limit值发生变化,被赋值为byte数组实际使用的字节大小;<br>
位 置(Position )
下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备
标记( Mark )
调用mark()来设置mark=position,再调用reset()可以让position恢复到标记的位置
缓存区常用操作
capacity()
设置/返回此缓冲区的容量
limit()
设置/返回此缓冲区的限制
mark()
设置/返回此缓冲区的标记
position()
设置/返回此缓冲区的位置
<font color="#c41230">flip()</font>
limit = position;position = 0;mark = -1; <br>
因为只有一个标识位控的指针position,所以需要翻转,处于<font color="#c41230">写</font>数据状态的指针变为<font color="#c41230">读</font>数据状态
<font color="#c41230">clear()</font>
position = 0;limit = capacity;mark = -1;
还原缓存区配置为初始的准备写数据状态
reset()
把position设置成之前mark的值
rewind()
可重复读:把position设为0,mark设为-1,不改变limit的值 <br>
remaining()
返回剩余未读的字节数,limit-position<br>
hasRemaining()
是否还有未读内容,position < limit
compact()
把position到limit间的内容移到0到limit间
如果先将position设置到limit再compact,那么相当于clear
ByteBuffer(字节缓冲区)
静态工厂方法
allocate()
从堆空间中分配一个容量大小为capacity的byte新数组作为缓冲区的byte数据存储器,返回ByteBuffer实例 <br>
<font color="#c41230">wrap()</font>
使用已有的byte数组包装到缓冲区,返回ByteBuffer实例
常用方法
<font color="#c41230">get() </font>
相对读,从position位置读取一个byte,并将position+1,为下次读写作准备 <br>
get(int index)
绝对读,读取byteBuffer底层的bytes中下标为index的byte,不改变position
get(byte[] dst, int offset, int length)
批量读,从position位置开始相对读,读length个byte,并写入dst下标从offset到offset+length的区域
<font color="#c41230">put()</font>
也有相对写、绝对写、批量写<br>
MappedByteBuffer(直接映射字节缓冲区)
java nio引入的文件直接内存映射方案,读写性能极高
将磁盘中的文件映射到JVM内存中,然后通过修改JVM内存的数据,从而间接修改了磁盘中的文件
DirectByteBuffer
由于MappedByteBuffer申请的是直接内存,因此不受Minor GC控制,只能在发生Full GC时才能被回收,因此Java提供了DirectByteBuffer类来改善<br>
它是MappedByteBuffer类的子类,同时它实现了DirectBuffer接口,维护一个Cleaner对象来完成内存回收。因此它既可以通过Full GC来回收内存,也可以调用clean()方法来进行回收。
基础概念
BIO&NIO对比
NIO解决了服务端有很多不活跃连接;当连接不多时,并且每个连接都很活跃时,BIO性能可能比非阻塞要好
NIO所有数据都通过Buffer缓冲区再写入通道,不会像BIO将<font color="#c41230">字节</font>一个一个直接写入流中,减少频繁的I/O操作<br>
Tomcat是BIO的典型代表,Netty是NIO的典型代表
文件描述符FD
操作文件的3个阶段
用户进程(Selector轮询Socket)--> 内核系统(Epoll轮询FD)<br>
内核监视所有的文件描述,进程对FD的操作都需要进行系统调用<br>
一个客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)
中断程序
中断的位置由“信号”决定,如:网卡输入、硬盘缺页中断、断电中断、键盘鼠标点击
中断程序的线程优先级比较高
<font color="#c41230">网络</font>I/O模型
同步阻塞I/O模型<br>
进程阻塞在recvfrom系统调用,直到数据包到达且被复制到<font color="#c41230">用户空间</font>的缓冲区<br>
recvfrom是阻塞方法,从而导致网络socket的连接accept()、读read()、写write()都是阻塞方法<br>
同步非阻塞I/O模型<br>
进程<font color="#c41230">轮询</font>调用recvfrom,<font color="#c41230">内核空间</font>缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误<br>
<b><font color="#c41230"> I/O复用模型</font><br><font color="#381e11">(异步阻塞I/O)</font></b>
原理
多路-指的是多个socket连接,复用-指的是复用一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)<br>
IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),BIO只调用了recvfrom(),而NIO先调用了select/poll/epoll()进行fd的监视,当有fd就绪时,内核系统立即回调函数rollback,获取就绪fd的socket列表,然后每个socket进行recvfrom()调用<br>
IO多路复用技术通过把多个IO的阻塞<font color="#c41230">复用</font>到同一个select的阻塞列表上,它通过记录传入的每一个IO流的状态来同时管理多个IO,从而避免每一个IO都阻塞一个线程<br>
多路复用使得系统在单线程的情况下可以同时处理多个客户端请求<br>
<font color="#c41230">Reactor</font>设计模式实现了IO多路复用模型
三种角色
Reactor,将I/O事件dispatch给对应的Handler
Acceptor,处理客户端新连接并请求到处理器链中<br>
Handlers,执行非阻塞读/写任务
三种模式
单Reactor单线程模型
Redis
单Reactor多线程模型
主从Reactor多线程模型
Nginx、Netty、Memcached
I/O多路复用常用<font color="#c41230">系统调用</font>
select、poll、epoll是用来管理大量的文件描述符<br>
select
每个Socket都有一个<font color="#c41230">监视等待队列</font>,线程A的main方法遍历Socket时没有数据需要将线程A添加到每个Socket的等待队列上。直到某个Socket上有数据,将所有等待队列上的线程A删除,main方法继续执行。<br>
进程受阻于select系统调用,等待一个或多个套接字变为可读<br>
因为每个Socket都有一个监视等待队列需要管理,所以有很大系统开销,单个进程所打开的FD是有一定限制的,默认值是<font color="#ff0000">1024</font><br>
当 socket 收到数据后,select不知道哪个Socket已经就绪,select/poll需要<font color="#c41230">线性扫描</font>全部的channel集合得到就绪Socket,复杂度是O(N)。IO效率会随着FD数目的增加而线性下降<br>
通过内存复制将内核把FD消息通知给用户空间<br>
<font color="#c41230">epoll</font>
将等待队列和阻塞分离,只要监视一个<font color="#c41230">等待队列</font>就好了<br>
进程受阻于epoll_wait系统调用,等待直到注册的事件发生<br>
因为只有一个等待队列,所以没有太大的开销,epoll并没有句柄数限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于 1024。在1GB内存的机器上大约是10万个句柄左右,只受限于操作系统的最大句柄数。<br>
当 socket 收到数据后,中断程序会给 eventpoll 的“<font color="#c41230">就绪列表(rdlist)</font>”添加 socket 引用,epoll只会对“活跃”的socket进行轮询处理,复杂度降低到了O(1),epoll不存在随着FD数目的增加而线性下降<br>
使用内存映射技术mmap加速内核与用户空间的消息传递<br>
epoll内部的3个系统调用<br>
epoll_create(int size)<br>
进程启动时,生成一个epoll专用的文件描述符。它其实是在内核申请一空间用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socket fd数。
epoll_ctl(epfd,op,fd,event)<br>
每当监听一个fd的事件,都要通过系统调用epoll_ctl()事先注册这个文件描述符,一旦基于这个文件描述符对应的事件就绪时,内核会采用类似callback的<font color="#c41230">回调机制</font>,迅速激活这个文件描述符<br>
epoll_wait(epfd,events,maxevents,timeout)<br>
阻塞主线程,收集在epoll监控的事件中已经发生的事件,epoll将会把发生的事件赋值到events数组中
maxevents告诉内核这个events数组的大小
参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)
epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可
信号驱动I/O模型<br>(异步非阻塞I/O模型)
开启套接口信号驱动I/O功能,通过系统调用sigaction,此系统调用是非阻塞的,立即返回。<br>
进程不受阻,可以继续进行
当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据
异步非阻塞I/O模型
经典的<font color="#c41230">Proactor</font>设计模式
异步I/O,当数据准备就绪时,也为该进程生成一个SIGIO信号
和信号量唯一的<font color="#c41230">区别</font>在于应用程序不用调用recvfrom来读取数据,而是由内核线程主动将内核空间缓存复制到用户空间缓存
<font color="#c41230">磁盘</font>I/O模型
虚拟地址&物理地址<br>
虚拟地址(逻辑地址)<br>
凡是在代码中书写的内存地址都是逻辑地址(虚拟地址)<br>
逻辑上将最高的 1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”<br>
逻辑上将较低的3G字节(从虚拟地址 0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间“<br>
线性地址<br>
<font color="#ff0000">线性地址</font>是逻辑地址到物理地址变换之间的<font color="#c41230">中间层</font>。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。<br>
CPU的<font color="#c41230">页</font>式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。<br>
物理地址<br>
真实的<font color="#c41230">物理内存</font><br>
虚拟内存<br>
逻辑上分配的虚拟地址大小称为虚拟内存<br>
多个虚拟地址(多个进程用户空间/内核空间)可以<font color="#c41230">映射</font>到同一个物理内存地址,实现物理内存<font color="#c41230">共享</font>。<br>
所以虚拟内存在逻辑上可以远大于物理内存的大小<br>
多个用户进程对同一个物理内存上的写操作相互可见,省去了内核与用户空间的往来拷贝,实现零拷贝。<br>
页<br>
为了在磁盘(虚拟内存)和主存(物理内存)之间更高效地来回传送数据,系统将虚拟内存以及物理内存按照固定、相同的大小分割为一个个的页,虚拟内存上的称为<font color="#c41230">虚拟页</font> ,而物理内存上的称为<font color="#c41230">物理页</font> 。<br>
页表<br>
存储文件物理内存地址和进程虚拟内存(磁盘)的映射关系<br>
记录下当前进程每个虚拟页的情况,包括这个虚拟页是否已被分配使用、是否已被缓存到物理内存<br>
系统为每个进程提供了单独的页表,从而也实现了进程间数据访问权限的管理以及数据的保护。<br>
缺页<br>
用户进程访问虚内存这一段映射地址,通过查询页表,发现这一段地址并不在物理内存上<br>
虚拟内存和物理内存之间的数据传送正是发生于缺页<br>
缓冲区技术(buffer)<br>
用户IO缓冲区
用户空间的缓冲区,每个用户进程都有一个C标准I/O缓冲,缓冲区是使用malloc申请的,所以缓冲区是在堆区<br>
作用:预读数据到用户空间,防止频繁的用户态和内核态切换
常用系统调用
fgetc
当用户进程第一次调用fgetc 读一个字节时,fgetc函数可能通过系统调用进入内核读4K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户进程,把读写位置指 向I/O缓冲区中的第二个字符,以后用户再调fgetc,就直接从I/O缓冲区中读取而不需要进内核
fputc
用户进程调用fputc通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc就通过系统调用把I/O缓冲区中的数据传内核,内核最终把数据写回磁盘或设备
fflush
有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备或磁盘,这称为Flush操作,对应的库函数是fflush
fclose
fclose函数在关闭文件之前也会做Flush操作
类型
全缓冲
缓冲区写满了就写回内核
文件通常是全缓冲的
行缓冲
用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核
标准输入和标准输出对应终端设备时通常是行缓冲
无缓冲
每次系统调用做写操作都要通过系统调用写回内核
标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备
内核缓冲区
内核空间的缓冲区
作用:预读数据到内核,解决内存和磁盘速度不匹配的问题
读写原理
传统拷贝
<span style="font-size: inherit;">文件的读写,是以</span><font color="#c41230" style="font-size: inherit;"><b>页</b></font><span style="font-size: inherit;"><b><font color="#c41230">为单位</font></b>的,页的大小通常为4kb,程序读取文件时,会执行一次<font color="#c41230">read系统调用</font>,由用户态转换为内核态,然后从磁盘读取</span><font color="#c41230" style="font-size: inherit;">一页</font><span style="font-size: inherit;">数据放到内核缓冲区。</span><br>
文件源 > 内核缓冲区 > 用户缓冲区(IO缓冲区)> 用户方法堆byte[] > 字符串<br>
read把数据从内核空间复制到IO缓冲区,write把数据从IO缓冲区复制到内核空间。read()方法每次只能从内存空间读取一个字节到用户空间,字节数组下标移动一位<br>
流是对IO字节数组byte[]的管理,channel是对IO缓冲区buffer的管理<br>
InputStream
假设系统一页大小为4kb,要读取的文件有10kb<br>
FileInputStream用read()第一次读取文件时,进行一次系统调用,由用户态切换到内核态,从文件中读取4kb存储到内核缓冲区,读取一个字节后切换到用户态;<br>
接着继续读取,每读一个字节,进行<font color="#c41230">一次</font>系统调用,经过<font color="#c41230">两次</font>上下文切换<br>
当内核缓冲区数据不足时,从磁盘文件中读取并填充。<br>
整个过程,复制磁盘数据到内核缓冲区3次,用户态从内核空间read()字节到用户空间1024*10次,用户态内核态上下文切换1024*10*2次<br>
BufferedInputStream
BufferedInputStream用read()第一次读取文件时,进行一次系统调用,由用户态切换到内核态,从文件中读取一页数据4kb存储到内核缓冲区;<br>
默认IO缓冲区大小为8kb,把内核缓冲区的4kb复制到IO缓冲区时,发现缓冲区没有填满,这个时候,会再次从磁盘读取一页数据4kb到内核缓冲区,然后再将内核缓冲区中的数据复制到IO缓冲区,IO缓冲区填满,第一次系统调用结束,切回用户态继续用户线程的执行;<br>
接着继续read时,不用进行系统调用,BufferedOutputStream直接从Buffer缓冲区读取。<br>
整个过程,复制磁盘文件数据到内核缓冲区3次,用户态内核态上下文切换4次,系统调用从内核缓冲区复制到用户缓冲区3次,用户态内核态上下文切换4次,用户态从Buffer缓冲区read()到用户空间1024*10次,大大减少了用户态和内核态的上下文切换<br>
零拷贝
当需要传输的数据远远大于内核缓冲区的大小时,内核缓冲区就会成为瓶颈<br>
零拷贝是指避免在用户态与内核态之间来回拷贝数据的技术,减少上下文切换以及CPU的拷贝时间,实现CPU的零参与<br>
DMA(直接内存存取)<br>
通过<font color="#c41230">sendFile</font>系统调用,触发DMA控制器将<font color="#c41230">磁盘</font>数据拷贝到<font color="#c41230">内核</font>空间,不需要经过CPU进行数据拷贝<br>
磁盘到内核空间属于<font color="#c41230">DMA拷贝</font>(内核空间到用户空间之间的数据拷贝需要cpu的参与)<br>
用户空间不能直接去磁盘空间中读取数据,必须由经由内核空间通过DMA来获取<br>
需要将DMA控制器设置好了它才会正常工作,如:设置好源地址、目的地址、触发信号<br>
直接内存映射<br>
通过<font color="#c41230">mmap</font>系统调用, 将一段用户空间内存映射到内核空间,<font color="#c41230">共享</font>物理内存。当映射成功后,用户对用户空间的修改可以直接反映到内核空间。<br>
创建虚拟空间<br>
在进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的<font color="#c41230">虚拟地址</font>作为映射区域(用户缓冲区)<br>
建立地址映射<br>
调用系统函数mmap(),在内核空间创建页表,存储文件物理地址和进程虚拟地址的映射关系<br>
将内核虚拟地址也映射到进程虚拟地址对应的物理地址,实现共享<br>
用户进程对映射区进行读写<br>
mmap读文件<br>
访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上,引发缺页异常<br>
CPU接受到缺页中断后,向DMA控制器发出指令<br>
DMA控制器开始将文件数据拷贝到物理内存,更新页表(新增虚拟内存地址到物理内存的映射)<br>
mmap写文件<br>
访问虚拟地址空间这一段映射地址,通过页表查看是否存在对应的物理内存,如果不存在,则通过缺页异常加载对应的页<br>
页表中查询到对应物理内存后,直接写入数据<br>
用户主动触发刷盘或内核按照某种规则触发刷盘<br>
DMA控制器将内核缓冲区数据写入磁盘<br>
sendFile和mmap对比<br>
mmap适合小数据读写, sendFile适合大文件传输<br>
0 条评论
下一页