欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

2.2磁盘IO&网络IO工作机制

程序员文章站 2024-03-24 13:51:04
...

磁盘I/O工作机制:访问文件

在Java中,读 & 写对应了 read() & write() 两个系统调用,但只要系统调用,就会存在内核空间地址用户空间地址切换的问题(操作系统为了保护系统安全,必须将内存空间和用户空间进行隔离),因为数据可能需要从内核空间用户空间复制。

如果遇到了非常耗时的操作,如磁盘I/O,数据得从磁盘–>内核空间–>用户空间,复制了两遍,将会非常缓慢。

因此操作系统在内核空间加入了缓存机制,也就是说:如果用户程序访问的是同一段磁盘地址的数据,将会从上一次在内核空间的缓存中直接取得,这样就只复制了一次。

接下来会介绍几种访问文件的方式:

  1. 标准访问文件方式

    • 当调用read()接口时,操作系统检查在内核的高速缓存中有没有数据,如果有就直接返回;否则从磁盘中读取,然后再缓存在内核中。

    • 当调用write()接口时,数据从用户地址空间–>内核地址空间的缓存中,操作完成。

      至于什么时候写到磁盘,由操作系统决定,除非调用sync同步,强制写入磁盘中。

    用户地址空间
    内核地址空间
    物理磁盘
    应用缓存1
    应用缓存2
    高速页缓存1
    高速页缓存2
    磁盘数据
    read方法
    write方法
  2. 直接I/O方式

    • 程序直接访问磁盘数据,不经过操作系统内核,这样做能够减少一次数据复制。
    • y优点:适用与由程序实现~~(而不是操作系统)~~的数据库管理系统等,因为操作系统不知道应该缓存哪些数据、失效哪些数据,但是程序知道。
    • 缺点:如果访问的数据不在缓存中,那么每次都会直接从磁盘加载,而这种加载相对非常慢,多次之后会变得十分低效。
    用户地址空间
    内核地址空间
    物理磁盘
    应用缓存1
    应用缓存2
    高速页缓存1
    高速页缓存2
    磁盘数据
    read方法
    write方法
  3. 同步访问文件方式

    • 数据的读&写都是同步操作,和标准访问不同的是:只有当数据被成功写到磁盘时,才返回标志给应用程序
    • 性能差,只针对于数据安全性要求高的场景,通常由硬件完成
    用户地址空间
    内核地址空间
    物理磁盘
    应用缓存1
    应用缓存2
    高速页缓存1
    高速页缓存2
    磁盘数据
    read方法
    write方法
  4. 异步访问文件方式

    • 当访问数据的线程发出请求后,线程会接着处理其他事情,而不是阻塞(等待),只有当数据返回时才继续处理后续操作。
    • 能提高程序效率,但不是访问文件的效率
    用户地址空间
    内核地址空间
    物理磁盘
    应用缓存1
    应用缓存2
    高速页缓存1
    高速页缓存2
    磁盘数据
    read方法
    write方法
  5. 内存映射方式

    • 将操作系统内存中的某块区域和磁盘中的文件夹关联,访问内存时转化为访问磁盘文件的某段数据(和直接IO的区别就是这个映射,相当于一个定位,就快的多)
    • 目的是减少数据的复制操作,此时这两个空间的数据都相当于是共享的
    用户地址空间
    内核地址空间
    物理磁盘
    地址映射
    地址映射
    应用缓存1
    应用缓存2
    高速页缓存1
    高速页缓存2
    磁盘数据
    read方法
    write方法

Java序列化&反序列化

​ Java序列化就是将一个对象转化为一串二进制的字节数组,然后通过保存这个字节数组进行持久化。而且达到持久化的目的,必须实现java.io.Serializable接口。

​ Java反序列化就是将字节数组再重新构造成对象。

序例化对象

要序例化一个对象,分如下步骤:

  • 创建某种OutputStream对象,然后将其封装在一个ObjectOutputStream对象内
  • 调用writeObject()即可将对象序列化(对象序列化基于字节,因此使用InputStream和OutputStream继承类层次结构)。

反序列化和序列化过程正好相反,需要将一个InputStream封装在ObjectInputStream内,然后调用readObject()获取一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们。

对于序列化对象的代码:
public class Serialize implements Serializable {
    public int num = 1;
    public long id = 777;

    public static void main(String[] args) throws IOException {
        //初始化
        FileOutputStream fos = new FileOutputStream("filepath");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        //开始干事情
        Serialize sl = new Serialize();//我自己写的对象
        oos.writeObject(Serialize);
        oos.flush();
        oos.close();
    }
}

另外,在纯Java环境下,Java序列化能够很好地工作;但是在多语言环境下,会出问题,尽量还是用JSON或XML这些通用的数据结构。

网络I/O工作机制

基础知识

首先你得懂TCP的三次握手和四次挥手,在《计算机网络》中有详细说明,不做过多解释。

要想加速网络的I/O,首先要分析一下影响网络的因素

  • 网络带宽:1s内能传输的最大比特数,平均网络带宽为1.7Mb/s
  • 传输距离:也就是数据要在光纤中走的距离,因为有一个折射率,所以大概只有光速的2/3,比如杭州和青岛的两台机子进行同步操作必定会有一个30ms的延时
  • TCP拥塞控制:详细见《计算机网络》

Java Socket工作机制

Socket 是描述计算机之间完成相互通信一种抽象功能。下面将通过一组比喻&一张图让你了解Socket的工作机制:
2.2磁盘IO&网络IO工作机制

  • 两台主机=两个城市
  • 物理链路=高速通道
  • Socket=交通工具
  • 要交付的数据=要运输的货物
  • 通信协议=交通工具的相关规则(比如说电动车不准上高速[狗头]

大致过程:

主机1的程序A和主机2的程序B通信
通过Socket建立连接
底层TCP/IP协议来建立TCP连接
底层IP协议来寻址网络中的主机

但是一台主机上可能运行着多个应用程序,所以,就要通过 TCP 或 UDP 的地址也就是端口号来指定,端口号又对应一个Socket。这样就可以通过一个 Socket 实例,唯一代表一条通信链路了。下面将具体介绍是如何建立链路的。

建立通信链路

当客户端要与服务端通信:

在客户端

  • 客户端首先要创建一个 Socket 实例,操作系统将为这个 Socket 分配一个端口号(port)
  • 创建一个套接字数据结构,包含本地和远程地址&端口号(这个数据结构将一直保存在系统中直到这个连接关闭)
  • 进行 TCP 的三次握手协议
  • Socket 实例的构造函数正确返回,Socket 实例对象就宣布正式创建完成,否则将抛出 IOException 错误。

在服务端

  • 服务端将创建一个 ServerSocket 实例(比较简单,只要端口号未占用,一般实例创建都会成功)

  • 操作系统也会为 ServerSocket 实例创建一个底层数据结构,包含指定监听的端口号和包含监听地址的通配符(通常情况下都是“*”即监听所有地址)

  • 调用 accept() 方法时,进入阻塞状态,等待客户端的请求。


  • 当一个新的请求到来时,将为这个连接创建一个新的套接字数据结构(包含请求源地址和端口)。

  • 这个数据结构,将会关联到 ServerSocket 实例的一个未完成的连接“数据结构“列表中(注意这时服务端与之对应的 Socket 实例并没有正式创建完成,待三次握手完成后,就会将这个 Socket 实例对应的数据结构从未完成列表中移到已完成列表中)

    所以 ServerSocket 所关联的列表中每个数据结构,都代表与一个客户端的建立的 TCP 连接(无论是否完成)

数据传输

传输数据是我们建立连接的主要目的,如何通过 Socket 传输数据,下面将详细介绍。

  • 当连接已经建立成功,服务端和客户端都会拥有一个 Socket 实例,两边的 Socket 实例都有一个 InputStreamOutputStream,正是通过这两个对象来交换数据。

  • Socket 对象正式创建完毕时,操作系统将会为 InputStreamOutputStream 分别分配一定大小的缓冲区,以便数据的写入和读取

  • 写入端将数据写到 OutputStream 对应的 SendQ 队列中,当队列填满时,数据就将会一起发送,到读取端的InputStreamRecvQ 队列中。(如果这时 RecvQ 已经满了,那么 OutputStreamwrite 方法将会阻塞直到 RecvQ 队列有足够的空间容纳 SendQ 发送的数据)

注意:

  • 这个缓存区的大小&写入端的速度&读取端的速度,非常影响该连接的数据传输效率,所以在写入和读取还要有一个协调的过程
  • 如果两边同时传送数据时可能会产生死锁,在后面 NIO 部分将介绍避免这种情况。
相关标签: 深入理解JavaWeb