LINUX.ORG.RU

sendto() on UDP-socket всегда успешен; а как тогда ведёт себя select(&writeSet)?

 , ,


0

2

Экспериментально обнаружил, что если вызвать sendto() на UDP-сокете 100500 раз подряд с маленькими сообщениями, то все вызовы вернут успех, но получателю приедет лишь малая доля отправленных сообщений.

Нагуглил:

For UDP sockets, there are no send buffers, so send() and sendto() never return EWOULDBLOCK

Packets are just silently dropped when a device queue overflows.

Получается, что в случае UDP-сокета, sendto() пишет сразу в буфер адаптера.

Я не стану задавать риторический вопрос, что мешало этим чертям проверять переполнение буфера адаптера и честно возвращать EWOULDBLOCK – чтобы отправитель мог сразу регулировать плотность трафика, а не через ожидание отсутствия подтверждения от получателя по таймауту (в результате чего у отправителя ВСЕГДА устаревшие данные о пропускной способности канала).

Мне интересно более практичное: если UDP-сокет никогда не возвращает EWOULDBLOCK (т.е. не проверяет переполнение буфера адаптера), есть ли смысл добавлять его во writeSet для select(), и как он себя в этом случае поведёт (ведь для срабатывания writeSet таки-нужно проверять наличие свободного места в буфере адаптера)?

Или же тупо писать в него безо всяких select(&writeSet)? Даже если writeSet сработает корректно, один хрен невозможно узнать, сколько можно записать и когда нужно остановиться.

★★★★★

Последнее исправление: dimgel (всего исправлений: 1)

Я не стану задавать риторический вопрос, что мешало этим чертям проверять переполнение буфера адаптера и честно возвращать EWOULDBLOCK – чтобы отправитель мог сразу регулировать плотность трафика,

Это годится только для локального адаптера, а он обычно совсем не узкое место, так что было бы, во-первых, бесполезно почти всегда, и, во-вторых, вредно: кодеры тестировали бы поведение в своих локалках, а потом программа бы шла вразнос при реальном применении через инет. UDP рассчитан на то, что программа сама выясняет что можно слать.

select по определению всегда вернёт ему что запись доступна, делать его таким образом незачем.

firkax ★★★★★
()

в результате чего у отправителя ВСЕГДА устаревшие данные о пропускной способности канала

Они и так были бы почти всегда устаревшими т.к. узкое место - не локальная сетевуха. А так, ну и что? Реализуй ретрансмиссию пропущеных пакетов, это не сложно.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Реализуй ретрансмиссию пропущеных пакетов

Я и выбрал UDP только потому что ретрансмиссия один хрен обеспечивается на уровне приложения. (Изначально я подразумевал Raft over UDP, т.к. у Raft своя логика подтверждений и ретрансмиссий, так что Raft over TCP это масляное масло; но потом стало понятно, что это универсальная идея.)

Но к слову, вот это недопонял:

select по определению всегда вернёт ему что запись доступна

Я как бы верю (иначе получается описанное в ОП противоречие), и так и поступлю (select только для readSet, т.к. у меня может быть несколько сокетов на обслуживании одного фонового потока, а запись в UDP – в лоб), но по какому-такому определению?

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 2)
Ответ на: комментарий от firkax

Они и так были бы почти всегда устаревшими т.к. узкое место - не локальная сетевуха.

И это, по размышлению, тоже недопонял. Допустим, у нас Sender —> Router —> Receiver. Если узкое место – Router, то разве на стороне Sender исходящие UDP-пакеты не будут накапливаться в буфере адаптера в ожидании, пока Router просрётся? (Собственно, в моём случае они и накапливались – первые 250 штук, пока буфер не заполнился.)

А если же в UDP всё настолько сурово, что никто никого никогда не ждёт, а чуть что все всё выбрасывают, то какой смысл хранить в исходящем буфере адаптера более одного пакета?

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 2)

Так, погодите, я правильно понял, что вы выбрали протокол, который не гарантирует доставку, для которого никогда не пишется обработка ошибок (поскольку нет гарантии доставки считаем что все пакеты могут не дойти), а теперь вы жалуетесь, что вам не дают гарантию отправки? Я правильно понял?

PPP328 ★★★★★
()
Ответ на: комментарий от PPP328

Но ведь ядро зачем-то предупреждает, например, что большой пакет не отправит (errno == EMSGSIZE = 90). Если считать, что все пакеты не дойдут, зачем лишний код, возвращающий EMSGSIZE? Дропнули бы такое и всё

mky ★★★★★
()
Ответ на: комментарий от dimgel

Покажите код.

Я вот написал тестовый пример (сonnect() и write()), который просто шлёт подряд udp-пакеты. Если там блокирующийся write(), то код выполняется со скоростью передачи пакетов (на 100 Мбит хорошо заметно). Если неблокиующийся, то получаю кучу EAGAIN и опять, время выполнения кода == объём/100Мбит.

mky ★★★★★
()
Ответ на: комментарий от dimgel

connect может использоваться на дейтаграмных сокетах, в частности, на UDP сокетах. После connect можно использовать send (без коннект только sendto) и recv (без connect только recvfrom).

После connect в сокет доставляются ошибки, полученные по ICMP. Например, целевой порт на целевом хосте никто не открыл. При получении UDP дейтаграммы на целевой порт целевой хост отправит ICMP port unreachable на исходный хост. Если на исходном хосте connect на сокет не вызывали, этот ICMP port unreachable будет выброшен в пропасть. Если вызывали, ошибка будет сохранена в сокет, и её оттуда можно будет взять с помощью getsockopt(SOL_SOCKET, SO_ERROR). В частности, при доставке ошибки в сокет он становится readable (в терминах select и poll).

iliyap ★★★★★
()
Последнее исправление: iliyap (всего исправлений: 1)
Ответ на: комментарий от dimgel

Чтобы писать в udp-сокет через write(), нужно указать адрес через connect(), то, что connect() в случае udp не приводит к отправке пакет, не означает, что он не используется.

mky ★★★★★
()
Ответ на: комментарий от iliyap

После connect в сокет доставляются ошибки, полученные по ICMP.

А если сокет-отправитель был bind() вместо connect(), ICMP тоже теряются?

И кстати, где в доках это описано? Чёт я видимо прощёлкал. Перегрузившись количеством нюансов.

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 1)
Ответ на: комментарий от dimgel

bind задаёт только локальный адрес:порт сокета. bind+connect задают и локальный и удалённый адрес:порт. Если выполнить только connect, то будет неявный bind на *:случайный_порт. ICMP port unreachable содержит заголовки IP и UDP оригинальной дейтаграммы. Чтобы по данным из этих заголовков (а именно по src_addr, dst_addr, proto, src_port, dst_port) найти сокет, в который надо доставить эту ICMP ошибку, надо чтобы у сокета был задан и локальный адрес:порт, и удалённый адрес:порт.

В https://man7.org/linux/man-pages/man7/udp.7.html написано:

When connect(2) is called on the socket, the default destination address is set and datagrams can now be sent using send(2) or write(2) without specifying a destination address.

И ещё в разделе ERRORS:

All errors documented for socket(7) or ip(7) may be returned by a send or receive on a UDP socket.

ECONNREFUSED

No receiver was associated with the destination address. This might be caused by a previous packet sent over the socket.

iliyap ★★★★★
()
Ответ на: комментарий от antech

Не пробовал. Гиговое сообщение – это не мой юз-кейс. Мне нужно утрясти в голове поведение на маленьких сообщениях. А гиговые, если и появятся, я буду фрагментировать сам. Тут вопрос только в PMTUD, который завязан на ICMP. Но это будет похоже отдельный вопрос: socket MTU (для выковыривания которого я нашёл API и в венде, и в линуксе) vs path MTU (который собственно и нужен, но с одного UDP-сокета можно с помощью sendto() посылать сообщения разным получателям с разными path MTU).

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 1)
Ответ на: комментарий от iliyap

Спасибо. Как раз крутился вопрос, имеет ли смысл вызывать и bind(), и connect() на одном сокете.

bind задаёт только локальный адрес:порт сокета. bind+connect задают и локальный и удалённый адрес:порт.

(тяжкий вздох) Вроде логично…

  • Но почему мой bind() без @mky’s connect() оно не возвращает ошибку EAGAIN, которая вообще про локальный буфер? Этой ошибке должно быть похрен от слова совсем на то, задан или нет default remote socket/port.

  • А случайно назначенный порт (connect() без bind()) - чем не порт для доставки ICMP?

найти сокет, в который надо доставить эту ICMP ошибку, надо чтобы у сокета был задан и локальный адрес:порт, и удалённый адрес:порт.

Уже не рад, что связался с этим UDP.

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 1)
Ответ на: комментарий от dimgel

UDP не имеет send буферов. Вызов send или sendto сразу идёт в драйвер адаптера и говорит отправь. Единственная локальная ошибка это EMSGSIZE если размер дейтаграммы превышает Path MTU и PMTU Discovery не отключен (по умолчанию включен).

iliyap ★★★★★
()
Ответ на: комментарий от iliyap

UDP не имеет send буферов. Вызов send или sendto сразу идёт в драйвер адаптера

Я то же самое написал (предположил) в ОП. А откуда тогда у @mky взялся EAGAIN? (Хотя моя исходная претензия была – схрена ли драйвер не может вернуть EAGAIN, если его собственные/аппаратные буфера забиты?)

Единственная локальная ошибка это EMSGSIZE если размер дейтаграммы превышает Path MTU и PMTU Discovery не отключен (по умолчанию включен).

Так-так… Это отдельный сенькс.

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 1)
Ответ на: комментарий от dimgel

(по умолчанию включен)

Прочитайте ″man 7 ip″, там IP_MTU_DISCOVER и опции сокета. По умолчанию, через /proc/ выбирается или IP_PMTUDISC_WANT или IP_PMTUDISC_DONT. А, чтобы EMSGSIZE возврашался для пакетов, которые меньше 64к, но больше PMTU, нужно IP_PMTUDISC_DO. Или от IP_PMTUDISC_PROBE можно получить EMSGSIZE, если пакет больше локального MTU.

Вы написали какой-то тестовый код, который отправляет 10000 или больше udp пакетов на один адрес, выполняется мнгновенно и не получает ошибок от send?

mky ★★★★★
()
Ответ на: комментарий от mky

Прочитайте ″man 7 ip″, там IP_MTU_DISCOVER и опции сокета. […]

Сенькс.

Вы написали какой-то тестовый код, который отправляет 10000 или больше udp пакетов на один адрес, выполняется мнгновенно и не получает ошибок от send?

Да. Два udp-сокета, оба bind(localhost:разные порты). Затем запускаю рабочий поток где while { select() }. Затем из основного потока вызываю 100500 sendto(" msg" + i) от первого сокета ко второму, после чего делаю condition_var.notify(). В рабочем потоке select(readSet) просыпается может быть и сразу же, но при первом пробуждении мой код ждёт condition_var, т.е. пока sendto() не вызовется 100500 раз из основного потока, в рабочем потоке я никакие сообщения из второго сокета не читаю. В результате sendto() всегда успешен, но во второй сокет приезжает только первые ~250 сообщений.

dimgel ★★★★★
() автор топика
Ответ на: комментарий от dimgel

Если localhost, то там как-бы бесконечная скорость, все пакеты тут же уходят и приходят в принимающий буфер, который конечного размера, там и дропаются. Так не поймать EAGAIN. EAGAIN будет, если отправлять пакеты по физической сети, на какой-нибудь существующий адрес/порт, чтобы не было ICMP unreachable. И тестовый код для приёма не нужен, просто отправка кучи пакетов, чтобы объём передаваемых данных требовал ощутимого (секунды) времени работы сетевого адаптера.

mky ★★★★★
()
Ответ на: комментарий от max_lapshin

почему бы ему не получить информацию от ядра, которая поможет снизить процент потерь

Дело не только и не столько в этом. Я изначально нарисовал было класс с большим буфером исходящих сообщений, из которого отдавал бы в сокет по мере возможности. Но без EAGAIN/EWOULDBLOCK «мера возможности» не определена, и накрылся мой буфер медным тазом. Собственно, именно на юнит-тестировании этого буфера я и вляпался. С одной стороны, и хрен бы с ним, один хрен есть ретрансмит на прикладном уровне…

В общем, говно собачье ваш линукс. Причём далеко не в первый раз; с ходу вспоминается 4 таких же корявых API, но упомянуть расположен только одно – чужую статейку с очень говорящим названием (в своё время сыгравшую ключевую роль в моём понимании её сабжа).

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 5)
Ответ на: комментарий от mky

Если localhost, то там […] не поймать EAGAIN.

Т.е. ошибку переполнения локального исходящего буфера нельзя поймать на локальном трафике, а можно только на удалённом. Charming, б@#$ь. Назвать это идиотизмом – это оскорбить всех идиотов. Дооптимизировались до мышей. Точнее, до слома select() API.

dimgel ★★★★★
() автор топика
Ответ на: комментарий от dimgel

С одной стороны, и хрен бы с ним

А с другой, послать длинное сообщение становится принципиально невозможно – фрагментуй-не фрагментуй. А поскольку на данный момент у меня основной кейс – именно localhost, то с сожалением вынужден таки-отказаться от UDP. В конце концов, даже автор 0mq/nng не выпендривайся, а юзал TCP.

dimgel ★★★★★
() автор топика
Последнее исправление: dimgel (всего исправлений: 2)