Разработка ядра Linux

Лав Роберт

Глава 10

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

 

 

Отслеживание хода времени очень важно для ядра. Большое количество функций, которые выполняет ядро, управляются временем (time driven), в отличие от тех функций, которые выполняются по событиям (event driven). Некоторые из этих функций выполняются периодически, как, например, балансировка очередей выполнения планировщика или обновление содержимого экрана. Такие функции вызываются в соответствии с постоянным планом, например 100 раз в секунду. Другие функции, такие как отложенные дисковые операции ввода-вывода, ядро планирует на выполнение в некоторый относительный момент времени в будущем. Например, ядро может запланировать работу на выполнение в момент времени, который наступит позже текущего на 500 миллисекунд. Наконец, ядро должно вычислять время работы системы (uptime), а также текущую дату и время.

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

Также следует обратить внимание на отличия между событиями, которые возникают периодически, и событиями, которые ядро планирует на выполнение в некоторый фиксированный момент времени в будущем. События, которые возникают периодически, скажем каждые 10 миллисекунд, управляются системным, таймером. Системный таймер — это программируемое аппаратное устройство, которое генерирует аппаратное прерывание с фиксированной частотой. Обработчик этого прерывания, который называется прерыванием таймера (timer interrupt), обновляет значение системного времени и выполняет периодические действия. Системный таймер и его прерывание являются важными для работы операционной системы Linux, и в текущей главе им уделяется главное внимание.

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

 

Информация о времени в ядре

 

Концепция времени для компьютера является несколько неопределенной. В действительности, для того чтобы получать информацию о времени и управлять системным временем, ядро должно взаимодействовать с системным аппаратным обеспечением. Аппаратное обеспечение предоставляет системный таймер, который используется ядром для измерения времени. Системный таймер работает от электронного эталона времени, такого как цифровые электронные часы или тактовый генератор процессора. Интервал времени системного таймера периодически истекает (еще говорят таймер срабатывает — hitting, popping) с определенной запрограммированной частотой. Эта частота называется частотой импульсов таймера, (tick rate). Когда срабатывает системный таймер, он генерирует прерывание, которое ядро обрабатывает с помощью специального обработчика прерывания.

Так как в ядре есть информация о запрограммированной частоте следования импульсов таймера, ядро может вычислить интервал времени между двумя успешными прерываниями таймера. Этот интервал называется временной отметкой или импульсом таймера (tick) и в секундах равен единице, деленной на частоту импульсов. Как будет показано дальше, именно таким способом ядро отслеживает абсолютное время (wall time) и время работы системы (uptime). Абсолютное время— это фактическое время дня, которое наиболее важно для пользовательских приложений. Ядро отслеживает это время просто потому, что оно контролирует прерывание таймера. В ядре есть семейство системных вызовов, которое позволяет пользовательским приложениям получать информацию о дате и времени дня. Это необходимо, так как многие программы должны иметь информацию о ходе времени. Разница между двумя значениями времени работы системы — "сейчас" и "позже" — это простой способ измерения относительности событий.

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

• Обновление значения времени работы системы (uptime).

• Обновление значения абсолютного времени (time of day).

• Для SMP-систем выполняется проверка балансировки очередей выполнения планировщика, и если они не сбалансированы, то их необходимо сбалансировать (как было рассказано в главе 4, "Планирование выполнения процессов").

• Проверка, не израсходовал ли текущий процесс свой квант времени, и если израсходовал, то выполнятся планирование выполнения нового процесса (как это было рассказано в главе 4).

• Выполнение обработчиков всех динамических таймеров, для которых истек период времени.

• Обновление статистики по использованию процессорного времени и других ресурсов.

Некоторые из этих действий выполняются при каждом прерывании таймера, т.е. эта работа выполняется с частотой системного таймера. Другие действия также выполняются периодически, но только через каждые n прерываний системного таймера. Иными словами, эти функции выполняются с частотой, которая равна некоторой доле частоты системного таймера. В разделе "Обработчик прерываний таймера" будет рассмотрена сама функция обработки прерываний системного таймера.

 

Частота импульсов таймера:

HZ

Частота системного таймера (частота импульсов, tick rate) программируется при загрузке системы на основании параметра ядра НZ, который определен с помощью директивы препроцессора. Значение параметра HZ отличается для различных поддерживаемых аппаратных платформ. На самом деле, для некоторых аппаратных платформ значение параметра HZ отличается даже для разных типов машин.

Данный параметр ядра определен в файле . Частота системного таймера равна значению параметра HZ, период таймера равен 1/HZ. Например, в файле include/asm-i386/param.h для аппаратной платформы i386 этот параметр определен следующим образом.

#define HZ 1000 /* internal kernel time frequency */

Поэтому для аппаратной платформы i386 прерывание таймера генерируется с частотой 1000 Гц, т.е. 1000 раз в секунду (каждую тысячную долю секунды или одну миллисекунду). Для большинства других аппаратных платформ значение частоты системного таймера равно 100 Гц. В табл. 10.1 приведен полный список всех поддерживаемых аппаратных платформ и определенных для них значений частоты системного таймера.

Таблица 10.1. Значение частоты системного таймера

Аппаратная платформа Частота (в герцах)
alpha 1024
arm 100
cris 100
h8300 100
i386 1000
ia64 32 или 1024 [54]
m68k 100
m68knommu 50, 100 или 1000
mips 100
mips64 100
parisc 100 или 1000
ppc 100
ppc64 1000
s390 100
sh 100
spare 100
sparc64 100
um 100
v850 24, 100 или 122
x86-64 1000

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

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

 

Идеальное значение параметра

HZ

Для аппаратной платформы i386, начиная с самых первых версий операционной системы Linux, значение частоты системного таймера было равно 100 Гц. Однако во время разработки ядер серии 2.5 это значение было увеличено до 1000 Гц, что (как всегда бывает в подобных ситуациях) вызвало споры. Так как в системе очень многое зависит от прерывания таймера, то изменение значения частоты системного таймера должно оказывать сильное влияние на систему. Конечно, как в случае больших, так и в случае маленьких значений параметра HZ есть свои положительные и отрицательные стороны.

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

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

• Увеличивается точность выполнения событий во времени.

Разрешающая способность увеличивается во столько же раз, во сколько раз возрастает частота импульсов. Например, гранулярность таймеров при частоте импульсов 100 Гц равна 10 миллисекунд. Другими словами, все периодические события выполняются прерыванием таймера, которое генерируется с предельной точностью по времени, равной 10 миллисекунд, и большая точность не гарантируется. При частоте, равной 1000 Гц, разрешающая способность равна 1 миллисекунде, т.е. в 10 раз выше. Хотя ядро позволяет создавать таймеры с временным разрешением, равным 1 миллисекунде, однако при частоте системного таймера в 100 Гц нет возможности гарантированно получить временной интервал, короче 10 миллисекунд.

Точность измерения времени также возрастает аналогичным образом. Допустим, что таймеры ядра запускаются в случайные моменты времени, тогда в среднем таймеры будут срабатывать с точностью по времени до половины периода прерывания таймера, потому что период времени таймера может закончиться в любой момент, а обработчик таймера может выполниться, только когда генерируется прерывание таймера. Например, при частоте 100 Гц описанные события в среднем будут возникать с точностью ±5 миллисекунд от желаемого момента времени. Поэтому ошибка измерения в среднем составит 5 миллисекунд. При частоте 1000 Гц ошибка измерения в среднем уменьшается до 0.5 миллисекунд — получает десятикратное улучшение.

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

• Таймеры ядра выполняются с большим разрешением и с лучшей точностью (это позволяет получить много разных улучшений, некоторые из которых описаны дальше).

• Системные вызовы, такие как poll() и select(), которые позволяют при желании использовать время ожидания (timeout) в качестве параметра, выполняются с большей точностью.

• Измерения, такие как учет использования ресурсов или измерения времени работы системы, выполняются с большей точностью.

• Вытеснение процессов выполняется более правильно.

Некоторые из наиболее заметных улучшений производительности — это улучшения точности измерения периодов времени ожидания при выполнении системных вызовов poll() и select(). Это улучшение может быть достаточно большим. Прикладная программа, которая интенсивно использует эти системные вызовы, может тратить достаточно много времени, ожидая на прерывания таймера, хотя в действительности интервал времени ожидания уже истек. Следует вспомнить, что средняя ошибка измерения времени (т.е. потенциально зря потраченное время) равна половине периода прерывания таймера.

Еще одно преимущество более высокой частоты следования импульсов таймера — это более правильное вытеснение процессов, что проявляется в уменьшении задержки за счет планирования выполнения процессов. Вспомним из материала главы 4, что прерывание таймера ответственно за уменьшение кванта времени выполняющегося процесса. Когда это значение уменьшается до нуля, устанавливается флаг need_resched, и ядро активизирует планировщик как только появляется такая возможность. Теперь рассмотрим ситуацию, когда процесс в данный момент выполняется и у него остался квант времени, равный 2 миллисекундам. Это означает, что через 2 миллисекунды планировщик должен вытеснить этот процесс и запустить на выполнение другой процесс. К сожалению, это событие не может произойти до того момента, пока не будет сгенерировано следующее прерывание таймера. В самом худшем случае следующее прерывание таймера может возникнуть через 1/HZ секунд! В случае, когда параметр HZ=100, процесс может получить порядка 10 лишних миллисекунд. Конечно, в конце концов все будет сбалансировано и равнодоступность ресурсов не нарушится, потому что все задания планируются с одинаковыми ошибками, и проблема состоит не в этом. Проблемы возникают из-за латентности, которую вносят задержки вытеснения процессов. Если задание, которое планируется на выполнение, должно выполнить какие-нибудь чувствительные ко времени действия, как, например, заполнить буфер аудиоустройства, то задержка не допустима. Увеличение частоты до 1000 Гц уменьшает задержку планировщика в худшем случае до 1 миллисекунды, а в среднем — до 0.5 миллисекунды.

Должна, однако, существовать и обратная сторона увеличения частоты системного таймера, иначе она была бы с самого начала равна 1000 Гц (или даже больше). На самом деле существует одна большая проблема. Более высокая частота вызывает более частые прерывания таймера, что означает большие накладные затраты. Чем выше частота, тем больше времени процессор должен тратить на выполнение прерываний таймера. Это приводит не только к тому, что другим задачам отводится меньше процессорного времени, но и к периодическому трешингу (trashing) кэша процессора (т.е. кэш заполняется данными, которые не используются процессором). Проблема, связанная с накладными расходами, вызывает споры. Ясно, что переход от значения HZ=100 до значения HZ=1000 в 10 раз увеличивает накладные затраты, связанные с прерываниями таймера. Однако от какого реального значения накладных затрат следует отталкиваться? Если "ничего" умножить на 10, то получится тоже "ничего". Решающее соглашение состоит в том, что по крайней мере для современных систем, значение параметра HZ=1000 не приводит к недопустимым накладным затратам. Тем не менее для ядер серии 2.6 существует возможность скомпилировать ядро с другим значением параметра HZ.

Возможна ли операционная система без периодических отметок времени

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

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

Когда обработчик таймера сработает, создается новый таймер для следующего события и так повторяется постоянно. При таком подходе не требуется периодическое прерывание таймера и нет необходимости в параметре HZ .

Однако при указанном подходе необходимо решить две проблемы. Первая проблема — это как в таком случае реализовать концепцию периодических отметок времени, хотя бы для того, чтобы ядро могло отслеживать относительные интервалы времени. Эту проблему решить не сложно. Вторая проблема — это как избежать накладных затрат, связанных с управлением динамическими таймерами, даже при наличии оптимизации. Данную проблему решить сложнее. Накладные расходы и сложность реализации получаются настолько высокими, что в операционной системе Linux такой подход решили не использовать. Тем не менее так пробовали делать, и результаты получаются интересными. Если интересно, то можно поискать в Интернет-архивах.

 

Переменная

jiffies

 

Глобальная переменная jiffies содержит количество импульсов системного таймера, которые были получены со времени загрузки системы. При загрузке ядро устанавливает значение этого параметра в нуль и он увеличивается на единицу при каждом прерывании системного таймера. Так как в секунду возникает HZ прерываний системного таймера, то за секунду значение переменной jiffies увеличивается на HZ. Время работы системы (uptime) поэтому равно jiffies/HZ секунд.

Этимология слова jiffy

Происхождение слова jiffy (миг, мгновение) точно неизвестно. Считается, что фразы типа " in a jiffy " (в одно мгновение) появились в Англии в восемнадцатом веке. В быту термин jiffy ( миг ) означает неопределенный, но очень короткий промежуток времени.

В научных приложениях слово jiffy используется для обозначения различных интервалов времени (обычно порядка 10 ms). В физике это слово иногда используется для указания интервала времени, который требуется свету, чтобы пройти определенное расстояние (обычно, фут, сантиметр, или расстояние, равное размеру нуклона).

В вычислительной технике термин jiffy — это обычно интервал времени между двумя соседними импульсами системного таймера, которые были успешно обработаны. В электричестве jiffy — период переменного тока. В США jiffy — это 1/60 секунды.

В приложении к операционным системам, в частности к Unix, jiffy  — это интервал времени между двумя соседними успешно обработанными импульсами системного таймера. Исторически это значение равно 100 ms. Как уже было показано, интервал времени jiffy в операционной системе Linux может иметь разные значения.

Переменная jiffies определена в файле следующим образом.

extern unsigned long volatile jiffies;

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

(секунды * HZ)

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

(jiffies / HZ)

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

unsigned long time_stamp = jiffies;    /* сейчас */

unsigned long next_tick = jiffies + 1; /* через один импульс таймера

                                          от текущего момента */

unsigned long later = jiffies + 5*HZ;  /* через пять секунд от текущего

                                          момента */

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

Заметим, что переменная jiffies имеет тип unsigned long и использовать какой-либо другой тип будет неправильным.

 

Внутреннее представление переменной

jiffies

Переменная jiffies исторически всегда представлялась с помощью типа unsigned long и, следовательно, имеет длину 32 бит для 32-разрядных аппаратных платформ и 64 бит для 64-разрядных. В случае 32-разрядного значения переменной jiffies и частоты появления временных отметок 100 раз в секунду, переполнение этой переменной будет происходить примерно каждые 497 дней, что является вполне возможным событием. Увеличение значения параметра HZ до 1000 уменьшает период переполнения до 49.7 дней! В случае 64-разрядного типа переменной jiffies, переполнение этой переменной невозможно за время существования чего-либо при любых возможных значениях параметра HZ для любой аппаратной платформы.

Из соображений производительности и по историческим причинам — в основном, для совместимости с уже существующим кодом ядра — разработчики ядра предпочли оставить тип переменной jiffies — unsigned long. Для решения проблемы пришлось немного подумать и применить возможности компоновщика.

Как уже говорилось, переменная jiffies определяется в следующем виде и имеет тип unsigned long.

extern unsigned long volatile jiffies;

Вторая переменная определяется в файле в следующем виде.

extern u64 jiffies_64;

Директивы компоновщика ld(1), которые используются для сборки главного образа ядра (для аппаратной платформы x86 описаны в файле arch/i386/kernel/vmlinux.lds.S), указывают компоновщику, что переменную jiffies необходимо совместить с началом переменной jiffies_64.

jiffies = jiffies_64;

Следовательно, переменная jiffies — это просто 32 младших разряда полной 64-разрядной переменной jiffies_64. Так как в большинстве случаев переменная jiffies используется для измерения промежутков времени, то для большей части кода существенными являются только младшие 32 бит.

В случае применения 64-разрядного значения, переполнение не может возникнуть за время существования чего-либо. В следующем разделе будут рассмотрены проблемы, связанные с переполнением (хотя переполнение счетчика импульсов системного таймера и не желательно, но это вполне нормальное и ожидаемое событие). Код, который используется для управления ходом времени, использует все 64 бит, и это предотвращает возможность переполнения 64-разрядного значения. На рис. 10.1 показана структура переменных jiffies и jiffies_64.

Рис. 10.1. Структура переменных jiffies и jiffies_64

Код, который использует переменную jiffies, просто получает доступ к тридцати двум младшим битам переменной jiffies_64. Функция get_jiffies_64() может быть использована для получения полного 64-разрядного значения. Такая необходимость возникает редко, следовательно большая часть кода просто продолжает считывать младшие 32 разряда непосредственно из переменной jiffies.

На 64-разрядных аппаратных платформах переменные jiffies_64 и jiffies просто совпадают. Код может либо непосредственно считывать значение переменной jiffies, либо использовать функцию get_jiffies_64(), так как оба этих способа позволяют получить аналогичный эффект.

 

Переполнение переменной

jiffies

Переменная jiffies, так же как и любое целое число языка программирования С, после достижения максимально возможного значения переполняется. Для 32-разрядного беззнакового целого числа максимальное значение равно 2³²- 1. Поэтому перед тем как счетчик импульсов системного таймера переполнится, должно прийти 4294967295 импульсов таймера. Если значение счетчика равно этому значению и счетчик увеличивается на 1, то значение счетчика становится равным нулю.

Рассмотрим пример переполнения.

unsigned long timeout = jiffies + HZ/2; /* значение лимита времени

                                           равно 0.5 с */

/* выполним некоторые действия и проверим, не слишком ли это много

   заняло времени ... */

if (timeout < jiffies) {

 /* мы превысили лимит времени — это ошибка ... */

} else {

 /* мы не превысили лимит времени — это хорошо ... */

}

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

В данном примере может возникнуть несколько потенциальных проблем, связанных с переполнением. Рассмотрим одну из них. Что произойдет, если переменная jiffies переполнится и снова начнет увеличиваться с нуля после того, как ей было присвоено значение переменной timeout? При этом условие гарантированно не выполнится, так как значение переменной jiffies будет меньше, чем значение переменной timeout, хотя логически оно должно быть больше. По идее значение переменной jiffies должно быть огромным числом, всегда большим значения переменной timeout. Так как эта переменная переполнилась, то теперь ее значение стало очень маленьким числом, которое, возможно, отличается от нуля на несколько импульсов таймера. Из-за переполнения результат выполнения оператора if меняется на противоположный!

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

#define time_after(unknown, known) ((long)(known) - (long)(unknown) < 0)

#define time_before(unknown, known) \

 ((long) (unknown) - (long) (known) < 0)

#define time_after_eq(unknown, known) \

 ((long)(unknown) - (long) (known) >= 0)

#define \

 time_before_eq(unknown, known) ((long)(known) - (long) (unknown) >= 0)

Параметр unknown — это обычно значение переменной jiffies, а параметр known — значение, с которым его необходимо сравнить.

Макрос time_after(unknown, known) возвращает значение true, если момент времени unknown происходит после момента времени known, в противном случае возвращается значение false. Макрос time_before(unknown, known) возвращает значение true, если момент времени unknown происходит раньше, чем момент времени known, в противном случае возвращается значение false. Последние два макроса работают аналогично первым двум, за исключением того, что возвращается значение "истинно", если оба параметра равны друг другу.

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

unsigned long timeout = jiffies + HZ/2; /* значение лимита времени

                                           равно 0.5 с */

/* выполним некоторые действия и проверим, не слишком ли это много

   заняло времени ... */

if (time_after(jiffies, timeout}) {

 /* мы превысили лимит времени — это ошибка ... */

} else {

 /* мы не превысили лимит времени — это хорошо ... */

}

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

 

Пространство пользователя и параметр

HZ

Раньше изменение параметра НZ приводило к аномалиям в пользовательских программах. Это происходило потому, что значения параметров, связанных со временем, экспортировались в пространство пользователя в единицах, равных количеству импульсов системного таймера в секунду. Так как такой интерфейс использовался давно, то в пользовательских приложениях считалось, что параметр HZ имеет определенное конкретное значение. Следовательно, при изменении значения параметра HZ изменялись значения, которые экспортируются в пространство пользователя, в одинаковое число раз. Информация о том, во сколько раз изменились значения, в пространство пользователя не передавалась! Полученное от ядра значение времени работы системы могло интерпретироваться как 20 часов, хотя на самом деле оно равнялось только двум часам.

Чтобы исправить это, код ядра должен нормировать все значения переменной jiffies, которые экспортируются в пространство пользователя. Нормировка реализуется путем определения константы USER_HZ, равной значению параметра HZ, которое ожидается в пространстве пользователя. Так как для аппаратной платформы x86 значение параметра HZ исторически равно 100, то значение константы USER_HZ=100. Макрос jiffies_to_clock_t() используется для нормировки значения счетчика импульсов системного таймера, выраженного в единицах HZ, в значение счетчика импульсов, выраженное в единицах USER_HZ. Используемый макрос зависит от того, кратны ли значения параметров HZ и USER_HZ один другому. Если кратны, то этот макрос имеет следующий очень простой вид.

#define jiffies_to_clock_t(x) ((x) / (HZ / USER_HZ))

Если не кратны, то используется более сложный алгоритм.

Функция jiffies_64_to_clock_t() используется для конвертирования 64-битового значения переменной jiffies из единиц HZ в единицы USER_HZ.

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

unsigned long start = jiffies;

unsigned long total_time;

/* выполнить некоторую работу ... */

total_time = jiffies - start;

printk("Это заняло %lu импульсов таймера\n",

 jiffies_to_clock_t(total_time));

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

printk("Это заняло %lu секунд\n", total time / HZ);

 

Аппаратные часы и таймеры

 

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

 

Часы реального времени

Часы реального времени (real-time clock, RTC) представляют собой энергонезависимое устройство для сохранения системного времени. Устройство RTC продолжает отслеживать время, даже когда система отключена, благодаря небольшой батарее, которая обычно находится на системной плате. Для аппаратной платформы PC устройство RTC интегрировано в КМОП-микросхему BIOS. При этом используется общая батарея и для работы устройства RTC и для сохранения установок BIOS.

При загрузке ядро считывает информацию из устройства RTC и использует ее для инициализации значения абсолютного времени, которое хранится в переменной xtime. Обычно ядро не считывает это значение снова, однако для некоторых поддерживаемых аппаратных платформ, таких как x86, значение абсолютного времени периодически записывается в устройство RTC. Тем не менее, часы реального времени важны в первую очередь на этапе загрузки системы, когда инициализируется переменная xtime.

 

Системный таймер

Системный таймер играет более значительную роль для отслеживания хода времени ядром. Независимо от аппаратной платформы, идея, которая лежит в основе системного таймера, одна и та же — это обеспечение механизма управления прерываниями, которые возникают периодически с постоянной частотой. Для некоторых аппаратных платформ это реализуется с помощью электронных часов, которые генерируют колебания с программируемой частотой. В других аппаратных платформах используется декрементный счетчик (decrementer), куда можно записать некоторое начальное значение, которое будет периодически, с фиксированной частотой, уменьшаться на единицу, пока значение счетчика не станет равным нулю. Когда значение счетчика становится равным нулю, генерируется прерывание. В любом случае эффект получается один и тот же.

Для аппаратной платформы x86 главный системный таймер — это программируемый интервальный таймер (programmable interval timer, PIT). Таймер PIT существует на всех машинах платформы PC. Co времен операционной системы DOS он используется для управления прерываниями. Ядро программирует таймер PIT при загрузке, для того чтобы периодически генерировать прерывание номер нуль с частотой HZ. Этот таймер— простое устройство с ограниченными возможностями, но, тем не менее, хорошо выполняющее свою работу. Другие эталоны времени для аппаратной платформы x86 включают таймер APIC (Advanced Programmable Interrupt Controller, расширенный программируемый контроллер прерываний) и счетчик отметок времени (TSC, Time Stamp Counter).

 

Обработчик прерываний таймера

Теперь, когда мы разобрались, что такое jiffies и HZ, а также какова роль системного таймера, рассмотрим реализацию обработчика прерываний системного таймера. Обработчик прерываний таймера разбит на две части: часть, зависимую от аппаратной платформы, и независимую часть.

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

• Захватывается блокировка xtime_lock, которая защищает доступ к переменной jiffies_64 и значению текущего времени— переменной xtime.

• Считывается или сбрасывается состояние системного таймера, если это необходимо.

• Периодически записывается новое значение абсолютного времени в часы реального времени.

• Вызывается аппаратно-независимая подпрограмма таймера do_timer().

Аппаратно-независимая функция do_timer() выполняет значительно больше действий.

• Увеличивается значение переменной jiffies_64 на единицу (это безопасная операция даже для 32-разрядных аппаратных платформ, так как блокировка xtime_lock была захвачена раньше).

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

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

• Вызывается функция scheduler_tick(), как было рассмотрено в главе 4.

• Обновляется значение абсолютного времени, которое хранится в переменной xtime.

• Вычисляются значения печально известной средней загруженности системы (load average).

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

void do_timer(struct pt_regs *regs) {

 jiffies_64++;

 update_process_times(user_mode(regs));

 update_times();

}

Макрос user_mode() просматривает состояние регистров процессора, regs, и возвращает значение 1, если прерывание таймера возникло в пространстве пользователя, и значение 0 — если в пространстве ядра. Это позволяет функции update_process_times() учесть, что за время между предыдущим и данным импульсами системного таймера процесс выполнялся в режиме задачи или в режиме ядра.

void update_process_times(int user_tick) {

 struct task_struct *p = current;

 int cpu = smp_processor_id();

 int system = user_tick ^ 1;

 update_one_process(p, user_tick, system, cpu);

 run_local_timers();

 scheduler_tick(user_tick, system);

}

Функция update_process() собственно обновляет значения параметров времени выполнения процесса. Эта функция тщательно продумана. Следует обратить внимание, каким образом с помощью операции исключающее ИЛИ (XOR) достигается, что одна из переменных user_tick и system имеет значение, равное нулю, а другая— единице. Поэтому в функции update_one_process() можно просто прибавить необходимое значение к соответствующим счетчикам без использования оператора ветвления.

/*

* увеличиваем значения соответствующего

* счетчика импульсов таймера на единицу

*/

p->utime += user;

p->stime += system;

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

Далее функция run_local_timers() помечает отложенные прерывания, как готовые к выполнению (см. главу 7, "Обработка нижних половин и отложенные действия"), для выполнения всех таймеров, для которых закончился период времени ожидания. Таймеры будут рассмотрены ниже, в разделе "Таймеры".

Наконец, функция schedule_tick() уменьшает значение кванта времени для текущего выполняющегося процесса и устанавливает флаг need_resched при необходимости. Для SMP-машин в этой функции также при необходимости выполняется балансировка очередей выполнения. Все это обсуждалось в главе 4.

После возврата из функции update_process_times() вызывается функция update_times(), которая обновляет значение абсолютного времени.

void update_times(void) {

 unsigned long ticks;

 ticks = jiffies - wall_jiffies;

 if (ticks) {

  wall_jiffies += ticks;

  update_wall_time(ticks);

 }

 last_time_offset = 0;

 calc_load(ticks);

}

Значение переменной ticks вычисляется как изменение количества импульсов системного таймера с момента последнего обновления абсолютного времени. В нормальной ситуации это значение, конечно, равно 1. В редких случаях прерывание таймера может быть пропущено, и в таком случае говорят, что импульсы таймера потеряны. Это может произойти, если прерывания запрещены в течение длительного времени. Такая ситуация не является нормальной и часто указывает на ошибку программного кода. Значение переменной wall_jiffies увеличивается на значение ticks, поэтому она равна значению переменной jiffies в момент самого последнего обновления абсолютного времени. Далее вызывается функция update_wall_time() для того, чтобы обновить значение переменной xtime, которая содержит значение абсолютного времени. Наконец вызывается функция calc_load() для того, чтобы обновить значение средней загруженности системы, после чего функция update_times() возвращает управление.

Функция do_timer() возвращается в аппаратно-зависимый обработчик прерывания, который выполняет все необходимые завершающие операции, освобождает блокировку xtime_lock и в конце концов возвращает управление.

Всё это происходит каждые 1/HZ секунд, т.е. 1000 раз в секунду на машине типа PC.

 

Абсолютное время

Текущее значение абсолютного времени (time of day, wall time, время дня) определено в файле kernel/timer.c следующим образом.

struct timespec xtime;

Структура данных timespec определена в файле в следующем виде.

struct timespec {

 time_t tv_sec; /* seconds */

 long tv_nsec; /* nanoseconds */

};

Поле xtime.tv_sec содержит количество секунд, которые прошли с 1 января 1970 года (UTC, Universal Coordinated Time, всеобщее скоординированное время). Указанная дата называется epoch (начало эпохи). В большинстве Unix-подобных операционных систем счет времени ведется с начала эпохи. В поле xtime.tv_nsec хранится количество наносекунд, которые прошли в последней секунде.

Чтение или запись переменной xtime требует захвата блокировки xtime_lock. Это блокировка — не обычная спин-блокировка, а секвентная блокировка, которая рассматривается в главе 9, "Средства синхронизации в ядре".

Для обновления значения переменной xtime необходимо захватить секвентную блокировку на запись следующим образом.

write_seqlock(&xtime_lock);

/* обновить значение переменной xtime ... */

write_sequnlock(&xtime_lock);

Считывание значения переменной xtime требует применения функций read_seqbegin() и read_seqretry() следующим образом.

do {

 unsigned long lost;

 seq = read_seqbegin(&xtime_lock);

 usec = timer->get_offset();

 lost = jiffies — wall_jiffies;

 if (lost)

  usec += lost * (1000000 / HZ);

 sec = xtime.tv_sec;

 usec += (xtime.tv_nsec / 1000);

} while (read_seqretry(&xtime_lock, seq));

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

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

asmlinkage long sys_gettimeofday(struct timeval *tv,

 struct timezone *tz) {

 if (likely(tv !=NULL)) {

  struct timeval_ktv;

  do_gettimeofday(&ktv);

  if (copy_to_userftv, &ktv, sizeof(ktv))

   return -EFAULT;

 }

 if (unlikely(tz != NULL)) {

  if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))

   return -EFAULT;

 }

 return 0;

}

Если из пространства пользователя передано ненулевое значение параметра tv, то вызывается аппаратно-зависимая функция do_gettimeofday(). Эта функция главным образом выполняет цикл считывания переменной xtime, который был только что рассмотрен. Аналогично, если параметр tz не равен нулю, пользователю возвращается значение часового пояса (time zone), в котором находится операционная система. Этот параметр хранится в переменной sys_tz. Если при копировании в пространство пользователя значения абсолютного времени или часового пояса возникли ошибки, то функция возвращает значение -EFAULT. В случае успеха возвращается нулевое значение.

Ядро предоставляет системный вызов time(), однако системный вызов gettimeofday() полностью перекрывает его возможности. Библиотека функций языка С также предоставляет другие функции, связанные с абсолютным временем, такие как ftime() и ctime().

Системный вызов settimeofday() позволяет установить абсолютное время в указанное значение. Для того чтобы его выполнить, процесс должен иметь возможность использования CAP_SYS_TIME.

Если не считать обновления переменной xtime, то ядро не так часто использует абсолютное время, как пространство пользователя. Одно важное исключение— это код файловых систем, который хранят в индексах файлов значения моментов времени доступа к файлам.

 

Таймеры

 

Таймеры (timers), или, как их еще иногда называют, динамические таймеры, или таймеры ядра, необходимы для управления ходом времени в ядре. Коду ядра часто необходимо откладывать выполнение некоторых функций на более позднее время. Здесь намеренно выбрано не очень четкое понятие "позже". Назначение механизма нижних половин — это не задерживать выполнение, а не выполнять работу прямо сейчас. В связи с этим необходим инструмент, который позволяет задержать выполнение работы на некоторый интервал времени. Если этот интервал времени не очень маленький, но и не очень большой, то решение проблемы — таймеры ядра.

Таймеры очень легко использовать. Необходимо выполнить некоторые начальные действия, указать момент времени окончания ожидания, указать функцию, которая будет выполнена, когда закончится интервал времени ожидания, и активизировать таймер. Указанная функция будет выполнена, когда закончится интервал времени таймера. Таймеры не являются циклическими. Когда заканчивается интервал времени ожидания, таймер ликвидируется. Это одна из причин, почему таймеры называют динамическими. Таймеры постоянно создаются и ликвидируются, на количество таймеров не существует ограничений. Использование таймеров очень популярно во всех частях ядра.

 

Использование таймеров

Таймеры представлены с помощью структур timer_list, которая определена в файле следующим образом.

struct timer_list {

 struct list_head entry; /* таймеры хранятся в связанном списке */

 unsigned long expires; /* время окончание срока ожидания в

                           импульсах системного таймера (jiffies) */

 spinlock_t lock; /* блокировка для защиты данного таймера */

 void (*function)(unsigned long); /*функция-обработчик таймера */

 unsigned long data; /* единственный аргумент обработчика */

 struct tvec_t_base_s *base; /* внутренние данные таймера, не трогать! */

};

К счастью, использование таймеров не требует глубокого понимания назначения полей этой структуры. На самом деле, крайне не рекомендуется использовать поля этой структуры не по назначению, чтобы сохранить совместимость с возможными будущими изменениями кода. Ядро предоставляет семейство интерфейсов для работы с таймерами, чтобы упростить эту работу. Все необходимые определения находятся в файле . Большинство реализаций находится в файле kernel/timers.

Первый шаг в создании таймера — это его объявление в следующем виде.

struct timer_list my_timer;

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

init_timer(&my_timer);

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

my_timer.expires = jiffies + delay; /* интервал времени таймера

                                       закончится через delay импульсов */

my_timer.data = 0; /* в функцию-обработчик будет передан параметр,

                       равный нулю */

my_timer.function = my_function; /* функция, которая будет выполнена,

                            когда интервал времени таймера истечет */

Значение поля my_timer.expires указывает время ожидания в импульсах системного таймера (необходимо указывать абсолютное количество импульсов). Когда текущее значение переменной jiffies становится большим или равным значению поля my_timer.expires, вызывается функция-обработчик my_timer.function с параметром my_timer.data. Как видно из описания структуры timer_list, функция-обработчик должна соответствовать следующему прототипу.

void my_timer_function(unsigned long data);

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

Последняя операция — это активизация таймера.

add_timer(&my_timer);

И таймер запускается! Следует обратить внимание на важность значения поля expired. Ядро выполняет обработчик, когда текущее значение счетчика импульсов системного таймера больше, чем указанное значение времени срабатывания таймера, или равно ему. Хотя ядро и гарантирует, что никакой обработчик таймера не будет выполняться до истечения срока ожидания таймера, тем не менее возможны задержки с выполнением обработчика таймера. Обычно обработчики таймеров выполняются в момент времени, близкий к моменту времени срабатывания, однако они могут быть отложены и до следующего импульса системного таймера. Следовательно, таймеры нельзя использовать для работы в жестком режиме реального времени.

Иногда может потребоваться изменить момент времени срабатывания таймера, который уже активизирован. В ядре реализована функция mod_timer(), которая позволяет изменить момент времени срабатывания активного таймера.

mod_timer(&my_timer, jiffies + new_delay); /* установка нового времени

                                              срабатывания */

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

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

del_timer(&my_timer);

Эта функция работает как с активными, так и неактивными таймерами. Если таймер уже неактивен, то функция возвращает значение 0, в другом случае возвращается значение 1. Следует обратить внимание, что нет необходимости вызывать эту функцию для таймеров, интервал ожидания которых истек, так как они автоматически деактивизируются.

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

del_timer_sync(&my_timer);

В отличие от функции del_timer(), функция del_timer_sync() не может вызываться из контекста прерывания.

 

Состояния конкуренции, связанные с таймерами

Так как таймеры выполняются асинхронно по отношению к выполняемому в данный момент коду, то потенциально могут возникнуть несколько типов состояний конкуренции за ресурсы. Во-первых, никогда нельзя использовать следующий код, как замену функции mod_timer().

del_timer(my_timer);

my_timer->expires = jiffies + new_delay;

add_timer(my_timer);

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

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

 

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

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

void run_local_timers(void) {

 raise_softirq(TIMER_SOFTIRQ);

}

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

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

 

Задержка выполнения

 

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

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

 

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

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

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

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

while (time_before(jiffies, delay))

 ;

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

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

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

while (time_before(jiffies, delay))

 ;

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

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

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

unsigned long delay = jiffies + 5*HZ;

while (time_before(jiffies, delay))

 cond_resched();

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

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

 

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

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

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

void udelay(unsigned long usecs);

void mdelay(unsigned long msecs);

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

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

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

Мой 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 .

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

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

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

 

Функция

schedule_timeout()

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

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

set_current_state(TASK_INTERRUPTIBLE);

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

schedule_timeout(s * HZ);

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

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

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

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);

 timeout = expire - jiffies;

out:

 return timeout < 0 ? 0 : timeout;

}

Эта функция создает таймер timer и устанавливает время срабатывания в значение timeout импульсов системного таймера в будущем. В качестве обработчика таймера устанавливается функция process_timeout(), которая вызывается, когда истекает период времени таймера. Далее таймер активизируется, и вызывается функция schedule(). Так как предполагается, что текущее задание находится в состоянии TASK_INTERRUPTIBLE или TASK_UNINTERRUPTIBLE, то планировщик не будет выполнять текущее задание, а выберет для выполнения другой процесс.

Когда интервал времени таймера истекает, то вызывается функция process_timeout(), которая имеет следующий вид.

void process_timeout(unsigned long data) {

 wake_up_process((task_t*)data);

}

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

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

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

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

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

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

 

Время вышло

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

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

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