Отложенный запуск

Предыдущая  Содержание  Следующая V*D*V

Драйверам устройств часто необходимо отложить выполнение определённого фрагмента кода на какой-то период времени, обычно, чтобы позволить аппаратуре выполнить какую-то задачу. В этом разделе мы рассмотрим различные методы получения задержек. Обстоятельства каждой ситуации определяют, какую технику использовать лучше всего; пройдёмся по всем и укажем преимущества и недостатки каждой из них.

 

Важно рассмотреть, как требуемая задержка сравнивается с тактовыми тиками, учитывая разницу HZ на различных платформах. Задержки, которые гарантировано больше, чем такт тика, и не страдают от его грубой точности, могут воспользоваться системными часами. Очень короткие задержки обычно должны быть реализованы программными циклами. Между этими двумя случаями лежит переходная зона. В этой главе мы используем фразу "длинная" задержка для обозначения многотиковой задержки, которая может быть на некоторых платформах так же мала, как и несколько миллисекунд, но всё же длинной по мнению процессора и ядра.

 

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

Длинные задержки

Иногда драйверу необходимо отложить исполнение на относительно длительные периоды - больше, чем один тактовый тик. Есть несколько способов выполнить такого рода задержки; мы начнём с самой простой техники, затем перейдём к более совершенным техникам.

Ожидание в состоянии занятости

Если вы хотите отложить исполнение на много тактовых тиков, допуская некоторый люфт в значении, самой простой (но не рекомендуемой) реализацией является цикл, который мониторит счётчик тиков. Реализация ожидания в состоянии занятости (busy-waiting) обычно выглядит как следующий код, где j1 является значением jiffies по истечении задержки:

 

while (time_before(jiffies, j1))

cpu_relax( );

 

Вызов cpu_relax является архитектурно-зависимым способом сообщить, что вы ничего не делаете с процессором на данный момент. Во многих системах он совсем ничего не делает; на симметричных многопоточных ("hyperthreaded") системах он может переключить ядро на другой поток. В любом случае, когда возможно, этот подход определённо следует избегать. Мы показываем его здесь, потому что иногда вы можете запустить этот код, чтобы лучше понять внутренности другого кода.

 

Давайте посмотрим, как работает этот код. Цикл гарантированно рабочий, потому что jiffies объявлена заголовками ядра как volatile (нестабильная) и, таким образом, извлекается из памяти каждый раз, когда какой-либо код Си обращается к ней. Хотя технически правильный (в этом он работает как задумано), этот цикл ожидания серьёзно снижает производительность системы. Если вы не настроили ядро для вытесняющих операций, цикл полностью блокирует процессор на продолжительность задержки; планировщик никогда не вытеснит процесс, который работает в пространстве ядра, и компьютер выглядит совершенно мёртвым до достижения время j1. Проблема становится менее серьёзной, если вы работаете на ядре с вытеснением, потому что пока код удерживает блокировку, какое-то время процессора может быть использовано для других целей. Однако, ожидание готовности всё же дорого и на системах с вытеснением.

 

Ещё хуже, если случится, что прерывания запрещены при входе в цикл, jiffies не будет обновляться и условие while останется верным навсегда. Запуск вытесняющего ядра также не поможет и вы будете вынуждены нажать большую красную кнопку.

 

Эта реализация кода задержки, как и последующих, доступна в модуле jit. Файлы /proc/jit*, создаваемые модулем, создают задержку в целую секунду каждый раз, когда вы читаете строку текста и строки гарантировано будут 22 байта каждая. Если вы хотите протестировать код ожидания, вы можете читать /proc/jitbusy, который имеет цикл ожидания в одну секунду перед возвращением каждой строки.

 

Предупреждение

Обязательно читайте, самое большее, одну строку (или несколько строк) за раз из /proc/jitbusy. Упрощённый механизм  ядра для регистрации файлов /proc вызывает метод read снова и снова для заполнения буфера данных по запросу пользователя. Следовательно, такая команда, как cat /proc/jitbusy, если она читает 4 Кб за раз, "подвесит" компьютер на 186 секунд.

 

Предлагаемой командой для чтения /proc/jitbusy является dd bs=22 < /proc/jitbusy, которая так же опционально определяет и количество блоков. Каждая 22-х байтовая строка, возвращаемая файлом, отображает младшие 32 бита значения счётчика тиков до и после задержки. Это пример запуска на ничем не загруженном компьютере:

 

phon% dd bs=22 count=5 < /proc/jitbusy

   1686518    1687518

   1687519    1688519

   1688520    1689520

   1689520    1690520

   1690521    1691521

 

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

 

phon% dd bs=22 count=5 < /proc/jitbusy

   1911226    1912226

   1913323    1914323

   1919529    1920529

   1925632    1926632

   1931835    1932835

 

Здесь, каждый системный вызов read задерживается ровно на одну секунду, но ядру может потребоваться более 5-ти секунд, перед передачей управления процессу dd, чтобы он мог сделать следующий системный вызов. Это ожидаемо в многозадачной системе; процессорное время является общим для всех запущенных процессов, а интенсивно занимающий процессор процесс имеет динамически уменьшаемый приоритет. (Обсуждение политик планирования выходит за рамки этой книги.)

 

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

 

Если вы повторите команду во время работы вытесняющего ядра, вы не найдёте заметной разницы при незанятом процессоре и поведением под нагрузкой:

 

phon% dd bs=22 count=5 < /proc/jitbusy

   14940680  14942777

   14942778  14945430

   14945431  14948491

   14948492  14951960

   14951961  14955840

 

Здесь не существует значительной задержки между окончанием системного вызова и началом следующего, но отдельные задержки гораздо больше, чем одна секунда: до 3.8 секунды в показанном примере и со временем возрастают. Эти значения демонстрируют, что этот процесс был прерван во время задержки переключением на другой процесс. Промежуток между системными вызовами является не только результатом переключения для этого процесса, поэтому здесь невозможно увидеть никакой специальной задержки. (возможно, речь идёт о том, что на фоне такой задержки от планировщика процессов невозможно заметить собственно заданную задержку)

Уступание процессора

Как мы видели, ожидание готовности создаёт большую нагрузку на систему в целом; мы хотели бы найти лучшую технику. Первым изменением, которое приходит на ум, является явное освобождение процессора, когда мы в нём не заинтересованы. Это достигается вызовом функции schedule, объявленной в <linux/sched.h>:

 

while (time_before(jiffies, j1)) {

    schedule( );

}

 

Этот цикл может быть протестирован чтением /proc/jitsched, так же, как выше читается /proc/jitbusy. Однако, он всё ещё не является оптимальным. Текущий процесс ничего не делает, он освобождает процессор, но остаётся в очереди выполнения. Если это единственный исполняемый процесс, он на самом деле работает (вызывает планировщик, который выбирает тот же самый процесс, который вызывает планировщик, который ...). Иными словами, загрузка машины (среднее количество запущенных процессов) является по крайней мере единицей, а задача простоя (idle) (процесс с номером 0, называемый также по исторической причинам swapper) никогда не работает. Хотя этот вопрос может показаться неуместным, работа задачи простоя, когда компьютер не используется, снимает нагрузку на процессор, снижая его температуру, и увеличивает срок его службы, а также срок работы батарей, если компьютер является вашим ноутбуком. Кроме того, поскольку процесс фактически выполняется во время задержки, он несёт ответственность за всё время, которое потребляет.

 

Поведение /proc/jitsched фактически аналогично работе /proc/jitbusy с вытесняющим ядром. Это пример работы на незагруженной системе:

 

phon% dd bs=22 count=5 < /proc/jitsched

   1760205    1761207

   1761209    1762211

   1762212    1763212

   1763213    1764213

   1764214    1765217

 

Интересно отметить, что каждый read иногда заканчивается ожиданием несколько больших тактовых тиков, чем запрашивалось. Эта проблема становится всё сильнее и сильнее, когда система становится занятой, и драйвер может в конечном итоге ожидать больше, чем предполагалось. После того, как процесс освободил процессор с помощью schedule, нет никаких гарантий, что этот процесс получит процессор обратно в ближайшее время. Поэтому, таким образом, вызов schedule является небезопасным решением для потребностей драйвера, помимо того, что будет плохим для вычислительной системы в целом. Если протестировать jitsched время работы load50, можно увидеть, что задержка, связанная с каждой строкой, будет увеличена на несколько секунд, потому что когда истекает время ожидания, процессор используют другие процессы.

Время ожидания

Условно-оптимальные циклы задержки, показанные до сих пор, работают, наблюдая за счётчиком тиков, ничего никому не сообщая. Но самым лучшим способом реализовать задержку, как вы можете себе представить, обычно является попросить ядро сделать это за вас. Существуют два способа создания основанных на тиках ожиданий (timeouts), в зависимости от того, ждёт ваш драйвер другого события или нет.

 

Если ваш драйвер использует очередь ожидания, чтобы дождаться какого-то другого события, но вы также хотите быть уверены, что она работает в течение определённого периода времени, можно использовать wait_event_timeout или wait_event_interruptible_timeout:

 

#include <linux/wait.h>

long wait_event_timeout(wait_queue_head_t q, condition, long timeout);

long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout);

 

Эти функции засыпают в данной очереди ожидания, но они возвращаются после истечения времени ожидания (в пересчёте на тики). Таким образом, они реализуют ограниченный сон, который не длится вечно. Обратите внимание, что время ожидания представляет собой число тиков ожидания, а не абсолютное значение времени. Значение представлено знаковым числом, потому что иногда это результат вычитания, хотя функции жалуются через оператор printk, если установленное время ожидания отрицательно. Если время ожидания истекает, функции возвращают 0; если процесс разбужен другим событием, он возвращает оставшуюся задержку выраженную в тиках. Возвращаемое значение не может быть отрицательным, даже если задержка больше, чем ожидалось из-за загрузки системы. (верно для wait_event_timeout, прерываемая версия вернёт -ERESTARTSYS)

 

Файл /proc/jitqueue показывает задержку на основе wait_event_interruptible_timeout, хотя модуль не ждёт никакого события и использует 0 в качестве условия:

 

wait_queue_head_t wait;

init_waitqueue_head (&wait);

wait_event_interruptible_timeout(wait, 0, delay);

 

Наблюдаемое поведение при чтении /proc/jitqueue почти оптимальное даже при нагрузке:

 

phon% dd bs=22 count=5 < /proc/jitqueue

   2027024    2028024

   2028025    2029025

   2029026    2030026

   2030027    2031027

   2031028    2032028

 

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

 

wait_event_timeout и wait_event_interruptible_timeout были разработаны имея в виду аппаратный драйвер, когда исполнение может быть возобновлено любым из двух способов: либо кто-то вызовет wake_up в очереди ожидания, или истечёт время ожидания. Это не применимо к jitqueue, так как для очереди ожидания никто не вызывает wake_up (в конце концов, другой код даже не знает об этом), поэтому процесс всегда просыпается по истечении времени задержки. Для удовлетворения этой самой ситуации, где вы хотите, чтобы задержка исполнения не ожидала каких-либо особых событий, ядро предлагает функцию schedule_timeout, так что вы можете избежать объявления и использования лишнего заголовка очереди ожидания:

 

#include <linux/sched.h>

signed long schedule_timeout(signed long timeout);

 

Здесь timeout - число тиков для задержки. Возвращается значение 0, если функция вернулась перед истечением данного времени ожидания (в ответ на сигнал). schedule_timeout требует, чтобы вызывающий сначала устанавливал текущее состояние процесса, поэтому типичный вызов выглядит следующим образом:

 

set_current_state(TASK_INTERRUPTIBLE);

schedule_timeout (delay);

 

Предыдущие строки (из /proc/jitschedto) заставляют процесс спать, пока заданное время не прошло. Так как wait_event_interruptible_timeout опирается внутри на schedule_timeout, мы не будем надоедать показом возвращаемых jitschedto чисел, потому что они такие же, как и для jitqueue. Опять же, следует отметить, что может получиться дополнительное временной интервал между истечением времени ожидания и началом фактического выполнения процесса.

 

В только что показанном примере первая строка вызывает set_current_state, чтобы настроить всё так, что планировщик не будет запускать текущий процесс снова, пока программа не поместит его обратно в состояние TASK_RUNNING. Для обеспечения непрерываемой задержки используйте взамен TASK_UNINTERRUPTIBLE. Если вы забыли изменить состояние текущего процесса, вызов schedule_timeout ведёт себя как вызов schedule (то есть ведёт себя как jitsched), устанавливая таймер, который не используется.

 

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

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

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

 

Функции ядра ndelay, udelay и mdelay хорошо обслуживают короткие задержки, задерживая исполнение на указанное число наносекунд, микросекунд или миллисекундах соответственно. (* u в udelay представляет греческую букву мю и используется как микро.) Их прототипы:

 

#include <linux/delay.h>

void ndelay(unsigned long nsecs);

void udelay(unsigned long usecs);

void mdelay(unsigned long msecs);

 

Фактическая реализация функций находится в <asm/delay.h>, являясь архитектурно-зависимой, и иногда построена на внешней функции. Каждая архитектура реализует udelay, но другие функции могут быть не определены; если их нет, <linux/delay.h> предлагает по умолчанию версию, основанную на udelay. Во всех случаях задержка достигает, по крайней мере, желаемого значения, но может быть больше; на деле, в настоящее время платформы не достигают точности до наносекунд, хотя некоторые из них предлагают субмикросекундную точность. Задержка более чем запрошенное значение, как правило, не проблема, так как небольшие задержки в драйвере обычно необходимы для ожидания оборудования и требуются ожидания по крайней мере заданного промежутка времени.

 

Реализация udelay (и, возможно, ndelay тоже) использует программный цикл на основе расчёта быстродействия процессора во время загрузки, используя целочисленную переменную loops_per_jiffy. Однако, если вы хотите посмотреть реальный код, необходимо учитывать, что реализация для x86 весьма сложна из-за разных исходных текстов, базирующихся на типе процессора, выполняющего код.

 

Чтобы избежать переполнения целого числа в расчётах цикла, udelay и ndelay ограничивают передаваемое им значение сверху. Если ваш модуль не может загрузиться и выводит неопределённый символ (unresolved symbol), __bad_udelay, это значит, вы вызвали udelay со слишком большим аргументом. Однако, следует отметить, что во время компиляции проверка может быть выполнена только на постоянные значения, и что это реализовано не на всех платформах. Как правило, если вы пытаетесь получить задержку на тысячи наносекунд, вы должны использовать udelay вместо ndelay; аналогично, задержки масштаба миллисекунд должны выполняться mdelay, а не одной из более точных функций.

 

Важно помнить, что эти три функции задержки являются ожидающими в состоянии занятости (busy-waiting); другие задачи не могут быть запущены в течение этого времени цикла. Таким образом, они повторяют, хотя и в другом масштабе, поведение jitbusy. Таким образом, эти функции должны быть использованы только когда нет другой альтернативы.

 

Существует и другой способ получения миллисекундных (и более) задержек, которые не выполняют ожидание в состоянии занятости. Файл <linux/delay.h> декларирует следующие функции:

 

void msleep(unsigned int millisecs);

unsigned long msleep_interruptible(unsigned int millisecs);

void ssleep(unsigned int seconds);

 

Первые две функции помещают вызывающий процесс в сон на заданное число millisecs. Вызов msleep является непрерываемым; вы можете быть уверены, что процесс спит по крайней мере заданное число миллисекунд. Если ваш драйвер сидит в очереди ожидания и вы хотите пробуждения для прерывания сна, используйте msleep_interruptible. Возвращаемое значение msleep_interruptible обычно 0; однако, если этот процесс проснулся раньше, возвращаемое значение является числом миллисекунд, оставшихся от первоначально запрошенного периода сна. Вызов ssleep помещает процесс в непрерываемый сон на заданное число секунд.

 

В общем, если вы можете мириться с задержками больше, чем запросили, вы должны использовать schedule_timeout, msleep или ssleep.

Предыдущая  Содержание  Следующая