- PVSM.RU - https://www.pvsm.ru -
Продолжаем изучать планирование маленьких потоков. Я уже рассказала про два средства в ядре Linux, которые часто используются для отложенной обработки прерываний. Сегодня речь пойдет о совсем другой сущности — protothread [1] Adam Dunkels, которые хоть и выбиваются из ряда, но в контексте рассматриваемой темы совсем не лишние.
А также:
Объединить отложенную обработку прерываний и кооперативную многозадачность в одном цикле статей я решила не случайно. Те сущности, что я рассматриваю в цикле, и идеи, которые они реализуют, имеют большое значение не только в контексте обозначенных задач, но также в целом для систем реального времени.
Главным образом их объединяет вопрос планирования. Tasklet’ы, речь о которых шла в первой статье, например, работают по правилам невытесняющей многозадачности. Кстати, про кооперативную многозадачность я уже довольно подробно рассказывала в одной из предыдущих статьей [4].
Protothread’ы — это легковесные бесстековые потоки, реализованные на чистом C. Одно из возможных применений — реализация кооперативной многозадачности, не требующей больших накладных расходов. Например, их можно использовать во встроенных системах с жестким ограничением по памяти или в узлах беспроводной сенсорной сети.
На хабрахабре уже есть хорошая статья [5] ldir [6] про многозадачность на основе protothreads, в ней рассматривается практическая сторона вопроса: возможности библиотеки, как ее применять. Статья сопровождается интересными и показательными примерами.
Здесь же будет больше о том, как это работает. Далее в этом разделе я приведу вольный и несколько переработанных перевод информации с сайта Adam Dunkels [1], создателя protothreads, где подробно описана как сама библиотека, так и детали реализации, так что желающие могут насладиться оригиналом и перейти к следующему разделу.
Основные особенности и преимущества protothreads:
Protothread’ы реализованы с помощью продолжений (continuation), которые представляют текущее состояние исполнения в конкретном месте в программе, но при этом не поддерживает историю вызовов и локальные переменные. Здесь нужно быть особо внимательными — использовать локальные переменные нужно очень аккуратно! Выделенного стека у protothread’ов нет, локальные переменные хранить негде. Если поток используется лишь однажды, то переменные можно объявить как статические. В противном случае можно, например, обернуть protothread в свою структуру, и хранить переменные прямо там. В общем, я хочу сказать, что нужно отдавать себе отчет в том, что неправильное использование локальных переменных может привести к непредсказуемым результатам.
В рассматриваемой реализации роль состояния играет целое положительное число — текущий номер строки программы. Управление происходит с помощью switch(), наподобие того, как это происходит в устройстве Даффа [7] и со-программах реализации Simon Tatham [8]. Protothread’ы похожи на генераторы в Python, только предназначены они для разных целей: protothread’ы предоставляют блокировку внутри функции на C, тогда как Python предоставляет множественный выход из генератора.
Важное ограничение реализации: код внутри protothread сам не может в полной мере использовать оператор switch(). Впрочем, это ограничение можно с помощью возможностей некоторых компиляторов, например, gcc.
Вся библиотека реализована на макросах, так как макросы, в отличие от простых функций, могут изменять поток управления только лишь средствами стандартных конструкций C.
Основное API включает в себя следующие макросы:
Разберемся теперь, как работают макросы. Для начала рассмотрим программу, использующую protothread’ы. Protothread example выполняет вечный цикл, где он сначала ждет, пока счетчик достигнет определенного значения, потом выводит сообщение и сбрасывает счетчик.
#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 reachedn");
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 (сокращенно от local continuation). Обратите внимание на PT_BEGIN и PT_END, которые соответственно открывают и закрывают оператор switch, а также на чуть более сложный макрос PT_WAIT_UNTIL. В нем используется встроенный макрос __LINE__, возвращающий номер текущей строки программного файла.
Сравним исходную и развернутую препроцессором версии example:
|
|
Protothread example теперь выглядит как обычная функция на C. Возвращаемое значение используется для того, чтобы определить статус protothread: заблокирован ли он в ожидании чего-то, завершен, вышел, или сгенерировал очередное значение. PT_BEGIN макрос содержит инструкцию case 0, таким образом, код, идущий сразу после этого макроса, будет исполняться первым, ведь начальное значение pt->lc и есть 0.
Посмотрите, во что развернулся макрос PT_WAIT_UNTIL. Полю pt->lc теперь присваивается 12, это номер строки, и тут же появляется case 12 — благодаря этому switch точно знает, куда прыгать. В случае, если условие не выполнено, функция возвращает 0, что означает, что поток ожидает (в самой библиотеки все эти константы вынесены). В следующий раз, когда в main вызовется example(), выполнение, соответственно, продолжится с case 12, то есть с проверки, выполнено ли условие ожидания. Как только счетчик достигнет 1000, условие станет истинным, и example() теперь не вернет 0, а напечатает сообщение и обнулит счетчик. Дальше, как положено, переходим в начало тела цикла.
В одной из предыдущих статей [4] я приводила код примитивного кооперативного планировщика (раздел “Невытесняющий планировщик с сохранением порядка вызовов”). Сделав незначительные изменения, можно адаптировать его под protothread’ы. Это довольно просто, поэтому код я приводить не буду. Но желающим предлагаю поиграться.
Напоследок предлагаю сравнить tasklet, workqueue и protothread. На самом деле, конечно, с одной стороны не очень корректно сравнивать protothread со средствами обработки bottom-half прерываний — все-таки они созданы для разных задач, нельзя так просто подменить одно другим. С другой стороны, workqueue тоже несколько выбивается из тройки: в отличие от остальных, он работает по правилам вытесняющего планирования, область применения у него куда шире.
Сравнение я привожу скорее для того, чтобы извлечь полезные идеи.
А вот и сравнительная таблица:
| Tasklet | Workqueue | Protothreads | |
|---|---|---|---|
| Наличие своего стека | Нет — обрабатываются как softirq (на специально выделенном общем для всех обработчиков стеке, по крайней мере в Linux на x86) | Да — исполняются на стеке worker-потоков, число которых сильно меньше, чем число задач | Нет |
| Скорость | Быстрые — минимальная дополнительная обработка | Быстрые, но не как tasklet’ы, требуется переключение контекста, когда worker’ы сменяют друг друга | Очень быстрые — практически нет дополнительной обработки, больше простора для оптимизации компилятором |
| Примитивы синхронизации | Отсутствуют (за исключением spinlock) | Присутствуют в полном объеме | Псевдо-семафоры; примитивное ожидание событий |
| Концепция планирования | Кооперативный планировщик как softirq; tasklet’ы вытесняются только аппаратными прерываниями | Worker’ы играют роль планировщика для work’ов, сами управляются основным планировщиком | Кооперативный планировщик, реализуется пользователем |
Каждый из этих подходов имеет свои плюсы и минусы, используются они для разных задач, в контексте которых отвечают запросам пользователей.
Например, в Embox [9] у нас возникла идея реализовать свои легкие потоки, у которых не будет своего стека, подобно protothread и tasklet, при этом будут управляться основным планировщикам, как это реализовано в workqueue, а также будут хоть в каком-то виде поддерживать механизм ожидания событий (даже более того — использовать подмножество того же API, что используют и полноценные потоки). У такого подхода есть несколько привлекательных для нас применений:
О том, как это у нас получилось, каких результатов мы добились, я расскажу в следующий раз, через некоторое время.
Автор: LifeV
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/kernel/75716
Ссылки в тексте:
[1] protothread: http://dunkels.com/adam/pt/
[2] Многозадачность в ядре Linux: прерывания и tasklet’ы: http://habrahabr.ru/post/244071/
[3] Многозадачность в ядре Linux: workqueue: http://habrahabr.ru/post/244155/
[4] одной из предыдущих статьей: http://habrahabr.ru/post/219431/
[5] статья: http://habrahabr.ru/post/143318/
[6] ldir: http://habrahabr.ru/users/ldir/
[7] устройстве Даффа: http://en.wikipedia.org/wiki/Duff%27s_device
[8] со-программах реализации Simon Tatham: http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
[9] Embox: https://code.google.com/p/embox/
[10] Источник: http://habrahabr.ru/post/244361/
Нажмите здесь для печати.