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