Запускаем Linux на Python
На чем только уже не запускали Doom. Мы же будем запускать Linux. Да не где-нибудь, а на Python. Да-да, прямо внутри него, где в качестве среды выполнения будет выступать интерпретатор Python. Ну как… Не будем пытаться переписать ядро и другие части Linux на этот язык, а попробуем написать (точнее портировать) виртуальную машину на Python и уже в ней запускать ОС.
Начнем с позитивного, а именно с плюсов такого решения.
— Можно будет запустить Linux вообще везде, где есть интерпретатор Python.
— Можно использовать как бенчмарк конкретного интерпретатора.
— Веселимся, пока все это пишем и отлаживаем. Пожалуй, это самый главный плюс.
Минусы: будет работать оооочень не быстро (ну логично же).
Немного технических подробностей. Внезапно поработаем с нейросетями, посмотрим, что получится и насколько быстро будет работать.
Статья не претендует на какие-то сильно новые знания, и все это было сделано Just for Fun и ради эксперимента.
Приступаем!
На старт
В качестве референса, из которого родилась эта идея, была уже не самая свежая новость. Смотрим, что к чему, и идем по самому простому пути, а именно пытаемся сделать то же самое, но с другого бока.
Итак, эмулятор RISC-V, который умеет запускать образ Linux, у нас имеется. Первый — это простенькая виртуальная машина с поддержкой 32х-битного RISC-V процессора без MMU и прочих аппаратных «излишеств». Второй — это собранный для этой архитектуры такой же простенький Linux без поддержки MMU. Осталось дело за малым: портировать первый с Си на Python.
Можно, конечно, руками переписать на красивый код, но как будто это слишком рутинная операция в наше прогрессивное время. Попробуем вооружиться нейросетями, и пусть они впахивают, а не мы. Для перевода кода из одного языка программирования в другой существует, например, такой онлайн конвертер (не реклама, если что, одно из первых, что попалось в Google). Скармливаем наш код этому AI-конвертеру иии… он выплевывает наружу что-то вполне корректное. С кучей оговорок, конечно же, но то, что этот код похож на оригинал, уже радует. Не совсем красивый, каким мог бы быть. Например, рабочее, но сомнительное решение:
elif (ir >> 12) & 0x7 == 1:
if rs1 != rs2:
pc = immm4
elif (ir >> 12) & 0x7 == 4:
if rs1 < rs2:
pc = immm4
elif (ir >> 12) & 0x7 == 5:
if rs1 >= rs2:
pc = immm4
elif (ir >> 12) & 0x7 == 6:
...
Ну да ладно, сами виноваты, что не пишем код сами. Главное, чтобы было корректно. А оптимизацию и рефакторинг оставим на потом.
Итак, у нас имеется переведенный с Си на Python код. Уже неплохо.
Внимание
С первой попытки ничего, конечно же, не заработало. «Переводчик» удалил почему-то части кода, которыми не знает, как пользоваться (а скармливал я ему по одному файлу). Быстренько допиливаем и запускаем.
Разбираться, где еще нейросеть недосмотрела, и вникать в тонкости работы архитектуры не хочется, поэтому пытаемся отлавливать ошибки по ходу работы. А по ходу дела начинают происходить все более изощренные баги, особенно в работе нашего виртуального RISC-V.
Сделаем такой трюк. Считаем, что вся загрузка ядра — это очень детерминированный процесс. Поэтому, сколько бы раз мы ни запускали ОС, мы должны получать на каждом шаге загрузки всегда одно и то же состояние системы. Состояние нашего эмулятора будет обусловлено всего двумя факторами:
— внутренними регистрами виртуального процессора;
— памятью виртуального процессора.
Вроде больше ничего. Плюс, конечно же, состояние периферии, но ее не так много и на начальном этапе загрузки она не используется.
Поскольку у нас есть оригинальная виртуальная машина на Си, которая точно так же работает, давайте логировать состояние системы там и одновременно в нашем эмуляторе на Python. Затем сравним эти логи и посмотрим, на каком шаге пошли отличия. Если такие отличия есть, нужно уже отлаживать конкретно этот шаг эмулятора.
Единственное уточнение: нет необходимости в сохранении всей памяти виртуального процессора на каждом шаге, так как считаем, что доступ к нашей памяти имеет только виртуальный процессор, а он может делать там изменения, только если происходят специальные команды записи в память, поэтому логируем только их.
Быстро дописываем код для логирования и там и там, ну и далее по циклу: запускаем-логируем-смотрим-дорабатываем и опять запускаем-логируем-смотрим-дорабатываем, пока все не запустится. Благо, в RISC-V команд процессора не так много и, соответственно, ошибок тоже не так много можно допустить.
Самые частые ошибки, которые сделал AI-переводчик следующие:
— Ошибки переполнения. Так как Int в Python условно безграничный, он может хранить бо'льшие значения, чем RISC-V (помним, что последний у нас 32х-битный). Поэтому нужно искусственно расставлять ограничения на длину числа после операций с регистрами.
— Отступы не всегда корректны в функциях, а в Python-e это важно. Просто молча правим.
— Некорректное поведение «переводчика». Например, это:
uint32_t * dtb = (uint32_t*)(ram_image + dtb_ptr);
if( dtb[0x13c/4] == 0x00c0ff03 )
он преобразует в это:
dtb = struct.unpack('
Ошибку, думаю, показывать не надо. В таких случаях думаем, что да как, после чего так же молча правим.
Марш
Через некоторое время отладки и запусков видим:
Ура, оно живое! На удивление, наш Linux достаточно быстро запускается, что не может не радовать.
Надо бы протестировать, как быстро такое решение работает в более конкретных цифрах. В образе Linux заботливо оставлен Coremark для тестирования производительности. Запустим его пару раз и сравним скорость работы Linux в виртуальной машине на Python и на Си. А еще, ради интереса, протестируем пару нестандартных Python интерпретаторов/компиляторов.
Итого, сравнительная табличка производительности для coremark:
Компилятор/интерпретатор | Результат в попугаях в Iterations/Sec |
Visual C++ 14.0 | 798.258345 |
CPython 3.13 | 2.132803 |
CPython 3.12 | 2.418575 |
PyPy 3.10 | 4.362685 |
CPython 3.12+Nutika | 3.552819 |
Не густо. Python не быстрый. Этого стоило ожидать. Улучшать и оптимизировать код нашей Python-виртуальной машины можно и, соответственно, улучшить результат, но примем результат, какой он есть.
Финиш
Эта статья получилась из разряда «что получится, если…». AI-переводчик сэкономил немало времени на разработку этого всего, но и добавил другой работы по исправлению. Но теперь всё позади, можем смело запускать Linux на Python. Пусть и с ограничениями по скорости, да и вообще, у нас виртуальная машина и Linux без поддержки MMU, но можем же.
В самом лучшем случае (при помощи JIT компилятора PyPy) такое решение почти в 183 раза хуже по производительности нативной виртуальной машины. И промолчим, что вообще-то можно нативно скомпилировать и запустить Linux на реальном железе и будет еще быстрее и разница будет еще существеннее. Но зато, наше решение полностью портативное и может запускаться в любой (или почти любой) Python-среде исполнения. А еще получилось крайне компактно — всего порядка 900 строк рабочего кода. Все исходники и инструкция по запуску, конечно же, есть на github.