Эффективный перенос данных с помощью zero copy

Эффективный перенос данных с помощью zero copy

Многие Web-приложения выдают значительный объём статического контента, что означает чтение данных с диска и их последующую запись в отвечающий сокет. Возможно, эта деятельность требует относительно маленькой загрузки процессора, но и эффективность её невелика: ядро считывает данные с диска, отправляет их через границу ядро-пользователь в приложение, а затем приложение возвращает эти же данные через границу ядро-пользователь, чтобы записать в сокет. На деле приложение работает как неэффективное промежуточное звено, передающее данные с диска в сокет.

Каждый раз, когда данные пересекают границу пользователь-ядро, их необходимо копировать; при этом затрачиваются ресурсы процессора и памяти. К счастью, этого копирования можно избежать с помощью технологии с соответствующим названием— zero copy. Приложения, использующие zero copy, запрашивают ядро о копировании данных прямо с диска в сокет, не затрагивая приложения. Zero copy значительно улучшает производительность приложений и уменьшает число контекстных переключений между режимами пользователя и ядра.

Библиотеки классов Java поддерживают zero copy в системах Linux и UNIX с помощью метода transferTo() из java.nio.channels.FileChannel. Метод transferTo() позволяет передать информацию прямо из канала, в котором они вызваны, в другой канал с поддержкой записи без необходимости пропускать данные через приложение. В данной статье сначала наглядно описываются системные издержки, возникающие при простой передаче файлов с помощью традиционной семантики копирования, а затем показывается, как с помощью технологии zero-copy, использующей transferTo() повышается производительность.

Передача данных: традиционный метод

Рассмотрим сценарий чтения из файла и передачу данных в другую программу по сети. (Этот сценарий описывает поведение многих серверных приложений, включая Web-приложения, выдающие статическое содержимое, FTP серверов, почтовые серверы и т.д.) Суть операции заключается в двух вызовах, показанных в Листинге 1 (ссылка на полный пример кода приведена в разделе Загрузка):

Листинг 1. Копирование байтов из файла в сокет
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

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

Рисунок 1. Традиционный подход к копированию данных

На рисунке 2 показано переключение контекста:

Рисунок 2. Традиционное переключение контекста

Этапы операции:
Вызов read() производит переключение контекста (см. рисунок 2) из режима пользователя в режим ядра. Внутри запускается sys_read() (или его эквивалент), чтобы прочитать данные из файла. Первое копирование (см. Figure 1) совершается с помощью механизма прямого доступа в память (DMA), который считывает содержимое файла с диска и сохраняет его в буфере пространства адресов ядра.

Требуемое количество данных копируется из буфера чтения в пользовательский буфер, и возвращается вызов read() . Ответ из вызова производит еще одно переключение контекста из режима ядра обратно в пользовательский режим. Теперь данные находятся в буфере пользовательского пространства адресов.

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

Send() возвращает системный вызов, инициируя четвертое переключение контекста. Независимо и асинхронно происходит четвертое копирование, так как механизм DMA передает данные из буфера ядра в механизм протокола.

Использование промежуточного буфера ядра (вместо прямой передачи данных в пользовательский буфер) кажется неэффективным. Но промежуточные буфера ядра были введены в процессы, чтобы улучшить производительность. Применение промежуточного буфера на стороне чтения позволяет буферу ядра действовать как «кэш опережающего считывания», когда приложение не запрашивало столько данных, сколько содержится в буфере ядра. Это существенно улучшает производительность, когда количество требуемых данных меньше размера буфера ядра. Промежуточный буфер на стороне записи позволяет завершить запись асинхронно.

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

Метод Zero copy улучшает производительность, устраняя необходимость лишнего копирования данных.

 

Комментарии

Комментариев пока нет. Почему бы ’Вам не начать обсуждение?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *