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 Метаданные: маленькие, но главные
Кроме двух слотов нужна запись о том, что в них лежит и какой грузить. В модели это 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, как до обрыва. Загрузчик взял её, прибор загрузился как ни в чём не бывало.
| Сценарий | Во что загрузились | Слот A | Слот B |
|---|---|---|---|
| 1. обычное обновление | B, v2 | v1 хороший | v2 хороший |
| 2. новая версия падает | B, v2 (откат на 4-й загрузке) | v3 плохой | v2 хороший |
| 3. обрыв при записи образа | B, v2 | пусто | v2 хороший |
| 4. обрыв при записи метаданных | B, v2 | пусто | v2 хороший |
195.4 Порядок действий, в котором вся соль
Схема устойчива только при строгом порядке записи. У обновителя:
- Объявить запасной слот пустым — до того, как тронуть его содержимое.
- Записать образ.
- Перечитать и сверить CRC (или подпись, раздел 48а).
- Только теперь: слот «на проверке», попыток 3, следующая загрузка — он.
Поменяйте 1 и 2 местами — и при обрыве на шаге 2 в метаданных будет «хороший» слот с половиной образа. Уберите шаг 3 — и загрузчик получит образ, который никто не проверял.
У загрузчика: сначала уменьшить попытки и записать это, потом грузить. Наоборот нельзя — зависшая новая версия будет грузиться вечно, ведь запись о попытке так и не случится.
У приложения: подтверждать после настоящей проверки здоровья — служба поднялась, связь с микроконтроллером есть (раздел 194), сеть отвечает. Иначе откат не сработает никогда: сломанная версия успеет подтвердить себя сама.
Кто перезагружает зависшую версию. Счётчик попыток работает, только если
сломанная система перезагрузится сама. После паники ядро Linux перезагружает прибор
лишь с параметром panic= в командной строке (число — секунды до перезагрузки; в
обычной сборке ядра по умолчанию 0 — ядро просто стоит), а зависшие служба или ядро
прибор не перезагрузят вовсе: попытки не расходуются, и v3 висит до прихода
человека. Нужен аппаратный сторожевой таймер: загрузчик включает его ещё до старта
ядра, а кормит служба после проверки здоровья. Тогда зависшая v3 перезагрузится по
сторожу, и попытки кончатся так же, как при падении.
195.5 Как это выглядит в настоящей системе
- Слоты — два раздела с корневой файловой системой на eMMC или SD (или два тома в UBI на NAND); часто только для чтения.
- Метаданные и счётчик попыток — в переменных окружения загрузчика или в отдельном маленьком разделе; у загрузчиков встраиваемого Linux для этого есть готовый механизм счёта загрузок.
- Обновитель — готовые системы обновления делают то же, что модель: пишут в неактивный слот, проверяют подпись и CRC, переключают.
- Данные пользователя — в третьем разделе, который обновление не трогает: иначе откат вернёт и старые настройки.
- Цена схемы — двойной объём под систему. На приборах с малой памятью вместо второй полной копии держат маленькую систему восстановления.
195.6 Грабли
Прибор не грузится после обрыва питания. Обновляли «на месте», поверх работающей системы. Только в запасной слот.
Обновление «прошло», а прибор грузит половину образа. Слот пометили до записи или не перечитали после. Сначала «пусто», потом запись, потом проверка, потом «на проверке».
Сломанная версия грузится снова и снова. Счётчик попыток уменьшают после загрузки, а не до. Или приложение подтверждает себя сразу при старте.
Новая версия зависла, а отката нет. Сторожевого таймера нет, panic= не
задан: прибор не перезагружается, попытки не расходуются.
После обрыва при записи метаданных — хаос. Копия метаданных одна. Нужны две, с номером записи и CRC; писать — в ту, что старее.
Откат вернул и старые настройки. Данные пользователя лежат в слоте системы. Им место в отдельном разделе.
Откатываться некуда. Второе обновление начали, пока первое ещё «на проверке», — и переписали запасной слот, единственный рабочий. Новое обновление — только после подтверждения текущей версии.
195.7 Итоги
- Два слота: работает один, новый пишется во второй; старый — запас для отката.
- Новая версия — «на проверке» с тремя попытками; рабочей становится только после подтверждения самим приложением.
- Модель: сломанная v3 откатилась на v2 на 4-й загрузке; обрыв при записи образа не включил слот; обрыв при записи метаданных оставил целой прежнюю копию (номер записи 12) — прибор загрузился.
- Метаданные — 52 байта в двух копиях, каждая под CRC-32; читается новейшая целая.
- Порядок: «пусто» → запись → проверка → «на проверке»; попытку записывают до загрузки; подтверждают после проверки здоровья.
Полные исходники
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».