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