Прежде всего, я должен сказать, что в следствии многозадачной природы Linux, вы не можете гарантировать точную синхронизацию в пользовательской программе. Ваш процесс может выйти из расписания выполняемых задач на интервал от 10 миллисекунд до нескольких секунд (на системе с высокой загрузкой). Тем не менее, к большинству приложений, использующих порты ввода/вывода, это не относится. Чтобы свести это к минимуму, вы можете выполнять свой процесс при высоком приоритете (см. руководство nice(2)) или использовать расписание задач в реальном времени (см. ниже).
Если вам нужна более точная синхронизация в обычных пользовательских программах, существует несколько возможностей поддержки `реального времени' в пользовательском режиме. Ядра Linux версий 2.x имеют относительную поддержку реального времени; см. руководство по sched_setscheduler(2). Кроме того, существует специальное ядро, жестко поддерживающее реальное время; см. http://luz.cs.nmt.edu/~rtlinux/.
Итак, начнем с самых простых процедур синхронизации. Для задержки в несколько секунд, лучше всего использовать sleep(). Для задержек, с точностью до десятков миллисекунд может работать usleep(). Во время выполнения этих функций, процессор приостанавливает данный процесс на заданное время (``засыпает'') и выполняет другие процессы. Для детальной информации см. руководство sleep(3) и usleep(3).
Задержки менее 50 миллисекунд (в зависимости от скорости процессора, машины, и загрузки системы) не представляются возможными, т.к. для возвращения управления процессу в Linux тратится, как минимум, 10-30 миллисекунд. Указание таких задержек для usleep(3), на самом деле, приведет к большей задержке (уж, как минимум, в 10 мс).
В серии 2.0.x ядр Linux существует системный вызов nanosleep() (см. руководство nanosleep(2)), который позволяет выполнить самую короткую задержку (в несколько микросекунд и выше).
Задержки <= 2 мс, если (и только если) ваш процесс находится в относительном режиме реального времени (soft real time scheduling), nanosleep() выполняет при помощи цикла; иначе он работает так же, как и usleep().
В циклах используется udelay()(внутренняя фунция ядра), а длина цикла вычисляется в зависимости от значения BogoMips. Для более детальной информации, как это работает см. /usr/include/asm/delay.h.
Другой способ обеспечить задержку в несколько микросекунд - это порты ввода/вывода. Запись или чтение любого значения из порта 0x80 (о том, как это сделать, см. выше) обеспечивает задержку почти точно равную одной микросекунде, независимо от типа и скорости вашего процессора. Чтобы обеспечить задержку в несколько микросекунд, вы можете выполнить эту операцию несколько раз. Запись в этот порт не оказывает никакого влияния на компьютер (и некоторые драйверы используют это). Таким образом, обычно выполняется задержка в {in|out}[bw]_p() (см. asm/io.h).
На самом деле, операции ввода/вывода на большинство портов в диапазоне от 0 до 0x3ff тоже дают задержку около одной микросекунды. Так что, если вы, например, производите запись в параллельный порт, для обеспечения задержки, просто добавьте дополнительные inb() из порта.
Если вы знаете тип и частоту процессора машины, на которой будет работать программа, вы можете вставить меньшие задержки, путем выполнения определенных инструкций ассемблера (но помните, что ваш процесс может быть в любое время исключен из расписания, так что задержка может быть на много больше, чем вы хотите). В нижеуказанной таблице скорость процессора определяет число необходимых тактовых циклов для задержки; например, для процессора с частотой 50МГц (напр. 486DX-50 или 486DX2-50), один такт дает 1/50000000 секунд (=200 наносекунд).
Инструкция такты i386 такты i486 nop 3 1 xchg %ax,%ax 3 3 or %ax,%ax 2 1 mov %ax,%ax 2 1 add %ax,0 2 1 |
(Прошу прощения, но я не знаю о Пентиуме; скорее всего, они близки к i486. На i386 я не смог найти инструкцию, выполняемую за один такт. В общем случае, лучше всего использовать одно-тактовые инструкции, иначе конвейерная обработка в современных процессорах может сократить задержку.)
Инструкции nop и xchg не должны давать посторонние эффекты. Остальные команды могут изменять регистр флагов процессора. Впрочем, это уже не имеет значение, т.к. gcc должен заметить это. Самый лучший выбор это nop.
Чтобы использовать это метод, выполните в вашей программе функцию asm("инструкция"). Синтаксис инструкций указан в таблице выше; если вы хотите указать несколько инструкций в одном asm(), разделите их точкой с запятой. Например, asm("nop ; nop ; nop ; nop") выполняет четыре инструкции nop, делая задержку в четыре такта на i486 или Pentium (или 12 тактов на i386).
Gcc непосредственно вставляет код из asm() в программу (inline), так что здесь нет никаких других задержек, связанных с вызовом функций.
Задержки менее одного такта невозможны на архитектуре Intel x86.
На процессорах Пентиум вы можете узнать количество тактов, прошедших со времени последней перезагрузки, при помощи следующей функции:
extern __inline__ unsigned long long int rdtsc() { unsigned long long int x; __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x)); return x; } |
Вы можете отслеживать это значение для задержки на любое количество тактов, на сколько хотите.
Для задержек с точностью до одной секунды, лучше всего использовать time(). Для более точных задержек, gettimeofday() имеет точность около одной микросекунды (см. выше, насчет многозадачности). На пентиумах же функция rdtsc (см. выше) дает точность до одного такта.
Если после истечения какого-то количества времени вы хотите получить в программу сигнал, используйте setitimer() или alarm(). Для подробной информации об этих функциях см. соответствующее руководство.