Билет 02 - honeycarbs/bmstu-os-6sem GitHub Wiki
Управление внешними устройствами: специальные файлы устройств, идентификация внешних устройств в системе (тип dev_t), символьные и блочные устройства и их inode (структуры, описывающие символьные и блочные устройства). Система прерываний: типы прерываний и их особенности. Быстрые и медленные прерывания. Обработчики аппаратных прерываний: регистрация в системе, примеры. Верхние и нижние половины обработчиков прерываний. Нижние половины: тасклеты и очереди работ — объявление, создание, постановка работы в очередь, планирование (пример лаб. раб).
Управление внешними устройствами
Cпециальные файлы устройств
Специальные файлы устройств обеспечивают единообразный доступ к внешнему устройству, или переферии. Эти файлы обеспечивают связь файловой системы и драйверов. В отличие от обычных файлов, специальные файлы устройств в действительности являются только указателями на соответствующие драйверы устройств в ядре. Такая интерпретация специальных файлов обеспечивает доступ к внешнему устройству как к обычному файлу. Так же как и обычный файл, специальный файл устройства может быть открыт, закрыт, в него можно писать и из него можно читать.
Каждому внешнему устройству unix\linux стваит в соответствие как минимум один специальный файл. Обычно эти файлы можно увидеть в каталоге /dev корневой файловой системы. Подкаталог /dev/fd содержит файлы с именами 0,1,2, но в некоторых системах имеются файлы с именами /dev/stdin, /dev/stdout, /dev/stderr.
Система поддерживает 2 типа специальных файлов устройств:
- блочный(буферизуемый) - внешние запоминающие устройства;
- символьный(небуферизуемый) - все остальные.
В unix\linux связь имени специального файла с конкретным внешним устройством обеспечивает inode (индексный идентификатор). Взаимодействие прикладных программ с аппаратной частью компа под управлением ОС unix\linux осуществяется по следующей схеме:
УСТРОЙСТВО <--> ЯДРО --> ДРАЙВЕР <--> VFS <--> ПРИЛОЖЕНИЕ
Драйвер - Это часть кода ядра, которая предназначена для упраления конкретным внешним устройством. Драйверы в любой системе пишутся по правилам этой стсиемы на основании структур ядра, определенных в системе. Такие структуры перечисляют точки входа в драйвер и другие параметры. Задача драйвера - управлять внешним устройством. Драйвер должен преобразовывать данные поступающие от устройства и поступающие в устройство. Если драйверу нужно передать данные устройству, ему нужно передать их в формате, определенном на устройстве. Формат данных, полученных от устройства должен быть преобразован в формат, понятный приложению.
Драйверы бывают трех видов:
- встроенные - выполнение которых инициализируется при запуске системы. (IDE, материнская плата, последовательные\переллельные порты);
- драйвера, реализованные как загружаемые модули ядра (часто используется для упарвления такими устройствами как SCSI, звуковые, сетевые карты). Располагаются в /lib/modules. При инсатляции системы указывается перечент модулей, который будет подключатся на этапе загрузки. Список - в /etc/modules/. /etc/modules/modules.conf - перечень опций для таких модулей. Подключепние и работа: lsmod, insmod, rmmod, modprobe(автоматически загружает модули)
- Код, поделенный между ядром и специальной утилитой.
Идентификация внешних устройств
Для идентификации внешних спейиальных файлов устройств имеется соответсвующая система идентификации. Старший и младший(дополнительный) номера устройств - major, minor. Общий подход, как символьных так и блочных устройств.
Посмотреть старшие и младшие номера устройств -
ls -l
.
Старший и младший номера идентифицируют драйвер, который связан с устройством.
Соверменная ОС linux позволяет множеству драйверов разделять старшие номера. Но большинство устройств, которые можно видеть в /dev все еще огранизованы по принципу один старший номер на один драйвер. Младшие номера определяют конкретное устройство.
dev_t - внутреннее представление драйверов устройств.
Стандарт POSIX.1 определяет существование этого типа, но не оговаривает формат полей. Тип
dev_t
, определён в
<linux/types.h>
. Начиная с версии ядра 2.6.0, dev_t является 32-х разрядным, в котором 12 бит отведены для старшего номера и 20 - для младшего.
Код драйвера не должен интерпертировать эти значения. Он должен их использовать.
<linux\kdev_t.h>
C
MAJOR(dev_t dev);
MINOR(dev_t dev);
Если имеются старший и младший номера, то возможно обратное действие:
C
MKDEV(int major, int minor);
Одно из первых действий, которое драйвер должен сделать, когда устанавливается символьное устройство - получить один и более номеров устройств.
TODO: адресация внешних устройств (45.00 9 лек)
Система прерываний
Типы прерываний и их особенности
Основой работы ОС является система прерываний. В монолитном ядре все построено на прерываниях.
Классификация:
- Системные вызовы (аппаратные) - вызываются искусственно с помощью соответствующей команды из программы, предназначены для выполнения некоторых действий ОС (фактически запрос на услуги ОС), является синхронным событием.
- Аппаратные - возникают как реакция микропроцессора на физический сигнал от некоторого устройства (клавиатура, мышь и т. д.), по времени возникновения эти прерывания асинхронны, т. е. происходят в случайные моменты времени.
- От таймера
- От действий оператора
- От устройств ввода/вывода (посылается сигнал о завершении процесса вв/выв на контроллер прерываний).
- Исключения — являются реакцией микропроцессора на нестандартную ситуацию, возникшую внутри микропроцессора во время выполнения некоторой команды программы (деление на ноль, прерывание по флагу TF(трассировка)), являются синхронным событием.
- Исправимые - приводят к вызову определенного менеджера системы, в результате работы которого может быть продолжена работа процесса (пр. страничная неудача с менеджером памяти).
- Неисправимые — в случае сбоя или в случае ошибки программы (пр. ошибка адресации). В этом случае процесс завершается.
Аппаратные прерывания предназначены для информирования процессора. Идея аппаратных прерываний: процесс должен быть проинформирован о завершении ввода-вывода. Поскольку процессор выполняет какую-то другую работу, его работа организована следующим образом: В цикле выполнения каждой команды процессор проверяет наличие сигнала прерывания на своей выделенной ножке. Если сигнал прерывания пришел, то процессор переходит на обработку этого прерывания. Из 1 семестра: лабораторная работа по защищенному режиму. Прерывание от устройства ввода/вывода поступает, когда устройство завершило процесс ввода/вывода. Процессор освобождается от проверки флагов и может переключиться на другую работу. Метод требует включения в состав ОС контроллера прерываний. Контроллер прерываний посылает по шине управления сигнал. В конце выполнения каждой команды процессор проверяет входной сигнал с шины управления (если прерывания не замаскированы в ОС). Если получен сигнал - посылается ответный сигнал контроллеру прерываний, в ответ контроллер прерываний формирует и посылает вектор прерывания. Вектор передается по шине данных. Полученный вектор используется для процедуры обработки прерывания.
Прерывания в последовательности ввода-вывода

Быстрые и медленные прерывания
В современных linux-системах к быстрым прерываниям относятся только прерывания от системного таймера. Все осталные являются медленными. В версии 2.6.19 все флаги, связанные с прерываниями были радикально изменены. В старых версиях флаги имели приставку SA. В частности, SA_INTERRUPT (быстрые прерывания) в новых версиях заменен на флаг IRQF_TIMER.
Когда выполняются аппаратные прерывания, никакая другая работа не может быть выполнена. SMP (Симметричная многопроцессорность) - архитектура внесла в этот тезис некоторые изменения. В ней имеются равноправные процессоры, которые работают с общей памятью. Поскольку процессоров несколько и они выполняют работу параллельно, возникает ситуация: на процессоре, который выполняет обработчик возникшего прерывания, запрещены все прерывания. Для остальных процессоров запрещены прерывания по этой линии IRQ.
Обработчики аппаратных прерываний должны завершаться как можно быстрее. Если они будут выполняться длительное время, то это скажется на отзывчивости системы (быстродействии). Поэтому обработчики АП выполняют минимально необходимый набор действий. Поэтому обработчики прерываний делятся на две части: верхняя и нижняя половина. Аппаратное прерывание, которым является top half, также инициализирует выполнение отложенных действий для того чтобы система могла завершить обработку ввода-вывода.
В современном unix-linux различается 3 типа нижних половин:
- softirq — гибкие прерывания;
- tasklet;
- 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),
};
Например:
static irqreturn_t intr_handler (int irq, void *dev) {
if(! /* проверка того, что обслуживаемое устройство запросило прерывание*/)
return IRQ_NONE;
/* код обслуживания устройства */
return IRQ_HANDLED;
}
Первым параметром функции request_irq() является unsigned int irq, который определяет номер прерывания. Для некоторых устройств, например унаследованных (legacy) PC устройств таких, как таймер или клавиатура, это значение обычно жестко определено. Для большинства других устройств эта величина подбирается или назначается динамически и программно. Функция вызывается всякий раз, когда возникает прерывание с соответствующим значением irq. Третий параметр – irqflags – может быть или нулем, или битовой маской одного или нескольких следующих флагов, которые используются только ядром как часть IRQ-обработчиков:
#define IRQF_SHARED 0x00000080 /* разрешить разделение линии IRQ несколькими устройствами */
#define __IRQF_TIMER 0x00000200 /* прерывание маркируется как прерывание по таймеру.*/
#define IRQF_PROBE_SHARED 0x00000100 /* устанавливается вызывающим, когда он предполагает возможные проблемы с совместным использованием. */
devname – имя устройства, связанного с прерыванием. Например, для клавиатуры это - "keyboard". Это имя используется в /proc/irq и /proc/interrupt.
dev_id – используется для разделения линий прерывания. Когда обработчик прерывания освобождается, dev_id предоставляет уникальный файл, позволяющий удалить с линии irq соответствующий обработчик прерывания. Без данного файла не будет известно, какой обработчик удалять с линии прерывания. Можно установить значение NULL, если линия прерывания не разделена.
Важно отметить, что функция request_irq() может блокироваться и поэтому не может быть вызвана из контекста прерывания.
Когда драйвер выгружается, необходимо отменить регистрацию соответствующего обработчика прерывания. Для этого существует функция:
void free_irq(unsigned int irq, void *dev_id);
Если указанная линия прерывания не является разделяемой, то функция free_irq() удаляет обработчик и отключает линию. Если линия прерывания разделяется, то обработчик, определённый dev_id удаляется, но линия прерывания будет отключена только при удалении последнего обработчика.
Рабочие очереди
Работа - та работа, которую должен выполнить обработчик отложенного действия. работы ставятся в некоторую очередь, при этом в одну очередь работ может быть поставлено много работ. Работа связывается с конкретной очередью.
В системе определен worker - поток ядра (work thread). В системе имеется worker pull - множество worker, один worker может принадлежать одному pull.
Посредник, ответственный за установку отношений между workqueue и worker pull.
Существенные отличия между тасклетами и очередями работ:
- Тасклеты выполянются в констексте прерывания, в результате чего код тасклета должен быть неделимым(atomic). В отличие от тасклетов, workqueue выполняются в контексте специальных потоков ядра, и как результат имеют большую свободу действий, и в частности очереди работ могут блокироваться или засыпать.
- Тасклеты всегда выполняются на процессоре, на котором выполнялось АП, запланировавшее данный тасклет. Очереди по умолчанию также выполняются на том же процесосоре, но могут выполняться и на других процессорах.
- Код ядра требует, чтобы выполнение функций очередей работ откладывалось на определенный интервал времени.
- Ключевое отличие - тасклеты выполняются за короткий период времени после того, как были запланированы, а очереди работ имеют значительно большие задержки и необязательно должны быть атомарными, неделимыми.
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.
#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);
}