Продолжаем изучать планирование малых потоков.
Я уже говорил о двух инструментах ядра Linux, которые часто используются для обработки отложенных прерываний.
Сегодня мы поговорим совсем о другой сущности – протопоток Адама Данкелса, которые хоть и из ряда вон выходящие, но совершенно не лишние в контексте рассматриваемой темы.
И:
- Многозадачность в ядре Linux: прерывания и тасклеты
- Многозадачность в ядре Linux: рабочая очередь
- Протопотоки и кооперативная многозадачность
Что это такое и зачем все это нужно?
Я не случайно решил объединить в одной серии статей обработку отложенных прерываний и кооперативную многозадачность.Сущности, которые я рассматриваю в цикле, и идеи, которые они реализуют, имеют большое значение не только в контексте обозначенных задач, но и в целом для систем реального времени.
Их объединяет главным образом вопрос планировки.
Тасклеты, о которых говорилось в первой статье, например, работают по правилам невытесняющей многозадачности.
Кстати, о кооперативной многозадачности я уже довольно подробно рассказывал в одна из предыдущих статей .
Кооперативная многозадачность и протопотоки
Протопотоки — это легкие потоки без стека, реализованные на чистом языке C. Одним из возможных вариантов использования является реализация совместной многозадачности, не требующей больших накладных расходов.Например, их можно использовать во встроенных системах с серьезными ограничениями памяти или в узлах беспроводной сенсорной сети.
На Хабрахабре уже есть хороший статья лдир про многозадачность на основе протопотоков обсуждается практическая сторона вопроса: возможности библиотеки, как ее использовать.
Статья сопровождается интересными и наглядными примерами.
Подробнее о том, как это работает, будет здесь.
Далее в этом разделе я приведу вольный и несколько переработанный перевод информации из Сайт Адама Данкелса , создатель протопотоков, который подробно описывает как саму библиотеку, так и детали реализации, чтобы желающие могли насладиться оригиналом и перейти к следующему разделу.
Основные особенности и преимущества протопотоков:
- Реализация последовательного потока управления без использования сложных конечных автоматов и полной многопоточности,
- Используя условную блокировку внутри функций C,
- Протопотоки очень легкие, поскольку у них нет стека.
- Протопотоки могут вызывать другие функции, но не могут блокироваться внутри вызываемой функции, поэтому вам необходимо обернуть каждую функцию, использующую блокировку, в дочерний протопоток.
Таким образом, используется только явная блокировка.
Здесь нужно быть особенно внимательным — локальные переменные нужно использовать очень осторожно! Протопотоки не имеют выделенного стека; локальные переменные хранить негде.
Если поток используется только один раз, то переменные можно объявить как статические.
В противном случае вы можете, например, обернуть протопоток в свою собственную структуру и хранить переменные прямо там.
В целом я хочу помнить, что неправильное использование локальных переменных может привести к непредсказуемым результатам.
В рассматриваемой реализации роль состояния играет целое положительное число — номер текущей строки программы.
Управление происходит с помощью switch(), аналогично тому, как это происходит в Метод Даффа И программы совместного осуществления Саймон Тэтэм .
Протопотоки похожи на генераторы в Python, но служат разным целям: протопотоки обеспечивают блокировку внутри функции C, а Python обеспечивает несколько выходных данных генератора.
Важное ограничение реализации: код внутри самого протопотока не может в полной мере использовать оператор switch().
Однако такое ограничение возможно, используя возможности некоторых компиляторов, например, gcc. Вся библиотека реализована в макросах, поскольку макросы, в отличие от простых функций, могут изменять поток управления только с помощью стандартных конструкций C. Основной API включает в себя следующие макросы:
- Макрос инициализации PT_INIT,
- Макросы PT_BEGIN и PT_END, которые объявляют начало и конец протопотока,
- Макросы ожидания условий PT_WAIT_UNTIL и PT_WAIT_WHILE,
- Макросы ожидания выполнения дочерних протопотоков PT_WAIT_THREAD и PT_SPAWN,
- Перезапустите макрос PT_RESTART и выйдите из PT_EXIT,
- Макрос планирования (по сути запуска) PT_SCHEDULE,
- Макрос для произведения значения PT_YIELD,
- Макросы для использования семафоров PT_SEM_INIT, PT_SEM_WAIT и PT_SEM_SIGNAL.
Сначала давайте посмотрим на программу, использующую протопотоки.
В примере Protothread выполняется вечный цикл, в котором он сначала ожидает достижения счетчиком определенного значения, затем печатает сообщение и сбрасывает счетчик.
Теперь давайте посмотрим на упрощенную версию макросов, использованных в примере:#include "pt.h" static int counter; static struct pt example_pt; static PT_THREAD(example(struct pt *pt)) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, counter == 1000); printf("Threshold reached\n"); counter = 0; } PT_END(pt); } int main(void) { counter = 0; PT_INIT(&example_pt); while(1) { example(&example_pt); counter++; } return 0; }
struct pt { unsigned short lc; };
#define PT_THREAD(name_args) char name_args
#define PT_BEGIN(pt) switch(pt->lc) { case 0:
#define PT_WAIT_UNTIL(pt, c) pt->lc = __LINE__; case __LINE__: \
if(!(c)) return 0
#define PT_END(pt) } pt->lc = 0; return 2
#define PT_INIT(pt) pt->lc = 0
Структура pt состоит из одного поля lc (сокращение от локального продолжения).
Обратите внимание на PT_BEGIN и PT_END, которые соответственно открывают и закрывают оператор переключения, а также на немного более сложный макрос PT_WAIT_UNTIL. Он использует встроенный макрос __LINE__, который возвращает текущий номер строки программного файла.
Давайте сравним исходную версию примера и версию, развернутую на препроцессоре:
static
PT_THREAD(example(struct pt *pt))
{
PT_BEGIN(pt);
while(1) {
PT_WAIT_UNTIL(pt,
counter == 1000);
printf("Threshold reached\n");
counter = 0;
}
PT_END(pt);
}
|
static
char example(struct pt *pt)
{
switch(pt->lc) { case 0:
while(1) {
pt->lc = 12; case 12:
if(!(counter == 1000)) return 0;
printf("Threshold reached\n");
counter = 0;
}
} pt->lc = 0; return 2;
}
|
Макрос PT_BEGIN содержит инструкцию регистра 0, поэтому код сразу после этого макроса будет выполнен первым, поскольку начальное значение pt-> lc равно 0. Посмотрите, во что превратился макрос PT_WAIT_UNTIL. Полю pt-> lc теперь присвоено значение 12, это номер строки, и сразу появляется регистр 12 — благодаря этому коммутатор точно знает, куда переходить.
Если условие не выполнено, функция возвращает 0, что означает, что поток ожидает (все эти константы включены в саму библиотеку).
В следующий раз, когда example() будет вызвана в main, выполнение соответственно продолжится со случая 12, то есть будет проверяться, удовлетворено ли условие ожидания.
Как только счетчик достигнет 1000, условие станет истинным, и функция example() больше не будет возвращать 0, а напечатает сообщение и обнулит счетчик.
Затем, как и ожидалось, переходим к началу тела цикла.
В одна из предыдущих статей Я предоставил код примитивного кооперативного планировщика (раздел «Невытесняющий планировщик с сохранением порядка вызовов»).
Внеся небольшие изменения, вы сможете адаптировать его к протопотокам.
Это довольно просто, поэтому код показывать не буду.
Но предлагаю поиграть желающим.
Сравнение
Наконец, я предлагаю сравнить тасклет, рабочую очередь и протопоток.На самом деле, конечно, с одной стороны, сравнивать протопоток со средствами обработки прерываний нижней половины не очень корректно — ведь они созданы для разных задач, одно на другое так просто не заменишь.
С другой стороны, workqueue тоже несколько выбивается из тройки лидеров: в отличие от остальных, она работает по правилам упреждающего планирования, а сфера ее применения гораздо шире.
Я привожу это сравнение скорее для того, чтобы почерпнуть полезные идеи.
А вот сравнительная таблица:
Тасклет | Рабочая очередь | Протопотоки | |
---|---|---|---|
Наличие собственного стека | Нет — они обрабатываются как softirq (в специально выделенном стеке, общем для всех процессоров, по крайней мере, в Linux на x86) | Да — они выполняются на стеке рабочих потоков, количество которых намного меньше количества задач | Нет |
Скорость | Быстро – минимальная дополнительная обработка | Быстрый, но не такой как тасклеты, требует переключения контекста, когда рабочие заменяют друг друга.
|
Очень быстро — практически никакой дополнительной обработки, больше возможностей для оптимизации со стороны компилятора.
|
Примитивы синхронизации | Нет (кроме спин-блокировки) | Полностью присутствует | Псевдосемафоры; примитивное ожидание событий |
Концепция планирования | Кооперативный планировщик, такой как softirq; тасклеты вытесняются только аппаратными прерываниями | Рабочие играют роль планировщика работ и сами контролируются главным планировщиком.
|
Кооперативный планировщик, реализованный пользователем |
Например, в Эмбокс у нас возникла идея реализовать собственные легковесные потоки, которые не будут иметь своего стека, как protothread и Tasklet, но будут управляться основными планировщиками, как это реализовано в workqueue, а также будут хоть в какой-то форме поддерживать механизм для ожидания событий (даже более того, используйте подмножество того же API, что и полноценные потоки).
Этот подход имеет несколько привлекательных для нас приложений:
- Замена механизма обработки отложенных прерываний;
- Использование только легких потоков в планировщике позволит обеспечить практически полную многозадачность даже для встраиваемых систем с ограниченными ресурсами;
- Предыдущий сценарий хорош другим: при портировании системы на новую процессорную архитектуру в первую очередь хочется получить хоть сколько-нибудь работоспособную систему.
Реализация полноценного context_switch в новом ассемблере в этот момент — лишняя головная боль.
Чтобы использовать только облегченные потоки, это не требуется.
Теги: #ОС #ядро #многопоточность #протопотоки #Системное программирование #C++
-
Картриджи С Тонером Повышенной Емкости
19 Oct, 24 -
Microsoft Dynamics Gp: Внедрение Автосалонов
19 Oct, 24 -
Гибридный Логический Нейрон
19 Oct, 24 -
Дуть Или Не Дуть?
19 Oct, 24 -
Я Mac: Половина Youtube
19 Oct, 24 -
Symfony Code`n`coffe (Июль) Москва
19 Oct, 24