Выбрать главу

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

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

Реализация системы обработки прерываний

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

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

Рис. 6.1. Прохождение запроса на прерывание в аппаратном обеспечении и в ядре

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

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

Функция do_IRQ() определена следующим образом.

unsigned int do_IRQ(struct pt_regs regs);

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

int irq = regs.orig_eax & 0xff;

После вычисления значения номера линии прерывания, функция do_IRQ() отправляет уведомление о получении прерывания и запрещает доставку прерываний с данной линии. Для обычных машин платформы PC, эти действия выполняются с помощью функции mask_and_ack_8295A(), которую вызывает функция do_IRQ(). Далее функция do_IRQ() выполняет проверку, что для данной линии прерывания зарегистрирован правильный обработчик прерывания, что этот обработчик разрешен и что он не выполняется в данный момент. Если все эти условия выполнены, то вызывается функция handle_IRQ_event(), которая выполняет установленные для данной линии обработчики прерывания. Для аппаратной платформы x86 функция handle_IRQ_event() имеет следующий вид.

int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,

 struct irqaction *action) {

 int status = 1;

 if (!(action->flags & SA_INTERRUPT))

  local_irq_enable();

 do {

  status != action->flags;

  action->chandler(irq, action->dev_id, regs);

  action = action->next;

 } while (action);

 if (status & SA_SAMPLE_RANDOM)

  add_interrupt_randomness(irq);

 local_irq_disable();

 return status;

}

Так как процессор запретил прерывания, они снова разрешаются, если не указан флаг SA_INTERRUPT при регистрации обработчика. Вспомним, что флаг SA_INTERRUPT указывает, что обработчик должен выполняться при всех запрещенных прерываниях. Далее в цикле вызываются все потенциальные обработчики прерываний. Если эта линия не является совместно используемой, то цикл заканчивается после первой итерации. В противном случае вызываются все обработчики. После этого вызывается функция add_interrupt_randomness(), если при регистрации указан флаг SA_SAMPLE_RANDOM. Данная функция использует временные характеристики прерывания, чтобы сгенерировать значение энтропии для генератора случайных чисел. В приложении Б, "Генератор случайных чисел ядра", приведена более подробная информация о генераторе случайных чисел ядра.

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

Функция ret_from_intr(), так же как и код входа, написана на языке ассемблера. Эта функция проверяет, есть ли ожидающий запрос на перепланирование выполнения процессов (следует вспомнить главу 4, "Планирование выполнения процессов", и флаг need_resched). Если есть запрос на перепланирование и ядро должно передать управление в пространство пользователя (т.е. прерывание прервало работу пользовательского процесса), то вызывается функция schedule(). Если возврат производится в пространство ядра (т.е. прерывание прервало работу кода ядра), то функция schedule() вызывается, только если значение счетчика preempt_count равно нулю (в противном случае небезопасно производить вытеснение кода ядра), После возврата из функции schedule() или если нет никакой ожидающей работы, восстанавливаются первоначальные значения регистров процессора и ядро продолжает работу там, где оно было прервано.

Для платформы x86, подпрограммы, написанные на языке ассемблера, находятся в файле arch/i386/kernel/entry.S, а соответствующие функции на языке С — в файле arch/i386/kernel/irq.с. Для других поддерживаемых аппаратных платформ имеются аналогичные файлы.