杂学Linux-IO篇

本文深入探讨Linux系统中的IO机制,从普通文件IO、缓冲区IO到高效的FileChannel直接内存映射。通过示例解释了ByteBuffer的不同使用方式以及系统调用在IO过程中的角色,阐述了如何通过NIO提高文件写入效率,减少用户态与内核态切换的成本。
摘要由CSDN通过智能技术生成

本文来介绍一下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模型,请关注笔者后续的文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值