- PVSM.RU - https://www.pvsm.ru -
В предыдущей своей статье [1] я затронула тему многопоточности. В ней речь шла о базовых понятиях: о типах многозадачности, планировщике, стратегиях планирования, машине состояний потока и прочем.
На этот раз я хочу подойти к вопросу планирования с другой стороны. А именно, теперь я постараюсь рассказать про планирование не потоков, а их “младших братьев”. Так как статья получилась довольно объемной, в последний момент я решила разбить ее на несколько частей:
В третьей части я также попробую сравнить все эти, на первый взгляд, разные сущности и извлечь какие-нибудь полезные идеи. А через некоторое время я расскажу про то, как нам удалось применить эти идеи на практике в проекте Embox [2], и про то, как мы запускали на маленькой платке нашу ОС с почти полноценной многозадачностью.
Рассказывать я постараюсь подробно, описывая основное API и иногда углубляясь в особенности реализации, особо заостряя внимание на задаче планирования.
Аппаратное прерывание (IRQ) — это внешнее асинхронное событие, которое поступает от аппаратуры, приостанавливает ход программы и передает управление процессору для обработки этого события. Обработка аппаратного прерывания происходит следующим образом:
Функция-обработчик может быть достаточно большой, что непозволительно с учетом того, что выполняется она в контексте отключенных аппаратных прерываний. Поэтому придумали делить обработку прерываний на две части (в Linux они называются top-half и bottom-half):
Так мы подошли к отложенной обработке прерываний. В Linux для этих целей используются tasklet и workqueue.
Если коротко, то tasklet — это что-то вроде очень маленького потока, у которого нет ни своего стека, ни контекста. Такие “потоки” отрабатывают быстро и полностью. Основные особенности tasklet’ов:
Заглянем же теперь “под капот” и посмотрим, как они работают. Во-первых, сама структура tasklet (определяемая в <linux/interrupt.h>):
struct tasklet_struct
{
struct tasklet_struct *next; /* Следующий tasklet в очереди на планирование */
unsigned long state; /* TASKLET_STATE_SCHED или TASKLET_STATE_RUN */
atomic_t count; /* Отвечает за то, активирован tasklet или нет */
void (*func)(unsigned long); /* Основная функция tasklet’а */
unsigned long data; /* Параметр, с которым запускается func */
};
Прежде, чем пользоваться tasklet’ом, его сначала нужно инициализировать:
/* По умолчанию tasklet активирован */
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data); /* деактивированный tasklet */
Планируются tasklet’ы просто: tasklet помещается в одну из двух очередей в зависимости от приоритета. Очереди организованы как односвязные списки. Причем, у каждого CPU эти очереди свои. Делается это с помощью функций:
void tasklet_schedule(struct tasklet_struct *t); /* с нормальным приоритетом */
void tasklet_hi_schedule(struct tasklet_struct *t); /* с высоким приоритетом */
void tasklet_hi_schedule_first(struct tasklet_struct *t); /* вне очереди */
Когда tasklet запланирован, ему выставляется состояние TASKLET_STATE_SCHED, и он добавляется в очередь. Пока он находится в этом состоянии, запланировать его еще раз не получится — в этом случае просто ничего не произойдет. Tasklet не может находиться сразу в нескольких местах в очереди на планирование, которая организуется через поле next структуры tasklet_struct. Это, впрочем, справедливо для любых списков, связанных через поле объекта, как, например, <linux/list.h>.
На время исполнения tasklet’у присваивается состояние TASKLET_STATE_RUN. Кстати, из очереди tasklet достается перед своим исполнением, а состояние TASKLET_STATE_SCHED снимается, то есть, его можно запланировать снова во время его исполнения. Это может сделать как он сам, так и, к примеру, прерывание на другом ядре. В последнем случае, правда, вызван он будет только после того, как он закончит свое исполнение на первом ядре.
Довольно интересно, что tasklet можно активировать и деактивировать, причем рекурсивно. За это отвечают следующие функции:
void tasklet_disable_nosync(struct tasklet_struct *t); /* деактивация */
void tasklet_disable(struct tasklet_struct *t); /* с ожиданием завершения работы tasklet’а */
void tasklet_enable(struct tasklet_struct *t); /* активация */
Если tasklet деактивирован, его по-прежнему можно добавить в очередь на планирование, но исполняться на процессоре он не будет до тех пор, пока не будет вновь активирован. Причем, если tasklet был деактивирован несколько раз, то он должен быть ровно столько же раз активирован, поле count в структуре как раз для этого.
А еще tasklet’ы можно убивать. Вот так:
void tasklet_kill(struct tasklet_struct *t);
Причем, убит он будет только после того, как tasklet исполнится, если он уже запланирован. Если вдруг tasklet планирует сам себя, то нужно перед вызовом этой функции не забыть запретить ему это делать — это на совести программиста.
Интереснее всего функции, которые играют роль планировщика:
static void tasklet_action(struct softirq_action *a);
static void tasklet_hi_action(struct softirq_action *a);
Так как они практически одинаковые, то нет смысла приводить код обеих функций. Но вот на одну из них стоит взглянуть, чтобы разобраться поподробнее:
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __this_cpu_read(tasklet_vec.head);
__this_cpu_write(tasklet_vec.head, NULL);
__this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
Обратите внимание на вызов функций tasklet_trylock() и tasklet_lock(). tasklet_trylock() выставляет tasklet’у состояние TASKLET_STATE_RUN и тем самым блокирует tasklet, что предотвращает исполнение одного и того же tasklet’а на разных CPU.
Эти функции-планировщики, по сути, реализуют кооперативную многозадачность, которую я подробно рассматривала в своей статье [1]. Функции регистрируются как обработчики softirq, который инициируется при планировании tasklet’ов.
Реализацию всех вышеописанных функций можно посмотреть в файлах include/linux/interrupt.h и kernel/softirq.c.
В следующей части я расскажу о гораздо более мощном механизме — workqueue, который также часто используется для отложенной обработки прерываний.
P.S. На правах рекламы. Ещё я хочу пригласить всех, кому интересен наш проект, на встречу [3], организованную codefreeze.ru (анонс на хабре [4]). На ней можно будет пообщаться вживую, задать интересующие вопросы главному злодею abondarev [5] и покритиковать в лицо, в конце концов :)
Автор: LifeV
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/kernel/75376
Ссылки в тексте:
[1] предыдущей своей статье: http://habrahabr.ru/post/219431/
[2] проекте Embox: https://code.google.com/p/embox/
[3] встречу: http://codefreeze.timepad.ru/event/163097/
[4] анонс на хабре: http://habrahabr.ru/company/codefreeze/blog/243659/
[5] abondarev: http://habrahabr.ru/users/abondarev/
[6] Источник: http://habrahabr.ru/post/244071/
Нажмите здесь для печати.