Почему измеренная задержка в сети меняется, если я использую режим сна?

Я пытаюсь определить, сколько времени потребуется машине, чтобы получить пакет, обработать его и вернуть ответ.

Эта машина, которую я назову "сервером", запускает очень простую программу, которая получает пакет (recv(2)) в буфер, копирует полученный контент (memcpy(3)) в другой буфер и отправляет пакет обратно (send(2)). Сервер работает на NetBSD 5.1.2.

Мой клиент измеряет время туда-обратно несколько раз (pkt_count):

struct timespec start, end;
for(i = 0; i < pkt_count; ++i)
{
    printf("%d ", i+1);

    clock_gettime(CLOCK_MONOTONIC, &start);        
    send(sock, send_buf, pkt_size, 0);
    recv(sock, recv_buf, pkt_size, 0);
    clock_gettime(CLOCK_MONOTONIC, &end);        

    //struct timespec nsleep = {.tv_sec = 0, .tv_nsec = 100000};
    //nanosleep(&nsleep, NULL);

    printf("%.3f ", timespec_diff_usec(&end, &start));
}   

Я удалил проверки ошибок и другие мелочи для ясности. Клиент работает на 64-битной Ubuntu 12.04. Обе программы работают с приоритетом в реальном времени, хотя только ядро ​​Ubuntu работает в режиме реального времени (-rt). Связь между программами TCP. Это прекрасно работает и дает мне в среднем 750 микросекунд.

Тем не менее, если я включаю закомментированный вызов nanosleep (со сном 100 мкс), мои измерения падают на 100 мкс, давая в среднем 650 мкс. Если я сплю в течение 200 мкс, показатели снижаются до 550 мкс и так далее. Это продолжается до 600 мкс, что в среднем составляет 150 мкс. Затем, если я поднимаю сон до 700 мкс, мои измерения в среднем доходят до 800 мкс. Я подтвердил меры моей программы с Wireshark.

Я не могу понять, что происходит. Я уже установил опцию сокета TCP_NODELAY как на клиенте, так и на сервере, без разницы. Я использовал UDP, без разницы (такое же поведение). Таким образом, я думаю, что это поведение не из-за алгоритма Nagle. Что бы это могло быть?

[ОБНОВИТЬ]

Вот скриншот вывода клиента вместе с Wireshark. Теперь я запустил свой сервер на другой машине. Я использовал ту же ОС с той же конфигурацией (как Live System на флеш-накопителе), но оборудование другое. Такое поведение не проявлялось, все работало, как ожидалось. Но остается вопрос: почему это происходит на предыдущем оборудовании?

Сравнение выхода

[ОБНОВЛЕНИЕ 2: Больше информации]

Как я уже говорил, я протестировал свою пару программ (клиент / сервер) на двух разных серверных компьютерах. Я нанес два полученных результата.

Сравнение между двумя серверами

Первый сервер (самый странный) - это одноплатный компьютер RTD с интерфейсом Ethernet 1 Гбит / с. Второй сервер (обычный) - это одноплатный компьютер Diamond с интерфейсом Ethernet 100 Мбит / с. Оба они запускают одну и ту же ОС (NetBSD 5.1.2) из ​​одного и того же привода.

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

5 ответов

ОК, я пришел к выводу.

Я попробовал свою программу, используя Linux вместо NetBSD на сервере. Он работал так, как ожидалось, т. Е. Независимо от того, сколько я сплю в этой точке кода, результат один и тот же.

Этот факт говорит мне, что проблема может заключаться в драйвере интерфейса NetBSD. Чтобы определить драйвер, я прочитал dmesg выход. Это соответствующая часть:

wm0 at pci0 dev 25 function 0: 82801I mobile (AMT) LAN Controller, rev. 3
wm0: interrupting at ioapic0 pin 20
wm0: PCI-Express bus
wm0: FLASH
wm0: Ethernet address [OMMITED]
ukphy0 at wm0 phy 2: Generic IEEE 802.3u media interface
ukphy0: OUI 0x000ac2, model 0x000b, rev. 1
ukphy0: 10baseT, 10baseT-FDX, 100baseTX, 100baseTX-FDX, 1000baseT, 1000baseT-FDX, auto

Итак, как вы можете видеть, мой интерфейс называется wm0, В соответствии с этим (стр. 9) я должен проверить, какой драйвер загружен, просмотрев файл sys/dev/pci/files.pciлиния 625 ( здесь). Это показывает:

# Intel i8254x Gigabit Ethernet
device  wm: ether, ifnet, arp, mii, mii_bitbang
attach  wm at pci
file    dev/pci/if_wm.c         wm

Затем поиск по исходному коду драйвера (dev/pci/if_wm.c здесь), я нашел фрагмент кода, который может изменить поведение драйвера:

/*
 * For N interrupts/sec, set this value to:
 * 1000000000 / (N * 256).  Note that we set the
 * absolute and packet timer values to this value
 * divided by 4 to get "simple timer" behavior.
 */

sc->sc_itr = 1500;              /* 2604 ints/sec */
CSR_WRITE(sc, WMREG_ITR, sc->sc_itr);

Затем я изменил это значение 1500 на 1 (пытаясь увеличить количество разрешенных прерываний в секунду) и на 0 (пытаясь полностью исключить удушение прерываний), но оба эти значения дали один и тот же результат:

  • Без нано-сна: задержка ~400 мкс
  • При наносне 100 мкс: латентность ~230 мкс
  • При наносне 200 мкс: латентность ~120 мкс
  • При наносне 260 мкс: латентность ~70 мкс
  • При наносне 270 мкс: задержка ~60 мкс (минимальная задержка, которую я мог бы достичь)
  • С наносном, превышающим 300 мкс: ~420 мкс

Это, по крайней мере, лучше вести себя, чем в предыдущей ситуации.

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

Это (надеюсь, образованное) предположение, но я думаю, что это может объяснить то, что вы видите.

Я не уверен, как в реальном времени ядро ​​Linux. Это может быть не полностью упреждающим... Итак, с этим отказом от ответственности, продолжая:)

В зависимости от планировщика, задача, возможно, будет иметь то, что называется "квантами", и это всего лишь количество времени, которое она может выполнить, прежде чем будет запланирована другая задача с таким же приоритетом. Если ядро ​​не является полностью приоритетным, это также может быть момент, когда может выполняться задача с более высоким приоритетом. Это зависит от деталей планировщика, о которых я не знаю достаточно.

В любом месте между вашим первым временем получения и вторым временем получения ваша задача может быть прервана. Это просто означает, что он "приостановлен", и другая задача получает использование ЦП на определенное время.

Цикл без сна может пойти примерно так

clock_gettime(CLOCK_MONOTONIC, &start);        
send(sock, send_buf, pkt_size, 0);
recv(sock, recv_buf, pkt_size, 0);
clock_gettime(CLOCK_MONOTONIC, &end);  

printf("%.3f ", timespec_diff_usec(&end, &start));

clock_gettime(CLOCK_MONOTONIC, &start);        

<----- PREMPTION .. your tasks quanta has run out and the scheduler kicks in
       ... another task runs for a little while     
<----- PREMPTION again and your back on the CPU

send(sock, send_buf, pkt_size, 0);
recv(sock, recv_buf, pkt_size, 0);
clock_gettime(CLOCK_MONOTONIC, &end);  

// Because you got pre-empted, your time measurement is artifically long
printf("%.3f ", timespec_diff_usec(&end, &start));

clock_gettime(CLOCK_MONOTONIC, &start);        

<----- PREMPTION .. your tasks quanta has run out and the scheduler kicks in
       ... another task runs for a little while     
<----- PREMPTION again and your back on the CPU

and so on....

Когда вы включаете наносекундный сон, это, скорее всего, точка, в которой планировщик может запускаться до истечения квантов текущей задачи (то же самое относится и к recv(), который блокирует). Так что, возможно, вы получите что-то вроде этого

clock_gettime(CLOCK_MONOTONIC, &start);        
send(sock, send_buf, pkt_size, 0);
recv(sock, recv_buf, pkt_size, 0);
clock_gettime(CLOCK_MONOTONIC, &end);  

struct timespec nsleep = {.tv_sec = 0, .tv_nsec = 100000};
nanosleep(&nsleep, NULL);

<----- PREMPTION .. nanosleep allows the scheduler to kick in because this is a pre-emption point
       ... another task runs for a little while     
<----- PREMPTION again and your back on the CPU

// Now it so happens that because your task got prempted where it did, the time
// measurement has not been artifically increased. Your task then can fiish the rest of 
// it's quanta
printf("%.3f ", timespec_diff_usec(&end, &start));

clock_gettime(CLOCK_MONOTONIC, &start);        
... and so on

Затем произойдет какое-то чередование, когда иногда вы попадаете между двумя gettime(), а иногда и вне их из-за наноспала. В зависимости от x, вы можете попасть в приятное место, где вы (случайно) получите свою точку упреждения, в среднем, за пределами своего блока измерения времени.

Во всяком случае, это стоит моих двух пенни, надеюсь, это поможет объяснить вещи:)

Небольшая заметка о "наносекундах", чтобы закончить с...

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

Обычно ОС будет иметь системный тик, генерируемый, возможно, за 5 мс. Это прерывание, генерируемое, скажем, RTC (часы реального времени - просто немного оборудования). Используя эту "галочку", система генерирует свое внутреннее представление времени. Таким образом, средняя ОС будет иметь временное разрешение всего несколько миллисекунд. Причина, по которой этот тик не является более быстрым, заключается в том, что необходимо соблюдать баланс между сохранением очень точного времени и не перегружая систему прерываниями таймера.

Не уверен, что я немного устарел с вашим средним современным ПК... Я думаю, что некоторые из них имеют более высокие таймеры разрешения, но все еще не достигают диапазона наносекунд, и они могут даже бороться при 100 мкс.

Итак, в заключение, имейте в виду, что наилучшее временное разрешение, которое вы, вероятно, получите, обычно находится в диапазоне миллисекунд.

РЕДАКТИРОВАТЬ: Просто вернуться к этому и подумал, что я бы добавил следующее... не объясняет, что вы видите, но может предоставить другой путь для расследования...

Как уже упоминалось, точность хронометража наносонера вряд ли будет лучше, чем миллисекунды. Также ваша задача может быть прервана, что также вызовет проблемы со временем. Существует также проблема, заключающаяся в том, что время, необходимое для прохождения пакета по стеку протоколов, может варьироваться, а также задержка в сети.

Одна вещь, которую вы можете попробовать, если ваш сетевой адаптер поддерживает IEEE1588 (он же PTP). Если ваш сетевой адаптер поддерживает его, он может помечать временные пакеты PTP-событий по мере их выхода и вводить PHY. Это даст вам наиболее вероятную оценку задержки сети. Это устраняет любые проблемы, которые могут возникнуть у вас с упреждением программного обеспечения и т. Д. Я знаю, что я знаю, что такое Linux PTP, но вы можете попробовать http://linuxptp.sourceforge.net/

У меня есть совет о том, как создать более точное измерение производительности. Используйте инструкцию RDTSC (или даже лучше встроенную функцию __rdtsc()). Это включает считывание счетчика ЦП, не покидая ring3 (без системного вызова). Функции gettime почти всегда включают системный вызов, который замедляет работу.

Ваш код немного сложен, так как включает в себя 2 системных вызова (send / recv), но в целом лучше вызвать sleep (0) перед первым измерением, чтобы гарантировать, что очень короткое измерение не получит переключение контекста. Конечно, код измерения времени (и Sleep ()) должен быть отключен / включен через макросы в функциях, чувствительных к производительности.

Некоторые операционные системы могут быть обмануты в повышении приоритета вашего процесса, если ваш процесс освободит его окно времени выполнения (например, sleep (0)). На следующем тике по расписанию ОС (не все) повысит приоритет вашего процесса, так как он не завершил свою квоту времени выполнения.

Если объем данных, отправляемых приложением, большой и достаточно быстрый, он может заполнять буферы ядра, что приводит к задержке при каждом send(). Поскольку сон находится за пределами измеряемой секции, он будет использовать время, которое в противном случае было бы потрачено на блокировку вызова send().

Один из способов помочь проверить этот случай - запустить с относительно небольшим числом итераций, а затем с умеренным количеством итераций. Если проблема возникает с небольшим количеством итераций (скажем, 20) с небольшими размерами пакетов (скажем, <1k), то это, скорее всего, неверный диагноз.

Имейте в виду, что ваш процесс и ядро ​​могут легко перегружать сетевой адаптер и скорость передачи данных в сети Ethernet (или другом типе носителя), если отправка данных осуществляется по такой замкнутой схеме.

У меня проблемы с чтением снимков экрана. Если wireshark показывает постоянную скорость передачи по проводу, то это говорит о том, что это правильный диагноз. Конечно, выполнение математических операций - деление скорости передачи на размер пакета (+ заголовок) - должно дать представление о максимальной скорости, с которой пакеты могут быть отправлены.

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

Я думаю, что кванта - лучшая теория для объяснения. На Linux это частота переключения контекста. Ядро дает процессу кванты времени. Но процесс прерывается в двух ситуациях:

  1. Системная процедура вызова процесса
  2. Квантовое время закончено
  3. идет аппаратное прерывание (от сети, жесткого диска, USB, часов и т. д.)

Неиспользованное время квантов назначается другому готовому процессу, используя приоритеты / RT и т. Д.

Фактически частота переключения контекста настроена на 10000 раз в секунду, это дает около 100us для квантов. но переключение контента требует времени, это зависит от процессора, смотрите это: http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html я не понимаю, почему частота контента так высока, но это обсуждение форума ядра Linux.

частично похожую проблему вы можете найти здесь: https://serverfault.com/questions/14199/how-many-context-switches-is-normal-as-a-function-of-cpu-cores-or-other

Другие вопросы по тегам