I/O基本概念
缓冲区基础
缓冲区是I/O的基础, 进程使用read(), write()将数据读出/写入从缓冲区中; 当缓冲区写满, 内核向磁盘发出指令, 将缓冲区中数据写入磁盘中(这一步不需要CPU), 当磁盘控制器将缓冲区装满, 内核将缓冲区数据拷贝到进程中指定的缓冲区; 操作如下图:
当中忽略了很多细节, 只涉及简单的步骤
上面的进程通常是用户进程, 需要指出的一点就是, 当内核接受到read指令的时候, 首先会去内核内部的缓冲区寻找所需数据, 如果所需的数据不在缓冲区中, 那么用户进程将被挂起, 直到缓冲区中存在数据, 最后内核再将缓冲区中数据拷贝到进程内部的缓冲区中.
注:
1.使用内核的意义:
1. 用户进程不能直接访问磁盘, 需要使用中间层进行访问;
2. 磁盘中的数据总是块状, 而用户需要的数据可能是任意大小的;
3. 内核充当的中间人的角色;
2.读取过程存在的优化方案:
1.进程将所有缓冲区的内存地址交给内核. 进程read操作: 内核根据缓冲区地址, 将数据发散到进程中每个缓冲区的; 进程write操作: 将进程中每个缓冲区的数据集聚, 然后一起存入内核缓冲区; 这样做避免了每一次读写操作都要进行磁盘操作, 减少性能损耗
2.使用虚拟内存. 将内核缓冲区地址与进程缓冲区地址映射到同一片虚拟内存上面, 这样当**磁盘控制器**操控内核缓冲区的时候就等价于操控进程缓冲区, 整个过程避免了拷贝操作这样做的前提是内核和用户的缓冲区必须使用相同的页对齐(固定的大小字节组,一般为512字节大小)具体见下图:
3.使用分页技术进行操作系统I/O:a.确定请求数据在文件系统的哪些磁盘区域, 这些数据可能横跨多个文件系统, 且位置不连续b.内核空间分配足够多的内存页(缓冲区), 用于容纳确定的文件系统页'c.建立内存页与磁盘文件之间的联系, 二者建立映射d.为每一个内存页进行检查e.根据d操作中的检查结果, 决定每个内存页是否执行读写操作f.从磁盘中读取文件内容, 将数据导入, 文件系统对导入数据进行解析
进程与内核的缓冲区共享同一片内存区域
Java中的缓冲区(Buffer)
缓冲区基础
- 属性:
容量(Capacity): 缓冲区能容纳数据元素的最大量
上界(Limit): 代表缓冲区中最后一个元素的位置, 也代表着缓冲区中元素个数
位置(Position): 下一个被读写位置的索引
标记(Mark): 记录位置
属性范围大小:
0 <= mark <= position <= limit <=capacity
- 存取
Buffer内部使用get(),put()函数进行数据存储, get/put当index位置超出范围,抛BufferOverflowException - 转置: 使缓冲区的内容逆置, 只需要修改position与limit指向的位置, 让position执行末尾
- 清空缓冲区: 使用clear(), clear并没有改变缓冲区中元素, 他所改变的就是设定了limit值, 并让其指向0号位置
- 复制缓冲区内容:
当缓冲区属性为只读, 那么复制缓冲区内容就是浅复制, 对一个缓冲区的改变会反映到另外一个缓冲区上面, 新的缓冲区将会继承旧的缓冲区所有的属性. 当对只读缓冲区进行put操作, 将抛ReadOnlyBufferException异常
注:
如果只读缓冲区与可写缓冲区共享一片内存区域, 可写缓冲区进行改变(如put操作), 这种改变将会体现在只读缓冲区上面; 就所谓的浅复制
字节缓冲区
- 使用字节缓冲区作为通道执行I/O操作的源和目标, 而向通道中传递一个非ByteBuffer对象的时候将会出现如下的问题:
1.创建一个临时的ByteBuffer对象
2.将目标对象内容复制到临时的ByteBuffer中
3.使用临时ByteBuffer进行I/O操作
4.结束操作, 回收无用数据
上面的过程导致的问题就是严重性能损耗, 当非ByteBuffer对象很多的时候
通道(Channel)
Channel基本概述
通道使用ByteBuffer作为端点, 使文件系统, 进程, 网络等等进行交互, 这种交互总是最小的开销(通道只支持字节操作);
如下图: FileSystem与NetWork的I/O操作是通过Channel执行
-
打开通道:
通道分为2种类型(File, Socket), File: FileChannel, Socket: SocketChannel, ServerSocketChannel, DatagramChannel;
获得方式:
1.FileChanne fc = new FileInputStream(new File(PATH)).getChannel();
//文件有FileInputStream, FileOutputStream, RandomAccessFile
2.SocketChannel sc = SocketChannel.open();
其余Socket的Channel同2
注: 在java.net中的socket使用getChannel获得Channel, 但这样的Channel并不是新通道(它永运不会创建新通道), 只有存在一个与Socket关联的通道, 这样获得的才是新通道, 否则就是一个假通道 -
使用通道:
通道间的数据可以是单向的也可以是双向的, 默认的ByteChannel接口实现的是双向数据传输, 但是遇到这样的问题的时候双向传输将会抛异常: FileInputStream的getChannel获得的FileChannel对象是只读的, 但是由于FileInputStream实现了ByteChannel接口, 因此可以调用read, write操作, 当这个管道调用write将会抛未经检查异常NonWritableChannelException;
通道可以是阻塞, 或非阻塞; 非阻塞: 通道永远不会让调用线程休眠, 请求的操作立即完成, 要么返回获得的数据, 要么返回未获得数据;
注: 只有sockets, pipes才能使用非阻塞模式 -
关闭通道:
使用close()关闭, 关闭通道将会导致底层I/O服务线程暂时阻塞, 即使该通道处于非阻塞模式, close多次调用没有影响(close也会阻塞, 对已经关闭的通道使用close不会产生任何操作, 只会立即返回); 关闭的时候可以使用isOpen()判断通道开放状态. 对于已经关闭的Channel使用读写都将抛CloseChannelException
注:
1.当通道实现了InterruptibleChannel接口, 那么当某一线程在该通道上阻塞并且被中断, 那么该通道将被关闭, 被阻塞的线程抛ClosedByInterruptException(在Selectors上阻塞的中断线程不会导致通道关闭); InterruptibleChannel的检查手段是通过isInterrupted()判断线程的interrupt status
看似上面这种操作过于苛刻, 线程阻塞且中断就关闭对应的Channel, 但这完全是考虑因为操作系统而导致的I/O问题, 增强程序健壮性.
2.中断的线程可以使用异步关闭, 实现了InterruptibleChannel的线程接口的通道可以在任何时候被关闭, 一个通道关闭的时候在这个通道上的所有阻塞的线程都将被唤醒并接受到一个AsynchronousCloseException
3.不实现InterruptibleChannel接口的通道通常不进行底层特殊操作, 这些通道永远不会出现阻塞的问题 -
Scatter/Gather
此处的Scatter/Gather就与缓冲区基础中读取优化中的第2点一样, 在多个缓冲区上实现一个I/O操作.
Scatter: 对于进程read, 从通道读取的数据会按顺序散布到多个缓冲区, 直到缓冲区或通道中数据用完.
Gather: 对于进程write, 数据从多个缓冲区按顺序抽取, 然后将数据放入通道中.
下面使用Gather演示从缓冲区中获取数据写入管道中(Scatter与Gather相反, 就是将管道中数据存入缓冲区):
缓冲区中元素根据Position, Limit定位每个缓冲区中取到的字符串, 使用标号对应记录缓冲区内容在Channel中顺序
优点: 避免数据的来回拷贝, 可以按照不同的方式组合缓冲区数据的引用
下面给出演示代码:
package com.demo1;import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.GatheringByteChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
public class JavaNIOGatherTest{private static final String DEMOGRAPHIC = "c:\\Users\\regotto\\Desktop\\blahblah.txt";public static void main (String [] argv) throws Exception{//默认Buffer10个大小int reps = 10;if (argv.length > 0) {reps = Integer.parseInt (argv [0]);}FileOutputStream fos = new FileOutputStream (DEMOGRAPHIC);GatheringByteChannel gatherChannel = fos.getChannel( );ByteBuffer [] bs = utterBS (reps);//读取到文件末尾, 自动停止writewhile (gatherChannel.write (bs) > 0) {}System.out.println ("Mindshare paradigms synergized to "+ DEMOGRAPHIC);fos.close( );}//模拟缓冲区内容private static String [] col1 = {"Aggregate", "Enable", "Leverage","Facilitate", "Synergize", "Repurpose","Strategize", "Reinvent", "Harness"};private static String [] col2 = {"cross-platform", "best-of-breed", "frictionless","ubiquitous", "extensible", "compelling","mission-critical", "collaborative", "integrated"};private static String [] col3 = {"methodologies", "infomediaries", "platforms","schemas", "mindshare", "paradigms","functionalities", "web services", "infrastructures"};//System.getProperty("line.separator");获取换行private static String newline = System.getProperty ("line.separator");//获取缓冲区内容private static ByteBuffer [] utterBS (int howMany)throws Exception{List list = new LinkedList( );for (int i = 0; i < howMany; i++) {//缓冲区中每一个字符串都随机给一个Position, Limitlist.add (pickRandom (col1, " "));list.add (pickRandom (col2, " "));list.add (pickRandom (col3, newline));}ByteBuffer [] bufs = new ByteBuffer [list.size( )];list.toArray (bufs);return (bufs);}private static Random rand = new Random( );private static ByteBuffer pickRandom (String [] strings, String suffix)throws Exception{String string = strings [rand.nextInt (strings.length)];int total = string.length() + suffix.length( );//为每一个字符串分配对应total容量的ByteBufferByteBuffer buf = ByteBuffer.allocate (total);//编码转换buf.put (string.getBytes ("US-ASCII"));buf.put (suffix.getBytes ("US-ASCII"));buf.flip( );return (buf);}
}运行结果如下(txt文件中存储的内容):
字符串顺序是随机的, 前面代码中使用随机数取Position, Limit
Enable compelling methodologies
Enable frictionless platforms
Facilitate cross-platform paradigms
Reinvent collaborative platforms
Repurpose extensible infomediaries
Strategize integrated paradigms
Strategize ubiquitous platforms
Leverage collaborative infomediaries
Harness collaborative schemas
Harness extensible paradigms
阻塞, 非阻塞; 同步, 异步简述
-
同步, 异步, 阻塞, 非阻塞简述:
同步和异步是相对于操作结果来说,会不会等待结果返回。
阻塞和非阻塞是相对于线程是否被阻塞。 -
这两者存在本质的区别:
它们的修饰对象是不同的。阻塞和非阻塞是指进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪。
而同步和异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞,异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。
网络I/O
-
I/O模型:
输入操作包含:等待数据; 从内核向进程复制数据; 对于Socket, 先是等待数据从网络到达, 随后将到达的数据复制到内核的缓冲区, 最终将内核缓冲区数据复制到应用进程缓冲区 -
Unix的5种I/O模型
阻塞式IO: 阻塞当前应用, 直到数据从内核缓冲区复制到应用进程缓冲区才返回
非阻塞式IO:内核返回数据错误, 应用程序依旧执行, 但每隔一段时间就要执行系统调用IO是否完成,轮询(polling).当内核有数据时就通知应用等待进行复制(内核复制过程仍具有阻塞)调用更多的底层, CPU利用率低
IO复用(select, poll): 使用select/poll等待数据, 采用多个socket进行等待, 其中一个出现变为可读,就将内核数据复制到进程中(此过程将会出现阻塞状态), 这种处理方式使得当个线程具有多个IO处理能力, 体现了IO的复用,又称为事件驱动IO
信号驱动式IO(SIGIO):应用程序使用sigaction系统调用, 在等待数据阶段应用进程是非阻塞的, 当内核获得数据就向应用进程发送SIGIO信号, 通知程序处理数据的复制过程(此过程将会出现阻塞状态).相比于非阻塞式IO, 信号驱动IO的CPU利用率更高
异步IO:应用程序使用aio_read系统调用, 应用继续执行, 处理其他数据(不存在阻塞状态), 当内核完成所有需要的IO操作, 再通知应用程序可直接获取自身进程缓冲区数据, 也就不存在复制过程的阻塞情况###相对于信号驱动IO, 异步IO的信号处理是在IO已经完成的时候, 而信号驱动是在IO开始的时候
模型图例如下:
- 5种IO模型比较
同步IO: 将数据从内核缓冲区复制到应用缓冲区阶段, 应用进程会阻塞阻塞式IO, 非阻塞式IO, IO复用, 信号驱动式IO, 主要区别在于第一阶段: 非阻塞式IO, 信号驱动式IO, 异步IO第一阶段不会阻塞
异步IO: 不会阻塞
- IO复用:
包含select/poll/epoll, select出现最早, 然后是poll, 再是epoll
包含select/poll/epoll, select出现最早, 然后是poll, 再是epollselect:int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);readfds, writefds, exceptfds代表读,写,异常条件描述符集合(使用fd_set类型的数组实现,大小为FD_SETSIZE)timeout表示select调用会一直阻塞直到超出timeout, 调用成功返回0, 异常-1, 超时0 poll:int poll(struct pollfd *fds, unsigned int nfds, int timeout);使用链表实现
-
select与poll二者的区别:
select会修改描述符, poll不会, select描述符使用数组fd_set实现, 默认大小1024, 只能监听1024个描述符, 需要修改FD_SETSIZE, poll没有描述符数量限制poll提供更多的事件类型, 描述符重用率比select高一个线程对某个描述符调用select或poll, 另一个线程关闭该描述符, 会导致调用结果不确定二者的速度都慢, 每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区select和poll的返回结果中没有声明哪些描述符已经准备好了, 当返回值大于0, 采用轮询的方式找到IO完成描述符所有系统都支持select, 只有较新的系统支持poll
-
epoll:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
用于向内核注册新的描述符或改变某个文件描述符的状态, 已注册的描述符使用红黑树维护, 通过回调函数内核会将IO准备好的描述符加入一个链表中管理
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);获得事件完成的描述符
只需要将描述符从进程缓冲区向内核缓冲区拷贝一次, 进程不需要通过轮询的方式来获得事件完成的描述符