06 сем Лекция 8 9 07.06 - chrislvt/OS GitHub Wiki
Любое отложенное действие в системе выполняется как отдельный поток. Но потоки эти разные так как в ядре потоки разные, выполняющие разную работу. И как раз мы понимаем это когда рассматриваем отложенные действия. Мы уже с вами рассмотрели ksoftirqd который предназначен для запуска отложенных действий типа softirq к которым относятся также тасклеты. При этом существуют высокоприоритетные тасклеты и просто тасклеты.
С очередями работ по другому. Основная проблема в ядре это управление параллельным выполнением очередей работ. То есть у нас с вами многопроцессорные системы, то есть в системе имеется реальная параллельность, и работа в такой системе выполняется с учетом этой параллельности.
CPU0
per_cpu(cpu_work_pool, cpu)[0]
normal(nice=0)
struct wokers_pool:
и так для каждого cpu Фактически мы видим что в каждом cpu стоит 2 очереди:обычных воркеров (существует внутренняя структура ядра, она даже не декларируется для работы с воркерами - worker_pool) в этом воркер пуле находятся воркеры, из этого пула работы выбираются, видите nice=0, и высокоприоритетная очередб nice=20 (высокоприоритетная очередь). Здесь это обозначается буквой "H". Надо посмотреть в системе эти ковокеры, посмотреть что все именно так и реализовано. Естесственно существует соответствующее описание. Эта иллюстрация приводится для демонстрации того, что очереди работ выполняются другими потоками ядра, они называются воркеры.
Это всё нулевой процессор. Дальше в этой картинке есть процессор cpun. В каждом процессоре организуются две очереди, обычного приоритета и hight. Причем это внутренние структуры ядра, они не нужны для того чтобы создать свою очередь работ, работу или использовать глобальную очередь работ. Но если мы рассматриваем ядро надо понимать каким образом в ядре выполняются эти работы. Просто из-за того что эти структуры мы не рассматривали для создания очереди работ, для создания отдельной работы в очередь, то это внутренние структуры ядра, которые нужны для того чтобы организовать такую очередь. Соотвественно это тоже должно быть прописано в ядре.
Здесь есть соответствующий код. Сама работа называется Linux workqueue. Несколько освновных понятий и сокращений: CMWQ (Concurrency Managed Workqueue). Фактически workqueue pull это потоки. Любое отложенное действие выполняется в системе как поток, но это разные механизмы и довольно навороченные.
Был такой вопрос, когда очередь процесс когда он блокируется, он уходит из очереди и переходит в другое состояние. В активной очереди - в очереди из которой выбираются работы находятся процессы и зомби. И там еще куча всяких очередей, в частности блокированных процессов. С работами так поступать невозможно, так как это НЕ процесс. Та диаграмма состояний которую мы с вами рассматривали это диаграмма состояний которую мы рассматривали (1 сем?), это диаграмма состояний процессов в стадии выполнения, а мы говорим о ядре и там уже нет такой диаграммы. И такая работа не удаляется из очереди. Она просто пропускает свою очередь (ну как у людей лол).
Задачи мы должны знать сами. Если рассматривать ядро системы, то очень большой процент команд предназначен для выполнения управления внешними устройствами. Это одна из самых сложных задач которые решаютс системой. В частности немного уже столкнулись с этими сложностми когда говорили о файловых системах. Задача файловых систем - обеспечить возможность хранения и обращения сохранённой на внешних энергонезависимых носителях информации. Речь идёт о внешних устройствах.
Если внешними устройствами являются внешние устройства типа дисков - это собственно предоставить в распоряжение пользователя интерфейс с помощью которого пользователь может получить доступ к файлу. Но при этом диск сам по себе является внешним устройством. Ну и здесь конечно возникает очень сложная для системы задача управления внешними устройствами. При этом внешних устройств много, разных типов внешних устройств много. Мыши, клавиатуры, сканеры, принтеры, джойстики и тд. И всё это устройства совершенно разного типа которые сами по себе решают разные задачи. Но система не может с каждым устройством работать индивидуально. Система построена таким образом, чтобы с каждым устройством работать как с неким обобщенным устройством. Одна из главных парадигм юникс - в юникс все файл. Устройства тоже файл. Это довольно условно, но с одной стороны условно так как это внешнее устройство, с другой стороны действительно это так. Когда мы работаем с устройством, мы можем расматривать это устройство как открытый файл.
Устройство в системе описывается в системе struct inode. И мы использум struct file_operations для того чтобы определить свои функции для работы с конкретным устройством. Но устйство как везде почеркивается это файл устройства - device file - это специальный файл. И в итоге мы с этим устройством должны работать.
Мы можем посмотреть так называемый файл устройств в файловой системе в директории /dev. Там увидим файлы с буквой с - Character devices и где-то там сможем увидеть девайсы с буквой b - block devices. Очевидно что то что я сказала это более высокий уровень обращения к внешним устройствам, но ядро должно каким-то образом идентифицировать устройство. Для идентификации устройства ядро использует старший(major) и младший (minor) номера устройств.
В системе устройства делятся на наборы, которые определяются номерами major - старшими номерами. Например все SCSI диски имеют старший номер 8. Каждое отдельное устройство на диске это так называемый partition(разделение). Адресное пространство диска мы можем разделить и каждое такое выделенное непрерывное адресное пространство диска может быть определено как partition. И эти самые partition будут иметь младшие номера(minor).
Номера major и minor идентифицируют устройства в ядре. Но мы в файловой системе увидим имена. У них в файловой системе есть имена которые используются. Например: cat /dev/mouse, less_t/dev/hda, /dev/zero, /dev/null и тд. Указывается что имена файлов устройств в некотором смысле являются произвольными, как inode. Когда-то кто-то придумал inode, теперь это историческое называние дескриптора файла. Имена устройств выбираются для удобства и согласованности. Соответственно можно увидеть и старший и младший номера в листинге SCSI диска если набрать /dev/sda:vs мы увидим
brw-rw---- | root disk 8, 0 may 5 1998 /dev/sda
для того чтобы посмотреть номера надо обратиться к /proc/devices. С помощью команды ls -l /dev/ | grep ^C на экран будет выведен список символьных устройств системы.
В разрабатываемом ПО можно получить номера устройств несколькими способами. Это могут быть макросы, а могут быть функции. Во-первых это определено в системе и это тип dev_t.
Этот тип определен в <linux/types.h>. Существуют разные описания типов, в версии 2.6.0 это 32-х разрядное число. 12 разрядов для старшего номера, 20 разрядов для младшего.
Начнем с макросов.
major(dev_t dev);
minor(dev_t dev);
Понятно, что часто возникает задача идентификации устройства, тогда надо иметь объединение старшего и младшего устройств. Это делается с помощью mkdev(int major, int minor). При этом следует особо отметить, что на самом деле страший номер устройства сообщает ядру Linux с каким драйвером устройства оно должно работать. Младший же номер конкретизирует устройство. Например, в системе у вас может быть 1 контроллер SATA. Но к нему может быть подключено несколько жестких дисков. Есть функции при этом указывается что они не определены в POSIX1, но испольщуются в некоторых системах.
unsigned int major(dev_t dev);
unsigned int minor(dev_t dev);
dev_t makedev(unsigned int maj, unsigned int min);
В структуре struct stat
{
dev_t st_dev; //идентификатор устройства на котором находится файл. Соответсвенно мы можем получить идентификатор устройства, с помощью major и minor получить старший и младший номера этого устройства.
};
struct statx
{
...
//в этой структуре есть соответствующие поля со следующим комментарием
//если этот файл представляет устройство, тогда следующие два поля содержат идентификатор устройства
_u32 stx_rdev major; //r-real (физическая)
_u32 stx_rdev minor;
...
//следующие 2 поля содержат идентификатор устройства содержащий файловую систему в которой располагаются файлы.
//Эти поля в структуре statx последние
_u32 stx_dev_major;
_u32 stx_dev_minor;
};
Если посмотреть на struct inode, в ней имеется поле i_rdev. Это физическое устройство, его идентификатор на котором находится файл.
Если мы хотим написать драйвер для какого-то нашего устройства, каким-то образом хотим идентифицировать наш драйвер в системе, то несмотря на то что в системе имеются зарезервированные старшие номера, то разработчики ядра специально подчеркивают что для новых драйверов они строго указывают чтобы разработчики использовали динамическое выделение старших номеров устройства.
Есть разные источники, относительно макросов которые мы описали, они находятся в <linux/kdev_t.h>.
Получить динамически (в процессе выполнения) соответствующие номера можно с помощью функций alloc_chrdev_region(), register_chrdev_region(). Имеется ввиду char device.
<linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, char *name).
Если вы пишете собственный драйвер символьного устройства, то следует как указывают разработчики ядра, ориентироваться на динамическое выделение номеров устройств. В этой функции first - начальный номер диапазона номеров устройств, который вы желаете выделить. Насчет минорной части нет никаких требований и очень часто это 0.
Несморя на то что указывается, что такое назначение не очень удобно для разработчика (неудобство динамического назначения главного номера заключается в том , что соответствующий главный номер в вашем модуле может быть другой). То есть для нормального использования драйвера это довольно сложная проблема, потому что когда-то номер был выделен и его вы могли прочитать в /proc/devices, но в следующий раз он будет другой. Тем не менее это довольно строгая рекомендация разработчиков ядра.
Само название драйвер предполагает что это ПО которое предназначено для управления внешними устройствами. На семинарах уже коротко говорили о том что современные системы предлагают в некотором смысле схожие способы изменения функциональности - работы внешнего устройства, добавления своей функциональности. Для того чтобы это лучше понять классифицируем драйверы в Linux.
Классификация драйверов в Linux.
1) Драйверы, встроенные в ядро.
То есть это те драйверы которые инициализируются при запуске системы. Такие драйверы позволяют автоматически находить сооветветствующие устройства при обращении к ним. К ним будут обращаться приложения! Всё написано для чего? для того чтоб запускать программы).
Примеры таких драйверов являются WGA драйвера. Драйвера управляющие WGA-контроллером, устаревшие контроллеры IDE, SATA, SCSI.
2) Драйверы, реализованные как загружаемые модули ядра.
Загружаемые модули ядра часто используются для управления звуковыми сетевыми картами с квазиадаптерами. Файлы модуля ядра располагаются в /lib/modules. Обычно при инсталяции системы указывается перечень модулей которые будут автоматически полключаться на этапе загрузки. Такой перечень находится в файле /etc/modules. В файле etc/modules.conf находится перечень опций для таких модулей. Редактировать такие модули не рекомендуется. Для их редактирования существуют специальные скрипты типа update-modules. Соответственно знакомые нам команды (утилиты) позволяют нам посмотреть список загруженных в текущий момент модулей (lsmod), (insmod) позволяет загрузить модуль из командной строки. Для выгрузки используем команду rmmod. Есть команда которую мы не рассматривали - modprobe. та команда автоматически загружает модули. Для этого чтобы вывести на экран текущую конфигурацию модулей мож.но воспользоваться командной modeprobe - C.
3) Код драйверов третьего типа поделен между ядром и специальной утилитой.
Например у драйвера принтера ядро отвечает за взаимодействие с параллельным портом. А формирование управляющих сигналов для принтера выполняет демон печати lpd. Который использует для этого специальую программу фильтор. Ещё одним примером драйверов такого типа являются драйверы модемов. В системе имеются структуры (специальные структуры). Самой низкоуровневой структурой, которая фактически может и не присутсвовать нигде в коде, она называется базовая структура устройства - struct device.
//тут еще есть информация о шине, важнейшая информация но Н.Ю. ее забыла написать. Задача шины заключается в передаче данных от устройства к устройству и от процессора к процессору. Но устройство может и напрямую передавать данные в память или считывать данные из памяти. Это все делается для того чтобы пользователь мог ввести и вывести информацию в удобном для него виде. dma важно так как иначе каждый байт информации передавался бы через процессор, dma освобождает процессор от необхоимости определенных действий (как и контроллер прерывания - освобождает от постоянного опроса), dma освобождает от пересылки данных.
struct device
{
struct device * parent;
...
struct char *init_name; //первоначальное имя устройства
const struct device_type *type; //структура для описания типа устройства, содержит специфическую информацию для типа
...
struct device_driver *driver; // драйвер который описывает данное устройство
# ifdef CONFIG NUMA
int numa_node;
#endif;
//NUMA - Not Uniform Memory Access(неравноверный доступ к памяти) - схема организации памяти в мультипроцессорных системах когда время доступа к памяти определяется ее расположением по отношению к процессору
...
struct device_dma_parameters *dma_parms; //dma - direct memory access. Таких полей много.
//устройство может записывать в оперативную память и читать из нее минуя регистры процессора.Процессорное время используется эффективно. Для этого в состав системы входят разные контроллеры DMA. Фактически отдельные устройства можут содержать свой контроллер dma.
...
struct class *class; //устройство относится к определенному классу.
...
void (*release)(struct device *dev); // в этой структуре существует указатель на одну единственную функцию - release. - освободить систему от данного устройства
...
};
Базовая структура устройства. Некоторые поля могут иметь значение null, например поле parent. Мы понимаем что это устройство к которому подключено другое устройство. Это поле может быть NULL, соответственно устройство само по себе. Toplevel device.
В отличие от struct device_driver, в struct device поля которые отражают специфику работы данного устройства в системе. В частности NUMA и DMA.
//содержит точки входа в драйвер
struct device_driver
{
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name;
...
int (*probe)(struct device *dev); /*Функция probe - точка входа. Место с которого начинается выполнение драйвера. Она вызывается чтобы запросить существование конкретного устройства. При этом если драйвер может работать с вызываемы устройством и устройство готово, то будет возвращен 0.*/
int (*remove)(struct device *dev); /*функция вызывается для удаления устройства из системы и разорвать связь между устройством и его драйвером. Suspend вызывается для того чтобы перевести устройство в режим сна. Resume ля того чтобы вывести его из этого состояния.*/
int (*shutdown)(struct device *d_dv);
int (*suspend)(struct device *d_dv); //перевод устройства в режим сна
int (*resume)(struct device *d_dv); //вывод устройства из режима сна
...
};
Рассмотрим некоторые важнейшие функции например для создания собственного символьного устройства. Понятно, что если мы пишем драйвер ввиду загружаемого модуля ядра, то в функции init надо вызвать определенные функции. Посмотрим на примере предполагаемого драйвера что же надо делать.
static init__init ex_drivers_init(void)
{
Первое что надо сделать - выделить старший номер. Для этого вызываем alloc_chardev_region.
/*Allocationg major number*/
if (alloc_chardev_region(&dev, 0, 1, "ext_dev")) < 0)
{
printk(KERN_INFO "Cannot allocate\n");
return -1;
}
//если все благополучно, то можем посмотреть полученные страший и младший номера с помощью printk
printk(KERN_INFO "Major=%d, minor=%d\u", major(dev), minor(dev));
...
//идетифицированное char устройство добавляется в систему
//etx_cdev - инициализированная структура
/*Adding char dev to the system*/
if (cdev_add(&etx_cdev, dev, 1)
{--//--} //проверка на ошибку
/* Creatig struct class */
if ((dev_class = class_create(THIS_MODULE, "etx_class")) == NULL)
{ -//-} //ошибка
/* creating device */
if ((device_create(dev_class, NULL, dev, NULL, "etx_device")) == NULL)
{...}
//некоторые функции опустили, но затем должны зарегестрировать некоторые функции драйвера в наш обработчик прерывания
...
//(void *)(irq_handler) - передаем обработчик прерывания чтобы освободить от него IRQ
if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "etx_device", (void *)(irq_handler))){
-//- //опять ошибка
}
// создаётся очередь работ
own_workqueue= create_workqueue("own_wq");
...
}
В этом примере создается очередь работ. После того как удалось зарегестрировать обработчик прерывания делаем create_workqueue.
//надо объявить работу!!!(в примере этого нет)
//в этом обработчике прерывания ставится работа в очередь
static irqreturn_t irq_handler(int irq, void *dev_id)
{
...
queue_work(own_workqueue, &work);
return IRQ_HANDLED;
}
в то же время в драйвере мы можем определить свои операции используя struct file_operations и зарегестрировать свои функции.
static struct file_operations fops =
{
.owner = THIS_MODULE,
.read = etx_read,
.write = etx_write,
.open = etx_open,
.release = etx_release,
};
Если речь идёт о блочном устройстве, то опять же в __init мы должны вызвать специальную функцию.
static int __init sblkdev_init(void)
{
init ret = SUCCESS;
_sblkdev_major = register_blkdev(_sblkdev_major, _sblkdev_name);
/* register_blkdev возвращает страший номер, регистрирует блочное устройство ему выделяется major node. Если не удалось это сделать, то надо сделать unregister_blkdev */
if (_sblkdev_major <= 0){ // если возвращено отрицательное значение
printk(KERN_WARNINGS, "...");
return _EBUSY;
}
//затем необходимо добавить устройство
ret = sblkdev_add_device();
/*Функция sblkdev_add_device выполняет инициализацию блочного устройства. Это наша функция. В этой функции выделяется память для соответствующей структуры, выделяется буффер. */
if (ret) {...}
...
}
Посмотреть самостоятельно структуру описывающую символьное устройство и структуру описывающую блочное.
Драйверы пишутся по разному. Если используется struct device_driver, то там определены точки входа. Если создаем свое устройство, то используются функции для создания устройства. Это все правильные действия которые могут быть реализованы в ваших драйверах.
Структура которая представляет устройство:
typedef struct sblkdev_device_s
{
sector_t capacity; //device size in bytes
u8 *data;
atomic_t open_counter;
struct request_queue *queue; // эта очередь не имеет никакого отношения к тем очередям работ о которых говорили. Это очередь к устройству. В соотвествии с этой очередью будут выполняться определенные действия. К такому блочному устройству приложения выполняют какие-то запросы и эти запросы выстраиваются в некоторую очередь. Такая очередь должна быть инициализирована.
...
struct gendisk *disk;
} sblkdev_device_t;
static int sblkdev_add_device(void)
{
...
sblkdev_device_t *dev = kzalloc(sizeof(sblkdev_device_t), GFP_KERNEL);
}
Необходимо инициализировать так называемую очередь обработки(эта строка пропущена). После инициализации очереди необходимо выделить диск функцией: struct gendisk *disk = alloc_disk(1); фактически выделяется одна partition. У диска должен быть major number, ему присваивается значение котороые мы получили ранее. Disk first minor присваивается 0. То есть очевидно, что необходимо выполнить большой объем действий для работы с этим блочным устройством. Фактически можно еще раз перечислить действия которые необходимо выполнить.
1)Выделить память под структуру для того чтобы хранить данные которые мы запишем в поля этой структуры
2)Инициализируется очередь запросов обработки для того чтобы иметь возможность действительно обрабатывать эти запросы.
Надо сказать, что здесь на блочных устройствах определена структура
static struct blk_mq_ops_mq_ops = {
.queur_rq = queue_rq,
};
В этой структуре 1 точка входа, эта функция и выполняет обработку запросов. Но это не единственная структура. Существует struct block_device_operations.
static const struct block_device_operations_fops = {
.owner = THIS_MODULE,
.open = __open,
.release = __release,
.ioctl = __ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = __compat_ioctl,
#endif
};
Три точки входа
В функции exit мы вызываем функцию remove_device.
static void __exit sblkdev_exit(void)
{
sblkdev_remove_device();
...
}
В функции remove_device мы должны удалить gendisk, очистить очередь, если выделяли буфер, то должны освободить буфер и затем должны фактически переменной присвоить NULL.
static void sblkdev_remove_device(void)
{
...
dcl_gendisk(dev->disk);
...
blk_cleanup_queue(dev->queue);
...
_sblkdev_device = NULL;
}
Данный пример рассматривается как одна из возможностей курсовой работы.
USB шина является удобной и быстрой. Полностью обеспечивает и скорость работы и предоставляет удобный интерфейс. В системе имеется специальная структура которая описывает USB-драйвера.
Коротко можно продемонстрировать поля этой структуры следующим образом.
struct usb_driver
{
const char *name; //чтобы увидеть драйвер в системе
int (*probe)(struct usb_interface *intf, const struct usb_device_id *id); //функция проб для usb драйверов прежде всего определена для интерфейса.
//prob используется для определения что устройство доступно и данная структура предназначена для работы именно с этим устройством
void (*disconnect)(struct usb_interface *intf);
...
const struct usb_device_id *id_table;
};
Естественно существуют другие точки входа. Например suspend, resume.
const struct usb_device_id структура определяющая id_table и драйверы USB-устройств используют id таблицу для поддержания (горячего подключения)hot plugging. Фактически это таблица, то есть таблица struct usb_device_id содержит список всех различных видов sb устройств, которые может распознать данный драйвер. Если эта переменная не определена, то callback функция обратного вызова probe никогда не будет вызвана. Существуют примеры заполнения этой структуры, обычно в них указывается vendor_id и product_id. Будет выслан соответствующий материал.