Embedded HandbookПриложения
Все разделы › XXIV. Встраиваемый Linux
· уровень средний · 13 мин чтения
Открыть в интерактивном справочнике

195. Обновление по схеме A/B: два слота, подтверждение и откат

В разделе 193 программу прибора обновляли, подменяя файл и перезапуская службу. Для обновления всей системы — ядра, библиотек, службы вместе — так нельзя: пропадёт питание посреди записи, и прибор не загрузится вовсе. Схема A/B решает это двумя копиями системы. Работает одна, новая пишется во вторую; загрузчик переключается на неё только после полной записи, а если новая не заработала — возвращается на старую сам.

Раздел — модель такой схемы на C (_verify/linux195/ab195.c): вместо разделов флеш-памяти — файлы, но логика та же, что у настоящих систем обновления. Модель прогнана на четырёх сценариях, включая обрыв питания посреди записи образа и посреди записи метаданных. Сверяет _verify/linux195_check.py.

Как это называется в документации

Оригинал Перевод
A/B (dual-copy) update обновление с двумя копиями системы
slot, active / inactive slot слот, рабочий и запасной
boot counter, boot attempts счётчик попыток загрузки
mark good, commit подтверждение: новая версия признана рабочей
rollback, fallback откат на прежнюю версию
atomic update неделимое обновление: либо целиком, либо никак
power-fail safe устойчивое к пропаданию питания

195.1 Три участника

Кто Что делает Когда
обновитель (программа в работающей системе) пишет новый образ в запасной слот, проверяет его, помечает «на проверке» и назначает следующей загрузкой когда пришло обновление
загрузчик читает метаданные, у слота «на проверке» уменьшает число попыток; попыток нет — помечает слот плохим и грузит хороший при каждом включении
приложение проверяет, что всё работает, и подтверждает слот после загрузки

Ключевое слово — подтверждение. Новая версия считается рабочей не когда записана и не когда загрузилась, а когда сама сказала «я в порядке». Не сказала за три попытки — откат.


195.2 Метаданные: маленькие, но главные

Схема A/B: два слота, двойные метаданные, путь загрузчика и отката

Кроме двух слотов нужна запись о том, что в них лежит и какой грузить. В модели это 52 байта: номер записи, какой слот загружать, и для каждого слота — состояние, версия, оставшиеся попытки, размер и CRC-32 образа; вся запись закрыта своим CRC-32.

Метаданные меняются при каждой загрузке пробной версии — и тоже могут оборваться на середине. Поэтому их две копии: новая запись всегда идёт в ту копию, что старее, а при чтении берётся самая новая целая:

int va = read_file("meta0.bin", &a, sizeof a) == sizeof a && meta_ok(&a);
int vb = read_file("meta1.bin", &b, sizeof b) == sizeof b && meta_ok(&b);
if (!va && !vb) return -1;
*m = (va && (!vb || a.seq > b.seq)) ? a : b;

Оборвалась запись — у этой копии не сойдётся CRC, и загрузчик возьмёт вторую, с предыдущим состоянием. Неделимость здесь даёт не файловая система, а порядок записи.


195.3 Четыре сценария

Завод: версия 1 в слоте A, подтверждена.

1. Обычное обновление до v2. Образ пишется в B, B — «на проверке», попыток 3. Загрузка: слот B, попыток осталось 2. Приложение проверило себя и подтвердило B. Итог: A — v1 хороший, B — v2 хороший, грузится B. Старая версия осталась на месте — это и есть запас для отката.

2. Обновление до v3, которое падает при каждом запуске. v3 пишется в A (теперь запасной — он). Три загрузки подряд, попыток остаётся 2, 1, 0 — и каждый раз приложение падает до подтверждения. Четвёртая загрузка: «попытки кончились — слот A плохой, откат на B». Итог: A — v3 плохой, B — v2 хороший, прибор работает на v2 без вмешательства человека.

3. Пропало питание посреди записи образа v4. Записалось 3000 байт из 8000; обновитель перечитал слот, CRC не сошёлся — слот не включён. Метаданные ещё до записи объявили этот слот пустым, так что загрузчик и не пытался его грузить. Итог: грузится B, v2.

4. Пропало питание посреди записи метаданных. Запись оборвалась на 10-м байте. После этого целой осталась одна копия — прежняя: номер записи 12, как до обрыва. Загрузчик взял её, прибор загрузился как ни в чём не бывало.

Сценарий 2 по шагам: три загрузки новой версии с падением, на четвёртой — откат на старый слот

Сценарий Во что загрузились Слот A Слот B
1. обычное обновление B, v2 v1 хороший v2 хороший
2. новая версия падает B, v2 (откат на 4-й загрузке) v3 плохой v2 хороший
3. обрыв при записи образа B, v2 пусто v2 хороший
4. обрыв при записи метаданных B, v2 пусто v2 хороший

195.4 Порядок действий, в котором вся соль

Схема устойчива только при строгом порядке записи. У обновителя:

  1. Объявить запасной слот пустым — до того, как тронуть его содержимое.
  2. Записать образ.
  3. Перечитать и сверить CRC (или подпись, раздел 48а).
  4. Только теперь: слот «на проверке», попыток 3, следующая загрузка — он.

Поменяйте 1 и 2 местами — и при обрыве на шаге 2 в метаданных будет «хороший» слот с половиной образа. Уберите шаг 3 — и загрузчик получит образ, который никто не проверял.

У загрузчика: сначала уменьшить попытки и записать это, потом грузить. Наоборот нельзя — зависшая новая версия будет грузиться вечно, ведь запись о попытке так и не случится.

У приложения: подтверждать после настоящей проверки здоровья — служба поднялась, связь с микроконтроллером есть (раздел 194), сеть отвечает. Иначе откат не сработает никогда: сломанная версия успеет подтвердить себя сама.

Кто перезагружает зависшую версию. Счётчик попыток работает, только если сломанная система перезагрузится сама. После паники ядро Linux перезагружает прибор лишь с параметром panic= в командной строке (число — секунды до перезагрузки; в обычной сборке ядра по умолчанию 0 — ядро просто стоит), а зависшие служба или ядро прибор не перезагрузят вовсе: попытки не расходуются, и v3 висит до прихода человека. Нужен аппаратный сторожевой таймер: загрузчик включает его ещё до старта ядра, а кормит служба после проверки здоровья. Тогда зависшая v3 перезагрузится по сторожу, и попытки кончатся так же, как при падении.


195.5 Как это выглядит в настоящей системе


195.6 Грабли

Прибор не грузится после обрыва питания. Обновляли «на месте», поверх работающей системы. Только в запасной слот.

Обновление «прошло», а прибор грузит половину образа. Слот пометили до записи или не перечитали после. Сначала «пусто», потом запись, потом проверка, потом «на проверке».

Сломанная версия грузится снова и снова. Счётчик попыток уменьшают после загрузки, а не до. Или приложение подтверждает себя сразу при старте.

Новая версия зависла, а отката нет. Сторожевого таймера нет, panic= не задан: прибор не перезагружается, попытки не расходуются.

После обрыва при записи метаданных — хаос. Копия метаданных одна. Нужны две, с номером записи и CRC; писать — в ту, что старее.

Откат вернул и старые настройки. Данные пользователя лежат в слоте системы. Им место в отдельном разделе.

Откатываться некуда. Второе обновление начали, пока первое ещё «на проверке», — и переписали запасной слот, единственный рабочий. Новое обновление — только после подтверждения текущей версии.


195.7 Итоги

Полные исходники

ab195.c — модель обновления A/B с откатом на файлах.

/* Раздел 195: обновление по схеме A/B с откатом — модель на файлах.
 *
 * «Флеш» — каталог: slotA.img, slotB.img и две копии метаданных meta0.bin, meta1.bin.
 * Метаданные: номер записи (растёт), какой слот загружать, для каждого слота — состояние,
 * версия, оставшиеся попытки, CRC-32 образа; вся запись под своим CRC-32.
 * Запись метаданных — всегда в ту копию, что старее; при чтении берётся самая новая целая.
 * Загрузчик: слот «на проверке» грузит, уменьшив попытки; попытки кончились — слот «плохой»,
 * грузится «хороший». Приложение после проверки здоровья подтверждает слот. */
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

enum state { EMPTY, PENDING, GOOD, BAD };
static const char *const ST[] = {"пусто", "на проверке", "хороший", "плохой"};

struct slot { uint32_t state, version, tries, size, crc; };
struct meta { uint32_t seq, boot; struct slot s[2]; uint32_t crc; };

static char dir[256];

static uint32_t crc32(const void *p, size_t n)
{
    const uint8_t *b = p;
    uint32_t c = 0xFFFFFFFFu;
    while (n--) {
        c ^= *b++;
        for (int k = 0; k < 8; k++)
            c = (c >> 1) ^ (0xEDB88320u & -(c & 1));
    }
    return ~c;
}

static void path(char *out, const char *name) { snprintf(out, 300, "%s/%s", dir, name); }

static int write_file(const char *name, const void *p, size_t n, size_t cut)   /* cut — оборвать после стольких байт */
{
    char f[300];
    path(f, name);
    FILE *fp = fopen(f, "wb");
    if (!fp) return -1;
    fwrite(p, 1, cut < n ? cut : n, fp);
    fflush(fp);
    fsync(fileno(fp));
    fclose(fp);
    return cut < n ? -1 : 0;
}

static size_t read_file(const char *name, void *p, size_t n)
{
    char f[300];
    path(f, name);
    FILE *fp = fopen(f, "rb");
    if (!fp) return 0;
    size_t r = fread(p, 1, n, fp);
    fclose(fp);
    return r;
}

static int meta_ok(const struct meta *m) { return m->crc == crc32(m, offsetof(struct meta, crc)); }

static int load_meta(struct meta *m)
{
    struct meta a, b;
    int va = read_file("meta0.bin", &a, sizeof a) == sizeof a && meta_ok(&a);
    int vb = read_file("meta1.bin", &b, sizeof b) == sizeof b && meta_ok(&b);
    if (!va && !vb) return -1;
    *m = (va && (!vb || a.seq > b.seq)) ? a : b;
    return (va && vb) ? 2 : 1;                       /* сколько копий целы */
}

static void save_meta(struct meta *m, size_t cut)
{
    struct meta cur;
    int have = load_meta(&cur);
    m->seq = have > 0 ? cur.seq + 1 : 1;
    m->crc = crc32(m, offsetof(struct meta, crc));
    /* пишем в ту копию, что НЕ содержит текущей записи */
    struct meta a;
    int va = read_file("meta0.bin", &a, sizeof a) == sizeof a && meta_ok(&a);
    const char *target = (va && have > 0 && a.seq == cur.seq) ? "meta1.bin" : "meta0.bin";
    write_file(target, m, sizeof *m, cut);
}

static uint8_t img[8192];

static void make_image(uint32_t version, size_t n)
{
    for (size_t i = 0; i < n; i++)
        img[i] = (uint8_t)(version * 31 + i * 7);
}

/* ── обновитель: пишет в неактивный слот, проверяет, ставит «на проверке» ── */
static int update(uint32_t version, size_t n, size_t power_cut_at)
{
    struct meta m;
    load_meta(&m);
    int tgt = 1 - (int)m.boot;
    make_image(version, n);
    const char *name = tgt ? "slotB.img" : "slotA.img";
    m.s[tgt] = (struct slot){EMPTY, 0, 0, 0, 0};       /* сначала объявить слот пустым */
    save_meta(&m, (size_t)-1);
    write_file(name, img, n, power_cut_at);
    /* после (возможного) обрыва — проверка того, что реально лежит в слоте */
    static uint8_t back[8192];
    size_t got = read_file(name, back, sizeof back);
    if (got != n || crc32(back, n) != crc32(img, n)) {
        printf("  обновление v%u: образ в слоте %c не сошёлся по CRC (%zu из %zu байт) — слот не включён\n",
               version, "AB"[tgt], got, n);
        return -1;
    }
    load_meta(&m);
    m.s[tgt] = (struct slot){PENDING, version, 3, (uint32_t)n, crc32(img, n)};
    m.boot = (uint32_t)tgt;
    save_meta(&m, (size_t)-1);
    printf("  обновление v%u: записано в слот %c, слот «на проверке», попыток 3\n", version, "AB"[tgt]);
    return 0;
}

/* ── загрузчик ── */
static int boot(void)
{
    struct meta m;
    int copies = load_meta(&m);
    int b = (int)m.boot;
    if (m.s[b].state == PENDING) {
        if (m.s[b].tries == 0) {
            m.s[b].state = BAD;
            b = 1 - b;
            m.boot = (uint32_t)b;
            save_meta(&m, (size_t)-1);
            printf("  загрузка: попытки кончились — слот %c плохой, откат на %c\n", "AB"[1 - b], "AB"[b]);
        } else {
            m.s[b].tries--;
            save_meta(&m, (size_t)-1);
        }
    }
    /* образ проверяется всегда: загрузчик не запускает то, что не сходится с CRC в метаданных */
    static uint8_t back[8192];
    size_t got = read_file(b ? "slotB.img" : "slotA.img", back, sizeof back);
    int intact = got == m.s[b].size && crc32(back, got) == m.s[b].crc;
    printf("  загрузка: слот %c, v%u, %s, попыток осталось %u, образ %s, копий метаданных целы: %d\n",
           "AB"[b], m.s[b].version, ST[m.s[b].state], m.s[b].tries, intact ? "цел" : "ИСПОРЧЕН", copies);
    return b;
}

/* ── приложение: проверка здоровья и подтверждение ── */
static void confirm(int healthy)
{
    struct meta m;
    load_meta(&m);
    int b = (int)m.boot;
    if (healthy && m.s[b].state == PENDING) {
        m.s[b].state = GOOD;
        m.s[b].tries = 0;
        save_meta(&m, (size_t)-1);
        printf("  приложение v%u: проверка прошла, слот %c подтверждён\n", m.s[b].version, "AB"[b]);
    } else if (!healthy) {
        printf("  приложение v%u: упало до подтверждения\n", m.s[b].version);
    }
}

static void status(const char *title)
{
    struct meta m;
    load_meta(&m);
    printf("  [%s] seq=%u загружать %c | A: v%u %s | B: v%u %s\n", title, m.seq, "AB"[m.boot],
           m.s[0].version, ST[m.s[0].state], m.s[1].version, ST[m.s[1].state]);
}

int main(void)
{
    snprintf(dir, sizeof dir, "/tmp/ab195");
    mkdir(dir, 0755);
    /* завод: v1 в слоте A, подтверждён */
    make_image(1, 6000);
    write_file("slotA.img", img, 6000, (size_t)-1);
    struct meta m = {0, 0, {{GOOD, 1, 0, 6000, crc32(img, 6000)}, {EMPTY, 0, 0, 0, 0}}, 0};
    save_meta(&m, (size_t)-1);
    status("завод");

    printf("1) обычное обновление до v2\n");
    update(2, 7000, (size_t)-1);
    boot();
    confirm(1);
    status("после 1");

    printf("2) обновление до v3, которое падает при каждом запуске\n");
    update(3, 7500, (size_t)-1);
    for (int i = 0; i < 4; i++) {
        int b = boot();
        struct meta mm;
        load_meta(&mm);
        if (mm.s[b].version == 3)
            confirm(0);
        else
            confirm(1);
    }
    status("после 2");

    printf("3) пропало питание посреди записи образа v4\n");
    update(4, 8000, 3000);
    boot();
    status("после 3");

    printf("4) пропало питание посреди записи метаданных\n");
    load_meta(&m);
    struct meta before = m;
    m.s[0].version = 99;                                /* какая-то правка метаданных... */
    save_meta(&m, 10);                                  /* ...оборвалась на 10-м байте */
    struct meta after;
    int copies = load_meta(&after);
    printf("  метаданные после обрыва: целых копий %d, seq=%u (был %u), A: v%u\n", copies, after.seq, before.seq,
           after.s[0].version);
    boot();
    status("после 4");
    printf("meta_bytes=%zu\n", sizeof(struct meta));
    return 0;
}

Дальше: раздел 196 «Мини-проект: термостат на BluePill».

Открыть в интерактивном справочнике