Как происходит этот процесс, в основном зависит от автора драйвера и оборудования, но для драйверов, которые я просматривал или писал, и оборудования, с которым я работал, обычно это работает следующим образом:
- При инициализации драйвера он выделяет некоторое количество буферов и передает их сетевой карте.
- Когда сетевой адаптер получает пакет, он извлекает следующий адрес из своего списка буферов, записывает данные прямо в него и уведомляет драйвер через прерывание.
- Драйвер получает прерывание и может либо передать буфер ядру, либо выделить новый буфер ядра и скопировать данные. «Сеть с нулевым копированием» относится к первому варианту и, очевидно, требует поддержки со стороны операционной системы. (подробнее об этом ниже)
- Драйвер должен либо выделить новый буфер (в случае нулевого копирования), либо повторно использовать буфер. В любом случае буфер возвращается сетевой карте для будущих пакетов.
Сеть с нулевым копированием внутри ядра не так уж и плоха. Полное отсутствие копирования вплоть до пользовательского пространства намного сложнее. Userland получает данные, но сетевые пакеты состоят как из заголовка, так и из данных. По крайней мере, истинное нулевое копирование на всем пути к пользовательскому пространству требует поддержки со стороны вашей сетевой карты, чтобы она могла размещать пакеты DMA в отдельных буферах заголовков/данных. Заголовки перерабатываются после того, как ядро направит пакет к месту назначения и проверит контрольную сумму (для TCP, либо аппаратно, если сетевая карта поддерживает это, либо программно, если нет; обратите внимание, что если ядру необходимо вычислить контрольную сумму самому, оно также может копировать данные: просмотр данных приводит к промахам кеша, а копирование их в другое место может быть бесплатным с настроенным кодом).
Даже если предположить, что все звезды сошлись, данные на самом деле не находятся в вашем пользовательском буфере, когда они получены системой. Пока приложение не запросит данные, ядро не знает, где они окажутся. Рассмотрим случай многопроцессорного демона, такого как Apache. Есть много дочерних процессов, и все они прослушивают один и тот же сокет. Вы также можете установить соединение, fork()
, и оба процесса смогут recv()
получать входящие данные.
TCP-пакеты в Интернете обычно содержат 1460 байт полезной нагрузки (MTU 1500 = 20-байтовый IP-заголовок + 20-байтовый TCP-заголовок + 1460 байт данных). 1460 не является степенью двойки и не будет соответствовать размеру страницы ни в одной системе, которую вы найдете. Это создает проблемы для повторной сборки потока данных. Помните, что TCP ориентирован на поток. Нет различий между операциями записи отправителя, и две операции записи по 1000 байт, ожидающие получения, будут полностью израсходованы при чтении 2000 байт.
Продолжая это, рассмотрим пользовательские буферы. Они выделяются приложением. Чтобы его можно было использовать для нулевого копирования на всем протяжении, буфер должен быть выровнен по страницам и не должен делиться этой страницей памяти с чем-либо еще. Во время recv()
ядро теоретически может переназначить старую страницу на страницу, содержащую данные, и "перевернуть" ее на место, но это осложняется описанной выше проблемой повторной сборки, поскольку последующие пакеты будут находиться на отдельных страницах. Ядро может ограничить данные, которые оно возвращает, полезной нагрузкой каждого пакета, но это будет означать множество дополнительных системных вызовов, переназначение страниц и, вероятно, снижение пропускной способности в целом.
Я действительно только царапаю поверхность по этой теме. В начале 2000-х я работал в нескольких компаниях, пытаясь распространить концепцию нулевого копирования на пользовательскую среду. Мы даже реализовали стек TCP в пользовательской среде и полностью обошли ядро для приложений, использующих стек, но это принесло свой собственный набор проблем и никогда не было качеством производства. Это очень сложная проблема.
person
Steve Madsen
schedule
27.04.2010