Немного знаний о нулевом копировании

Java

предисловие

Понятно из буквального значения состоит в том, чтобы скопировать данные взад-вперед и назад не требуется, значительно улучшит производительность системы; слово мы часто слышим в Java Nio, Netty, Kafka, RocketMQ и других рамках, часто как основной момент его улучшения производительности ; ниже нескольких концепций от начала ввода / вывода, нулевой копии дальнейшего анализа.

Концепция ввода/вывода

1. Буфер

Буфер является основой всех операций ввода-вывода. Ввод-вывод — это не что иное, как перемещение данных в буфер или из него; когда процесс выполняет операцию ввода-вывода, он отправляет запрос операционной системе либо на очистку данных, в буфере (запись) или заполнение буфера (чтение); давайте посмотрим на общую блок-схему java-процесса, инициирующего запрос на чтение для загрузки данных:

После того, как процесс инициирует запрос на чтение, после того как ядро ​​получит запрос на чтение, оно сначала проверит, существуют ли данные, требуемые процессом, уже в пространстве ядра.Если они уже существуют, оно будет напрямую копировать данные в буфер процесс; если нет ядра, он будет управлять диском немедленно. Контроллер выдает команду на чтение данных с диска, и контроллер диска записывает данные непосредственно в буфер чтения ядра. Этот шаг завершается DMA, следующий шаг — ядро ​​копирует данные в буфер процесса;
Если процесс инициирует запрос на запись, ему также необходимо скопировать данные из пользовательского буфера в буфер сокета ядра, а затем скопировать данные на сетевую карту через DMA и отправить их наружу;
Вы можете подумать, что это пустая трата места, вам нужно каждый раз копировать данные из пространства ядра в пространство пользователя, поэтому появление нулевой копии должно решить эту проблему;
Что касается нулевого копирования, предусмотрено два метода: метод mmap+write и метод sendfile;

2. Виртуальная память

Все современные операционные системы используют виртуальную память, используя виртуальные адреса вместо физических.Преимущества этого:
1. Несколько виртуальных адресов могут указывать на один и тот же адрес физической памяти.
2. Объем виртуальной памяти может быть больше фактического доступного физического адреса;
Используя первую функцию, адрес пространства ядра и виртуальный адрес пространства пользователя могут быть сопоставлены с одним и тем же физическим адресом, чтобы DMA мог одновременно заполнять буфер, видимый для процессов ядра и пространства пользователя, как показано на следующем рисунке. фигура:

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

3. метод mmap+write

Используйте метод mmap+write, чтобы заменить исходный метод чтения+записи. реализуется виртуальный адрес в виртуальном адресном пространстве процесса.Отношение отображения один к одному; таким образом, исходный буфер чтения ядра может быть сохранен для копирования данных в пользовательский буфер, но буфер чтения ядра по-прежнему требуется чтобы скопировать данные в буфер сокета ядра, как показано на следующем рисунке:

4. метод отправки файла

Системный вызов sendfile был представлен в версии ядра 2.1 для упрощения процесса передачи данных между двумя каналами по сети. Введение системного вызова sendfile не только снижает репликацию данных, но и уменьшает количество переключений контекста, как показано на следующем рисунке:

Передача данных происходит только в пространстве ядра, поэтому переключение контекста уменьшено, но копия все же есть, можно ли эту копию опустить? адрес, смещение) записываются в соответствующий буфер сокета, так что сохраняется даже копия процессора в пространстве ядра;

Нулевая копия Java

1.MappedByteBuffer

FileChannel, предоставляемый java nio, предоставляет метод map(), который может установить сопоставление виртуальной памяти между открытым файлом и MappedByteBuffer. файл на диске; вызов метода get() позволит получить данные с диска, что отражает текущее содержимое файла, а вызов метода put() обновит файл на диске, а изменения, внесенные в файл, повлияют на другие Читатель тоже виден, давайте посмотрим на простой пример чтения, а затем проанализируем MappedByteBuffer:

public class MappedByteBufferTest {

    public static void main(String[] args) throws Exception {
        File file = new File("D://db.txt");
        long len = file.length();
        byte[] ds = new byte[(int) len];
        MappedByteBuffer mappedByteBuffer = new FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY, 0,
                len);
        for (int offset = 0; offset < len; offset++) {
            byte b = mappedByteBuffer.get();
            ds[offset] = b;
        }
        Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");
        while (scan.hasNext()) {
            System.out.print(scan.next() + " ");
        }
    }
}

Отображение в основном реализуется через функцию map(), предоставляемую FileChannel.Метод map() выглядит следующим образом:

    public abstract MappedByteBuffer map(MapMode mode,
                                         long position, long size)
        throws IOException;
        

Предоставляются три параметра: MapMode, Position и size соответственно:
MapMode: режим отображения, варианты включают: READ_ONLY, READ_WRITE, PRIVATE;
Позиция: с какой позиции начать отображение, позиция количества байтов;
Размер: сколько байтов назад от позиции;

Давайте сосредоточимся на MapMode. Пожалуйста, два представляют только чтение и чтение-запись. Конечно, запрошенный режим карты ограничен правами доступа объекта Filechannel. Если READ_ONLY включен для файла без разрешения на чтение, будет выдано исключение NonReadableChannelException ; ЧАСТНЫЙ режим Представляет карту с копированием при записи, что означает, что любое изменение, выполненное с помощью метода put(), приводит к созданию частной копии данных, которая видна только экземпляру MappedByteBuffer; этот процесс не вносит никаких изменений в базовый file , и как только буфер будет очищен от мусора, эти изменения будут потеряны; взгляните на исходный код метода map():

    public MappedByteBuffer map(MapMode mode, long position, long size)
        throws IOException
    {
            ...省略...
            int pagePosition = (int)(position % allocationGranularity);
            long mapPosition = position - pagePosition;
            long mapSize = size + pagePosition;
            try {
                // If no exception was thrown from map0, the address is valid
                addr = map0(imode, mapPosition, mapSize);
            } catch (OutOfMemoryError x) {
                // An OutOfMemoryError may indicate that we've exhausted memory
                // so force gc and re-attempt map
                System.gc();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException y) {
                    Thread.currentThread().interrupt();
                }
                try {
                    addr = map0(imode, mapPosition, mapSize);
                } catch (OutOfMemoryError y) {
                    // After a second OOME, fail
                    throw new IOException("Map failed", y);
                }
            }

            // On Windows, and potentially other platforms, we need an open
            // file descriptor for some mapping operations.
            FileDescriptor mfd;
            try {
                mfd = nd.duplicateForMapping(fd);
            } catch (IOException ioe) {
                unmap0(addr, mapSize);
                throw ioe;
            }

            assert (IOStatus.checkAll(addr));
            assert (addr % allocationGranularity == 0);
            int isize = (int)size;
            Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
            if ((!writable) || (imode == MAP_RO)) {
                return Util.newMappedByteBufferR(isize,
                                                 addr + pagePosition,
                                                 mfd,
                                                 um);
            } else {
                return Util.newMappedByteBuffer(isize,
                                                addr + pagePosition,
                                                mfd,
                                                um);
            }
     }

Общий смысл в том, чтобы получить адрес карты памяти через нативный метод, если не получится, вручную gc map снова, наконец, экземпляр MappedByteBuffer создается через адрес карты памяти, сам MappedByteBuffer является абстрактным классом, по сути , реальный экземпляр здесь — DirectByteBuffer;

2.DirectByteBuffer

DirectByteBuffer наследуется от MappedByteBuffer.Из названия можно догадаться, что открыта прямая память, и она не занимает пространство памяти jvm; MappedByteBuffer, отображенный Filechannel в предыдущем разделе, на самом деле является DirectByteBuffer.Конечно, помимо этим методом вы также можете создать пространство вручную:

ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);

Пространство прямой памяти объемом 100 байт открывается, как указано выше;

3. Межканальная передача

Часто бывает необходимо перенести файлы из одного места в другое. FileChannel предоставляет метод transferTo() для повышения эффективности передачи. Давайте сначала рассмотрим простой пример:

public class ChannelTransfer {
    public static void main(String[] argv) throws Exception {
        String files[]=new String[1];
        files[0]="D://db.txt";
        catFiles(Channels.newChannel(System.out), files);
    }

    private static void catFiles(WritableByteChannel target, String[] files)
            throws Exception {
        for (int i = 0; i < files.length; i++) {
            FileInputStream fis = new FileInputStream(files[i]);
            FileChannel channel = fis.getChannel();
            channel.transferTo(0, channel.size(), target);
            channel.close();
            fis.close();
        }
    }
}

Данные файла передаются в канал System.out через метод TransferTo() FileChannel Интерфейс определяется следующим образом:

    public abstract long transferTo(long position, long count,
                                    WritableByteChannel target)
        throws IOException;

Некоторые параметры также легче понять, а именно позиция начала передачи, количество байтов для передачи и целевой канал TransferTo() позволяет перекрестно соединить один канал с другим, не требуя промежуточного буфера для передачи данных;
Примечание. Здесь есть два значения того, что промежуточный буфер не нужен: первому уровню не нужен буфер пользовательского пространства для копирования буфера ядра, а два других канала имеют свои собственные буферы ядра, и два буфера ядра также могут использоваться для копирования буфера ядра. нет необходимости копировать данные;

Нетти нулевая копия

Netty предоставляет буфер с нулевым копированием. При передаче данных окончательно обработанные данные должны быть объединены и разделены в одном переданном сообщении. Собственный ByteBuffer Nio не может этого сделать. Netty предоставляет Composite (комбинацию) и Slice через предоставленные (Split) два буфера для добиться нулевой копии, будет нагляднее увидеть следующую картину:


Сообщение HTTP на уровне TCP разделено на два ChannelBuffers, которые не имеют смысла для нашей логики верхнего уровня (обработка HTTP). Однако комбинация двух ChannelBuffers становится осмысленным HTTP-сообщением. ChannelBuffer, соответствующий этому сообщению, — это то, что можно назвать «сообщением», и здесь используется слово «Virtual Buffer».
Вы можете взглянуть на исходный код CompositeChannelBuffer, предоставленный netty:

public class CompositeChannelBuffer extends AbstractChannelBuffer {

    private final ByteOrder order;
    private ChannelBuffer[] components;
    private int[] indices;
    private int lastAccessedComponentId;
    private final boolean gathering;
    
    public byte getByte(int index) {
        int componentId = componentId(index);
        return components[componentId].getByte(index - indices[componentId]);
    }
    ...省略...

Компоненты используются для сохранения всех полученных буферов, индексы записывают начальную позицию каждого буфера, lastAccessedComponentId записывает последний доступный ComponentId; CompositeChannelBuffer не открывает новую память и напрямую копирует все содержимое ChannelBuffer, а сохраняет его напрямую Все ссылки ChannelBuffer читаются и записываются в дочернем ChannelBuffer, достигнув нулевой копии.

другие

Сообщения RocketMQ последовательно записываются в файл журнала фиксации, а затем используют файл очереди потребления в качестве индекса; RocketMQ использует mmap+write с нулевым копированием для ответа на запросы потребителей;
Точно так же существует процесс сохранения большого количества сетевых данных на диск и отправки дисковых файлов по сети в kafka.Kafka использует метод нулевого копирования sendfile;

Суммировать

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

Ссылаться на

<<java_nio>>