Билет 11 - honeycarbs/bmstu-os-6sem GitHub Wiki

Аппаратные прерывания в Linux: запрос прерывания и линии IRQ. Простейшая схема аппаратной поддержки прерываний (концептуальная трех шинная архитектура системы). Быстрые и медленные прерывания, пример быстрого прерывания, флаги. Нижняя и верхняя половины обработчиков прерываний: регистрация обработчика аппаратного прерывания, функция регистрации и ее параметры. Нижние половины: softirq, tasklet, work queue — особенности реализации и выполнения в SMP-системах. Примеры, связанные с планированием отложенных действий (лаб. раб.)

Запрос прерывания и линии IRQ

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

Простейшая схема аппаратной поддержки прерываний

irq

Прерывание от устройства вводв-вывода поступает, когда оно завершило процесс вводв-вывода. Процессор освобождается от проверки флагов и может переключиться на другую работу. Метод требует включения в состав ОС контроллера прерываний. Контроллер прерываний посылает по шине управления сигнал. В конце выполнения каждой команды процессор проверяет входной сигнал с шины управления (если прерывания не замаскированы в ОС). Если получен сигнал – посылается ответный сигнал контроллеру прерываний, в ответ контроллер прерываний формирует и посылает вектор прерывания. Вектор передается по шине данных. Полученный вектор используется для процедуры обработки прерывания.

Процесс с точки зрения контроллера:

  1. Контроллер получает от ЦП команду (пр., read)
  2. Контроллер переходит к считыванию данных со своего устройства. Эти данные записываются в регистры контроллера.
  3. Посылается сигнал прерывания.
  4. После получения вектора ЦП‐ом, контроллер посылает по шине данных данные из своих регистров.

Процесс с точки зрения процессора:

  1. Процессор генерирует команду read.
  2. ЦП выполняет вызов библиотечной функции, который переводит процессор в режим ядра, выполняющийся процесс блокируется, процессор переходит на выполнение др. работы. 
  3. В конце каждого цикла команд процессор проверяет наличие прерываний.
  4. При поступлении прерывания ЦП сохраняет контекст выполненяемой задачи и переходит к выполнению п/п обработки прерывания.
  5. При этом процессор читает слова из контроллера ввода-вывода и пересылает их в память (через свои регистры). 
  6. Восстановление контекста.

Быстрые и медленные прерывания, пример быстрого прерывания, флаги

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

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

#define IRQF_SHARED		0x00000080 /*разрешить разделение линии IRQ несколькими устройствами*/
#define IRQF_PROBE_SHARED	0x00000100 /*устанавливается вызывающими, когда они ожидают, что произойдет несовпадение при обмене*/
#define __IRQF_TIMER		0x00000200 /* прерывание помечается как прерывание по таймеру.*/
#define IRQF_PERCPU		0x00000400 /*прерывание устанавливается на процессор*/
#define IRQF_NOBALANCING	0x00000800 /*флаг, чтобы исключить это прерывание из балансировки irq*/
#define IRQF_IRQPOLL		0x00001000 /*прерывание используется для опроса (только соображение производительности, которое зарегистрировано первым в общем прерывании, рассматривается)*/
#define IRQF_ONESHOT		0x00002000 /*Прерывание не включается после завершения работы обработчика hardirq. Используется потоковыми прерываниями, которые должны держать линию irq отключенной до тех пор, пока не будет запущен потоковый обработчик*/
#define IRQF_NO_SUSPEND		0x00004000 /*не отключайте этот IRQ во время приостановки. Не гарантирует, что это прерывание выведет систему из приостановленного состояния. См. Documentation / power / suspend-and-interrupts.txt*/
#define IRQF_FORCE_RESUME	0x00008000 /* принудительно включить его при возобновлении, даже если установлен IRQF_NO_SUSPEND*/
#define IRQF_NO_THREAD		0x00010000 /*Прерывание не может быть связано*/
#define IRQF_EARLY_RESUME	0x00020000 /* Возобновить IRQ на ранней стадии во время syscore, а не во время возобновления работы устройства*/
#define IRQF_COND_SUSPEND	0x00040000 /*если IRQ используется совместно с пользователем NO_SUSPEND, запустите этот обработчик прерываний после приостановки прерываний. Для системных устройств пробуждения пользователи должны реализовать обнаружение пробуждения в своих обработчиках прерываний*/

Нижняя и верхняя половины обработчиков прерываний

После инициирования прерывания его обработчик должен выполняться быстро, чтобы не прерывать текущую активность на длительное время. Обработчики аппаратных прерываний выполняются на очень высоком уровне приоритета и блокируют возникновение других запросов прерываний. Не все задачи по обработке прерывания можно выполнить за несколько инструкций и это приводит к необходимости отложить продолжительную работу и выполнять ее вне контекста IRQ драйвера устройства. Именно поэтому в ОС существует система «softIRQ» - отложенных прерываний. Чтобы сократить время выполнения обработчиков прерываний обработчики медленных аппаратных прерываний делятся на две части, которые называются верхней и нижней половинами. Верхними половинами остаются обработчики, устанавливаемые функцией request_irq() на определенных IRQ. Выполнение нижних половин инициируется обработчиками прерываний.

перед завершением обработчик АП, который наз. top half инициализирует выполнение своей нижней половины (bottom half), отложенного действия. После завершения выполнения обрабочика АП (т.е. когда выполнена команда return), завершается взаимодействие с контроллером прерываний, т.е. код аппаратного прерывания выполнен, восстанавливаются локальные прерывания (т.е. разрешаются) на том процессоре, на котором выполнялся обратчик АП, восстанавливается старая маска прерываний (команда iret).

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

В современных ОС Linux имеется три типа нижних половин:

  • softirq — отложенные прерывания;
  • tascklet — тасклеты;
  • workqueue — очереди работ.

Функция request_irq() предназначена для регистрации обработчика прерывания на определенной линии прерывания.

request_irq(
	unsigned int irq,
	irqreturn_t (*handler)(int, void *, struct pt_regs *),
	unsigned long irqflags,
	const char *devname,
	void *dev_id
);

Обработчики прерываний в драйверах устройств отвечают за взаимодействие с внешними устройствами на этапе передачи данных от устройств. Драйвер устройства регистрирует в системе один обработчик прерывания. Делается это с помощью функции request_irq(), которой в качестве параметра передается указатель на обработчик, обслуживающий конкретное прерывание: **irqreturn_t (handler)(int, void ). Эта функция будет вызвана при возникновении конкретного прерывания в системе. Прототип обработчика принимает три параметра и возвращает значение типа irqreturn_t. Тип irqreturn_t определяется следующим образом:

enum irqreturn {
	IRQ_NONE		= (0 << 0),
	IRQ_HANDLED		= (1 << 0),
	IRQ_WAKE_THREAD		= (1 << 1),
};

Первым параметром функции request_irq() является unsigned int irq, который определяет номер прерывания. Для некоторых устройств, например унаследованных (legacy) PC устройств таких, как таймер или клавиатура, это значение обычно жестко определено. Для большинства других устройств эта величина подбирается или назначается динамически и программно. Функция вызывается всякий раз, когда возникает прерывание с соответствующим значением irq.

Третий параметр – irqflags – может быть или нулем, или битовой маской одного или нескольких следующих флагов, которые используются только ядром как часть IRQ-обработчиков (флаги были указаны выше).

Четвертый параметр – devname – имя устройства, связанного с прерыванием. Например, для клавиатуры это - "keyboard". Это имя используется в /proc/irq и /proc/interrupt.

Пятый параметр – dev_id – используется для разделения линий прерывания. Когда обработчик прерывания освобождается, dev_id предоставляет уникальный файл, позволяющий удалить с линии irq соответствующий обработчик прерывания. Без данного файла не будет известно, какой обработчик удалять с линии прерывания. Можно установить значение NULL, если линия прерывания не разделена.

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

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

void free_irq(unsigned int irq, void *dev_id); 

Softirq

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

struct softirq_action
{
	/* ф-я которая должна выполняться */
	void(*action)(struct softirq_action*);
};

Когда ядро выполняет обработчик отложенного прерывания типа softirq, функция action вызывается с указателем на структуру softirq_action в качестве параметра. Количество типов softirq (имеет числовой индекс) статически определено в системе. Существует 10 обработчиков.

const char * const softirq_to_name[NR_SOFTIRQS] = {
	"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
	"TASKLET", "SCHED", "HRTIMER", "RCU"
};

/proc/softirqs - здесь можно увидеть эту информацию, т.е. все перечисленные имена. Определена функция, которая заполняет вектор softirq_vec заданным типом softirq_action:

void open_softirq(int nr, void (*action)(struct softirq_action*))
{
	softirq_vec[nr].action = action;
}

Для того, чтобы зарегистрированное отложенное прерывание было поставлено в очередь на выполнение, необходимо вызывать функцию raise_softirq. Добавить новый уровень softirq можно только путем перекомпиляции ядра. Т.е. число обработчиков softirq не может быть изменено динамически. При этом смысл имеет только новое softirq, имеющее индекс на 1 меньше индекса tasklet, т.к. нет смысла переопределять кол-во softirq, т.к. можно использовать тасклет. Из перечисления видно, что тасклет является одним из типов softirq.

Проверка ожидающих выполнения обработчиков отложенных действий типа softirq выполняется в следующих случаях:

  1. При возврате из аппаратного прерывания.
  2. В контексте потока ядра ksoftirqd.
  3. В любом коде ядра, в котором явно проверяются и запускаются ожидающие выполнения обработчики softirq (например, как это делается в сетевой подсистеме).

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

Tasklet

Tasklet - частный случай реализации softirq, но в отличие от softirq, которые пишутся таким образом, чтобы одновременно в системе могло выполняться некоторое количество одних и тех же softirq (в полном объеме реализовано взаимоисключение), на tasklet накладывается следующее ограничение: обработчик тасклета в каждый конкретный момент времени может выполняться только на одном процессоре, т.е. один и тот же тасклет не может выполняться параллельно, в отличие от softirq. Поэтому в системах, в которых требуется быстрая реакция, предпочтение отдается softirq. В отличие от softirq, которые регистрируются при компиляции системы и их количество определено в системе, асклеты могут быть зарегистрированы как статически, так и динамически.

#ifndef DECLARE_TASKLET_OLD
#define DECLARE_TASKLET_OLD(arg1, arg2) DECLARE_TASKLET(arg1, arg2, 0L)
#endif

DECLARE_TASKLET_OLD(my_tasklet, my_tasklet_handler);

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

Свойства тасклетов:

  • Если вызывается функция tasklet_schedule(), то тасклет гарантировано будет выполняться на каком-то процессоре, хотя бы один раз после этого.
  • Если тасклет уже запланирован, но его выполнение еще не началось, то он обязательно будет один раз выполнен через какое-то время.
  • Если тасклет уже выполняется на другом процессоре или планирование тасклета вызвано из самого кода тасклета, то его перепланирование будет отложено.
  • Если один и от же тасклет выполняется несколько раз (он может даже сам себя запланировать), то для монопольного доступа к разделяемым данным необходимо использовать спинлоки.
  • В системе имеется определенная на тасклетах функция блокировки: tasklet_trylock().

Workqeue (Очереди работ)

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

В системе определен worker - поток ядра (work thread). В системе имеется worker pull - множество worker, один worker может принадлежать одному pull.

Посредник, ответственный за установку отношений между workqueue и worker pull.

Существенные отличия между тасклетами и очередями работ:

  1. Тасклеты выполянются в констексте прерывания, в результате чего код тасклета должен быть неделимым(atomic). В отличие от тасклетов, workqueue выполняются в контексте специальных потоков ядра, и как результат имеют большую свободу действий, и в частности очереди работ могут блокироваться или засыпать.
  2. Тасклеты всегда выполняются на процессоре, на котором выполнялось АП, запланировавшее данный тасклет. Очереди по умолчанию также выполняются на том же процесосоре, но могут выполняться и на других процессорах.
  3. Код ядра требует, чтобы выполнение функций очередей работ откладывалось на определенный интервал времени.
  4. Ключевое отличие - тасклеты выполняются за короткий период времени после того, как были запланированы, а очереди работ имеют значительно большие задержки и необязательно должны быть атомарными, неделимыми.
struct workqueue_struct {
	struct list_head	pwqs;
	struct list_head	list;

	struct mutex		mutex;
    ...

    struct rcu_head		rcu;
    ...
}

struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

List_head - список всех очередей работ, но на конкретное CPU существует свой список rcu_head, чтобы не перебирать все очереди работ.
Так же как для тасклетов, работу (задачу как отложенное действие) можно поместить в очередь работ как статически, так и динамически.

Если имеется хоть какая-то возможность, что структура уже инициализирована, лучше вместо init_work() использовать prepare_work(). Для создания очереди работ, до 2.6.36, использоваласть create_workqueue(), в более современных версиях - alloc_workqueue(). Flags определяет как очередь работ будет выполняться, max_active - ограничивает число задач, которые одновременно будут стоять в очереди к cpu.

Примеры

Тасклет

#ifndef DECLARE_TASKLET_OLD
#define DECLARE_TASKLET_OLD(arg1, arg2) DECLARE_TASKLET(arg1, arg2, 0L)
#endif

void my_tasklet_handler(unsigned long data);
static irqreturn_t my_interrupt_handler(int irq, void *dev_id);

DECLARE_TASKLET_OLD(my_tasklet, my_tasklet_handler);

static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
    printk(KERN_INFO "== Called my_interrupt_handler\n");
    if (irq == 1)
    {
        tasklet_schedule(&my_tasklet);
        printk_tasklet_info("In interrupt handler");
        printk(KERN_INFO "== Tasklet scheduled\n");
        return IRQ_HANDLED;
    }
    else
        return IRQ_NONE;
}

...

static int __init module_init(void)
{
    request_irq(IRQ_NUM, my_interrupt_handler, IRQF_SHARED,
                "my_interrupt_tasklet", &my_dev_id)
    ....
}

static void __exit module_exit(void)
{
    tasklet_kill(&my_tasklet);
    free_irq(IRQ_NUM, &my_dev_id);
}

Очередь работ

#define IRQ_NAME "myirq"
#define WQ_NAME "workqueue"

static struct workqueue_struct *my_wq;

irqreturn_t my_irq_handler(int irq_num, void *dev_id)
{
    if (irq_num == 1)
    {
        if (work_1)
            queue_work(my_wq, (struct work_struct *)work_1);
        if (work_2)
            queue_work(my_wq, (struct work_struct *)work_2);

        return IRQ_HANDLED;
    }
    return IRQ_NONE;
}

typedef struct
{
    struct work_struct work;
    int work_num;
} my_work_t;

static my_work_t *work_1;
static my_work_t *work_2;

oid my_bottom_half(struct work_struct *work)
{
    /* работа, которую выполняют work-и */
}

irqreturn_t my_irq_handler(int irq_num, void *dev_id)
{
    if (irq_num == 1)
    {
        if (work_1)
            queue_work(my_wq, (struct work_struct *)work_1);
        if (work_2)
            queue_work(my_wq, (struct work_struct *)work_2);

        return IRQ_HANDLED;
    }
    return IRQ_NONE;
}

static int __init my_module_init(void)
{

    request_irq(IRQ_NUM, my_irq_handler, IRQF_SHARED, IRQ_NAME,     
                my_irq_handler)

    work_1 = (my_work_t *)kmalloc(sizeof(my_work_t), GFP_KERNEL);
    work_2 = (my_work_t *)kmalloc(sizeof(my_work_t), GFP_KERNEL);

    INIT_WORK((struct work_struct *)work_1, my_bottom_half);
    work_1->work_num = 1;

    INIT_WORK((struct work_struct *)work_2, my_bottom_half);
    work_2->work_num = 2;


    my_wq = create_workqueue(WQ_NAME);
    ...

    return 0;
}

static void __exit my_module_exit(void)
{
    free_irq(IRQ_NUM, my_irq_handler);

    flush_workqueue(my_wq);
    destroy_workqueue(my_wq);

    ...
    
    kfree(work_1);
    kfree(work_2);
}