IO知识整理

本文详细梳理了IO知识,从系统层面的page cache、ByteBuffer、mmap和Direct IO,到网络IO的TCP、Socket、各种IO模型,包括BIO、NIO、多路复用IO以及Netty的实现。讨论了TCP的三次握手、四次分手,Socket的四元组,以及内核级别的配置如backlog、nodelay等。最后探讨了C10K问题和IO数据流向,揭示了不同IO模型在处理大量并发连接时的优缺点。
摘要由CSDN通过智能技术生成

IO

面向系统IO

page cache

程序虚拟内存到物理内存的转换依靠cpu中的mmu映射

物理内存以page(4k)为单位做分配

多个程序访问磁盘上同一个文件,步骤

  • kernel将文件内容加载到pagecache
  • 多个程序读取同一份文件指向的同一个pagecache
  • 多个程序各自维护各自的fd,fd中seek记录偏移量,指向具体数据

pagecache特点

  • 为内核维护的中间层
  • 淘汰机制
  • 持久化机制,是否会丢失数据
  • 占用内存大小

Java输出流

  • 不使用buffer的输出流
  • 使用buffer的输出流
    • 速度超过不使用buffer的输出流,是因为先在jvm内存中写入,默认到8kb写完调用一次systemcall
  • 随机输出流

内存充足的情况下,会优先写入pagecache,只要未达到脏页持久化阈值,就不会写入磁盘,不论pagecache里面多少数据,关机重启后将全部丢失;内存不充足的情况下,pagecache中的新数据的写入会导致老数据的持久化,进而写入磁盘。

可以通过手动调用系统flush将脏页写入磁盘

脏页持久化之后cache依然存在于内存,只有内存不够分配时才会淘汰cache,且淘汰的cache一定不是脏页。

ByteBuffer

@Test
public void whatByteBuffer() {
   
  // 堆内分配内存
  // ByteBuffer buffer = ByteBuffer.allocate(1024);
  // 堆外分配内存
  ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

  System.out.println("postition: " + buffer.position());
  System.out.println("limit: " + buffer.limit());
  System.out.println("capacity: " + buffer.capacity());
  // position起点 limit终点 capacity容量
  System.out.println("mark: " + buffer);

  // position->3 
  buffer.put("123".getBytes());

  System.out.println("-------------put:123......");
  System.out.println("mark: " + buffer);

  // 读写交替 position->0 limit->3
  buffer.flip();

  System.out.println("-------------flip......");
  System.out.println("mark: " + buffer);

  // 读取一个byte pos->1 limit->3
  buffer.get();

  System.out.println("-------------get......");
  System.out.println("mark: " + buffer);

  // 读写交替 pos->2 limit->1024 已读取的字节删除
  buffer.compact();

  System.out.println("-------------compact......");
  System.out.println("mark: " + buffer);

  buffer.clear();

  System.out.println("-------------clear......");
  System.out.println("mark: " + buffer);
}

mmap

堆外创建一个地址空间,只有文件系统可以直接调用

该内存空间的数据不需要通过系统调用及用户态和内核态的切换,直接写入数据就可以同步

但是依然收到系统page cache限制,存在数据丢失的可能

Direct IO

可以绕过系统控制的page cache,不受系统page cache参数控制

程序自己维护page cache,通过自定义代码逻辑维护一致性/dirty等…

好处是自己维护page cache刷磁盘的阈值,与系统通用配置隔离,但是依然存在丢失数据的逻辑

@Test
public void testRandomAccessFileWrite() throws  Exception {
   
  RandomAccessFile raf = new RandomAccessFile(path, "rw");

  raf.write("hello mashibing\n".getBytes());
  raf.write("hello seanzhou\n".getBytes());
  System.out.println("write------------");
  System.in.read();

  // 指针调整,往前调整后继续写会直接覆盖原有数据
  raf.seek(4);
  raf.write("ooxx".getBytes());

  System.out.println("seek---------");
  System.in.read();

  FileChannel rafchannel = raf.getChannel();
  // mmap  堆外(jvm堆外且是linux的java进程堆外的内存)  和文件映射的   byte  not  object
  // 此时文件大小会变为4096kb
  MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);


  map.put("@@@".getBytes());  //不是系统调用  但是数据会到达 内核的pagecache
  //曾经我们是需要out.write()  这样的系统调用,才能让程序的data 进入内核的pagecache
  //曾经必须有用户态内核态切换
  //mmap的内存映射,依然是内核的pagecache体系所约束的!!!
  //换言之,丢数据

  System.out.println("map--put--------");
  System.in.read();

  // 数据刷入磁盘 等于flush
  // map.force(); 

  raf.seek(0);

  ByteBuffer buffer = ByteBuffer.allocate(8192);
  // ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

  // 将channel中的数据读入buffer 等价于buffer.put()
  int read = rafchannel.read(buffer);   
  System.out.println(buffer);
  
  // 此时翻转后可以开始读取数据 pos->0 limit->4096
  buffer.flip();
  System.out.println(buffer);

  for (int i = 0; i < buffer.limit(); i++) {
   
    Thread.sleep(200);
    System.out.print(((char)buffer.get(i)));
  }
}

请添加图片描述

面向网络IO

TCP

面向连接的,可靠的传输协议

  • 三次握手
  • 四次分手
  • 内核级开辟资源,建立连接
    • 即使服务端没有accept,也可以建立连接(established状态),只是不分配具体的pid
    • 而从java进程内部,看到的连接依然是listen状态
    • 此时可以从客户端发送数据,而服务端不能接收,服务端开启accept后可以接收到已发送的数据

三次握手报文

  • win 滑动窗口长度 协商结果为115
    • tcp拥塞控制,提速
      • 服务端窗口已满
      • 客户端阻塞不再继续发包(如果继续发包会丢弃后发的数据)
  • seq 序列号 client->server server将序列号+1作为ack返回给client
  • mtu ifconfig可以看到网口字节长度
  • mss 实际数据字节长度,基本上为mtu-ip长度-port长度 各自20字节

请添加图片描述

四次分手

  • 异常状态
    • CLOSE_WAIT(接收方第三次FIN信号没有发送成功)
    • TIME_WAIT (连接关闭后,为防止接收方没有收到最后一次ACK,保留连接对应资源)
    • FIN_WAIT2(没有收到FIN信号)

请添加图片描述

Socket

四元组(cip + sport +sip + sport)

  • 服务端不需要给客户端连接分配新的端口号(四元组标识唯一,客户端服务端各存一份)

内核级别

关键配置

  • backlog

    • 配置后续队列,超过配置条数+1之后,会将连接状态置为SYNC_REC状态,不可连接

    • 应当根据处理速度、cpu条数来进行配置,防止建立过多连接

  • nodelay

    • false:使用优化,会积攒超过buffer长度的字节一次发到服务端,但是会有延时
    • true:不使用优化,根据内核调度尽快发送,会多出几次网络io
  • oobinline

    • 结合nodelay配置使用效果明显,会单独发出第一个字节
  • keepalive

    • 开启后会保持心跳,判断相互之间连接是否生效

IO模型

模型分类

  • 同步:程序自己进行R/W操作
  • 异步:内核完成R/W 程序像是不访问IO一样,直接使用buffer,linux不可用
  • 阻塞:BLOCKING BIO
  • 非阻塞:NONBLOCKING NIO

linux

  • 同步阻塞:BIO
  • 同步非阻塞:NIO、多路复用
BIO

多线程BIO模型,主线程accept+创建子线程,子线程做recv

public static void main(String[] args) throws Exception {
   
  ServerSocket server = new ServerSocket(9090,20);

  System.out.println("step1: new ServerSocket(9090) ");

  while (true) {
   
    // linux指令 socket() ->fd3 bind(fd3,8090) accept(fd3)->fd5
    Socket client = server.accept();  //阻塞1
    System.out.println("step2:client\t" + client.getPort());

    // 主线程只负责创建子线程用来接收数据
    new Thread(new Runnable(){
   

      public void run() {
   
        InputStream in = null;
        try {
   
          in = client.getInputStream();
          BufferedReader reader = new BufferedReader(new InputStreamReader(in));
          while(true){
   
            // linux指令 recv(fd5) 子线程负责读取数据
            String dataline = reader.readLine(); //阻塞2

            if(null != dataline){
   
              System.out.println(dataline);
            }else{
   
              client.close();
              break;
            }
          }
          System.out.println("客户端断开");

        } catch (IOException e) {
   
          e.printStackTrace();
        }

      }



    }).start();

  }

请添加图片描述

BIO多线程模型需要创建新线程(系统调用clone)+线程调度,导致速率降低

BIO的弊端主要来自于阻塞,系统内核级别阻塞(accept+recv)

  • accept等待连接和recv接收数据会阻塞,所以通过创建子线程的方式进行规避,但是clone指令+线程切换也是有很高成本的,当客户端连接数量增加时,处理速度会明显降低
  • 因为recv接收数据会发生阻塞,所以当多个客户端连接的时候只能使用多个线程的方式来进行读取,如果只使用一个线程,当连接阻塞时,没办法读取其他连接发送的数据
NIO

accept指令不会阻塞,如果没有连接会直接返回-1,在Java api中会体现为对象为null

不阻塞的好处

  • 可以不额外创建线程,在一个线程中执行建立连接和接收信息两个操作,节省了创建连接和线程切换的成本

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kpaWtaly-1676460489179)(IO.assets/image-20230209151431908.png)]

public static void main
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值