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

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

Эта машина, которую я назову "сервер", запускает очень простую программу, которая получает пакет (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 микросекунд.

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

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

[ОБНОВЛЕНИЕ]

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

Output Comparison

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

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

Comparison between two servers

Первый сервер (странный) - это одноплатный компьютер RTD с интерфейсом Ethernet 1 Гбит/с. Второй сервер (обычный) - это Diamond Single Board Computer с интерфейсом Ethernet 100 Мбит/с. Оба они запускают ОСИЮ ОСУШУЮ (NetBSD 5.1.2) из ​​SAME Pendrive.

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

Ответ 1

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

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

Этот факт говорит мне, что проблема может заключаться в драйвере интерфейса 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. Согласно this (стр. 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 (пытаясь полностью устранить дросселирование прерываний), но оба этих значения дали тот же результат:

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

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

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

Ответ 2

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

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

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

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

Цикл без сна может выглядеть примерно так:

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 quanta
printf("%.3f ", timespec_diff_usec(&end, &start));

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

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

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

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

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

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

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

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

EDIT: просто пересматриваю это и подумал, что добавлю следующее... не объясняет, что ваше видение, но может предоставить еще один путь для расследования...

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

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

Ответ 3

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

  • Процедура процедуры вызова вызова
  • время квантов завершено
  • происходит аппаратное прерывание (из сети, hdd, usb, clock и т.д.)

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

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

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

Ответ 4

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

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

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

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

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

Ответ 5

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

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

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