1. Тема дня

Нужно обновлять конфигурацию, пока realtime-задачи продолжают её читать. Например:

text
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:

text
threshold = generation 42
hysteresis = generation 41
debounce = generation 42

Нужна атомарная публикация целого snapshot.

Immutable snapshot

c
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

c
/* 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:

c
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:

c
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.

c
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:

text
parse -> validate -> write inactive persistent slot -> commit -> read-back -> publish runtime snapshot -> ACK backend

Для опасных сетевых настроек полезен trial mode: сначала применить runtime, проверить связь, потом persist.

4. Типичные ошибки

  1. Менять active snapshot на месте.
  2. Считать atomic pointer полной реализацией RCU.
  3. Переиспользовать inactive buffer сразу после swap.
  4. Держать hazard через сетевую операцию.
  5. Хранить pointer на временную строку внутри snapshot.
  6. Обновлять отдельные atomics вместо всей конфигурации.
  7. Публиковать candidate до validation.
  8. Проверять config в fast path.
  9. Забыть generation.
  10. ACK backend до persistent commit.
  11. Persist опасной сетевой config без trial.
  12. Использовать RCU там, где достаточно owner-task local copy.
  13. Читать сложный snapshot из ISR.

5. Практическое задание

Сделай runtime-config для phase и EC25:

c
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:

c
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.
После атомарной публикации snapshot B может ли writer сразу перезаписать старый snapshot A?

Задание

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 на определённой границе событий.