1. Тема дня
Нужно обновлять конфигурацию, пока realtime-задачи продолжают её читать. Например:
phase thresholds
debounce/hysteresis
GPIO masks
EC25 timeouts
retry/backoff policy
MQTT keepalive
CAN/RS-485 параметрыГлавная мысль: reader не должен видеть конфигурацию, которую writer ещё изменяет. Writer строит новый immutable snapshot и одной атомарной операцией публикует его.
2. Зачем это нужно
Для phase task mutex на каждом ADC block добавит jitter. Для EC25 важно не получить смесь старых и новых at_timeout_ms, retry_limit, max_backoff_ms. Для MQTT нельзя читать hostname, port и topic prefix в момент их частичного обновления.
3. Теория
Почему atomic-поля недостаточны
Если сделать атомарными каждое поле, reader может увидеть смесь generations:
threshold = generation 42
hysteresis = generation 41
debounce = generation 42Нужна атомарная публикация целого snapshot.
Immutable snapshot
typedef struct {
uint32_t schema;
uint32_t generation;
phase_config_t phase;
modem_config_t modem;
mqtt_config_t mqtt;
} app_config_snapshot_t;После публикации объект считается const.
Double buffer и проблема lifetime
Схема A/B решает publication, но не lifetime старого snapshot. Reader мог взять pointer на A, writer опубликовал B и начал переписывать A — возникает race.
Grace period и hazard pointer
/* Editorial alternative: a queue copies runtime_config_t by value.
* Only phase_task owns and reads local_cfg; no shared pointer is reclaimed.
* Create a bounded queue of sizeof(runtime_config_t) before starting tasks.
* Producers validate candidates and report a full queue instead of blocking forever.
*/
static void phase_apply_pending_config(QueueHandle_t queue,
runtime_config_t *local_cfg)
{
runtime_config_t candidate;
/* At most one candidate per cycle: bounded fast-path work. */
if (xQueueReceive(queue, &candidate, 0) == pdPASS &&
runtime_config_validate(&candidate)) {
*local_cfg = candidate;
}
}
/* Call at a cycle boundary, then process the whole cycle using local_cfg.
* Define generation/epoch acceptance separately; validation is not authorization.
*/Linux RCU: время жизни и grace period
Каждый reader публикует hazard pointer: «я сейчас использую этот snapshot». Reader pattern:
static const app_config_snapshot_t *config_read_acquire(config_reader_id_t reader)
{
for (;;) {
app_config_snapshot_t *snapshot = atomic_load_explicit(&s_active_config, memory_order_acquire);
atomic_store_explicit(&s_hazards[reader], snapshot, memory_order_release);
app_config_snapshot_t *verify = atomic_load_explicit(&s_active_config, memory_order_acquire);
if (snapshot == verify) {
return snapshot;
}
atomic_store_explicit(&s_hazards[reader], NULL, memory_order_release);
}
}Release:
static void config_read_release(config_reader_id_t reader)
{
atomic_store_explicit(&s_hazards[reader], NULL, memory_order_release);
}Короткий read section
Плохо удерживать shared snapshot через network operation или vTaskDelay(). Лучше скопировать маленькую subconfig и сразу отпустить shared snapshot.
phase_config_t local;
const app_config_snapshot_t *cfg = config_read_acquire(CONFIG_READER_PHASE);
local = cfg->phase;
uint32_t generation = cfg->generation;
config_read_release(CONFIG_READER_PHASE);
phase_process(samples, &local, generation);Persistent и runtime config
Persistent update и runtime publication — разные transaction bound- aries. Для durable remote config:
parse -> validate -> write inactive persistent slot -> commit -> read-back -> publish runtime snapshot -> ACK backendДля опасных сетевых настроек полезен trial mode: сначала применить runtime, проверить связь, потом persist.
4. Типичные ошибки
- Менять active snapshot на месте.
- Считать atomic pointer полной реализацией RCU.
- Переиспользовать inactive buffer сразу после swap.
- Держать hazard через сетевую операцию.
- Хранить pointer на временную строку внутри snapshot.
- Обновлять отдельные atomics вместо всей конфигурации.
- Публиковать candidate до validation.
- Проверять config в fast path.
- Забыть generation.
- ACK backend до persistent commit.
- Persist опасной сетевой config без trial.
- Использовать RCU там, где достаточно owner-task local copy.
- Читать сложный snapshot из ISR.
5. Практическое задание
Сделай runtime-config для phase и EC25:
typedef struct {
uint16_t on_mv;
uint16_t off_mv;
uint16_t debounce_ms;
} phase_channel_config_t;
typedef struct {
phase_channel_config_t red;
phase_channel_config_t yellow;
phase_channel_config_t green;
} phase_runtime_config_t;
typedef struct {
uint32_t at_timeout_ms;
uint32_t registration_timeout_ms;
uint32_t backoff_ms;
uint32_t max_backoff_ms;
uint8_t retry_limit;
} modem_runtime_config_t;
typedef struct {
uint32_t schema;
uint32_t generation;
phase_runtime_config_t phase;
modem_runtime_config_t modem;
} runtime_config_t;Реализуй validator:
static bool runtime_config_validate(const runtime_config_t *cfg)
{
if (cfg == NULL || cfg->schema != 1U) return false;
if (cfg->phase.red.off_mv >= cfg->phase.red.on_mv) return false;
if (cfg->modem.backoff_ms > cfg->modem.max_backoff_ms) return false;
if (cfg->modem.retry_limit > 10U) return false;
return true;
}Сделай два reader ID: PHASE, MODEM. Напиши host stress test: writer меняет generation, readers постоянно проверяют invariants.
6. Что попробовать дальше
- Сначала попробовать owner-task local config: для phase_task часто проще переслать новую subconfig сообщением.
- Для ISR публиковать только маленькие precomputed atomic scalars.
- Для быстрых последовательных обновлений перейти с dou- ble buffer на triple buffer.
Задание
Phase reader взял A, затем writer опубликовал B. Опишите race при немедленной перезаписи A. Назовите более простую owner-task альтернативу из урока, не объявляя показанные atomics полной переносимой реализацией RCU.
Критерии самопроверки: Разделите publication и lifetime; опишите race и альтернативу task-owned local copy.
Показать ответ автора
Reader читает A одновременно с перезаписью, поэтому configuration перестаёт быть согласованным immutable snapshot. Writer должен дождаться условий lifetime/reclamation либо отправить validated subconfiguration сообщением phase owner-task, которая обновляет и читает свою local copy на определённой границе событий.