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

FAQ

Многие 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 улучшает производительность, устраняя необходимость лишнего копирования данных.