Таймеры ядра

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

Если необходимо запланировать действия на позднее время без блокирования текущего процесса до наступления момента времени, инструментом для вас являются таймеры ядра. Такие таймеры используются для планирования выполнения функции в определённое время в будущем, основываясь на тактовых тиках, и могут использоваться для различных задач; например, опрос устройства путём проверки его состояния через регулярные промежутки времени, когда оборудование не может генерировать прерывания. Другим типичным использованием таймеров ядра является отключение двигателя дисковода или завершение другой длительной операции выключения. В таких случаях задержка возвращения из close создала бы ненужные (и неожиданные) трудности для прикладной программы. Наконец, само ядро использует таймеры в ряде ситуаций, включая реализацию schedule_timeout.

 

Таймер ядра является структурой данных, которая инструктирует ядро для выполнения заданных пользователем функции с заданным пользователем аргументом в заданное пользователем время. Реализация находится в <linux/timer.h> и kernel/timer.c и подробно описана в разделе "Реализация таймеров ядра".

 

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

 

Это асинхронное выполнение напоминает то, что происходит, когда происходит аппаратное прерывание (которое подробно рассматривается в Главе 10). В самом деле, таймеры ядра работают как результат "программного прерывания". При запуске в атомарном контексте этого рода на ваш код налагается ряд ограничений. Функции таймера должны быть атомарными всеми способами, которые мы обсуждали в разделе "Спин-блокировки и контекст атомарности" в Главе 5, но есть некоторые дополнительные вопросы, вызванные отсутствием контекста процесса. Теперь вы введём эти ограничения; они будут рассматриваться снова в нескольких местах в последующих главах. Повторение делается потому, что правила для атомарных контекстов должны усердно соблюдаться, или система окажется в тяжёлом положении. Некоторые действия требуют для выполнения контекст процесса. Когда вы находитесь за пределами контекста процесса (то есть в контексте прерывания), вы должны соблюдать следующие правила:

 

Не разрешён доступ к пользовательскому пространству. Из-за отсутствия контекста процесса, нет пути к пользовательскому пространству, связанному с любым определённым процессом.

Указатель current не имеет смысла в атомарном режиме и не может быть использован, так как соответствующий код не имеет связи с процессом, который был прерван.

Не может быть выполнено засыпание или переключение. Атомарный код не может вызвать schedule или какую-то из форм wait_event и не может вызвать любые другие функции, которые могли бы заснуть. Например, вызов kmalloc(..., GFP_KERNEL) идёт против правил. Семафоры также не должны быть использованы, поскольку они могут спать.

 

Код ядра может понять, работает ли он в контексте прерывания, вызовом функции in_interrupt( ), которая не имеет параметров и возвращает ненулевое значение, если процессор в настоящее время работает в контексте прерывания, аппаратного или программного. Функцией, связанной с in_interrupt() является in_atomic(). Она возвращает ненулевое значение, когда переключение не допускается; это включает в себя аппаратный и программный контексты прерывания, а также любое время, когда удерживается спин-блокировка. В последнем случае, current может быть действительным, но доступ к пользовательскому пространству запрещён, поскольку это может привести к переключению. Всякий раз, когда вы используете in_interrupt(), следует всерьёз рассмотреть вопрос, не является ли in_atomic() тем, что вы действительно имеете в виду. Обе функции объявлены в <asm/hardirq.h>.

 

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

 

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

 

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

API таймера

Ядро предоставляет драйверам ряд функций для декларации, регистрации и удаления таймеров ядра. Ниже приводится выдержка, показывающая основные стандартные блоки:

 

#include <linux/timer.h>

struct timer_list {

    /* ... */

    unsigned long expires;

    void (*function)(unsigned long);

    unsigned long data;

};

 

void init_timer(struct timer_list *timer);

struct timer_list TIMER_INITIALIZER(_function, _expires, _data);

 

void add_timer(struct timer_list * timer);

int del_timer(struct timer_list * timer);

 

Структура данных включает в себя больше полей, чем показано, но эти три предназначены для доступа снаружи кодом таймера. Поле expires представляет значение jiffies, которое таймер ожидает для запуска; в это время функция function вызывается с data в качестве аргумента. Если необходимо передать много объектов в аргументе, можно собрать их в единую структуру данных и передать указатель, приведя к unsigned long, это безопасная практика на всех поддерживаемых архитектурах и довольно распространена в управлении памятью (как описывается в Главе 15). Значение expires не является типом jiffies_64, поскольку не ожидается, что таймер сработает очень далеко в будущем и на 32-х разрядных платформах 64-х разрядные операции медленны.

 

Структуры должны быть проинициализированы перед использованием. Этот шаг гарантирует, что все поля правильно настроены, в том числе те, которые не видимы для вызывающего. Инициализация может быть осуществлена вызовом init_timer или присвоением TIMER_INITIALIZER статической структуре, в соответствии с вашими потребностями. После инициализации, перед вызовом add_timer, можно изменить три открытых поля. Чтобы отключить зарегистрированный таймер до его истечения, вызовите del_timer. Модуль jit включает файл примера, /proc/jitimer (для "только по таймеру"), который возвращает строку заголовка и шесть строк данных. Строки данных показывают текущее окружение, где выполняется код; первая создаётся файловой операцией read, остальные - по таймеру. Следующий вывод был записан во время компиляции ядра:

 

phon% cat /proc/jitimer

  time    delta inirq  pid   cpu command

 33565837   0     0    1269   0   cat

 33565847  10     1    1271   0   sh

 33565857  10     1    1273   0   cpp0

 33565867  10     1    1273   0   cpp0

 33565877  10     1    1274   0   cc1

 33565887  10     1    1274   0   cc1

 

В этом выводе, поле time является значением jiffies, когда код запускается, delta является изменением jiffies относительно предыдущей строки, inirq - это логическое значение, возвращаемое in_interrupt, pid и command относятся к текущему процессу и cpu является номером используемого процессора (всегда 0 на однопроцессорных системах).

 

Если вы прочитаете /proc/jitimer при выгрузке системы, вы обнаружите, что контекстом таймера является процесс 0, задача простоя (idle), которая называется “swapper” ("планировщик своппинга") в основном по историческим причинам.

 

Таймер используется для генерации данных /proc/jitimer по умолчанию каждые 10 тиков, но при загрузке модуля можно изменить значение, установив параметр tdelay (timer delay, задержка таймера).

 

Следующий отрывок кода показывает часть jit, связанную с таймером jitimer. Когда процесс пытается прочитать наш файл, мы устанавливаем таймер следующим образом:

 

unsigned long j = jiffies;

 

/* заполняем данные для нашей таймерной функции */

data->prevjiffies = j;

data->buf = buf2;

data->loops = JIT_ASYNC_LOOPS;

 

/* регистрируем таймер */

data->timer.data = (unsigned long)data;

data->timer.function = jit_timer_fn;

data->timer.expires = j + tdelay; /* параметр */

add_timer(&data->timer);

 

/* ждём заполнения буфера */

wait_event_interruptible(data->wait, !data->loops);

 

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

 

void jit_timer_fn(unsigned long arg)

{

    struct jit_data *data = (struct jit_data *)arg;

    unsigned long j = jiffies;

    data->buf += sprintf(data->buf, "%9li %3li %i %6i %i %s\n",

                            j, j - data->prevjiffies, in_interrupt( ) ? 1 : 0,

                            current->pid, smp_processor_id( ), current->comm);

 

    if (--data->loops) {

        data->timer.expires += tdelay;

        data->prevjiffies = j;

        add_timer(&data->timer);

    } else {

        wake_up_interruptible(&data->wait);

    }

}

 

API таймера включает в себя несколько больше функций, чем те, которые введены выше. Следующий набор завершает список предложения ядра:

 

int mod_timer(struct timer_list *timer, unsigned long expires);

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

 

int del_timer_sync(struct timer_list *timer);

Работает как del_timer, но также гарантирует, что когда она вернётся, таймерная функция не запущена на каком-то процессоре. del_timer_sync используется, чтобы избежать состояний состязания на многопроцессорных системах, и аналогична del_timer на однопроцессорных ядрах. Этой функции в большинстве ситуаций следует отдавать предпочтение перед del_timer. Эта функция может заснуть, если вызывается из неатомарного контекста, но находится в активном ожидании в других ситуациях. Будьте очень осторожны вызывая del_timer_sync при удержании блокировок; если таймерная функция попытается получить ту же блокировку, система может заблокироваться. Если эта таймерная функция перерегистрирует себя, вызывающий должен сначала убедиться, что эта перерегистрация не произошла; обычно это достигается установкой флага “shutting down” ("выключить"), который проверяется таймерной функцией.

 

int timer_pending(const struct timer_list * timer);

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

Реализация таймеров ядра

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

 

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

 

Управление таймером должно быть как можно более лёгким.

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

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

Таймер должен работать на том же процессоре, который его зарегистрировал.

 

Решение, разработанное разработчиками ядра, основано на копии структуры данных для каждого процессора. Структура timer_list включает в себя указатель на такую структуру данных в своём поле base. Если base является NULL, таймер не запланирован для запуска; в противном случае, указатель говорит, какая структура данных (и, следовательно, какой процессор) запускает его. Копии объектов данных для каждого процессора описываются в разделе "Копии переменных для процессора" в Главе 8.

 

Всякий раз, когда код ядра регистрирует таймер (через add_timer или mod_timer), операции в конечном итоге выполняются internal_add_timerkernel/timer.c), которая, в свою очередь, добавляет новый таймер в двусвязный список таймеров в рамках "каскадной таблицы", связанной с текущим процессором.

 

Каскадные таблицы работают так: если таймер истекает в следующих тиках от 0 до 255, он добавляется в один из 256 списков, посвященных таймерам малого диапазона, используя самые младшие биты поля expires. Если он истекает дальше в будущем (но до 16384 тиков), он добавляется в один из 64 списков на основе битов 9-14 поля expires. Для таймеров истекающих ещё позже, тот же приём используется для битов 15-20, 21-26 и 27-31. Таймеры с полем времени окончания ещё дальше в будущем (что может случиться только на 64-х разрядных платформах) делятся на задержки со значением 0xffffffff и таймеры с expires в прошлом планируются для запуска в следующем тике таймера. (Таймер, который уже истёк, иногда может быть зарегистрирован в ситуациях высокой нагрузки, особенно если вы работаете с вытесняющим ядром.)

 

После запуска __run_timers, он запускает все отложенные таймеры на текущий тик таймера. Если jiffies в настоящее время является кратной 256, функция также заново делит один из списков таймеров следующего уровня на 256 списков короткого диапазона, возможно каскадируя один или нескольких других уровней также в соответствии с битовым представлением jiffies.

 

Это подход, хотя и чрезвычайно сложный на первый взгляд, выполняется очень хорошо как с несколькими таймерами, так и с большим их числом. Время, необходимое для управления каждым активным таймером, не зависит от количества уже зарегистрированных таймеров и ограничено несколькими логическими операциями над двоичным представлением поля expires. Накладным расходом, связанным с этой реализацией, является память для 512 заголовков листов (256 краткосрочных списков и 4 группы из 64 списков более длительных диапазонов), то есть 4 Кб памяти.

 

Функция __run_timers, как показано /proc/jitimer, будет запущена в атомарном контексте. В добавок к уже описанным ограничениям, это приносит интересную особенность: таймер истекает только в заданное время, даже если вы не работаете на вытесняющем ядре и процессор занят в пространстве ядра. Вы можете видеть, что происходит, когда вы читаете /proc/jitbusy в фоновом режиме и /proc/jitimer с высоким приоритетом. Хотя система кажется прочно заблокированной системным вызовом с активным ожиданием, таймеры ядра всё же работают нормально.

 

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

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