Привет, Хабр! В этой статье я хочу рассказать о решении одной из типичных проблем, с которой Embox справляется лучше GNU/Linux. Речь идет о времени реакции на пакет, переданный по протоколу Ethernet. Как известно, основной характеристикой передачи данных по сети является пропускная способность, и с ней у GNU/Linux все хорошо. Но когда речь заходит об уменьшении времени на прием/передачу единичного сетевого пакета, могут возникнуть проблемы. В частности, у заказчика была плата DE0-Nano-SoC с Linux, и с помощью этой платы хотелось управлять неким объектом по сети. Топология сети — точка-точка, никаких роутеров и хабов нет. По модели управления время реакции должно быть меньше 100 мкс, а на базе Linux удавалось добиться только 500 мкс.
Для оценки времени передачи создаем стенд, состоящий из двух хостов.
В качестве первого хоста выступает компьютер общего назначения с GNU/Linux, в качестве второго хоста — отладочная плата DE0-Nano-SoC Kit с Embox. Эта плата содержит FPGA и HPS (Hard Processing System, т.е. обычный ARM), и именно на ней нужно уменьшать время отклика. Напишем тестовое приложение, которое просто будет отвечать UDP-пакетом с идентичным содержимым:
while (1) {
char buf[BUFLEN];
recvfrom(s, buf, BUFLEN);
sendto(s, buf, BUFLEN);
}
Его будем запускать на втором хосте, то есть на DE0-Nano-SoC.
На первом хосте программа будет посылать пакеты, ждать ответа и измерять время между отправкой и приемом:
for (int i = 0; i < N; i++) {
char buf_tx[BUFLEN], buf_rx[BUFLEN];
sprintf(buf_tx, "This is packet %d\n", i);
time_t time_begin = time_now();
sendto(s, buf_tx, BUFLEN);
recvfrom(s, buf_rx, BUFLEN);
time_t time_end = time_now();
if (memcmp(buf_tx, buf_rx, sizeof(buf))) {
printf("%d: Buffer mismatch\n", i);
}
if (time_end - time_begin > TIME_LIMIT) {
printf("Slow answer #%d: %d\n", i, time_end - time_begin);
}
}
При этом вычислим среднее, максимальное и минимальное время отклика.
Код есть на Github.
Сделав пробный запуск и удостоверившись, что пакеты приходят и уходят, на стороне отладочной платы сразу применим очевидные оптимизации:
- Уберём весь отладочный вывод в UART — это дало самый значительный выигрыш, уж слишком он медленный
- Соберём с флагом
-O2
- Включим кэш-контроллер второго уровня PL310 (это не помогло практически никак)
При отправке 500 000 пакетов (столько же отправлялось и в следующих примерах) получились такие значения:
Avg: 4.52ms
Min: 3.12ms
Max: 12.24ms
Это в несколько раз медленнее, чем ориентировочные значения, предоставленные со стороны заказчика — среднее значение должно быть хотя бы на порядок меньше. Заказчик говорил, что у него на Linux получались даже лучшие характеристики. Мы начали думать, что может быть не так.
Возможно, дело в других процессах? Но нет, кроме данной программы на плате ничего не запускается.
Возможно, много времени уходит на обработку каких-то прерываний, например, таймера? Тоже нет — снижение частоты его срабатываний никак не влияет на результат.
Как оказалось, дело было в скорости самого ethernet-соединения — использовался USB-адаптер, поддерживающий максимум 100Мбит/с, да и в драйвере не было поддержки гигабитной скорости.
После замены сетевой карты и добавления поддержки 1Гбит в драйвере получаем ещё один значительный скачок в производительности:
Avg: 0.08ms
Min: 0.07ms
Max: 4.31ms
Сравнение с Linux
Достаточно естественно сравнить это время с Linux. То же самое приложение очень просто кросс-компилировать: arm-linux-gnueabihf-gcc server.c -O2
. Получившийся ELF заливаем на плату и запускаем:
Avg: 0.77ms
Min: 0.74ms
Max: 5.31ms
Таким образом, Embox "отвечает" примерно в 9 раз быстрее, что не может не радовать!
Исследование разброса
Среднее время отклика вполне хорошее, но есть "выбросы", которые не только имеют слишком большую задержку, но и вносят значительную непредсказуемость.
Для того, чтобы определить источник задержек, было решено замерить аппаратным таймером время, которой занимает обработка UDP-пакета от его приёма до отправки соответствующего ответа.
Можно накапливать статистику на отладочной плате, но проще сразу же передать на хост данные прямо в UDP-пакете.
В итоге мы решили, что прямо в пакет UDP будем ставить метки времени прихода и отправления. Причём время получения пакета записывается до передачи пакета пользовательскому приложению, прямо в обработчике прерывания. Время отправления будет получать перед самой отправкой пакета в сеть. Ну а дальше приблизительно такой код:
int net_tx(...) {
if (is_udp_packet()) {
timestamp2 = timer_get();
memcpy(packet[UDP_OFFT],
×tamp1,
sizeof(timestamp1));
memcpy(packet[UDP_OFFT + sizeof(timestamp2)],
×tamp2,
sizeof(timestamp2));
...
}
}
В данном случае не важно, с какой частотой работает таймер, достаточно убедиться, что выбросы по времени совпадают с более долгой обработкой пакета внутри Embox.
Получили такие результаты
Avg: 8673
Min: 6191
Max: 11950
При изучении полученных данных оказалось, что разброс времени обработки пакета составляет (между средним и максимальным) где то 25%, что конечно никак не может объяснить разброс на хосте (Avg: 0.08ms Max: 4.31ms). То есть либо задержки происходят вне проверяемого интервала (после получения пакета, но до входа в соответствующий обработчик прерывания, либо после того, как начинается отправка), либо задержки возникают на другом конце провода. В любом случае, ситуацию программно уже не удастся улучшить, точнее её можно улучшить только на 25%.
Может, проблема на другой стороне?
Остаётся один вариант — задержки возникают на стороне приложения Linux, ведь мы для измерений использовали обычный хост.
Как это проверить?
Первое, что приходит в голову — запустить процесс с высоким приоритетом:
nice -n -20 ./client
Ощутимых изменений это не дало — казалось, небольшой прирост есть, но разброс от раза к разу всё равно значительно его перекрывал.
Ещё один способ — запустить процесс с алгоритмом планирования round robin и с высоким приоритетом, это можно сделать с помощью chrt
:
chrt --rr 99 ./client
И на этот раз нужный эффект действительно был достигнут — количество длительных задержек уменьшилось на порядок. Приведу гистрограмму распределения задержек для разных стратегий планировщика (масштаб оси ординат имеет логарифмический масштаб, т.к. при линейном столбцы после первого не различимы).
![](https://habrastorage.org/webt/rt/_f/cu/rt_fcuaj1cqxz6rzxy_gtvlxdwa.png)
Используя Embox удалось достаточно просто решить поставленную задачу, уменьшить время отклика на один пакет почти в 10 раз. При этом прикладное ПО у заказчика остается фактически без изменений, следовательно, его не нужно переписывать и отлаживать. Может кто нибудь подскажет, можно ли добиться оптимизации данного параметра средствами Linux, например используя какой-нибудь bpfilter.
Если есть какие-то вопросы — пишите в рассылку embox-devel@googlegroups.com, или в наш телеграм-чат, или в комментарии здесь.
zuborg
А сетевая карта и ядро в линуксе тюнились хоть как-то?
Interrupt coalescing выключить, как минимум, если задержка важна.
0xdde Автор
На хосте — обычный арч, на плате — тюнили заказчики, подробностей не знаю.
Я думаю, interrupt coalescing для сетевой карточки может помочь, если приходит много пакетов «разом» (а тут отправляющий хост шлёт их по одному и ждёт ответа), в других случаях это не должно значительно помогать. Или всё-таки помогает за счёт чего-то?
edo1h
его отключение может снизить задержки (прерывание будет генерироваться сразу же). снизит ли и насколько — зависит от конркетной сетевой карты и её драйвера.
latonita
А не пробовали qnx, например? Интересно, какой отклик из коробки.
abondarev
нет, не пробовали, но было бы интересно. Хотя как в статье написано, обычно оптимизация идет по параметру пропускной способности. Просто у Embox сразу получается конфигурировать под заданные характеристики. Скорее всего в QNX что то подобное должно быть