Точное время синхронизации Linux - что определяет разрешение clock_gettime()?

Мне нужно сделать точное время до уровня 1 us к времени изменения рабочего цикла волны pwm.

Фон

Я использую Gumstix Over Water COM (https://www.gumstix.com/store/app.php/products/265/), у которого есть одноядерный процессор ARM Cortex-A8, работающий на 499.92 BogoMIPS ( страница Gumstix требует до 1 ГГц с рекомендуемым 800 МГц) в соответствии с /proc/cpuinfo. ОС представляет собой версию Angstrom Image Linux на базе ядра версии 2.6.34, и она является запасом на Gumstix Water COM.

Проблема

Я довольно много читал о точном времени в Linux (и пробовал большую часть этого), и, похоже, консенсус заключается в том, что использование clock_gettime() и обращение к CLOCK_MONOTONIC - лучший способ сделать это. (Я бы хотел использовать регистр RDTSC для синхронизации, так как у меня есть одно ядро ​​с минимальными возможностями энергосбережения, но это не процессор Intel.) Итак, вот нечетная часть, а clock_getres() возвращает 1, предлагая разрешение в 1 нс, фактические временные тесты предполагают минимальное разрешение 30517 нс или (это не может быть совпадением) ровно время между отметками времени 32,768 кГц. Вот что я имею в виду:

// Stackoverflow example
#include <stdio.h>
#include <time.h>    

#define SEC2NANOSEC 1000000000

int main( int argc, const char* argv[] )
{               
    // //////////////// Min resolution test //////////////////////
    struct timespec resStart, resEnd, ts;
    ts.tv_sec  = 0; // s
    ts.tv_nsec = 1; // ns
    int iters = 100;
    double resTime,sum = 0;    
    int i;
    for (i = 0; i<iters; i++)
    {
        clock_gettime(CLOCK_MONOTONIC, &resStart);      // start timer
        // clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, &ts);
        clock_gettime(CLOCK_MONOTONIC, &resEnd);        // end timer
        resTime = ((double)resEnd.tv_sec*SEC2NANOSEC + (double)resEnd.tv_nsec 
                  - ((double)resStart.tv_sec*SEC2NANOSEC + (double)resStart.tv_nsec);
        sum = sum + resTime;
        printf("resTime = %f\n",resTime);
    }    
    printf("Average = %f\n",sum/(double)iters);
}

(Не волнуйтесь о двойном кастинге, tv_sec в time_t и tv_nsec длиннее.)

Скомпилировать с помощью:

gcc soExample.c -o runSOExample -lrt

Запустить с помощью:

./runSOExample

Когда нанослоп прокомментирован, как показано, результат равен 0ns или 30517ns, причем большинство из них составляет 0ns. Это заставляет меня думать, что CLOCK_MONOTONIC обновлен на 32,768 кГц, и большую часть времени часы не обновлялись до второго вызова clock_gettime(), и в тех случаях, когда результат 30517ns, часы были обновлены между вызовами.

Когда я делаю то же самое на своем компьютере разработки (шестиступенчатый процессор AMD FX (tm) -6100, работающий на 1,4 ГГц), минимальная задержка является более постоянной 149-151 нс без нулей.

Итак, сравните эти результаты со скоростью процессора. Для Gumstix это 30517ns (32,768 кГц) соответствует 15298 циклам 499,93 МГц. Для моего компьютера-разработчика, которому 150ns соответствует 210 циклам 1.4Ghz CPU.

При вызове clock_nanosleep() раскомментированы средние результаты: Gumstix: Среднее value = 213623, и результат изменяется, вверх и вниз, кратным минимальному разрешению 30517 нс Dev компьютер: 57710-68065 нс без четкой тенденции. В случае с dev-компьютером я ожидаю, что разрешение действительно будет на уровне 1 нс, а измеренное ~ 150ns - это время, прошедшее между двумя вызовами clock_gettime().

Итак, мой вопрос таков: Что определяет минимальное разрешение? Почему разрешение компьютера-разработчика 30000X лучше, чем Gumstix, когда процессор работает только на 2.6X быстрее? Есть ли способ изменить, как часто обновляется CLOCK_MONOTONIC и где? В ядре?

Спасибо! Если вам нужна дополнительная информация или пояснения, просто спросите.

Ответ 1

Как я понимаю, разница между двумя средами (Gumstix и вашим Dev-компьютером) может быть основным таймером h/w, который они используют.

Прокомментированный случай nanosleep():

Вы используете clock_gettime() дважды. Чтобы дать вам общее представление о том, что этот clock_gettime() в конечном итоге будет отображаться (в ядре):

clock_gettime → clock_get() → posix_ktime_get_ts → ktime_get_ts() → timekeeping_get_ns() - > clock- > read()

clock- > read() в основном считывает значение счетчика, предоставленного базовым драйвером таймера, и соответствующий h/w. Простая разница с сохраненным значением счетчика в прошлом и текущим значением счетчика, а затем математикой преобразования наносекунд даст вам пройденные наносекунды и обновит структуры данных, сохраняющие время в ядре.

Например, если у вас есть таймер HPET, который дает вам тактов на 10 МГц, счетчик h/w будет обновляться с интервалом 100 нс.

Давайте скажем, что на первом clock- > read() вы получите значение счетчика X.

Линейные структуры данных Linux будут считывать это значение X, получить разницу "D" по сравнению с каким-то старым значением хранимого счетчика. Некоторая противоположная математика "D" для наносекунд "n", структура по 'n' Допустим это новое значение времени в пользовательском пространстве.

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

Теперь замените этот таймер HPET медленными 32,768 кГц. Теперь счетчик clock- > read() будет обновляться только после 30517 нс секунд, поэтому, если вы выполняете второй вызов clock_gettime() до этого периода, вы получите 0 (что является большинством случаев), а в некоторых случаях ваш второй вызов функции будет помещен после того, как счетчик увеличится на 1, т.е. прошло 30517 нс. Следовательно, иногда значение 30517 нс.

Несмещенный случай Nanosleep(): Пусть трассировка clock_nanosleep() для монотонных часов:

clock_nanosleep() → nsleep → common_nsleep() → hrtimer_nanosleep() → do_nanosleep()

do_nanosleep() просто поместит текущую задачу в состояние INTERRUPTIBLE, будет ждать истечения срока действия таймера (который равен 1 нс), а затем снова установите текущую задачу в состояние RUNNING. Понимаете, сейчас есть много факторов, в основном, когда ваш поток ядра (и, следовательно, процесс пользовательского пространства) будет запланирован снова. В зависимости от вашей ОС вы всегда будете сталкиваться с некоторой задержкой при выполнении контекстного переключателя, и это то, что мы наблюдаем со средними значениями.

Теперь ваши вопросы:

Что определяет минимальное разрешение?

Я думаю, что разрешение/точность вашей системы будет зависеть от используемого оборудования таймера (предполагая, что ваша ОС может обеспечить эту точность для процесса пользовательского пространства).

* Почему разрешение компьютера-разработчика 30000X лучше, чем Gumstix, когда процессор работает только на 2.6X быстрее? *

Прости, я пропустил тебя здесь. Как это на 30000 раз быстрее? Для меня это похоже на что-то в 200 раз быстрее (30714 нс /150 нс ~ 200X?). Но в любом случае, как я понимаю, скорость процессора может быть или не быть связана с разрешением/точностью таймера. Таким образом, это предположение может быть правильным в некоторых архитектурах (когда вы используете TSC H/W), однако может оказаться неудачным в других (с использованием HPET, PIT и т.д.).

Есть ли способ изменить, как часто обновляется CLOCK_MONOTONIC и где? В ядре?

вы можете всегда заглядывать в код ядра для деталей (как я это рассмотрел). В коде ядра linux найдите эти исходные файлы и Документацию:

  • kernel/posix-timers.c
  • ядро ​​/hrtimer.c
  • Документация/таймеры/hrtimers.txt

Ответ 2

У меня нет gumstix под рукой, но похоже, что ваш clocksource медленный. run:

$ dmesg | grep clocksource

Если вы вернетесь

[ 0.560455] Switching to clocksource 32k_counter

Это может объяснить, почему ваши часы настолько медленны.

В последних ядрах есть каталог /sys/devices/system/clocksource/clocksource0 с двумя файлами: available_clocksource и current_clocksource. Если у вас есть этот каталог, попробуйте переключиться на другой источник, указав его имя во второй файл.