Содержание


Обслуживание периферии в коде модулей ядра: Часть 56. Обработка прерываний

Comments

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

Два подхода к организации обмена данными

Обмен данными с периферийным оборудованием, по существу, может происходить только двумя способами:

  • циклическим программным опросом (пулингом);
  • по прерываниям от периферийного оборудования.

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

Системы подобного рода строятся на абсолютно тех же процессорах общего применения, но обмениваются данными с многочисленной периферией не по прерываниям, а методами циклического программного опроса (пулинга), часто с периодом опроса миллисекундного диапазона или даже ниже. Не взирая на некоторую обособленность этой ветви развития, она занимает (в финансовых объёмах) весьма существенную часть компьютерной индустрии, где преуспели такие производители как Schneider Electric, Siemens и др. Примечательно, что целый ряд известных моделей PLC может работать и под операционной системой Linux, но работа с данными в них основывается на совершенно иных принципах. Эта отрасль заслуживает упоминания, но находится далеко от основной темы нашей статьи, и поэтому к особенностям её реализации мы возвращаться не будем.

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

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

  1. это параллельная ветвь выполнения (относительно прерванной ветви);
  2. эта параллельная ветвь принципиально асинхронная.

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

Свою долю сложности привносит и то, что алгоритм обработки аппаратных прерываний — это принципиально архитектурно-зависимое действие, определяемое архитектурой процессорной платформы и связанное с непосредственным взаимодействием с контроллером прерываний. Аппаратная архитектура отображается в ядре Linux и тем, что всё современное ядро пронизано направленностью на многопроцессорность (SMP - Symmetric MultiProcessing — симметричное мультипроцессирование). Операции, выполняемые на единственном процессоре, рассматриваются только как тривиальный, вырожденный случай SMP.

Наконец, только в самых примитивных или ранних операционных системах (например, MS-DOS) все действия по обработке прерываний выполняются непосредственно по возникновению аппаратного прерывания. Во всех развитых системах по поступлению аппаратного прерывания выполняется только критически необходимая часть действий — это так называемая "верхняя половина" обработчика прерываний, выполняемая при запрещённых на это время всех последующих прерываниях (для локального процессора в SMP). В так называемую "нижнюю половину" обработчика откладываются все остальные действия по обработке поступившего прерывания, которые будут выполнены операционной системой в более подходящий момент. Такая схема, вносящая дополнительный параллелизм и планирование выполнения, также усложняет задачу, стоящую перед программистом, реализующим подсистему обработки прерываний.

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

Модель обработки прерывания

Алгоритм обработки аппаратных прерываний — это принципиально архитектурно-зависимое действие, связанное с непосредственным взаимодействием с контроллером прерываний. Функции из Linux API для работы с прерываниями пытаются нивелировать архитектурную зависимость, вводя логическую модель обработки прерываний, в которой аппаратно-зависимые элементы взаимодействия берёт на себя ядро, но разорвать аппаратную зависимость до конца невозможно. Далее мы рассмотрим схемы работы применительно к архитектуре x86, как к наиболее распространённой, но базовые принципы останутся верными и для других архитектур.

В ранних операционных системах схема обработки прерываний была проще. Например, для системы MS-DOS для процессоров x86 и "старого" контроллера прерываний (чип 8259) её можно было бы описать в виде "псевдо-ассемблера", как показано ниже:

  1. после возникновения аппаратного прерывания управление асинхронно получает ISR-функция (ISR —Interrupt SubRoutine, обработчик прерывания, написанный разработчиком), адрес которой записан в векторе (вентиле) прерывания.
  2. функция-обработчик выполняет обработку прерывания при глобально запрещённых последующих прерываниях.
  3. после завершения обработки прерывания функция-обработчик восстанавливает контроллер прерываний (чип 8259), посылая сигнал EOI о завершении прерывания.
  4. функция-обработчик завершается, возвращая управление командой iret (а не ret, как все прочие привычные нам функции, вызываемые синхронно!).

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

  1. Для линии IRQ регистрируется функция-обработчик "верхней половины" (это уже упоминавшаяся "верхняя половина" обработчика), которая выполняется при запрещённых прерываниях локального процессора. Именно этой функции и передаётся управление при возникновении аппаратного прерывания.
  2. Синтаксически функция-обработчик должна иметь строго описанный функциональный тип irq_handler_t и возвращать управление ядру системы традиционным return, с возвращаемым значением IRQ_NONE или IRQ_HANDLED.
  3. При возникновении аппаратного прерывания по линии IRQ функция-обработчик получает управление. Эта функция выполняется в контексте прерывания— это одно из самых важных ограничений, накладываемых Linux, и мы не раз будем возвращаться к нему. Перед своим завершением функция-обработчик регистрирует для последующего выполнения функцию-обработчик "нижней половины", которая и завершит позже начатую работу по обработке этого прерывания.
  4. В этой точке (после return из обработчика "верхней половины") ядро завершает всё взаимодействие с аппаратурой контроллера прерываний, разрешает последующие прерывания, восстанавливает контроллер командой завершения обработки прерывания (EOI) и возвращает управление из прерывания уже именно командой iret. После этого восстанавливается контекст прерванного процесса (потока).
  5. А вот запланированная выше к выполнению функция "нижней половины" будет вызвана ядром в некоторый момент позже (часто это может быть и непосредственно после завершения return из верхней половины, но это непредсказуемо), тогда, когда удобнее будет ядру системы. Принципиально важное отличие функции "нижней половины" состоит в том, что она выполняется уже при разрешённых прерываниях.

Исторически в Linux сменилось несколько разнообразных API для реализации этой схемы, а сами названия "верхняя половина" и "нижняя половина" — это названия рудиментарных механизмов одной из старых схем, которая к данному моменту уже удалена из ядра. С тех пор, как ядро Linux стало вытесняемым (preemptive, а это произошло в версиях 2.5 и далее), все новые схемы реализации обработчиков "нижней половины", которые будут рассматриваться далее, построены на организации обработчика "нижней половины" в виде отдельного потока ядра, хотя альтернативные варианты этого механизма могут различаться.

Означает ли это, что обработка прерывания в Linux всегда должна выполняться в два этапа? Нет, не означает, но это предпочтительная и оптимальная схема. Обработка прерывания вполне может быть завершена в функции "верхней половины", и её завершающий оператор return будет последним действием по обработке прерывания в случаях, когда:

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

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

  1. Пользовательский процесс (главный поток пользовательского приложения, представленный функцией main());
  2. Отдельный поток (POSIX pthread_t) в многопоточном пользовательском процессе;
  3. Linux — операционная система с вытеснением в ядре. Поэтому в ядре существуют автономные потоки ядра, представляющие третью категорию.

Не взирая на различия в предназначении, выполняемые единицы всех этих категорий представляются в ядре одинаковой структурой задачиstruct task_struct, объявленной в файле <linux/sched.h>. Более того, для диспетчирования ядром все они включены в единую систему приоритетных циклических очередей. Структура struct task_struct— это очень крупная структура, содержащая полный набор текущих параметров состояния задачи, например, когда задача прерывается (совершенно не обязательно асинхронно и аппаратным прерыванием — просто у задачи может истечь выделенный ей квант времени, и она будет прервана диспетчером ядра). Таким образом, struct task_struct содержит полный необходимый и достаточный набор всех параметров, чтобы приостановленную задачу активировать в сколь угодно отдалённом будущем.

При возникновении аппаратного прерывания текущая задача прерывается, её контекст сохраняется, и управление передаётся ISR-функции "верхней половины". Но для выполняющегося ISR-кода не создаётся экземпляра struct task_struct. Такое выполнение называют контекстом прерывания. Поэтому стоит ISR-функции вызвать любую другую функцию API ядра, вызывающую блокирование вызывающего кода (начиная с такой безобидной паузы как msleep()), и управление будет передано коду какой-то из других задач, и уже никогда не возвратится обратно, так как у ISR-функции нет контекста задачи, в который можно возвратиться. И произойдёт подобное не только при прямом вызове таких блокирующих функций API, но и при их вызове в сколь угодно длинной последовательной цепочке вызовов (некоторые из которых могут быть написаны другим автором). А последствием такого неосмотрительного действия программиста станет общий крах ядра системы. В этом состоит, пожалуй, главная сложность обработки прерываний в модуле ядра: непрерывное отслеживание того, в каком контексте отрабатывает созданный вами код.

Заключение

В данной статье была представлена общая схема обработки аппаратных прерываний в ОС Linux. Для реализации обработчиков прерываний в коде остаётся только последовательно разобрать все перечисленные шаги описанной схемы. Этому подробному разбору и будут посвящены нескольких последующих частей цикла "Разработка модулей ядра Linux".


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=931594
ArticleTitle=Обслуживание периферии в коде модулей ядра: Часть 56. Обработка прерываний
publish-date=05282013