Есть ли портативный способ отбросить несколько читаемых байтов из дескриптора файла, подобного сокету?

Есть ли портативный способ отбросить несколько входящих байтов из сокета, не копируя их в пользовательское пространство? В обычном файле я мог бы использовать lseek(), но в сокете это невозможно. У меня есть два сценария, где мне это может понадобиться:

  1. Поток записей поступает в файловый дескриптор (который может быть TCP, доменным сокетом UNIX типа SOCK_STREAM или потенциально каналом). Каждой записи предшествует заголовок фиксированного размера, определяющий ее тип и длину, за которым следуют данные переменной длины. Я хочу сначала прочитать заголовок, и если он не того типа, который меня интересует, я хочу просто отбросить следующий сегмент данных, не передавая их в пользовательское пространство в фиктивный буфер.

  2. На файловый дескриптор поступает поток записей различной и непредсказуемой длины. Из-за асинхронного характера записи могут быть неполными, когда fd становится доступным для чтения, или они могут быть полными, но часть следующей записи уже может быть там, когда я пытаюсь прочитать фиксированное количество байтов в буфере. Я хочу прекратить чтение fd на точной границе между записями, чтобы мне не нужно было управлять частично загруженными записями, которые я случайно прочитал из fd. Итак, я использую recv() с флагом MSG_PEEK для чтения в буфер, анализирую запись, чтобы определить ее полноту и длину, а затем снова правильно читаю (таким образом, фактически удаляя данные из сокета) до точной длины. Это скопирует данные дважды - я хочу избежать этого, просто отбрасывая данные, буферизованные в сокете, на точную сумму.

Насколько я понимаю, в Linux этого можно добиться, используя splice() и перенаправляя данные в /dev/null, не копируя их в пользовательское пространство. Однако splice() предназначен только для Linux, а аналогичный sendfile(), поддерживаемый на других платформах, не может использовать сокет в качестве входных данных. Мои вопросы:

  1. Есть ли портативный способ добиться этого? Что-то, что будет работать и на других UNIX (в первую очередь на Solaris), у которых нет splice()?

  2. Является ли splice() преобразование в /dev/null эффективным способом сделать это в Linux, или это будет пустой тратой усилий?

В идеале мне бы хотелось иметь ssize_t discard(int fd, size_t count), который просто удаляет количество доступных для чтения байтов из файлового дескриптора fd в ядре (т.е. ничего не копирует в пользовательское пространство), блокирует блокируемый fd до тех пор, пока запрошенное количество байтов не будет отброшено, или не вернет количество успешно отброшенных байтов или EAGAIN для неблокирующего fd, как это сделал бы read(). И, конечно же, продвигает позицию поиска в обычном файле :)


person Angro    schedule 06.07.2018    source источник
comment
Эээ, только что прочитали их?   -  person user207421    schedule 07.07.2018
comment
Не существует портативной альтернативы чтению такого количества байтов. Я сомневаюсь, что вы заметите накладные расходы на чтение их в буфер пользовательского пространства, хотя вы и считаете это неприятностью. Конечно, вы должны оценить наличие проблемы, прежде чем затрачивать на нее усилия, а когда вы затрачиваете усилия, ожидайте, что вам придется использовать решения для конкретной платформы с read() в качестве запасного варианта. Если вы знаете, сколько нужно пропустить, убедитесь, что вы используете многобайтовые вызовы read() и что вызовы read() возвращают столько, сколько вы ожидаете (зацикливайте, если они возвращают меньше байтов, пока не достигнете требуемого числа).   -  person Jonathan Leffler    schedule 07.07.2018
comment
@JonathanLeffler: Ваше второе предложение в приведенном выше комментарии именно поэтому я написал свой ответ. Кроме того, я попытался показать лучшие подходы на уровне общей идеи.   -  person Nominal Animal    schedule 07.07.2018
comment
Сращивание с /dev/null — интересный теоретический вопрос. Я тоже задаюсь вопросом, насколько (не)эффективно это.   -  person user185953    schedule 03.03.2021


Ответы (1)


Короткий ответ: Нет, переносимого способа сделать это не существует.

Подход sendfile() специфичен для Linux, потому что в большинстве других ОС, реализующих его, источником должен быть файл или объект общей памяти. (Я даже не проверял, поддерживается ли/в каких версиях ядра Linux sendfile() от дескриптора сокета до /dev/null. Если честно, я бы очень подозрительно отнесся к коду, который это делает.)

Глядя на например. Исходники ядра Linux, и, учитывая, как мало ssize_t discard(fd, len) отличается от стандартного ssize_t read(fd, buf, len), очевидно, можно добавить такую ​​поддержку. Можно даже добавить его через ioctl (скажем, SIOCISKIP) для легкого обнаружения поддержки.

Однако проблема в том, что вы разработали неэффективный подход, и вместо того, чтобы исправить подход на алгоритмическом уровне, вы ищете костыли, которые позволили бы вашему подходу работать лучше.

Видите ли, очень сложно показать случай, когда «лишняя копия» (из буферов ядра в буферы пользовательского пространства) является реальным узким местом в производительности. Иногда количество системных вызовов (переключение контекста между пространством пользователя и пространством ядра). Если вы отправили патч вверх по течению, реализующий, например. ioctl(socketfd, SIOCISKIP, bytes) для сокетов потока домена TCP и/или Unix, они бы указали, что увеличение производительности, которого они надеются достичь, лучше достигается, если не пытаться получить данные, которые вам не нужны в первую очередь. (Другими словами, способ, которым вы пытаетесь что-то делать, по своей сути неэффективен, и вместо того, чтобы создавать костыли, чтобы заставить этот подход работать лучше, вы должны просто выбрать более эффективный подход.)

В вашем первом случае процесс, получающий структурированные данные, обрамленные идентификатором типа и длины, желающий пропустить ненужные кадры, лучше исправить, исправив протокол передачи. Например, принимающая сторона может информировать отправляющую сторону о том, какие кадры ее интересуют (т. е. базовый подход к фильтрации). Если вы застряли с глупым протоколом, который вы не можете заменить по внешним причинам, вы сами по себе. (Сообщество разработчиков FLOSS не несет и не должно нести ответственность за принятие глупых решений только потому, что кто-то о них плачет. Любой может это сделать, но им нужно будет сделать это таким образом, чтобы не требовать от других дополнительной работы. слишком.)

Во втором случае вы уже прочитали свои данные. Не делай этого. Вместо этого используйте буфер пользовательского пространства, достаточно большой, чтобы вместить два полноразмерных кадра. Всякий раз, когда вам нужно больше данных, но начало кадра уже находится за серединой буфера, memmove() кадр должен начинаться в начале буфера.

Когда у вас есть частично прочитанный кадр, и у вас осталось N непрочитанных байтов, которые вас не интересуют, прочитайте их в неиспользуемую часть буфера. Места всегда достаточно, потому что вы можете перезаписать часть, уже занятую текущим кадром, а его начало всегда находится в пределах первой половины буфера.

Если кадры маленькие, скажем, максимум 65536 байт, вы должны использовать настройку максимального размера буфера. На большинстве настольных и серверных машин с потоковыми сокетами с высокой пропускной способностью гораздо разумнее что-то вроде 2 МБ (2097152 байта или больше). Памяти тратится не так уж много, но вы редко делаете какие-либо копии памяти (а если и делаете, то они, как правило, короткие). (Можно даже оптимизировать перемещения памяти так, чтобы копировались и выравнивались только полные строки кеша, поскольку оставление почти одной строки кеша мусора в начале буфера не имеет значения.)

Я выполняю высокопроизводительные вычисления с большими наборами данных (включая молекулярные данные в текстовой форме, где записи разделяются символами новой строки, а специальные синтаксические анализаторы для преобразования десятичных целых чисел или значений с плавающей запятой используются для повышения производительности), и этот подход хорошо работает на практике. Проще говоря, пропуск данных, уже находящихся в вашем буфере, — это не то, что вам нужно оптимизировать; это незначительные накладные расходы по сравнению с простым нежеланием делать то, что вам не нужно.

Существует также вопрос о том, что вы хотите оптимизировать при этом: используемое время/ресурсы процессора или настенные часы, используемые в общей задаче. Это совершенно разные вещи.

Например, если вам нужно отсортировать большое количество текстовых строк из какого-то файла, вы используете наименьшее время процессора, если просто читаете весь набор данных в память, создаете массив указателей на каждую строку, сортируете указатели и, наконец, пишете каждую строку (используя внутреннюю буферизацию и/или POSIX writev(), так что вам не нужно выполнять системный вызов write() для каждой отдельной строки).

Однако, если вы хотите свести к минимуму используемое время настенных часов, вы можете использовать двоичную кучу или сбалансированное двоичное дерево вместо массива указателей и добавлять в кучу или вставлять по порядку каждую полностью прочитанную строку, чтобы, когда последняя строка наконец прочитано, у вас уже есть строки в правильном порядке. Это связано с тем, что ввод-вывод хранилища (для всех случаев, кроме патологических входных данных, что-то вроде односимвольных строк) занимает больше времени, чем их сортировка с использованием любого надежного алгоритма сортировки! Алгоритмы сортировки, которые работают в режиме реального времени (по мере поступления данных), обычно не так эффективны с точки зрения ЦП, как те, которые работают в автономном режиме (для полных наборов данных), поэтому в конечном итоге они потребляют несколько больше времени ЦП; но поскольку работа ЦП выполняется в то время, которое в противном случае тратится впустую на ожидание загрузки всего набора данных в память, она выполняется за меньшее время настенных часов!


Если есть необходимость и интерес, я могу привести практический пример для иллюстрации методов. Однако здесь нет абсолютно никакой магии, и любой программист на C должен быть в состоянии реализовать их (как схему буферизации, так и схему сортировки) самостоятельно. (Я рассматриваю возможность использования таких ресурсов, как справочные страницы Linux в Интернете, а также статьи Википедии и псевдокод, например, бинарные кучи делают это "самостоятельно". Если вы не просто копируете-вставляете существующий код, я считаю, что вы делаете это «самостоятельно», даже если кто-то или какой-то ресурс поможет вам найти хорошие, надежные способы сделать это.)

person Nominal Animal    schedule 06.07.2018
comment
Ответ хорош даже для сценария многоадресной потоковой передачи, где лучшие решения переводятся в: если пропуски отбрасывания огромны, более эффективно на некоторое время вообще прекратить прослушивание. Если они маленькие, более эффективно, но утомительно читать большие куски и фильтровать их самостоятельно. Если вы не хотите утомительного, подумайте о языке более высокого уровня, который сделает это за вас. Правильный? - person user185953; 03.03.2021