-
Notifications
You must be signed in to change notification settings - Fork 2
06 сем Семинар 9 03.06.21 Очереди работ
В лабе по тасклетам обработчик тасклета нигде явно не вызывается, но дело в том что его выполнение планируется.
Все отложенные действия выполняются в системе как потоки, то есть на самом деле запускается поток. Этими потоками управляет kernel softirq daemon (ksoftirqd). Он создается для каждого процессора. Набрать ps - ajx и посмотреть на этих демонов, в том числе на workers. Увидим что для каждого ядра есть соответствующий воркер. По разному запускаются потоки. Тасклет - один из типов софтирку. Между ними существенная разница. Один и тот же софтирку может выполняться параллельно. Для этого там необходимо строжайшим образом выполнять взаимоисключение. На самом деле в тасклетах тоже может быть взаимоисключение.
Каким образом это может делаться? Тасклеты не могут блокироваться, но в них могут использоваться спинлоки. И на тасклетах могут быть определены соответствующие спинлоки.
static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}
static inline void tasklet_unlock(struct tasklet_struct *t)
{
smp_mb_before_atomic(); //указывается что smp потому что многоядерная система, наши системы имеют smp архитектуру
clear_bit(TASKLET_STATE_RUN, &(t)->state);
}
На такслетах определены специальные функции tasklet_trylock и tasklet_unlock. И в них используется команда test_and_set.
В tasklet_schedule тоже имеется test_and_set. Флаг test_and_set проверяется потому что тасклет может выполняться только 1. Даже если в очереди запланирован этот тасклет несколько раз, он будет выполнен один раз. Если посмотреть, test_and_set_bit(TASKLET_STATE_SCHED, &t->state) это состояние что тасклет запланирован для выполнения. Если не запланирован, то будет вызыван __tasklet_schedule(t).
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
enum
{
TASKLET_STATE_SCHED,//тасклет запланирован для выпонения
TASKLET_STATE_RUN //только для SMP (на сегодняшний день это не актуально, так как все системы имеют SMP архитектуру)
}
ДВА ОДИНАКОВЫХ ТАСКЛЕТА НЕ МОГУТ ВЫПОЛНЯТЬСЯ ОДНОВРЕМЕННО!
Когда вызываем declare_tasklet то фактически, без вашего какого-то участия запускается поток. То есть поток ставится в очередь. В ps -ajx мы видим процессы. В UNIX ничего не стоит запустить процесс, но ядро UNIX монолитное. Монолитное ядро состоит из функций, вы это видете в коде. Но при этом всегда говорится, что оно многопоточное. Когда мы смотрим ps -ajx (с глобальной точки зрения это потоки), но это процессы. У каждого есть идентификатор. Любой процесс запускается как потомок какого-то процесса, то есть системным вызовом fork. Сам системный вызов fork это очень низкоуровневое действие. И запущеный тасклет, и запущеная работа из очереди работ это поток, запланировать мы можем только поток. Но так как мы с вами учили в курсе, когда изучали процессы, мы говорили об очереди процессов. Планируются к выполнению процессы.
Обратить внимание, что на тасклетах определены свои спинлоки и мы их рассмотрели.
Это так же отложенные действия. Это структуры и функции которые переписывались и безусловно они переписывались для того чтобы ещё более отличать их по действиям от тасклетов. Везде подчеркивается что тасклеты не могут блокироваться - вызывать слип, семафоры. Безусловно может оказаться такая ситуация когда разные тасклеты обращаются к одним и тем же данным. Нельзя гарантировать, что будет строго тасклет который выполняет какие-то действия и никаких пересечений с другими отложенными действиями у тасклета не будет. Поэтому и предусматривается возможность взаимоисключения, но со спинлоками. Доступ к разделяемым данным в тасклетам реализуется с помощью спинлоков. Специально определенные функции, но в них используется test_and_set. Это машинная команда, была определена впервые в IBM360.
Очереди работ так же отложенные действия, так же это потоки ядра, но запускаются по другому. Тасклеты и softirq запускаются ksoftirqd, а очереди работ запускаются по другому.
Давайте рассмотрим функции которые определены на очередях работ.
Прежде всего, очередь работ может инициализироваться статически и динамически. Для статической декларации работы используюется DECLARE_WORK. Но когда говорим о работах, мы всегда говорим о queue work. Подчеркивается, что существует очередь работ в которую помещаются работы. На лекции рассмотрели структуры struct work_struct и struct queuework_struct. Конечно struct work_struct меньше чем struct queuework_struct, так как задачи перед отдельными работами стоят более маленькие. В очередь работ помещаются какие-то работы и в результате эти работы запускаются из этой очереди работ. И это все потоки.
Для того чтобы статически инициализировать work используется макрос
DECLARE_WORK(name, void(*func)(void*))
//name - имя структуры work_struct
//void(*func)(void*) указатель на функцию (определяет выполняемую работу)
Динамически можем инициализировать работу с помощью INIT_WORK.
INIT_WORK(struct work_struct *work, void (*func)(void), void *data); //выполняет инициализацию работы более тщательно и рекомендуется использовать если работа инициализируется первый раз
PREPARE_WORK(--//--); // если работа уже инициализирована и требуется изменить какие-то параметры, то рекомендуется вызывать prepare_work.
Возвращаясь к загружаемому модулю ядра, в __init мы вызываем request_irq, делаем так же как делали в лр с тасклетом - передаем номер линии IRQ, handler, устанавливаем IRQF_SHARED. Если удалось это сделать, тогда мы создаем очередь. Здесь источники резко расходятся.
static struct workqueue_struct *queue;
...init()
{
...
queue = create_workqueue("workqueue");
if (!queue){
return error
}
}
Вызываем функцию create_workqueue в __init() и сюда передаём название - массив ascii символов. Если не удалось создать очередь, то выводим ошибку. Можно прочитать, что create_workqueue это устаревшая функция и надо вызывать alloc_workqueue(...,...,...,...);
struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, ...);
//const char *fmt - формат печати для имени workqueue.
//unsigned int flags - флаги для очередей работ.
/*max_active - ограничивает число задач которые могут выполняться одновременно в данной очереди работ на любом процессоре.
Allocate an odered workqueue. An odered workqueue executes at most one work item at any given time in quened order.(Выделите упорядоченную рабочую очередь. Упорядоченная рабочая очередь выполняет не более одного рабочего элемента в любой момент времени в определенном порядке.)*/
Фактически получается что размещается очередь работ, фактически создаётся.
Посмотрим create_workqueue(name)
#define create_workqueue(name)
alloc_workqueue("%s", _WQ_LEGACY|WQ_MEM_RECLAIM, 1, (name))
create_workqueue освобождает нас от необходимости указывать перечень параметров которые требует указывать функция alloc_workqueue.
Теперь необходимо поместить работу в очередь. Работа помещается в очередь с функции queue_work.
int queue_work(struct workqueue_struct *wq, struct work_struct *w);
//struct workqueue_struct *wq - указатель на очередь
//struct work_struct *w - указатель на работу. Работа это должен быть обработчик соответсвующий отложенного действия типа work_struct
Функция которая определяет номер процессора, на котором будет выплоняться работа.
(дано на семинаре)
int queue_work_on(int cpu, --//--)
(полная версия из интернета)
This puts work on a specific CPU.
int queue_work_on( int cpu, struct workqueue_struct *wq, struct work_struct *work );
Where,
cpu– cpu to put the work task on
wq – workqueue to use
work– job to be done
Кроме этих функций есть 2 дополнительные функции которые позволяют запланировать работу, не только ее запланировать но и отложить на неопределенное время.
int queue_dalayed_work(struct workqueue_struct *wq, struct delayed_work *dw, unsigned long delay);
Вместо struct work_struct используется структура struct delayed_work. То есть просто так отложить на какой-то интервал работу нельзя. Определена структура.
struct delayed_work {
struct work_struct work;
struct timer_list timer;
/* target workqueue and CPU ->timer uses to queue ->work */
struct workqueue_struct *wq;
int cpu;
};
Точно такая же функция есть
After waiting for a given time this puts a job in the workqueue on the specified CPU.
int queue_delayed_work_on( int cpu, struct workqueue_struct *wq,struct delayed_work *dwork, unsigned long delay );
Where,
cpu– CPU to put the work task on
wq – workqueue to use
dwork – work to queue
delay– number of jiffies to wait before queueing or 0 for immediate execution
Все перечисленные функции ставят работу созданную опять же в нашем загружаемом модуле ядра в очередь. Мы создали очередь, ставим в неё работы. То есть если вы пишете свой драйвер, например драйвер клавиатуры, например этот драйвер может работать как синтезатор, то лучше использовать ввиде отложенного действия очередь работ. Такой драйвер может быть написан как загружаемый модуль ядра. Linux позволяет изменять функциональность драйвера, написав как бы драйвер который будет перехватывать информацию, например от драйвера клавиатуры. Это похоже на то как в Windows реализованы многоуровневые драйверы. То есть в Windows существует стек драйверов.
В любой системе внешним устройством управляет так называемый функциональный драйвер. Как правило, функциональные драйверы поставляются вместе с внешним устройством. Сейчас такой драйвер можно скачать на сайте разработчика. Windows обычно сам содержит большой набор драйверов разных фирм(для принтерова например). В результате будет установлен драйвер который будет управлять работой внешнего устройства. Обычно внешним устройством управляют так называемые функциональные драйверы. У любого драйвера может быть 1 обработчик прерывания. Над этим драйвером можно установить свой драйвер. И этот драйвер будет загружаемым модулем ядра.
Кроме очередей которые мы сами создаем, есть некая глобальная очередь и в неё тоже можно поставить работу. Для этого используются другие функции.
int shedule_work(struct work_struct *w);
Обратите внимание, здесь в параметрах нет struct workqueue_struct, так как мы ее не создаём.
(в методе перечислены 4 функции)
Разница заключается в том, что в первом случае работа ставится в созданную нами очередь, а здесь работа ставится в глобальную очередь работ. Для того чтобы принудительно завершить или отменить работу из очереди используется функция flush_work.
int flush_work(struct work_struct *w);
Для того чтобы отменить работу из глобальной очереди
int flush_scheduled_work(void);
Уничтожение всей очереди
int flush_workqueue(struct workqueue_struct *wq);
В лр создать одну очередь struct worqueue_struct и две работы struct work_struct. Будет два обработчика, work1 и work2.Надо чтоб делали что-то отличающееся. Очереди вот эти надо продемонстировать что они могут блокироваться. Например work_handler который воспроизводит мелодии. В exit надо вызвать функцию destroy_workqueue. Основное требование это 2 работы и проследить их создание в логе. По структуре struct_work вывести информацию об этой работе. Лучше через proc, но можно не через proc. И информацию о созданной очереди.