Добавить в корзинуПозвонить
Найти в Дзене
Триалогия

Пишем операционную систему Триалогия - Консоль ядра против trish, то же имя и другой код

В конце прошлой части возник вопрос: кто вообще принимает команды и запускает программы? Ответ прост - оболочка. Именно она выводит приглашение, ждёт ввод и решает, что делать дальше. Однако в этой системе существует не одна оболочка, а сразу две. Причина вовсе не кроется ни в небрежности, ни в исторической случайности. Наоборот, именно здесь особенно хорошо видно устройство кольцевой архитектуры. На первый взгляд различий почти нет. Обе показывают знакомый prompt, обе понимают похожие команды. Стоит заглянуть глубже - и сходство заканчивается. Одну оболочку ядро включает в собственный код, другая существует как обычное пользовательское приложение. Поставим обе оболочки рядом. Внешний вид почти ничего не говорит. Главное различие определяет место выполнения. Первая оболочка - консоль ядра. Отдельного процесса для неё не существует. Код входит прямо в состав ядра и выполняет работу в ring 0. Таблица команд лежит в src/kernel/shell/commands.c. После ввода команды консоль выбирает нужную
Оглавление

Две оболочки: почему одна и та же команда существует дважды

В конце прошлой части возник вопрос: кто вообще принимает команды и запускает программы? Ответ прост - оболочка. Именно она выводит приглашение, ждёт ввод и решает, что делать дальше. Однако в этой системе существует не одна оболочка, а сразу две. Причина вовсе не кроется ни в небрежности, ни в исторической случайности. Наоборот, именно здесь особенно хорошо видно устройство кольцевой архитектуры.

На первый взгляд различий почти нет. Обе показывают знакомый prompt, обе понимают похожие команды. Стоит заглянуть глубже - и сходство заканчивается. Одну оболочку ядро включает в собственный код, другая существует как обычное пользовательское приложение.

Два мира

Поставим обе оболочки рядом. Внешний вид почти ничего не говорит. Главное различие определяет место выполнения.

Две оболочки: консоль ядра против trish
Две оболочки: консоль ядра против trish

Первая оболочка - консоль ядра. Отдельного процесса для неё не существует. Код входит прямо в состав ядра и выполняет работу в ring 0. Таблица команд лежит в src/kernel/shell/commands.c. После ввода команды консоль выбирает нужную функцию и сразу передаёт ей управление. Команда ls, например, вызывает vfs_list_folder напрямую. Системный вызов здесь попросту не нужен, код уже находится внутри ядра.

Такой подход открывает полный доступ ко всему внутреннему устройству системы. Консоль свободно читает данные планировщика, памяти, драйверов и других подсистем, куда пользовательские программы попасть не могут. Именно поэтому команды вроде dmesg, taskstats или vgainfo получают информацию непосредственно из ядра. Во время загрузки без графического режима именно эта оболочка принимает управление. Когда остальные компоненты ещё молчат или уже перестали работать, она остаётся единственной точкой управления системой.

Совсем иначе устроена trish, оболочка Триалогия. Перед нами обычная пользовательская программа. Файл /bin/shell представляет собой ELF-приложение, которое работает в ring 3, обладает собственным процессом и использует отдельное адресное пространство. Пользователь видит её внутри окна терминала.

После ввода ls trish не имеет возможности обратиться к функции ядра напрямую. Вместо этого оболочка ищет исполняемый файл /bin/ls, запускает новый процесс и ждёт завершения его работы. Среди встроенных команд остаётся лишь небольшой набор: cd, chp, history, exit и help. Всё остальное выполняют внешние программы.

Такое разделение приносит одновременно преимущества и ограничения. Консоль ядра получает практически неограниченные полномочия, однако любая ошибка способна обрушить всю систему. У trish возможностей значительно меньше, зато её сбой затрагивает только собственный процесс. Ядро продолжает работу, а оболочка просто запускает новый экземпляр.

То же имя, другой код

Именно здесь скрывается ловушка, которая регулярно приводит к недоразумениям. Обе оболочки понимают команду ls. Пользователь видит одно и то же имя, получает похожий результат и легко приходит к выводу, что внутри работает один и тот же код. На самом деле между двумя вариантами нет ничего общего.

Та же команда, два совершенно разных пути
Та же команда, два совершенно разных пути

В консоли ядра имя ls указывает на функцию из таблицы команд. Консоль находит cmd_ls, передаёт ей управление, после чего функция напрямую обращается к vfs_list_folder. Новый процесс никто не создаёт. Переключение контекста тоже не требуется. Вся работа проходит внутри ядра.

У trish путь выглядит совершенно иначе. После разбора команды оболочка проверяет список встроенных функций. Если подходящего имени нет, начинается поиск исполняемого файла в каталогах из переменной PATH. Для команды ls результатом такого поиска становится /bin/ls. Затем оболочка создаёт дочерний процесс, передаёт ему управление и ждёт завершения.

// в trish, упрощённо:
if (!is_builtin(argv[0])) {
libsh_find_in_path(argv[0], paths, fullpath, size); // → "/bin/ls"
libsh_execute(fullpath, argv, ...); // exec + wait_pid
}

Из такого устройства следует вполне практический вывод. Если команда исправно работает в одном окне и неожиданно отказывает в другом, противоречия здесь нет. Пользователь имеет дело с двумя независимыми реализациями, которые лишь используют одинаковое имя. Во время поиска ошибки полезно прежде всего выяснить, какая оболочка получила команду. Один короткий вопрос нередко экономит часы отладки.

Что trish делает со строкой

Теперь имеет смысл проследить весь путь команды внутри trish. Он хорошо показывает, насколько скромную работу выполняет сама оболочка.

В trish, шаг за шагом
В trish, шаг за шагом

Сначала trish принимает строку ввода. Пользователь может удалить символ клавишей Backspace или выбрать предыдущую команду через историю. Затем оболочка разбивает строку на отдельные аргументы и формирует массив argv. Если в конце стоит символ &, оболочка сразу отмечает необходимость фонового запуска.

Следующий этап посвящён перенаправлению вывода. При встрече с > или >> оболочка запоминает нужный режим записи, удаляет соответствующие элементы из списка аргументов и продолжает разбор уже без них.

После этого наступает главный момент. Первое слово определяет дальнейший путь. Если имя совпадает со встроенной командой, например cd, небольшая функция внутри самой оболочки выполняет работу сразу же. Новый процесс для такого случая не нужен.

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

Все остальные команды проходят другую дорогу. Trish ищет исполняемый файл, создаёт дочерний процесс и передаёт ему управление.

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

Есть ещё одна важная деталь. Одновременно работает только одна оболочка. Пока оконный менеджер управляет клавиатурой, консоль ядра полностью игнорирует ввод. Все нажатия получают окна терминала с trish. После загрузки без графического режима ситуация меняется. Тогда управление полностью переходит к консоли ядра, поскольку другой оболочки попросту нет.

Честное место

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

И сама trish пока остаётся довольно компактной. Оболочка уже умеет перенаправлять вывод через > и >>, но поддержку конвейеров с | ещё не получила. Ввод из файла через < тоже пока отсутствует. Символы * и ? оболочка не разворачивает, поэтому полные имена файлов приходится вводить вручную. Редактор командной строки тоже нельзя назвать завершённым. История команд работает, клавиши вверх и вниз уже доступны, а вот переход курсора влево и вправо внутри строки ещё ждёт своей очереди. Для молодой оболочки такая картина вполне естественна: основные возможности уже появились, остальные постепенно займут своё место.

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

Что дальше

Теперь понятно, кто принимает команды и каким путём они попадают в систему. Следующий вопрос возникает сам собой: какие возможности предоставляет сама консоль ядра?

Её встроенный набор команд открывает доступ к самым разным подсистемам. Одни показывают журнал сообщений, другие выводят статистику планировщика, третьи помогают изучить драйверы, память или внутреннее состояние системы. Именно с этих встроенных команд ядра начнётся следующая часть.

Было бы интересно увидеть ваши комментарии и улучшить статьи.

Предыдущая статья · Содержание · Следующая статья

*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением