Понимание NIO JAVA из ядра Linux

Java

предисловие

IO можно просто разделить на磁盘 IOи网络 IO ,磁盘 IOотносительно网络 IOСкорость будет быстрее, эта статья в основном знакомит磁盘 IO,网络 IOНапишите на следующей неделе.

Пара JAVANIOабстрактно какChannel , Channelможно еще разделить наFileChannel(Диск ио) иSocketChannel(сетевой ио).

Если ваше понимание IO остается только на уровне API, этого недостаточно.Вы должны понимать, как IO обрабатывается на системном уровне, чтобы не попасть в ненужные ямы.

Содержание этой статьи:

  • Использование FileChannel для чтения и записи для копирования файлов.
  • Введение в ByteBuffer
  • блокировка файлового процесса jvm, FileLock
  • Кто быстрее, HeapbyteBuffer, DirectbyteBuffer или MMAP?
  • отLinux 内核середина虚拟内存,系统调用,文件描述符,Inode,Page Cache,缺页异常Опишите весь процесс ввода-вывода
  • Как восстановить память DirectByteBuffer вне кучи jvm
image-20200711165857889

Схемы, относящиеся к компьютерной системе в этой статье, взяты из книги «Углубленное понимание компьютерных систем».

Понимание Linux приходит из книг и справочных материалов.Содержание этой статьи в основном является моим собственным пониманием и проверкой кода.Некоторые описания могут быть неточными, но процесса понимания достаточно.

NIO

NIOПредставленный в Java 1.4, он называется неблокирующим вводом-выводом, а также называется новым вводом-выводом.

NIO реферирует какChannelОн ориентирован на буфер (работает с фрагментом данных), неблокирующий ввод-вывод.

ChannelОтвечает только за передачу, данные отправляютсяBufferотвечает за хранение.

Buffer

Bufferсерединаcapacity,limitиpositionАтрибуты важнее, если вы их не понимаете, вы столкнетесь со многими ямами при чтении и записи файлов.

capacityлоготипBufferМаксимальная емкость данных, равная длине массива.

limitэто указатель, определяющий максимальный индекс данных, которыми можно манипулировать в текущем массиве.

positionПредставляется как индекс при чтении следующих данных

image-20200711202515462
@Test
public void run1() {
    // `DirectByteBuffer`
    final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
    // `HeapByteBuffer`
    final ByteBuffer allocate = ByteBuffer.allocate(1024);
}

HeapByteBufferбудет выделено вJvm堆内, ограниченный размером кучи JVM, скорость создания высокая, но скорость чтения и записи низкая. Фактический нижний слой представляет собой массив байтов.

DirectByteBufferназначитJvm 堆外, не ограниченный размером кучи JVM, медленный для создания, быстрый для чтения и записи.DirectByteBufferВ Linux память принадлежит куче процесса.DirectByteBufferВ зависимости от параметров JVMMaxDirectMemorySizeВлияние.

Установите кучу jvm на 100 м и запустите программу, чтобы сообщить об ошибкеException in thread "main" java.lang.OutOfMemoryError: Java heap space.因为指定了 jvm 堆为 100m,然后一些 class 文件也会放在 堆中的,实际堆内存时不足 100m,当申请 100m 堆内存只能报错了。

public class BufferNio {
    // -Xmx100m
    public static void main(String[] args) throws InterruptedException {
        // HeapByteBuffer 是 jvm 堆内,因为堆不足分配 100m(java 中的一些 class 也会占用堆),导致 oom
        System.out.println("申请 100 m `HeapByteBuffer`");
        Thread.sleep(5000);
        ByteBuffer.allocate(100 * 1024 * 1024);
    }
}

Установите кучу jvm на 100 м, MaxDirectMemorySize на 1 г и создайте бесконечный циклDirectByteBuffer, напечатать 10 раз申请 directbuffer 成功, сообщить об ошибкеException in thread "main" java.lang.OutOfMemoryError: Direct buffer memory, я расскажу об этом вне кучи позжеDirectByteBufferКак утилизировать.

public class BufferNio {
//    -Xmx100m -XX:MaxDirectMemorySize=1g
    public static void main(String[] args) throws InterruptedException {
        System.out.println("申请 100 m DirectByteBuffer");
        final ArrayList<Object> objects = new ArrayList<>();
        while (true) {
            // DirectByteBuffer 不在 jvm 堆内,所以可以申请成功,但是不是无限制的,也有限制(MaxDirectMemorySize)
            final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100 * 1024 * 1024);
            objects.add(byteBuffer);
            System.out.println("申请 directbuffer 成功");
            System.out.println(ManagementFactory.getMemoryMXBean().getHeapMemoryUsage());
            System.out.println(ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage());
        }
    }
}

FileChannel

прочитать файл

@Test
public void read() throws IOException {
    final Path path = Paths.get(FILE_NAME);
    // 创建一个 FileChannel,指定这个 channel 读写的权限
    final FileChannel open = FileChannel.open(path, StandardOpenOption.READ);
    // 创建一个和这个文件大小一样的 buffer,小文件可以这样,大文件,循环读
    final ByteBuffer allocate = ByteBuffer.allocate((int) open.size());
    open.read(allocate);
    open.close();
    // 切换为读模式,position=0
    allocate.flip();
    // 用 UTF-8 解码
    final CharBuffer decode = StandardCharsets.UTF_8.decode(allocate);
    System.out.println(decode.toString());
}

записать файл

@Test
public void write() throws IOException {
    final Path path = Paths.get("demo" + FILE_NAME);
    // 通道具有写权限,create 标识文件不存在的时候创建
    final FileChannel open = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
    final ByteBuffer allocate = ByteBuffer.allocate(1024);
    allocate.put("张攀钦aaaaa-1111111".getBytes(StandardCharsets.UTF_8));
    // 切换写模式,position=0
    allocate.flip();
    open.write(allocate);
    open.close();
}

копировать файл

@Test
public void copy() throws IOException {
    final Path srcPath = Paths.get(FILE_NAME);
    final Path destPath = Paths.get("demo" + FILE_NAME);
    final FileChannel srcChannel = FileChannel.open(srcPath, StandardOpenOption.READ);
    final FileChannel destChannel = FileChannel.open(destPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
    // transferTo 实现类中,用的是一个 8M MappedByteBuffer 做数据的 copy ,但是这个方法只能 copy 文件最大字节数为 Integer.MAX
    srcChannel.transferTo(0, srcChannel.size(), destChannel);
    destChannel.close();
    srcChannel.close();
}

FileLock

FileLcokЭто блокировка файла процесса jvm, которая действует между несколькими процессами jvm.Процесс имеет права на чтение и запись в файл, а также разделяемые блокировки и эксклюзивные блокировки.

Один и тот же процесс не может заблокировать повторяющуюся область одного и того же файла, и его можно заблокировать без дублирования.

В этом же процессе первый поток блокирует (0, 2) файла, а другой поток одновременно блокирует (1, 2).Если область блокировки файла повторяется, программа сообщит об ошибке .

Один процесс блокируется (0, 2), а другой процесс блокируется (1, 2), это нормально, потому чтоFileLockявляется блокировкой процесса JVM.

Запустите следующую программу дважды, чтобы распечатать результаты

Первая программа печатается гладко

获取到锁0-3,代码没有被阻塞
获取到锁4-7,代码没有被阻塞

Вторая программа печатает

获取到锁4-7,代码没有被阻塞
获取到锁0-3,代码没有被阻塞

Когда запускается первая программа,file_lock.txtПозиция 0-2 заблокирована.Первая программа удерживает блокировку 10 с.При запуске второй программы она заблокируется и будет ждать здесь.FileLock, пока первая программа не снимет блокировку.

public class FileLock {
    public static void main(String[] args) throws IOException, InterruptedException {
        final Path path = Paths.get("file_lock.txt");
        final FileChannel open = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.READ);
        final CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(() -> {
         
            try (final java.nio.channels.FileLock lock = open.lock(0, 3, false)) {
             
                System.out.println("获取到锁0-3,代码没有被阻塞");
                Thread.sleep(10000);
                final ByteBuffer wrap = ByteBuffer.wrap("aaa".getBytes());
                open.position(0);
                open.write(wrap);
                Thread.sleep(10000);
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        }).start();
        Thread.sleep(1000);
        new Thread(() -> {
            try (final java.nio.channels.FileLock lock = open.lock(4, 3, false)) {
                System.out.println("获取到锁4-7,代码没有被阻塞");
                final ByteBuffer wrap = ByteBuffer.wrap("bbb".getBytes());
                open.position(4);
                open.write(wrap);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        }).start();
        countDownLatch.await();
        open.close();
    }
}

При изменении второй поток вышеуказанной программы вjava.nio.channels.FileLock lock = open.lock(1, 3, false)Поскольку регион не позволяет повторение одного и того же файла блокировки процесса, программа будет жаловаться.

Exception in thread "Thread-1" java.nio.channels.OverlappingFileLockException

У кого выше эффективность чтения и записи между HeapByteBuffer и DirectByteBuffer?

FileChannelкласс реализацииFileChannelImpl, при чтении и письмеByteBufferбудет определять, будет лиDirectBuffer, если нет, создастDirectBuffer, скопируйте исходные данные буфера вDirectBufferИспользовать.所以读写效率上来说,DirectByteBuffer 读写更快。 ноDirectByteBufferСоздание относительных затрат времени.

несмотря на то чтоDirectByteBufferвне кучи, но когда использование памяти вне кучи достигает-XX:MaxDirectMemorySizeКогда нет возможности восстановить память за пределами кучи, также будет запущен FullGC, и будет выброшен OOM.

// 下面这个程序会一直执行下去,但是会触发 FullGC,来回收掉堆外的直接内存
public class BufferNio {
    //    -Xmx100m -XX:MaxDirectMemorySize=1g
    public static void main(String[] args) throws InterruptedException {
        System.out.println("申请 100 m `HeapByteBuffer`");
        while (true) {
            // 当前对象没有被引用,GC root 也就到达不了 DirectByteBuffer
            ByteBuffer.allocateDirect(100 * 1024 * 1024);
            System.out.println("申请 directbuffer 成功");
        }
    }
}

Создано бесконечным цикломDirectByteBufferЕсли не придет GC ROOT, объект будет переработан. При переработке он будет переработан только из кучи. Как выполняется переработка вне кучи?

отDirectByteBufferИсточник продолжить, вы можете увидеть, что он имеет переменную участникаprivate final Cleaner cleaner;, когда запускается FullGC, потому чтоcleanerкорень gc недоступен, что приводит кcleanerОн будет переработан, и он будет активирован, когда он будет переработан.Cleaner.clean(Инициируется вызовом метода Reference.tryHandlePending), преобразовательDirectByteBuffer.Deallocatorнапример, этот метод запуска, называемыйUnsafe.freeMemoryЧтобы освободить память вне кучи.

public class Cleaner extends PhantomReference<Object> {
 	 private final Runnable thunk;
     public void clean() {
        if (remove(this)) {
            try {
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }
}

карта памяти

image-20200712125657989

Когда приложение читает файл, данные должны быть прочитаны с диска в пространство ядра (сначала чтение и запись, без данных кэша страниц), а затем скопированы из пространства ядра в пространство пользователя, чтобы приложение можно использовать прочитанные данные data. Когда все данные файла находятся в кэше страниц ядра, нет необходимости читать их с диска, и они напрямую копируются из пространства ядра в пространство пользователя.

Когда приложение записывает данные в файл, оно сначала копирует записываемые данные в кэш страниц ядра, а затем вызываетfsyncДиск данных из ядра в файл (пока вызов возвращается успешно, данные не будут потеряны). или не звониfsyncКогда данные помещаются на диск, пока данные приложения записываются в кэш страниц ядра, операция записи завершается, и данные помещаются на диск путем内核Планировщик Io поместит диск в нужное время (внезапный сбой питания приведет к потере данных, а такие программы, как MySQL, сами сохранят размещение данных).

Мы можем видеть данные данных, пройдут через копию из пространства пользователя и пространства ядра. Если вы можете удалить эту копию, эффективность будет намного выше, это MMAP (карта памяти). Указывая на память о пространстве пользовательского пространства и пространства ядра к тому же части физической памяти.内存映射английский этоMemory Mapping,сокращениеmmap. Соответствующий системный вызовmmap

Таким образом, данные считываются и записываются в пользовательском пространстве, а фактическая операция также выполняется в пространстве ядра, что уменьшает количество копий данных.

image-20200712145306814

Как этого добиться, проще говоря, адрес процесса в linux — это виртуальный адрес, и процессор будет отображать виртуальный адрес на физический адрес физической памяти. mmap фактически сопоставляет виртуальный адрес пользовательского процесса и виртуальный адрес пространства ядра с одной и той же физической памятью, что уменьшило количество копий данных.

Пользовательская программа вызывает системный вызовmmapПоследующее чтение и запись данных не требует вызова системного вызоваreadиwrite.

Отображение виртуальной памяти на физическую память

Оперативную память компьютера можно представить как массив из M последовательных байтов, каждый байт имеет уникальный物理地址 (Physical Address).

Используемый ЦП虚拟寻址(VA,Virtual Address) Найти физический адрес.

image-20200711171400757

CPUбудет использоваться процессом虚拟地址Аппаратно на ЦП内存管理单元 (Memory Management Unit MMU) для выполнения преобразования адресов, чтобы найти физический адрес в физической основной памяти для получения данных.

Когда процесс загружается, система назначает虚拟地址空间, когда определенное место в виртуальном адресном пространстве虚拟地址При использовании он будет сначала сопоставлен с основной памятью.物理地址.

Когда нескольким процессам необходимо совместно использовать данные, им нужно только сопоставить некоторые виртуальные адреса в их виртуальном адресном пространстве с одним и тем же физическим адресом.

Обычно, когда мы оперируем данными, мы не оперируем байт за байтом, что слишком неэффективно Обычно к некоторым байтам обращаются постоянно. Таким образом, при управлении памятью пространство памяти делится на управляемые страницы, и物理页(Physical Page), в виртуальной памятиVirtual Pageсправляться. Типичный размер страницы составляет 4 КБ.

систему через MMU и页表(Page Table)справляться虚拟页и物理也Соответствующим отношением таблицы страниц является запись таблицы страниц (Page Table Entry,PTE) массив

image-20200711183510194

Когда действительность PTE равна 1, идентификационные данные находятся в памяти, а когда идентификация равна 0, идентификация находится на диске.

Когда данные, соответствующие доступному виртуальному адресу, больше не находятся в физической памяти, возможны два случая:

1. Когда памяти достаточно, данные на диске, соответствующие виртуальной странице, будут напрямую загружены в физическую память.

2. При нехватке памяти срабатывает своп, согласно LRU виртуальная страница, соответствующая последней использованной виртуальной странице, также будет исключена, записана на диск, а часть данных в физической памяти будет удалена. Соответствующая виртуальная страница устанавливается в 0, а затем данные с диска загружаются в память.

обрабатывать виртуальную память

LinuxКаждому процессу назначается отдельный адрес виртуальной памяти,

image-20200711174755550

Когда наша программа запускается, вместо одновременной загрузки всех файлов кода всей программы в память выполняется отложенная загрузка.

机械硬盘使用扇区来管理磁盘,磁盘控制器会通过块管理磁盘,系统通过 Page Cache 与磁盘控制器打交道。

一个块包含多个扇区,一个页也包含多个块。

磁盘上会有一个文件对应一个 Inode,Innode 记录文件的元数据及数据所在位置。

当系统启动的时候,这些 Inode 数据会被加载到主存中去。不过系统中的 Inode 还记录他们对应的物理内存中的位置(实际就是对应 Page Cache),有的 Inode 对应的数据没有加载到内存中,Inode 就不会记录其对应的内存地址。

程序执行之前会初始化其虚拟内存,虚拟内存会记录代码对应哪些 Innode。

Когда программа выполняется, система инициализирует виртуальную память текущей программы, а затем запускаетmainфункция, когда код выполнения найден, какой-то код не загружен в память, это вызовет исключение ошибки страницы, и будет найдена соответствующая виртуальная страница.Innoe, затем загрузить нужные данные с диска в память, затем пометить виртуальную страницу как загруженную в память, и следующий доступ будет прямо из памяти.

##mmap в Java

Глядя на исходный код, который мы нашлиopen.mapтакже вернулсяDirectByteBuffer, просто метод возвращаетDirectByteBufferиспользует другой конструктор, который связываетfd. Когда мы читаем и записываем данные, системные вызовы чтения и записи не будут запускаться, что является преимуществом отображения памяти.

public class MMapDemo {
    public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
        final URL resource = MMapDemo.class.getClassLoader().getResource("demo.txt");
        final Path path = Paths.get(resource.toURI());
        final FileChannel open = FileChannel.open(path, StandardOpenOption.READ);
        // 发起系统调用 mmap
        final MappedByteBuffer map = open.map(FileChannel.MapMode.READ_ONLY, 0, open.size());
        // 读取数据时,不会再出发调用 read,直接从自己的虚拟内存中即可拿数据
        final CharBuffer decode = StandardCharsets.UTF_8.decode(map);
        System.out.println(decode.toString());
        open.close();
        Thread.sleep(100000);
    }
}

Хотя это тожеDirectByteBuffer, но отличается от mmap тем, что не привязан к fd.При чтении и записи данных ему все равно придется проходить копирование из пространства пользователя в пространство ядра, а также будут происходить системные вызовы, что относительно неэффективно по сравнению с mmap .

public class MMapDemo {
    public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
        final URL resource = MMapDemo.class.getClassLoader().getResource("demo.txt");
        final Path path = Paths.get(resource.toURI());
        final FileChannel open = FileChannel.open(path, StandardOpenOption.READ);
        // 这个 DirectByteBuffer 使用的构造不一样,它会走系统调用 read
        final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
        final int read = open.read(byteBuffer);
        byteBuffer.flip();
        System.out.println(StandardCharsets.UTF_8.decode(byteBuffer).toString());
        Thread.sleep(100000);
    }
}

Системные вызовы для отслеживания кода, используемого в Linuxstrace

#!/bin/bash
rm -fr /nio/out.*
cd /nio/target/classes
strace -ff -o /nio/out java com.fly.blog.nio.MMapDemo

Скорость чтения и записи данныхmmapбольше, чемByteBuffer.allocateDirectбольше, чемByteBuffer.allocate.


Эта статья написанаБлог Чжан Паньциня www.mflyyou.cn/творчество. Ее можно свободно воспроизводить и цитировать, но с обязательной подписью автора и указанием источника статьи.

При перепечатке в публичную учетную запись WeChat добавьте QR-код публичной учетной записи автора в конец статьи. Имя общедоступной учетной записи WeChat: Mflyyou