Главная » Ядро Linux » Задержка выполнения

0

Часто  коду  ядра  (особенно драйверам) необходимо задерживать выполнение действий  на  некоторый период   времени без  использования  таймеров или  механизма нижних половин.  Это  обычно необходимо для  того,  чтобы  дать  аппаратному обеспечению время  на  завершение выполнения задачи.   Такой интервал времени обычно достаточно короткий. Например,  в спецификации сетевой  интерфейсной платы может  быть  указано время  изменения режима работы  Ethernel-контроллера,  равное

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

Ядро  предоставляет несколько решений этой  задачи, в зависимости от семантики  задержки. Эти  решения имеют  разные свойства. Некоторые решения во  время задержки загружают процессор,  не  давая  возможности  выполнять другую, более  полезную  работу.  Другие  решения не  загружают процессор,  но  не  дают  гарантии того, что  код  возобновит выполнение точно  в необходимый момент времени8 .

Задержка с помощью цикла

Наиболее простое для  реализации (хотя  обычно не оптимальное) решение — это использование задержки с помощью  цикла или  ожидания в состоянии занятости (busy loop,  busy wailing).  Эта  техника работает, только  если  интервал времени задержки является  кратным периоду  системного таймера или  когда  точность не  очень  важна.

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

unsigned long delay = jiffies + 10; /* десять импульсов таймера */

while (time_before (jiffies, delay));

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

Цикл будет  выполняться,  пока  значение переменной  jiffie s  не  станет   больше, чем  значение переменной delay , что может  произойти только  после  того, как  будут получены 10 импульсов системного таймера. Для  аппаратной платформы х86 со значением параметра HZ,  равным 1000, этот  интервал равен   10 миллисекунд.

Аналогично  можно поступить следующим образом.

unsigned long delay = jiffies + 2*HZ; /* две секунды */

while (time_before(jiffies, delay));

В этом  случае  цикл  будет  выполняться, пока  не  поступит 2*Н2  импульсов системного  таймера,  что  всегда  равно  2 секундам, независимо от  частоты  системного таймера.

Такой подход  не  очень  хорош  для  всей  системы. Пока код  ожидает,  процессор загружен выполнением бесполезного цикла и никакой полезной работы  при  этом  не выполняется!  На  самом  деле  к такому  "глупому"  подходу  нужно  прибегать по  возможности  реже, и он  показан здесь, потому  что является понятным и простым способом осуществить задержку. Его  можно встретить в чьем-нибудь не  очеиь  хорошем коде.

Лучшим решением япляется  перепланирование для  того, чтобы  процессор мог выполнить полезную работу, пока  ваш  код  ожидает:

unsigned long delay = jiffies + 5*HZ;

while (time_before(jiffies, delay))

cond_reschcd();

Вызов  функции  cond_resched( )   планирует выполнение  другого  процесса, но только  в случае, если  установлен флаг  need_resched . Другими словами, данное решение позволяет активизировать планировщик,  но  только   в случае, когда  есть  более  важное   задание,  которое нужно   выполнить.  Следует  обратить внимание,  что. поскольку используется планировщик,  такое  решение нельзя применять в контексте прерывания,  а только  в  контексте процесса.  Задержки лучше  использовать только в  контексте процесса,  поскольку обработчики прерываний должны выполняться по возможности быстро  (а цикл  задержки не дает такой  возможности!). Более  того, любые  задержки выполнения,  по  возможности, не  должны использоваться при  захваченных блокировках и при  запрещенных прерываниях.

Поклонники языка С  могут  поинтересоваться, какие есть  гарантии, что указанные  циклы будут действительно выполняться?  Обычно компилятор  С  может  выполнить  чтение  указанной переменной всего  один  раз.  В обычной ситуации нет никакой гарантии,  что  переменная  jiffie s  будет  считываться  на  каждой  итерации  цикла. Нам   же  необходимо,  чтобы   значение  переменной  jiffie s  считывалось  на  каждой итерации цикла, так как  это значение увеличивается в другом  месте, а именно в прерывании таймера. Именно поэтому данная переменная определена в файле  <linux / jiffies.h >   с  атрибутом  volatile .   Ключевое  слово   volatil e  указывает  компилятору,  что  эту  переменную необходимо  считывать из  того  места,  где  она  хранится в оперативной памяти, и никогда не использовать копию, хранящуюся в регистре процессора. Это  гарантирует, что указанный цикл  выполнится, как  и ожидается.

Короткие задержки

Иногда коду  ядра  (и  снопа  обычно  драйверам) необходимы задержки на  очень  короткие  интервалы времени (короче, чем период  системного таймера), причем интервал  должен   отслеживаться с  достаточно  высокой  точностью.  Это  часто  необходимо для  синхронизации с  аппаратным обеспечением, для  которого описано некоторое минимальное время   выполнения  действий,  и  которое часто  бывает  меньше одной миллисекунды.  В  случае  таких  малых  значений  времени  невозможно  использовать задержки  на  основании  переменной  jiffies , как  показано в  предыдущем  примере. При   частоте  системного таймера,  равной  100  Гц, значение периода системного таймера  достаточно  большое—  10  миллисекунд!  Даже  при  частоте   системного  таймера

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

Ядро  предоставляет две  функции для  обеспечения  микросекундных  и  миллисекундных  задержек,  которые определены в  файле   <linux/delay.h >  и  не  используют переменную  jiffies .

void udelay(unsigned long usecs);

void mdelay(unsigned long msecs);

Перва я  функция  позволяет задержать   выполнени е  на  указанное  количество микросекунд  с  использованием цикла.  Вторая  функция  задерживает выполнение  на указанное  количество миллисекунд.   Следует  вспомнить,  что  одна  секунда   равна   1000 миллисекундам, что  эквивалентно 1000000  микросекунд.  Использование этих  функций тривиально.

udelay(150); /* задержка на 150 ms */

Функция udela y ()   выполнена па  основе   цикла, для  которого известно,  сколько итераций необходимо выполнить за указанный период  времени. Функция mdelay  () выполнена на  основе  функции udela y () .  Так  как  в ядре  известно,  сколько циклов процессор  может  выполнить  в  одну  секунду   (смотрите  ниже   замечание  по  поводу характеристики BogoMlPS),  функция  udela y ()   просто   масштабирует  это  значение для  того,  чтобы  скорректировать количество итераций цикла  для  получения указанной  задержки.

Мой BogoMIPS больше, чем у Вас!

Характеристика BogoMlPS всегда была источником недоразумений и шуток. На самом деле вычисленное значение BogoMlPS не имеет ничего общего с производительностью компьютера и используется только для функций udelay( ) и mdelay() . Название этого параметра состоит из двух частей bogus (фиктивный) и MIPS (million of instructions per second, миллион инструкций в секунду).  Все знакомы  с сообщением, которое  выдается  при загрузке  системы  и похоже  на следующее (данное сообщение соответствует процессору Pentium III с частотой 1 ГГц).

Detected  1004.932  MHz processor. Calibrating delay loop… 1990.65 BogoMlPS

Значение параметра BogoMIPS это количество циклов, которые процессор может выполнить

за указанный период времени, В действительности эта характеристика показывает, насколько быстро процессор может ничего не делать! Это значение хранится в переменной loops_per _

jiffy , и его можно считать из файла /proc/cpuinfo .

Таймеры и управление временем

функции, использующие циклы задержки, используют данное значение, чтобы вычислить (и это получается достаточно точно), сколько итераций цикла необходимо выполнить, чтобы обеспечить необходимую задержку.

Ядро вычисляет значение переменной 1 oops_per_jiff у при загрузке системы в функции calibrate_delay() , реализация которой описана в файле init/main.c .

Функция  udelay( )  должна  вызываться только  для  небольших задержек, поскольку при  большом времени задержки на быстрой машине может  возникнуть переполнение  в переменных цикла. Общее  правило: по  возможности не использовать функцию udelay( )   для  задержек,  больше   одной   миллисекунды.  Для  более  продолжительных задержек хорошо  работает   функция mdelay() .  Так  же  как  и  другие  методы   задержки  выполнения, основанные на циклах, эти  функции (особенно функция mdelay() , так  как  она  дает  длительные задержки)  должны использоваться,  только   если  это абсолютно  необходимо.  Следует  помнить,  что  очень  плохо  использовать циклы задержек, когда  удерживается блокировка или  запрещены прерывания,  потому  что  это очень  сильно влияет  на  производительность и  время  реакции системы. Если  необходимо  обеспечить точное  время  задержки, то  эти  функции — наилучшее решение. Обычное использование этих  функций — это  задержки на не очень  короткий период времени, который лежит  в микросекундном диапазоне.

Функция  schedule_timeout( )

Более  оптимальный метод  задержки выполнения — это  использование функции schedule_timeoui t   () . Этот  вызов  переводит вызывающее задание в состояние ожидания   (sleep)   по  крайней до  тех пор, пока  не  пройдет указанный  период   времени. Нет  никакой гарантии, что  время  ожидания будет  точно равно  указанному значению, гарантируется только,  что  задержка будет не  меньше указанной. Когда  проходит указанный период  времени, ядро  возвращает задание в состояние готовности к  выполнению (wake  up)  и  помещает его в очередь  выполнения. Использовать эту функцию просто.

/* установить состояние задания в значение прерываемого ожидания */

set_current_state(TASK INTERRUPTIBLE);

/* перейти в приостановленное состояние на s секунд */

schedule_timeout(s * HZ);

Единственный параметр функции — эт о  желаемое относительно е  время, выраженное в  количестве  импульсов системного  таймера.  В  этом   примере задание переводится в  прерываемое состояние  ожидания,  которое будет  длиться  s  секунд. Поскольку задание отмечено как  TASK_INTERRUPTIBLE, то  оно  может  быть  возвращено к выполнению раньше времени, как  только  оно  получит   сигнал. Если  не нужно, чтобы  код  обрабатывал сигналы,  то  можно использовать состояние  TASK_ UNINTERRUPTIBLE. Перед  вызовом функции  schedule_tirneout( )  задание должно быть  в одном  из  этих  двух состояний, иначе  задание в состояние ожидания переведено  не  будет.

Следует  обратить внимание,  что  поскольку  функция  schedule_tiraeout( )   использует  планировщик, то  код, который ее  вызывает, должен  быть  совместим с состоянием ожидания. Обсуждение, посвященное атомарности и переходу  в состояние

ожидания,  приведено   в  главах 8  и  9.  Если  коротко,   то  эту  функцию   необходимо   вызывать  в  контексте   процесса   и  не  удерживать  при  этом  блокировку.

Функция   schedule_tiraeout( )   достаточно   проста.   Она  просто   использует   таймеры  ядра.   Рассмотрим  эту  функцию   подробнее.

signed long schedule_timeout(signed long timeout)

{

timer_t timer;

unsigned long expire;

switch (timeout)

{

case MAX_SCHEDULE_TIMEOUT:

schedule();

goto out;

default:

if (timeout < 0)

{

printk(KERN_ERR "schedule_timeout: wrong timeout " "value %lx from %p\n", timeout,

builtin_return_address(0));

current->state = TASK_RUNNING;

goto out;

}

}

expire = timeout + jiffies;

init timer(&timer);

timer.expires = expire;

timer.data = (unsigned long) current;

timer.function = process_timeout;

add_timer(&timer);

schedule();

del_timer_sync(&timer);

out:

}

timeout = expire jiffies;

return timeout < 0 ? 0 : timeout;

Эта  функция создает  таймер  time r и устанавливает время  срабатывания в значение  timeou t  импульсов системного таймера  в будущем.  В качестве  обработчика таймера  устанавливается функция proces s  timeou t () ,  которая  вызывается, когда истекает  период  времени таймера.  Далее  таймер  активизируется, и вызывается функция schedul e () . Так  как  предполагается, что текущее  задание  находится  в состоянии TASK_INTERRUPTIBLE или TASK_UNINTERRUPTIBLE, то планировщик не будет выполнять текущее  задание, а выберет  для выполнения другой  процесс.

Когда  интервал  времени таймера  истекает,  то  вызывается функция  process _

timeout  () ,  которая  имеет  следующий вид.

void process_timeout(unsigned long data)

{

wake_up_process((task t *) data);

}

Эта функция устанавливает задание в состояние TASK_RUNNING  и помещает его в очередь  выполнения.

Когда  задание снова  планируется на  выполнение, то оно  возвращается в функцию schedule_tiraeou t ()   (сразу  после  вызова  функции schedul e ()) .  Если  задание возвращается к выполнению преждевременно,  то  таймер  ликвидируется.  После  этого задание возвращается из  функции ожидания по  тайм-ауту.

Код  оператора  switc h ()   служит  для  обработки специальных случаев  и  не  является  основной частью  функции. Проверка на значение MAX_SCHEDULE_TIMEOUT  позволяет  заданию  находиться в  состоянии ожидания неопределенное время. В этом случае  таймер  не  устанавливается (поскольку нет  ограничений на  интервал времени ожидания), и сразу  же активизируется планировщик.  Если  вы  это  применяете, то, наверное, у вас  есть лучший  способ вернуть  задание в состояние выполнения!

Ожидание в очереди wait queue в течение интервала времени

В главе 4 рассматривалось, как  контекст процесса в ядре  может  поместить себя  в очередь  ожидания для  того,  чтобы  ждать  наступления некоторого события,  а затем вызвать  планировщик, который выберет  новое  задание для  выполнения. Если  где-то в  другом  месте  произойдет указанное  событие,  то  вызывается функция  wake_up  () для  всех  заданий,  которые ожидают в очереди. Эти  задания возвращаются к  выполнению и  могут  продолжать работу.

Иногда желательно ожидать  наступления  некоторого события  или пока  не  пройдет  определенный интервал времени,  в зависимости от  того,  что  наступит раньше, В  этом  случае  код  должен   просто   вызвать   функцию  schedule_timeou t  ()   вместо функции schedul e ()  после  того, как  он  поместил себя  в очередь  ожидания. Задание будет  возвращено к  выполнению, когда  произойдет  желаемое событие или  пройдет указанный интервал времени. Код  обязательно должен  проверить, почему он  возвратился  к выполнению — это  может  произойти потому, что  произошло событие, прошел  интервал времени или  был  получен сигнал — после  этого  необходимо соответственным образом продолжить выполнение.

Время вышло

В этой  главе  были  рассмотрены понятия, связанные с представлением о времени в  ядре  и  с тем, как  при  этом  происходит управление абсолютным и  относительным ходом  времени. Были показаны отличия абсолютного и относительного времени,  а также  периодических и  относительных событий. Далее  были  рассмотрены прерывания  таймера,  импульсы таймера, константа HZ   и  переменная jiffies .

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

Большая часть  кода  ядра, который вам  придется писать, требует  понимания того, как  время  течет  в ядре  и  как  его  отслеживать.  С  очень  большой вероятностью, особенно   при  разработке драйверов,  вам  необходимо  будет  иметь  дело  с  таймерами ядра.  Материал этой  главы  принесет практическую пользу.

Источник: Лав,  Роберт. Разработка ядра  Linux, 2-е  издание. : Пер.  с англ.  — М.  : ООО  «И.Д.  Вильяме» 2006. — 448 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии