OS·V Операционная система Глава 40 из 65

Спасательная операция

В 1998 году одна команда на сервере Pixar стёрла почти весь мультфильм «История игрушек 2», а резервные копии оказались негодными. Фильм спас компьютер, стоявший у сотрудницы дома. Впереди учения спасателей: пять аварий — погасший свет, сгоревший диск, тихая порча битов, ошибка человека и потерянная версия — и для каждой свой способ выжить, вплоть до git, который вы соберёте сами.

Университет 60 минут Операционные системы Практика История
OS·V

Операционная система

  1. 36 ОС
  2. 37 Планировщик
  3. 38 Виртуальная память
  4. 39 Конкурентность
  5. 40 Хранение вы здесь

Опирается на: 36 · Экскурсия по живой системе 17 · Сад деревьев поиска 16 · Хеш-таблица: атака и защита

Что вы унесёте из главы

  • сохранять файл так, чтобы сбой посередине не оставил половину: временный файл, fsync и атомарная замена
  • восстанавливать потерянный диск по чётности XOR и проверять данные контрольными суммами
  • понимать, как git хранит версии: объекты по адресу-хешу, деревья и коммиты, — и собрать свой маленький git

Прошлая глава закончилась тем, что все наши аккуратно сосчитанные просмотры живут в памяти процесса и исчезнут, стоит выключить свет. Память из триггеров, которую мы собирали в главе 31, помнит, только пока течёт ток. Чтобы данные пережили выключение, их кладут туда, где они держатся без электричества: на магнитный диск или во флеш-память. Но положить мало. Свет гаснет посреди записи, диски ломаются, биты портятся сами собой, а люди ошибаются чаще, чем железо. Эта глава — учения: пять аварий, и после каждой мы смотрим, что уцелело и что надо было сделать, чтобы уцелело всё.

Pixar, 1998. Фильм исчезает

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

Где лежат данные

Для программы диск выглядит как огромный массив пронумерованных блоков — как память из главы 14, только читать и писать его приходится целыми блоками, обычно по 4096 байт. Как диск устроен внутри, программе неважно. В жёстком диске это намагниченные участки на вращающихся пластинах: чтобы прочесть блок, головка должна доехать до нужной дорожки и дождаться, пока нужное место подплывёт под неё, — это миллисекунды, по меркам процессора вечность, как мы считали в главе 34. В твердотельном накопителе блоки лежат во флеш-памяти, механики нет, и блок читается примерно в сто раз быстрее. Зато у флеш-памяти свой характер: стирать её можно только большими кусками, а каждая ячейка выдерживает ограниченное число перезаписей. Поэтому контроллер SSD держит собственную таблицу «номер блока для программы → место в микросхеме» и раскладывает записи равномерно по всем ячейкам — та же идея, что таблица страниц из главы 38.

Файл всегда занимает целое число блоков, даже если в нём один байт.

Один байт занимает 4096 байт, а 4097 байт — уже 8192: второй блок начат, и он весь наш. Последняя строка — признание. Когда писалась глава, каталог /tmp песочницы был устроен как tmpfs — файловая система в оперативной памяти. Всё, что ваши ячейки сюда пишут, исчезает вместе с песочницей. Для опытов этой главы так даже удобнее: аварии мы будем устраивать понарошку, а законы, по которым живут блоки и файлы, от этого не меняются.

Оглавление: файловая система

Массив блоков — ещё не файлы. Надо где-то записать, какие блоки принадлежат какому файлу, как файл называется, кто его владелец и когда его меняли. Этим занимается файловая система. В Unix у каждого файла есть индексный узел, по-английски inode: запись с размером, владельцем, правами, временем изменения и списком блоков, где лежит содержимое. Узлы пронумерованы. Деннис Ричи потом говорил, что и сам не уверен, откуда буква i, и лучшая его догадка — index: номер узла служил индексом в массиве узлов на диске.

Имени в узле нет. Имена живут в каталогах, а каталог — это особый файл, содержимое которого — таблица «имя → номер узла». Каталоги лежат в каталогах, и получается дерево из главы 17: путь /tmp/film/woody.txt — это спуск от корня по веткам с именами tmp, film, woody.txt. Раз имя — всего лишь строка в таблице, у одного узла может быть несколько имён.

Оба имени ведут к одному узлу, и у узла счётчик имён — 2. Команда rm стирает не файл, а строку в каталоге и уменьшает счётчик. Пока у узла остались имена, содержимое живо; когда счётчик доходит до нуля, файловая система отмечает узел и его блоки свободными. Сами байты при этом обычно никто не затирает — они лежат, пока на их место не запишут что-то новое. Поэтому удалённое иногда удаётся достать специальными программами, и поэтому первое, что сделали в Pixar, — остановили серверы: каждая новая запись могла лечь на место стёртых сцен. Это знание полезно и вам: заметили, что стёрли нужное с флешки, — ничего на неё не пишите.

Учение 1. Погас свет

Сценарий первый: программа сохраняет документ, и посреди записи пропадает питание. Что окажется на диске?

Запись файла складывается из нескольких действий. open(path, "w") сразу обрезает файл до нуля: старой версии больше нет, а новой ещё нет. Потом новое содержимое уходит кусками. И даже «записанное» не обязательно на диске: write возвращается, как только ядро положило байты в свой кэш в памяти, а на диск они отправятся позже, когда ядру будет удобно, — это кэш из главы 34, только между памятью и диском. Выключить свет в песочнице мы не можем, но можем сделать почти то же: оборвать процесс посреди записи. Процесс-ребёнок сохраняет новую версию сцены, а родитель через 0,05 секунды убивает его сигналом, который нельзя перехватить.

Наивное сохранение оставило на диске обрубок: старых строк ноль, новых — сколько успело записаться (когда писалась глава, 454 из тысячи, а иногда 363: на один кусок меньше). Пропали обе версии. Безопасное сохранение оставило старую версию нетронутой: обрыв пришёлся на временный файл, а его потерять не жалко.

Рецепт безопасного сохранения стоит запомнить, им пользуются текстовые редакторы, базы данных и программы установки. Пишем новую версию во временный файл в том же каталоге. Вызываем os.fsync — он не возвращается, пока ядро не отправит байты файла на диск. И только потом os.replace переименовывает временный файл, и тот занимает место старого. Переименование внутри одной файловой системы атомарно: это одна правка строки в каталоге, и любой, кто откроет файл, увидит либо старую версию целиком, либо новую. После аварии на диске будет старый файл и, может быть, недописанный временный — но не обрубок. Для полной уверенности осторожные программы вызывают fsync ещё и для самого каталога, чтобы на диск попало и переименование.

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

Журнал

Та же беда подстерегает и саму файловую систему. Чтобы дописать в файл один блок, ей нужно изменить несколько мест на диске: сам блок с данными, карту свободных блоков (теперь этот блок занят), индексный узел (теперь блок в его списке, и размер другой). Свет может погаснуть между любыми двумя записями. Блок отмечен занятым, но ни одному файлу не принадлежит — место утекло. Узел ссылается на блок, в который так и не записали данные, — в файле чужой мусор. Раньше после каждого сбоя запускали программу проверки, которая обходила весь диск и сводила концы с концами; на больших дисках она работала часами.

Современные файловые системы ведут журнал. Прежде чем что-то менять на местах, они записывают в отдельную область все задуманные изменения одной записью и ставят в конце отметку «готово». Потом вносят изменения на места и стирают запись. Если свет погас до отметки, запись в журнале неполная: её выбрасывают, и диск остаётся в состоянии «до». Если после — при включении записи журнала повторяют, и диск приходит в состояние «после». Середины не бывает. Ту же идею, «сначала запиши, что собираешься сделать», мы встретим в базах данных в главе 46.

Выдерните шнур. Файловая система дописывает в файл новый блок: несколько записей в разные места диска. Нажмите «Выдернуть шнур» в любой момент, а проверка после включения покажет, что стало с диском. Потом включите журнал и попробуйте снова: где ни выдерни, диск окажется либо «до», либо «после».

Учение 2. Сгорел диск

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

Первое, что приходит в голову, — зеркало: два диска с одинаковым содержимым. Умер один — работаем на втором, ставим новый и копируем на него. Надёжно, но половина места уходит на копию. В 1988 году Дэвид Паттерсон, Гарт Гибсон и Рэнди Кац из Беркли опубликовали статью «Доводы в пользу избыточных массивов недорогих дисков» — так появилось слово RAID. Мысль была такая: вместо одного дорогого большого диска поставить много дешёвых маленьких. Но чем больше дисков, тем чаще какой-нибудь из них ломается, поэтому массиву нужна избыточность, и не обязательно вдвое. Хватает одного лишнего диска на весь массив, если хранить на нём чётность.

Чётность — это исключающее «или» из главы 29, применённое к целым блокам, бит за битом. У XOR два свойства, на которых держится всё остальное: $a \oplus a = 0$ и $a \oplus 0 = a$. Пусть на трёх дисках лежат блоки $d_1, d_2, d_3$, а на четвёртом — $p = d_1 \oplus d_2 \oplus d_3$. Если пропал $d_2$, сложим по XOR всё, что осталось: $d_1 \oplus d_3 \oplus p = d_1 \oplus d_3 \oplus d_1 \oplus d_2 \oplus d_3 = d_2$, потому что каждый из уцелевших встречается дважды и гасит сам себя.

Байты чётности сами по себе бессмысленны, но из них и двух уцелевших дисков BUZZ собирается обратно. Так устроен RAID-5. Данные режут на полосы: в каждой полосе на всех дисках, кроме одного, лежат блоки данных, а на оставшемся — их чётность. Диск с чётностью меняется от полосы к полосе, чтобы ни один диск не стал узким местом: чётность пересчитывается при каждой записи, и если бы она жила на одном диске, он работал бы за всех.

RAID-5 из четырёх дисков. Цветные клетки — блоки данных, клетки с ⊕ — чётность своей полосы. Нажмите на диск, чтобы он сгорел: массив продолжит отдавать данные, восстанавливая каждый блок по XOR уцелевших. «Заменить диск» запустит восстановление полоса за полосой. Попробуйте сжечь второй диск, пока первый ещё не восстановлен.

У RAID-5 есть слабое место — время восстановления. Чтобы пересчитать умерший диск, надо прочесть все остальные от начала до конца, а на больших дисках это часы и даже сутки. Если за это время умрёт второй диск, данные пропали: одно уравнение с двумя неизвестными не решить. Поэтому большие массивы держат две независимые чётности (RAID-6) и переживают смерть любых двух дисков.

RAID защищает от поломки железа, но не от ошибок. Команду rm -r -f * массив выполнит на всех дисках сразу и так же быстро, как один диск, — и чётность честно пересчитает пустоту. RAID — не резервная копия.

Учение 3. Тихая порча

Сценарий третий — самый коварный: диск жив, на вопросы отвечает, но в одном из блоков перевернулся бит. Виноваты могут быть частица космических лучей, стареющая ячейка флеш-памяти, ошибка в прошивке контроллера. Диск отдаёт блок как ни в чём не бывало, и испорченная фотография или таблица живёт годами, пока её не откроют. Она попадает в резервные копии, и порча расходится по всем.

Защита — контрольная сумма: короткое число, которое вычисляют по блоку при записи и хранят рядом, а при чтении вычисляют заново и сравнивают. Простейшая сумма нам знакома — бит чётности из главы 31, которым проверяли верёвочную память «Аполлона». Посмотрим, что она ловит, а что нет, рядом с двумя суммами посильнее.

Один перевёрнутый бит меняет чётность, а два возвращают её на место: бит чётности слеп к любому чётному числу ошибок. CRC-32 — циклический код, который считают в Ethernet, архивах ZIP и картинках PNG, — меняется в обоих случаях. Он придуман так, чтобы гарантированно ловить все короткие пачки испорченных битов подряд, а случайную порчу пропускает примерно в одном случае из четырёх миллиардов. SHA-256 — криптографическая хеш-функция, родственница хешей из главы 16, но построенная так, что подобрать два разных блока с одинаковым хешем никто не умеет даже нарочно. Она длиннее и медленнее, зато годится не только против случайной порчи, но и против подделки.

Файловые системы ZFS и Btrfs хранят контрольную сумму каждого блока и проверяют её при каждом чтении. Если диск отдал испорченный блок, а у массива есть зеркало или чётность, файловая система возьмёт исправный экземпляр и перепишет плохой — порча лечится сама, прежде чем её заметит человек. А контрольные суммы скачанных файлов, которые публикуют рядом со ссылками, отвечают на тот же вопрос в сети: дошёл ли файл таким, каким его отправили. Свою контрольную сумму — ту, что работает внутри каждого потока zlib, — вы посчитаете в задаче «Сумма с памятью».

Учение 4. Ошибся человек

Сценарий четвёртый — тот, что случился в Pixar. Железо исправно, биты целы, журнал ведётся. Человек отдал команду, система её выполнила. От этого не спасают ни журнал, ни RAID, ни контрольные суммы: все они берегут данные в том виде, в каком им велели их хранить. Спасает только копия, до которой команда не дотянулась. Сюда же относятся программа-вымогатель, которая шифрует файлы, кража ноутбука и пожар: беда приходит снаружи и задевает всё, что рядом.

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

Обе мысли собраны в правиле, которое часто называют «3-2-1»: держите не меньше трёх копий данных, на носителях двух разных видов, и одну из них — в другом месте. Три копии нужны потому, что две могут умереть одновременно, как диски в RAID-5 во время восстановления. Носители разных видов — потому что у одинаковых дисков из одной партии одинаковые болезни. А копия вдали спасает от пожара и вора: они не выбирают, какой из дисков в комнате забрать. Проверьте на учениях, какие копии что переживают.

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

Учения быстро показывают две ловушки. Синхронизация — папка, которая сама повторяет себя на другом компьютере или в облаке, — не резервная копия: она старательно повторяет и удаление, и шифрование. Зеркало и постоянно подключённый диск тоже повторяют всё, что случилось с оригиналом, или погибают вместе с ним. Спасают копии с историей версий — где можно достать вчерашнее состояние, даже если сегодняшнее испорчено, — и копии, которые большую часть времени отключены. Многие системы делают такие снимки сами: файловые системы ZFS и Btrfs умеют за долю секунды сохранить состояние всего диска, а программы резервного копирования хранят версии за часы, дни и месяцы назад.

Учение 5. Вернуть вчерашний день

Сценарий пятый: ничего не сгорело и никто ничего не стёр. Вчера программа работала, а после сегодняшних правок перестала, и непонятно, какая из сорока правок виновата. Или двое правили один файл, и надо свести их работу. Тут нужна история: все версии, с подписью, кто и зачем менял. Сегодня почти все программисты мира хранят её в git, а сам git родился как спасательная операция.

Git успели написать за несколько дней, потому что в основе у него одна простая мысль. Файл хранится под адресом, который вычисляется из его содержимого: SHA-1 от короткого заголовка и самих байтов. Это адресация по содержимому. Самого git в песочнице нет, но его адреса можно посчитать вручную, и они совпадут с теми, что выдаёт git hash-object на любом компьютере мира.

Строка hello world с переводом строки живёт в любом репозитории по адресу 3b18e51…, пустой файл — по адресу e69de29…. Из этой мысли сразу следуют три свойства. Одинаковые файлы хранятся один раз, сколько бы версий и копий их ни упоминали: адрес у них один. Изменённый файл получает новый адрес, а старый остаётся на месте — ничего не перезаписывается. А если байт в хранилище испортится, хеш перестанет совпадать с адресом, и порча выдаст себя: контрольная сумма из третьего учения встроена в само хранилище.

Остальное git строит из тех же кирпичей. Каталог — это объект-дерево: список строк «адрес файла и имя», и у дерева тоже есть адрес-хеш. Коммит — объект с адресом дерева, адресом родительского коммита, автором и сообщением. Вот игрушечная версия: хранилище — словарь, а коммит — текст.

Два коммита, а объектов всего семь: файлы Вуди и Базза во второй версии не изменились, и второе дерево ссылается на те же адреса. В коммите видна вся конструкция: адрес дерева, адрес родителя и сообщение. А последние строки показывают, что будет, если испортить один байт в старом файле: проверка находит объект, чьё содержимое больше не совпадает с адресом.

От адреса коммита зависит гораздо больше, чем один снимок. В него входит адрес дерева, то есть адреса всех файлов, и адрес родителя, а в того — адрес его родителя, и так до самого первого коммита. Поменяйте хоть байт в любой версии любого файла в прошлом, и изменятся адреса всех коммитов после неё. Сорок шестнадцатеричных цифр заверяют всю историю проекта. Коммиты со ссылками на родителей образуют ориентированный граф без циклов из главы 19: у обычного коммита один родитель, у коммита слияния — два, а ветка — всего лишь имя, которое указывает на какой-то коммит и переезжает на новый при каждом коммите в неё.

Игрушечный git. Меняйте файлы и делайте коммиты, заводите ветки, переключайтесь между ними и сливайте их обратно в main. Рядом с графом коммитов — рабочие файлы и хранилище объектов, где каждый объект подписан началом своего адреса. Сосчитайте, сколько новых объектов появится после коммита, в котором изменился один файл.

Git хранит не разницы между версиями, а полные снимки; разницу он вычисляет, когда вы её просите, — тем же поиском наибольшей общей подпоследовательности строк, что и программа diff в главе 22, только более быстрым её вариантом. Снимки не раздувают хранилище, потому что неизменённые файлы — общие, всё сжато, а при упаковке git всё-таки хранит похожие объекты как разницу друг от друга. И ещё одно: каждый клон репозитория — полная копия всей истории. Сервер с вашим проектом на другом конце света и есть копия «одна далеко» из правила 3-2-1. Свой git, который умеет не только сохранять, но и доставать версии и читать историю, вы соберёте в задаче «Свой git».

Задачи

Четыре задачи — четыре инструмента спасателя: чётность, восстановление массива, контрольная сумма и хранилище версий.

Напишите parity(blocks): блоки — непустой список строк байтов (bytes) одинаковой длины, а ответ — их XOR, байт за байтом, тоже bytes той же длины. Если список пуст или блоки разной длины, бросьте ValueError. Тесты проверяют и свойство, ради которого чётность заводят: XOR всех блоков, кроме одного, вместе с чётностью даёт пропавший блок. На шесть блоков по 200 КБ дано три секунды.

bytearray можно менять по индексу: result[i] ^= block[i]. Пройдите так по всем блокам и всем позициям. Проверки на пустой список и разную длину поставьте в самое начало: blocks[0] у пустого списка упадёт с IndexError.

Быстрый способ без цикла по байтам: int.from_bytes(block, "big") превращает весь блок в одно большое целое число, XOR таких чисел — то же, что XOR байтов, а to_bytes(len, "big") возвращает число обратно в байты.

У XOR нет переносов, он не смотрит на соседние разряды, поэтому XOR больших чисел совпадает с XOR их байтов по отдельности. Длину ответа нужно указать явно: если в старших байтах нули, число «забудет» о них, и to_bytes(size, ...) вернёт их на место.

Массив RAID-5 из $n \ge 3$ дисков устроен так. Данные режут на блоки по block байт. Полоса номер $s$ (с нуля) — это кусок длины block на каждом диске, начиная с байта $s \cdot \text{block}$. В полосе $s$ чётность лежит на диске номер $(n - 1 - s) \bmod n$, а $n - 1$ блоков данных — на остальных дисках по возрастанию их номеров. Напишите read_array(disks, block): disks — список содержимого дисков (bytes), мёртвый диск — None. Верните все данные по порядку: полосу за полосой, в каждой — блоки данных слева направо, без чётности. Если мёртвых дисков больше одного, бросьте ValueError. Массив из шести дисков по 120 КБ нужно прочесть быстрее четырёх секунд. Пример: три диска, block = 2, данные b"ABCDEFGH". В полосе 0 чётность на диске 2, в полосе 1 — на диске 1:

Восстанавливать удобно весь диск сразу, а не по блоку: XOR всех уцелевших дисков даёт содержимое мёртвого, потому что в каждой полосе блоки данных и чётность вместе дают ноль. Это ваша parity из прошлой задачи, применённая к целым дискам.

Число полос — len(disk) // block для любого живого диска disk. В полосе s блок диска d — это срез disk[s * block:(s + 1) * block]; добавляйте его в ответ, если d не диск с чётностью.

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

Контрольная сумма Adler-32 стоит в конце каждого потока zlib — формата сжатия, которым пользуются, например, картинки PNG. Её придумал Марк Адлер. Считается она так: $a$ начинается с 1, $b$ — с 0; для каждого байта по порядку $a$ увеличивается на значение байта, а $b$ — на новое значение $a$; обе суммы берутся по модулю 65521. Ответ — число $b \cdot 65536 + a$. Напишите adler32(data) для строки байтов. Тесты сверяют ответ со стандартной библиотекой, но пользоваться ею нельзя: модули zlib и binascii в решении запрещены. Мегабайт данных должен считаться быстрее трёх секунд.

В заготовке есть только $a$ — обычная сумма байтов. Она не замечает перестановок: у b"ab" и b"ba" она одна и та же. Добавьте вторую сумму $b$: после каждого байта прибавляйте к ней текущее $a$.

Не забудьте модуль и для $b$: без него на длинных данных $b$ вырастет далеко за 16 бит. Склеить две половины можно сдвигом: (b << 16) | a.

Почему $b$ замечает перестановки: байт, стоящий на месте $i$ из $n$, входит в $b$ столько раз, сколько после него осталось шагов, — с весом $n - i$. Поменяйте два разных байта местами, и их веса поменяются, а с ними и $b$. Модуль 65521 — самое большое простое число меньше $2^{16}$: с простым модулем суммы лучше перемешиваются. Adler-32 считается быстрее, чем CRC-32, но слабее на коротких данных, в несколько сотен байт. В файле PNG работают обе: Adler-32 в конце потока zlib проверяет все распакованные данные, а каждый отдельный кусок файла снабжён ещё и своей CRC-32.

Соберите хранилище версий. Класс Repo держит все объекты в словаре objects: адрес — 40 шестнадцатеричных цифр SHA-1 — указывает на байты объекта, и каждый объект лежит под SHA-1 своих собственных байтов. Методы:

  • put(data) — сохранить файл как в git: объект b"blob " + длина + b"\0" + data; вернуть адрес. Он должен совпасть с тем, что выдаёт сам git.
  • commit(files, parent, message) — files — словарь «имя → байты», parent — адрес прошлого коммита или None. Сохранить файлы, дерево и коммит, вернуть адрес коммита. Формат дерева и коммита — ваш, но адрес коммита должен меняться от любого изменения в файлах, их именах, родителе или сообщении и не должен зависеть от порядка файлов в словаре и от времени.
  • checkout(address) — словарь «имя → байты» из этого коммита.
  • log(address) — список сообщений от этого коммита до самого первого.

В последнем тесте цепочка из 2000 коммитов: на сами коммиты и log от последнего дано четыре секунды.

Начните с ячейки «игрушечный-git.py»: там уже есть store и commit. Чтобы адрес не зависел от порядка файлов, перебирайте имена в sorted(files). Чтобы прочесть объект обратно, отрежьте заголовок до первого нулевого байта: data[data.index(b"\0") + 1:].

Для checkout разберите коммит: строку tree … — это адрес дерева, а в дереве каждая строка — «адрес имя». Для log идите по строке parent …, пока родителя нет. Если первый коммит записывает родителя как None, при разборе получится строка "None" — договоритесь о понятном знаке, например -.

Хранилище не знает слова «версия»: в нём только объекты под своими адресами, а история возникает из ссылок коммитов на родителей. Настоящий git устроен так же, только дерево хранит ещё права файлов и вложенные каталоги как вложенные деревья, коммит — автора и время, а объекты сжаты. Время в коммите git есть, поэтому два одинаковых коммита, сделанных в разные секунды, получат разные адреса; здесь мы его нарочно не храним, чтобы результат можно было проверить.

Куда дальше

Все пять учений пройдены, и у каждой аварии нашлось своё средство: от fsync и журнала до git, где каждая версия заверена хешем.

Теперь вернитесь к спасению «Истории игрушек 2». Копия на домашнем компьютере Гэлин Сусман появилась не сама: студия раз за разом пересылала ей изменения по телефонной линии. Резервная копия «одна далеко» из правила 3-2-1 тоже должна как-то туда доехать, а git push отправляет коммиты на сервер на другом конце света. Везде одно и то же: компьютер не один. У двух машин нет общей памяти и общего диска, между ними только провод или радиоволна, по которой бегут сигналы, — а то и десяток посредников по дороге. Как две машины обмениваются данными, кто прокладывает путь через полмира и почему байты вообще доходят до адресата, расскажет следующая глава.