В данной статье говорится об особенностях разработки командной оболочки (shell) для UNIX систем. Приводятся блок-схемы работы основных алгоритмов работы командной оболочки. Подробно разбирается реализация конвейера (pipeline).
Ключевые слова: shell, командная оболочка, pipeline, конвейер, UNIX, разработка
Введение
В вычислительной технике оболочка операционной системы — программа, предоставляющая интерфейс для взаимодействия пользователя с функциями системы. В общем случае оболочки операционной системы используют либо интерфейс командной строки CLI (command line interface), либо графический пользовательский интерфейс GUI (graphical user interface).
Оболочки командной строки требуют, чтобы пользователь был знаком с командами и их синтаксисом вызова, а также понимал понятия о специфичном для оболочки языке сценариев (например, bash).
Графические оболочки создают низкую нагрузку на начинающих пользователей компьютеров и характеризуются простотой в использовании. Поскольку они также имеют определенные недостатки, большинство операционных систем с поддержкой графического интерфейса также предоставляют оболочки CLI.
В профессиональной среде программистов считается, что умение пользоваться командной строкой является необходимым для наиболее гибкой и тонкой настройки и эксплуатации персонального компьютера. Особенно это касается операционных систем семейств UNIX/GNU.
В данной статье будет рассмотрен процесс создания оболочки командной строки, включающий в себя базовый функционал (подробнее о нем ниже) популярной оболочки bash.
Разрабатываемый функционал
В данной статье будет показан принцип работы:
- выполнение программ в зависимости от пути в ОС до них;
- выполнение команд, встроенных в командную оболочку;
- обработка конвейера (pipeline, «|»);
Основная идея
Поскольку главная цель командной оболочки — интерактивное предоставление инструкций и данных операционной системе, то простейший цикл выполнения команды будет выглядеть как на рисунке 1.
Рис. 1. Основной цикл программы
Процесс считывание команды полностью приводить нецелесообразно, поскольку существует много нюансов обработки вводимых символов: разделители («;», «|», «&&», «||»), два типа кавычек (одинарные и двойные), специальные символы («$», «?», «*»), экранирование специальных символов и прочее.
Стоит сказать, однако, что после выполнения блока Read command мы должны получить структуру, в которой хранятся данные о названии команды, её аргументы, и необходимо ли результат команды передавать в конвейер (pipeline).
Выполнение команды
В блоке выполнения программы мы должны определить, какой тип команды мы собираемся выполнять (встроенную, или внешнюю). Разница заключается в том, что встроенные команды выполняются основном процессе командной оболочки, а внешние необходимо выполнять в дочернем процессе. Это связано с особенностью работы системного вызова exec: в UNIX системах новая программа полностью запускается в вызывающем процессе, и, после окончания выполнения, завершается вместе с вызвавшим его процессом.
Чтобы выполнить другую программу, не закрывая командную оболочку, необходимо создать копию процесса командной оболочки (системный вызов fork), и в дочернем процессе запустить необходимую команду. Блок-схема подпрограммы execute command представлена на рисунке 2.
После выполнения fork, основной (родительский) процесс начинает выполняться одновременно с дочерним. Родительский процесс ждет (wait pid), пока выполнится дочерний. Это необходимо, чтобы затем сообщить пользователю код завершения выполняемой программы.
Подпрограмма dup file descriptor выполняет подмену файловых дескрипторов. Файловый дескриптор простыми словами — это описатель потока ввода-вывода, т. е. сущность, благодаря которой мы можем передавать данные между процессами, и даже выводить их на экран для пользователя (используя стандартные файловые дескрипторы stdin, stdout, stderr). Подмена необходима для тех случаев, когда процессу необходимо считать данные из конвейера или записать их в него.
Рис. 2. Подпрограмма execute command
Реализация конвейера ( pipeline )
Для создания конвейера существует системный вызов pipe он возвращает два файловых дескриптора — вход и выход конвейера. Для соединения двух команд с помощью необходимо сделать следующее:
- создать конвейер;
- первая команда должна читать данные из fd = 0 (stdin), записывать во вход конвейера;
- закрыть вход конвейера;
- запомнить, из какого fd должна читать данные вторая команда;
- вторая команда должна читать данные из запомненного fd, и записывать результат в fd = 1 (stdout);
- закрыть выход конвейера.
В приведённом выше алгоритме подпрограмма execute command выполняется 2 раза (для первой и второй команды). Непосредственно перед fork необходимо запоминать из какого файлового дескриптора команда должна читать данные, и в какой файловый дескриптор записывать. А непосредственно после fork необходимо совершить подмену файловых дескрипторов с помощью системного вызова dup2.
Подмена заключается в следующем: например, программа должна считывать данные из fd = 3, перед exec мы заменяем fd = 0 на fd = 3 так, чтобы в fd = 0 находилась копия/ссылка на fd = 3 (exec всегда читает из fd = 0 и записывает в fd = 1).
Подмена файлового дескриптора в дочернем процессе не ломает файловые дескрипторы в основном, поэтому командная оболочка может продолжать работать в обычном режиме.
Вывод
Выше была показана схема работы командной оболочки поддерживающую конвейер. Понимание работы командных оболочек, а также работы сущностей, которые они используют (файловые дескрипторы, пиды (pid — process id), конвейеры и прочие) позволяют лучше понимать работу компьютера на низком уровне.