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