Командный язык и командный процессор
11.1. Командный язык и командный процессор
Команды представляют собой инструкции, сообщающие ОС, что нужно делать. Команды могут восприниматься и выполняться либо модулями ядра ОС, либо отдельным процессом, в последнем случае такой процесс называется командным интерпретатором (оболочкой - shell). Набор допустимых команд ОС и правил их записи образует командный язык (CL - control language).
Большинство запросов пользователя к ОС состоят из двух компонент, показывающих: какую операцию следует выполнить и в каком окружении (environment) должно происходить выполнение операции. Могут различаться внутренние и внешние операции-команды. Выполнение внутренних операций производится самим командным интерпретатором, выполнение внешних требует вызова программ-утилит. Наличие в составе командного языка внутренних команд представляется совершенно необходимым для тех случаев, когда командный интерпретатор является отдельным процессом. Среди команд имеются такие, в которых выполняются системные вызовы, изменяющие состояние самого командного интерпретатора, если для них интерпретатор будет создавать новые процессы, то изменяться будет состояние нового процесса, а не интерпретатора. Примеры таких команд: chdir - изменение рабочего каталога для интерпретатора, wait - интерпретатор должен ждать завершения порожденного им процесса и т.п. Программы-утилиты (их загрузочные модули) записаны в файлах на внешней памяти. При их вызове порождаются процессы, и утилиты выполняются в контексте этих процессов. Вызов и выполнение программ-утилит ничем не отличаются от вызова и выполнения приложений. Командный интерпретатор порождает процессы-потомки и выполняет в них заданные программы, используя для этого те же самые системные вызовы, которые мы рассмотрели в главе 4. Задание операции в командном языке, таким образом, может иметь вид имени программного файла, который должен быть выполнен.
Окружением или средой (далее эти слова используются как синонимы) называется то, что отличает одно выполнение программы от другого. Например, при выполнении программы-компилятора должны быть определены следующие компоненты выполнения:
- какую программу следует выполнить;
- откуда программа должна взять исходные данные;
- куда программа должна поместить результат компиляции;
- где находятся библиотеки системы программирования;
- должен или не должен формироваться листинг;
- должны ли выдаваться предупреждения о возможных ошибках;
- и т.д., и т.п.
Только первая из перечисленных составляющих задает саму программу, остальные составляют окружение. Окружение конкретного выполнения может формироваться одним из следующих способов или их комбинациями:
- командами установки локального окружения;
- параметрами программы;
- командами установки глобального окружения.
Окружение может быть локальным или глобальным. В первом случае параметры окружения устанавливаются только для данного конкретного выполнения данной конкретной программы-процесса и теряются по окончании выполнения. Во втором случае параметры окружения сохраняются и действуют все время до их явной отмены или переустановки.
Команды для установки локальных параметров окружения применяются обычно в системах, работающих в пакетном режиме. В таких системах основным понятием является задание - единица работы с точки зрения пользователя. Выполнение задания состоит из одного или нескольких шагов. Каждый шаг задания включает в себя оператор JCL (job control language - язык управления заданиями) вызова программы и операторов установки локальной среды. Интерпретатор JCL сначала вводит все операторы, относящиеся к шагу задания, и лишь затем их выполняет. Тот же способ может применяться и в интерактивных системах - в этом случае команды установки параметров локальной среды должны предшествовать команде вызова.
Параметры программы также задают локальную среду выполнения. Они являются дополнениями к команде вызова, вводятся в той же командной строке, что и команда вызова, или являются параметрами оператора вызова в JCL. Формы задания параметров можно, по-видимому, свести к трем вариантам: позиционные, ключевые и флаговые. Возможны также и их комбинации. Позиционная форма передачи параметров программе похожа на общепринятую форму передачи параметров процедурам: параметры передаются в виде списка значений, интерпретация значения зависит от его места в списке. При передаче параметров в ключевой форме задается обозначение (имя) параметра и его значение; имя от значения отделяется специфицированным разделителем. Иногда другой специфицированный разделитель ставится перед именем как признак ключевой формы. Флаговая форма применяется для параметров, которые могут иметь только логические значения типа "да"/"нет"; такой параметр обычно кодируется одним определенным для него символом, иногда перед ним ставится специфицированный разделитель. Для флагового параметра информативным является само его задание в команде вызова: если параметр не задан, применяется его значение по умолчанию, если задан - противоположное значение.
Ниже приводятся примеры передачи параметров:
- позиционная форма: pgm1 data1.dat data2.dat list pgm2 10,33,p12.txt
- ключевая форма: pgm1 fille1=data1.dat,file2=data2.dat,list=yes pgm2 /bsize:10 /ssize:33 /name:p12.txt
- флаговая форма: pgm1 /1 /2 /p pgm2 s,m,l
- комбинированная форма:
pgm1 data1.dat data2.dat #bsize=10 #ssize=33 #l #f
Наиболее универсальным типом, который позволяет передать программе любые параметры, является строка символов. Некоторые CL требуют сформировать такую строку при вызове явным образом в виде строковой константы с использованием соответствующих символов-ограничителей, в других эта задача выполняется командным интерпретатором. Программа сама интерпретирует свою строку параметров, выполняя ее лексический разбор. В реальных системах суммарный размер строки параметров обычно имеет некоторые разумные ограничения. Иногда командный интерпретатор выполняет предварительный лексический разбор строки параметров, выделяя из нее "слова", разделенные пробелами, и передавая программе параметры в виде массива строк переменной размерности.
Каким образом параметры могут быть переданы программе? Можно назвать такие возможные механизмы передачи параметров:
- если командный интерпретатор выполняется как процесс, то он может послать процессу-программе параметры в виде сообщения;
- если командный интерпретатор является ядром ОС, то он копирует параметры в системную область памяти, и программа может получить их при помощи специального системного вызова;
- если командный интерпретатор участвует в порождении процесса-программы (а это обычно так и бывает, независимо от того, является интерпретатор модулем ядра или процессом), то параметры могут быть записаны в адресное пространство нового процесса сразу при его создании.
В языках программирования, однако, механизм передачи параметров прозрачен для программиста, доступ к параметрам обеспечивает компилятор и в любом случае программа получает значения параметров уже в своем адресном пространстве. Так, в языке C для главной функции программы предопределен прототип:: int main(int argn, char *argv[]);
где argn - число строк-параметров, argv - указатель на массив строк-параметров.
Для установки глобального окружения применяются команды типа set. Операндами такой команды могут быть символьные строки, задающие в ключевой форме значения параметров окружения. Например: set tempdir=d:\util\tmp set listmode=NO, BLKSIZE=1024
Переменные окружения могут быть системными или пользовательскими. Системные имеют зарезервированные символьные имена и интерпретируются командным интерпретатором либо другими системными утилитами. Например, типичной является системная переменная окружения path, значение которой задает список каталогов для поиска программ командным интерпретатором. Пользовательские переменные создаются, изменяются и интерпретируются пользователями и приложениями. Чтобы окружение могло быть использовано, в системе должны быть средства доступа к нему. На уровне команд - это должна быть команда типа show, выводящая на терминал имена и значения всех переменных глобального окружения, на уровне API - системный вызов getEvironment, возвращающий адрес блока глобального окружения.
Для внутреннего представления глобального окружения в ОС возможны два варианта: либо хранить в системе единую таблицу со значениями всех переменных окружения, либо для каждого процесса создавать собственную копию такой таблицы. Чаще используется второй вариант, который обеспечивает, во-первых, лучшую защиту глобального окружения, а во-вторых, возможность варьировать окружения для разных процессов, используя глобальное окружение отчасти как локальное. Очень удобным является этот вариант для систем с иерархической структурой отношений между процессами: в этом случае глобальное окружение является частью наследства, передаваемого от предка к потомку. Системные вызовы, связанные с порождением новых процессов, должны обеспечивать возможность передавать потомку как точную копию глобального окружения родителя, так и оригинальное окружение, специально созданное родителем для потомка.
Внутреннее представление глобального окружения - всегда текстовое, представленное в ключевой форме, как в команде set. Это объясняется тем, что окружение интерпретируется прикладными процессами, а именно текстовый тип может быть интерпретирован наиболее гибким образом.
В ОС, применяющих "философию дешевых процессов", (см. главу 4), предполагается выполнение сложных действий как результата совместной (последовательной или параллельной) работы нескольких простых процессов. Поэтому командный язык должен включать в себя средства интеграции процессов. К числу таких средств относятся:
- командные списки;
- переадресация системного ввода-вывода;
- конвейеризация;
- параллельное выполнение.
Командные списки представляют собой простое перечисление в одной командной строке нескольких команд. Например, результат выполнения такой командной строки: pgm1 param11 param12; pgm2; pgm3 param31
будет таким же, как при последовательным выполнении трех строк: pgm1 param11 param12 pgm2 pgm3 param31
Командные списки представляют более удобную форму, но не открывают никаких новых возможностей.
Процессы вводят данные из файла системного ввода, с которым по умолчанию связана клавиатура, а выводят - в файл системного вывода, по умолчанию - на экран терминала. Переадресация ввода дает возможность использовать в качестве входных данных программы данные, заранее записанные в файл, причем программа вводит и интерпретирует эти данные как введенные с клавиатуры. Переадресация вывода сохраняет данные, которые должны выводиться на экран, в файле. Примеры: pgm1 < infile pgm2 > outfile
Соединение командного списка с переадресацией ввода-вывода обеспечивает конвейеризацию. В примере: pgm1 param11 param12 | pgm2 | pgm3 param31
выходные данные программы pgm1 направляются не на экран, а сохраняются и затем используются, как входные для программы pgm2. Выходные данные последней в свою очередь используются как входные для pgm3.
В ОС с "философией дешевых процессов" допускается параллельное выполнение любого количества процессов. В обычном режиме командный интерпретатор готов к приему следующей команды только после окончания выполнения предыдущей. Специальная команда запуска программы или какой-либо признак в командной строке (например, символ-амперсанд в конце ее) может применяться в качестве указания командному процессору вводить и выполнять следующую команду, не дожидаясь окончания выполнения предыдущей. Так, последовательность командных строк: pgm1 param11 param12 & pgm2 & pgm3 param31
может привести к параллельному выполнению трех процессов (если программа pgm1 не закончится прежде, чем будет введена третья строка).
Ниже приведен программный текст, представляющий в значительно упрощенном виде макет командного интерпретатора shell ОС Unix (синтаксис и семантика системных вызовов в основном тоже соответствуют этой ОС). Из этого примера можно судить о том, как shell реализует некоторые из описанных выше возможностей. 1 int fd, fds [2]; 2 int status, retid, numchars=256; 3 char buffer [numchars], outfile[80]; 4 . . . 5 while( read(stdin,buffer,numchars) ) { 6 <синтаксический разбор командной строки> 7 if (<не внутренняя команда>) { 8 if(<командная строка содержит &>) amp=1; 9 else amp=0; 10 if( fork() == 0 ) { 11 if( < переадресация вывода > ) { 12 fd = create(outfile,<режим>); 13 close(stdout); 14 dup(fd); 15 close(fd); 16 } 17 if(<переадресация ввода>) { 18 <подобным же образом> 19 } 20 if(<конвейер>) { 21 pipe (fds); 22 if ( fork() == 0 ) { 23 close(stdin); 24 dup(fds[0]); 25 close(fds[0]); 26 close(fds[1]); 27 exec(pgm2, <параметры> ); 28 } 29 else { 30 close(stdout); 31 dup(fds[1]); 32 close(ds[1]); 33 close(fds[0]); 34 } 35 } 36 exec(pgm1, <параметры>); 37 } 38 if ( amp == 0 ) retid = wait(&status); 39 } 40 } 41 . . .
Наш макет рассчитан на интерпретацию командной строки, содержащей либо вызов одной программы (pgm1), либо конвейер из двух программ (pgm1|pgm2). Макет также обрабатывает переадресацию ввода-вывода и параллельное выполнение.
Тело shell представляет собой цикл (5 - 39), в каждой итерации которого из файла стандартного ввода вводится (5) строка символов - командная строка. Далее shell выполняет разбор командной строки, выделяя и распознавая собственно команду (или команды), параметры и т.д. Если (7) распознанная команда не является внутренней командой shell (обработку внутренних команд мы не рассматриваем), а требует выполнения какой-то программы - безразлично, системной утилиты или приложения - то shell проверяет наличие в командной строке признака параллельности и соответственно устанавливает значение флага amp (8, 9). Затем shell порождает новый процесс (10) и весь следующий блок (11 - 37) выполняется только в процессе-потомке. Если shell распознал в команде переадресацию системного вывода (11), то выполняются соответствующие действия (11 - 16). Они состоят в том, что shell создает файл, в который будет перенаправлен поток вывода и получает его манипулятор (12). Затем закрывается файл системного вывода (13). Системный вызов dup (14) дублирует манипулятор fd в первый свободный манипулятор таблицы файлов процесса. Поскольку только что освободился файл системного вывода, манипулятор которого - 1, дублирование произойдет именно в него. Теперь два элемента таблицы файлов процесса - элемент с номером 1 и элемент с номером fd адресуют один и тот же файловый дескриптор - дескриптор только что созданного файла. Но элемент fd сразу же освобождается (15), и теперь только манипулятор 1, который интерпретируется в программе как манипулятор системного вывода, адресует новый файл. Мы предлагаем читателю самостоятельно запрограммировать действия shell при переадресации ввода (17-19). (Для справки: манипулятор системного ввода - 0.)
Если в командной строке задан конвейер (20), то процесс-потомок прежде всего создает канал (21). Параметром системного вызова pipe является массив из двух элементов, в который этот вызов помещает манипуляторы канала: fds[0] - для чтения и fds[1] - для записи. Затем процесс-потомок порождает еще один процесс (22). Следующий блок (23 - 27) выполняется только во втором процессе-потомке. Этот потомок переадресует свой стандартный ввод на манипулятор чтения канала (23 - 25). Манипулятор записи канала освобождается за ненадобностью (26). Затем второй потомок загружается программой, являющейся вторым компонентом конвейера (27). Следующий блок (28 - 34) выполняется только в первом потомке: он переадресует свой стандартный вывод на манипулятор записи канала и освобождает манипулятор чтения канала.
Системный вызов exec (36) выполняется в первом потомке (он является единственным, если конвейер не задан), процесс загружается программой, являющейся первым компонентом конвейера или единственным компонентом командной строки. Обратите внимание на то, что из функции exec нет возврата. При ее вызове выполняется новая программа, с завершением которой завершается и процесс. Процесс-родитель, который все время сохраняет контекст интерпретатора shell, после запуска потомка проверяет (38) флаг параллельного выполнения amp. Если этот флаг не установлен, то родитель выполняет вызов wait, блокирующий shell до завершения потомка. (Этот вызов возвращает идентификатор закончившегося процесса, а в параметре - код завершения, но в нашем макете эти результаты не обрабатываются). Если флаг параллельности установлен, то shell начинает следующую свою итерацию, не дожидаясь завершения потомка.
При разработке командного интерпретатора необходимо следовать "принципу пользователя", который может быть сформулирован так: те операции, которые часто выполняются, должны легко вызываться. Для более полного воплощения этого принципа в командный интерпретатор могут быть встроены сервисные возможности. Можно привести такие примеры этих возможностей:
- установки по умолчанию - относятся прежде всего к параметрам глобального окружения, обычно эти установки записываются в отдельный файл - командный файл (cм. следующий раздел) начальной загрузки типа AUTOEXEC.BAT или в пользовательский профиль;
- встроенные сокращенные формы команд; в некоторых интерпретаторах применяется метод автоматического завершения: пользователь, введя первые символы команды, нажимает определенную клавишу и интерпретатор выводит в командную строку остаток команды;
- интеграция команд - введение в состав CL сложных команд, эквивалентных цепочке простых команд, иногда пользователь имеет возможность создавать в командном интерпретаторе собственные интегрированные команды, но чаще такая возможность реализуется через командные файлы;
- сохранение истории - веденные командные строки запоминаются в стеке и по определенной клавише содержимое стека выбирается в командную строку.
В тех ОС, где командный интерпретатор является процессом, имеется возможность запуска вторичного интерпретатора. Эта возможность является очень удобной, если при работе в среде какого-либо приложения возникает необходимость выполнить команды ОС, но завершать приложение из-за этого нежелательно. Приложение является процессом-потомком командного интерпретатора. Оно порождает новый процесс (потомок приложения), в котором запускается еще одна копия интерпретатора, как показано на Рисунке 11.1. (Новый процесс-интерпретатор может разделять сегмент кодов с первичным интерпретатором). Теперь весь ввод в командную строку обрабатывается этим вторичным интерпретатором. Вторичный интерпретатор может запустить другое приложение и т.д. Вторичный интерпретатор запускается в синхронном режиме, и при его завершении (команда exit) продолжает работать запускавшее его приложение.