本文来介绍一下Linux系统IO相关知识,内容为自己所学所感,或许不是那么的准确,仅供参考,希望对初学者有一定的帮助,对于下面文章中提到的pageCache请参考我的另一篇文章杂学Linux-基础篇
IO
可以这么说计算机中由CPU、内存和IO三部分组成,IO代表着所有的输入输出设备,对于编程人员来说了解IO的底层过程对我们来说百利而无一害。
FileIO
普通IO
直接通过系统调用写入pageCache,效率较低。
NIO:
以下两图简明的展示了文件IO的过程,图中包含3种IO过程,ByteBuffer的两种和FileChannel的一种;
- ByteBuffer使用allocate在jvm堆中创建字节数组和使用allocateDirect在jvm堆外Java进程堆中创建字节数组的异同
它们的区别就是在不同的内存位置创建字节数组,在jvm中创建的效率相比于在jvm外创建的效率要低这是因为在jvm中创建的在写到pageCache中时需要做一些转化(转化为在jvm外的),然后再通过FileChannel中的read() 、write ()写到pageCache,但这两种方式还是需要系统调用
- FileChannel
FileChannel这种方式直接在java进程堆外的位置做了和pageCache的映射,直接与pageCache联系在了一起,在写入时不需要经过系统调用,效率最高。
- 普通文件:每次write都需要进行系统掉用,这会造成短时间内大量的用户态内核态的切换,造成IO的效率不高。
- 带buffer的写法:减少了用户态和内核态的切换
- FileChannel:
普通文件写法、buffer写法和NIO中FileChannel的参考使用
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class OSFileIO {
static byte[] data = "123456789\n".getBytes();
static String path = "/root/testfileio/out.txt";
public static void main(String[] args) throws Exception {
}
//最基本的file写法 短时间大量系统调用
public static void testBasicFileIO() throws Exception {
File file = new File(path);
FileOutputStream out = new FileOutputStream(file);
while(true){
Thread.sleep(10);
out.write(data);
}
}
//测试buffer文件IO
//使用buffer jvm会有一个默认8KB大小的字节数组做缓存 当缓存满了之后才会进行系统调用写入内核pageCache
public static void testBufferedFileIO() throws Exception {
File file = new File(path);
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
while(true){
Thread.sleep(10);
out.write(data);
}
}
//测试文件NIO
public static void testRandomAccessFileWrite() throws Exception {
RandomAccessFile raf = new RandomAccessFile(path, "rw");
raf.write("hello world\n".getBytes());
raf.write("hello Java\n".getBytes());
System.out.println("write------------");
System.in.read();
raf.seek(4);
raf.write("thth".getBytes());
System.out.println("seek---------");
System.in.read();
FileChannel rafchannel = raf.getChannel();
//mmap 堆外 和文件映射的byte数组
MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
map.put("@@@".getBytes()); //不是系统调用 但是数据会到达内核的pagecache
//曾经我们是需要out.write() 这样的系统调用,才能让程序的data 进入内核的pagecache 必须有用户态内核态切换
//mmap的内存映射,依然是有内核的pagecache体系所约束的!还会丢数据
//直接IO是忽略linux的pagecache
//是把pagecache 交给了程序自己开辟一个字节数组当作pagecache,动用代码逻辑来维护一致性/dirty......一系列复杂问题
System.out.println("map--put--------");
System.in.read();
// map.force(); // flush
raf.seek(0);
ByteBuffer buffer = ByteBuffer.allocate(8192);
// ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
int read = rafchannel.read(buffer); //相当于buffer.put()
System.out.println(buffer);
buffer.flip();
System.out.println(buffer);
for (int i = 0; i < buffer.limit(); i++) {
Thread.sleep(200);
System.out.print(((char)buffer.get(i)));
}
}
//ByteBuffer的基本使用
public void whatByteBuffer(){
// ByteBuffer buffer = ByteBuffer.allocate(1024); jvm对内分配
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // jvm外分配
//ByteBuffer的三个属性
//postition:字节数组的写入位置指针
//limit:在读数据时,指向可以读取范围的最后,防止读取时超出范围读,写入时指在数组的最后
//capacity:数组大小
System.out.println("postition: " + buffer.position());
System.out.println("limit: " + buffer.limit());
System.out.println("capacity: " + buffer.capacity());
System.out.println("mark: " + buffer);
buffer.put("123".getBytes());
System.out.println("-------------put:123......");
System.out.println("mark: " + buffer);
buffer.flip(); //读写交替 limit指针发生变化
System.out.println("-------------flip......");
System.out.println("mark: " + buffer);
buffer.get();
System.out.println("-------------get......");
System.out.println("mark: " + buffer);
buffer.compact(); //读写交替 postition和limit指针发生变化
System.out.println("-------------compact......");
System.out.println("mark: " + buffer);
buffer.clear();
System.out.println("-------------clear......");
System.out.println("mark: " + buffer);
}
}
SocketIO
SocketIO也称网络IO,它需要客户端和服务端建立TCP连接,然后才能通信;客户端和服务端建立的这个通信通道我们可以理解为就是我们的Socket;一个Socket由一个四元组唯一确定[客户端IP 端口号+ 服务端IP 端口号],一个四元组可以唯一标识一个Socket。
- Socket是内核级的,只要客户端和服务端经过3次握手建立了TCP连接后,内核就会在客户端和服务端开辟资源。
- 当服务端创建了ServerSocket,已经完成了绑定端口等操作而没进行accept()方法之前,客户端就可以和服务端建立TCP连接,建立了连接后,内核就会开辟资源,但是只有调用了accept()方法后,对应的通信进程才会拿到内核给予的FD(文件描述符),此时两进程间才可以通信了。否则只是在内核层面开辟资源,可以这么说在accept方法之前两通信计算机的内核已经建立了连接(维护有缓存buffer),但只有在调用了accept方法之后进程才能拿到内核分配的资源(FD)。
- 通信双方各自维持有自己的接收队列与发送队列,在3次握手后其实就可以收发消息了,但在没调用accept方法之前,消息会存在自己的队列中。
- 创建一个连接的过程内核层面会依次调用 socket() -> bind() -> listen() -> accept()方法。
- 在我们编写Socket时,也可设定一些参数来优化通信过程,具体的参数可在网上查找。
BIO模型
在IO的世界里,BIO是我们经常听到的一个词,那什么是BIO呢?其实BIO就是采用同步阻塞方式进行通信的IO模型,当客户端与服务端建立TCP连接后,服务端会阻塞在accept()方法,等待客户端的连接,当客户端发起连接后服务端会从主线程中克隆出一个线程来完成与客户端的连接通信任务,而服务端的主线程则再次等待其它客户端的连接,客户端和克隆出的新线程通信也是同步阻塞的,克隆出的线程会调用recv方法阻塞的等待消息的到来。
其实除了BIO这种模型外,还有其他的IO模型,请关注笔者后续的文章。