Как именно сокет получает работу на более низком уровне (например, socket.recv (1024))? - PullRequest
2 голосов
/ 17 июня 2020

Я читал много вопросов о переполнении стека, подобных этому, но не думаю, что какой-либо из ответов действительно удовлетворил мое любопытство. У меня есть пример, ниже которого я хотел бы получить некоторые пояснения.

Предположим, что клиент блокирует socket.recv (1024):

socket.recv(1024)
print("Received")

Кроме того, предположим, что у меня есть сервер, отправляющий 600 байт клиенту. Предположим, что эти 600 байтов разбиты на 4 небольших пакета (по 150 байтов каждый) и отправлены по сети. Теперь предположим, что пакеты достигают клиента в разное время с разницей в 0,0001 секунды (например, один пакет прибывает в 12.00.0001pm, а другой - в 12.00.0002pm, и так далее ..).

Как socket.recv (1024) решает, когда возвращать выполнение программе и разрешать выполнение функции print ()? Возвращает ли он выполнение сразу после получения 1-го пакета размером 150 байт? Или он ждет какое-то произвольное время (например, 1 секунду, за которую к тому времени должны были бы прийти все пакеты)? Если да, то как долго это «произвольное количество времени»? Кто это определяет?

Ответы [ 2 ]

1 голос
/ 22 июня 2020

Ну, это будет зависеть от многих вещей, включая ОС и скорость сетевого интерфейса. Для 100-гигабитного интерфейса 100us - это «навсегда», но для 10-мегабитного интерфейса вы даже не можете передавать пакеты так быстро. Так что я не буду уделять слишком много внимания точному времени, которое вы указали.

В те дни, когда разрабатывалась TCP, сети были медленными, а процессоры - слабыми. Среди флагов в заголовке TCP есть флаг «Pu sh», сигнализирующий о том, что полезная нагрузка должна быть немедленно доставлена ​​в приложение. Итак, если мы запрыгнем в машину Waybak, ответ будет примерно таким, как если бы он зависел от того, установлен ли в пакетах флаг P SH. Однако обычно нет API пользовательского пространства, чтобы контролировать, установлен ли флаг. Обычно случается, что для одной записи, которая разбивается на несколько пакетов, последний пакет будет иметь установленный флаг P SH. Таким образом, ответ для медленной сети и слабого процессора может заключаться в том, что если бы это была одна запись, приложение, скорее всего, получило бы 600 байт. Тогда вы можете подумать, что использование четырех отдельных записей приведет к четырем отдельным операциям чтения по 150 байтов, но после введения алгоритма Нэгла данные со второй по четвертую запись вполне могут быть отправлены в одном пакете, если алгоритм Нэгла не был отключен с помощью TCP_NODELAY. вариант сокета, поскольку алгоритм Нэгла будет ждать ACK первого пакета перед отправкой чего-либо, меньшего, чем полный кадр.

Если мы вернемся из нашей поездки на машине Waybak в современную эпоху, когда интерфейсы 100 Gigabit и 24 базовые машины являются общими, наши проблемы очень разные, и вам будет трудно найти явную проверку наличия флага P SH, установленного в ядре Linux. Конструкцией принимающей стороны движет то, что сети становятся намного быстрее, в то время как размер пакета / MTU в значительной степени фиксирован, а скорость процессора стабильна, но ядер много. Снижение накладных расходов на пакет (включая аппаратные прерывания) и эффективное распределение пакетов по нескольким ядрам являются обязательными. В то же время необходимо как можно скорее получить данные от этого 100+ гигабитного пожарного шланга до приложения. Сотня микросекунд данных на таком ni c - это значительный объем данных, который нужно хранить без всякой причины.

Я думаю, что это одна из причин, по которой возникает так много вопросов типа «Что за черт побери делать? " в том, что может быть трудно понять, что представляет собой полностью асинхронный процесс, если на стороне отправки есть более знакомый поток управления, где намного проще отслеживать поток пакетов к NI C и где мы находимся в полный контроль над тем, когда будет отправлен пакет. На стороне приема пакеты просто приходят, когда они этого хотят.

Предположим, что TCP-соединение установлено и находится в режиме ожидания, отсутствуют недостающие или неподтвержденные данные, считыватель заблокирован при приеме, а считыватель работает под управлением fre sh версии ядра Linux. Затем писатель записывает 150 байтов в сокет, и эти 150 байтов передаются в одном пакете. По прибытии в NI C пакет будет скопирован DMA в кольцевой буфер, и, если прерывания разрешены, он вызовет аппаратное прерывание, чтобы сообщить драйверу, что в кольцевом буфере есть fre sh данные. . Драйвер, желающий выйти из аппаратного прерывания за как можно меньшее количество циклов, отключает аппаратные прерывания, запускает мягкий опрос IRQ l oop, если необходимо, и возвращается из прерывания. Входящие данные от NI C теперь будут обрабатываться в опросе l oop до тех пор, пока из NI C не перестанут считываться данные, после чего аппаратное прерывание будет снова разрешено. Общая цель этой конструкции - снизить частоту аппаратных прерываний от высокоскоростного NI C.

Вот здесь все становится немного странно, особенно если вы смотрели на красивые четкие диаграммы модели OSI, где более высокие уровни стека четко помещаются друг на друга. О нет, друг мой, реальный мир намного сложнее. Например, NI C, который вы могли представить себе как простое устройство уровня 2, знает, как направлять пакеты из одного и того же потока TCP в один и тот же CPU / кольцевой буфер. Он также знает, как объединять соседние TCP-пакеты в более крупные пакеты (хотя эта возможность не используется Linux, а вместо этого выполняется программно). Если вы когда-либо смотрели на захват сети и видели jumbo-кадр и почесали голову, потому что уверены, что MTU составляет 1500, это связано с тем, что эта обработка находится на таком низком уровне, что происходит до того, как netfilter сможет получить в свои руки пакет. Это объединение пакетов является частью возможности, известной как разгрузка приема, и, в частности, позволяет предположить, что ваша сетевая карта / драйвер имеет общую c разгрузку приема (GRO) (что не является единственным возможным вариантом разгрузки приема), цель который должен уменьшить накладные расходы на пакет от вашего пожарного шланга NI C за счет уменьшения количества пакетов, которые проходят через систему.

Итак, что происходит дальше, так это то, что опрос l oop продолжает вытягивать пакеты из кольцевого буфера (до тех пор, пока поступает больше данных) и передачи его в GRO для консолидации, если это возможно, а затем он передается на уровень протокола. Насколько мне известно, стек TCP / IP Linux просто пытается как можно быстрее передать данные в приложение, поэтому я думаю, что ваш вопрос сводится к следующему: «Будет ли GRO выполнять какую-либо консолидацию моих 4 пакетов, и Есть ли какие-нибудь ручки, которые я могу повернуть, чтобы повлиять на это? "

Ну, первое, что вы можете сделать, это отключить любую форму разгрузки приема (например, через ethtool), что, я думаю, должно дать вам 4 чтения по 150 байтов для 4 пакетов, поступающих в таком порядке, но я готов к тому, что я пропустил еще одну причину, по которой стек TCP / IP Linux не отправляет такие данные прямо в приложение, если приложение заблокировано при чтении как в вашем примере.

Другой регулятор, который у вас есть, если GRO включен, - это GRO_FLUSH_TIMEOUT, который является таймаутом для каждого NI C в наносекундах, который может быть (и я думаю, что по умолчанию) 0. Если это 0, я думаю, ваши пакеты могут быть объединены (здесь много деталей, включая значение MAX_GRO_SKBS), если они прибывают во время мягкого опроса IRQ l oop для t NI C все еще активен, что, в свою очередь, зависит от многих вещей, не связанных с вашими четырьмя пакетами в вашем потоке TCP. Если ненулевое значение, они могут быть объединены, если они прибывают в течение наносекунд GRO_FLUSH_TIMEOUT, хотя, честно говоря, я не знаю, может ли этот интервал охватывать более одного экземпляра опроса l oop для NI C.

На стороне приема Linux ядра здесь есть хорошая запись, которая может помочь вам в реализации.

0 голосов
/ 17 июня 2020

Обычный блокирующий прием по TCP-соединению возвращается, как только остается хотя бы один байт для возврата вызывающей стороне. Если вызывающий хочет получить больше байтов, он может просто снова вызвать функцию приема.

...