Заметим также, что многие неудобства интерфейсов традиционных инструментальных средств ОС UNIX связаны с тем, что они ориентированы на использование и алфавитно-цифровых, и графических терминалов. Поэтому обычно эти средства поддерживают старомодный строчный интерфейс даже при наличии графического терминала. Естественно, в современных вариантах ОС UNIX все новые инструментальные средства поддерживают оконный графический интерфейс (и, следовательно, их невозможно использовать при наличии алфавитно-цифровых терминалов).
После того, как текстовый файл создан, его нужно откомпилировать для получения объектного файла. Наиболее популярными компиляторами для языка Си в среде ОС UNIX сейчас являются pcc (Ритчи и Томпсон) и gcc (Ричард Столлман). Оба эти компилятора являются полностью мобильными и обладают возможностью генерировать код для разнообразных компьютеров, т.е. эти компиляторы могут быть установлены практически на любой аппаратной платформе под управлением ОС UNIX.
Можно отметить следующие преимущества gcc. Во-первых, этот компилятор свободно, т.е. бесплатно (вместе со своими исходными текстами) распространяется Free Software Foundation. Во-вторых, gcc тщательно поддерживается и сопровождается. В-третьих, начиная с версии 2.0, gcc может компилировать программы, написанные на языках Си, Си++ и Objective C, а результирующая выполняемая программа может быть скомпонована из объектных файлов, полученных из текстовых файлов на любом из этих языков. В-четвертых, открытость исходных текстов gcc и тщательно разработанная структура компилятора позволяют сравнительно просто добавлять к gcc новые кодогенераторы. Относительным недостатком gcc является то, что используемый диалект языка Си включает слишком много расширений по сравнению со стандартом ANSI/ISO (однако имеется режим, в котором компилятор указывает все расширенные конструкции языка, встречающиеся в компилируемой программе).
Оба компилятора обрабатывают программу в два этапа. На первом этапе синтаксически правильный текст на языке Си преобразуется в текст на языке ассемблера. На втором этапе на основе текста на языке ассемблера генерируются машинные коды и получается объектный файл. Исторически в ОС UNIX использовались различные форматы объектных модулей. Для обеспечения совместимости с предыдущими версиями почти все они поддерживаются в современных версиях компиляторов. Однако в настоящее время преимущественно используется формат COFF (Common Object File Format). При желании можно остановить процесс компиляции после первого этапа и получить для изучения файл с текстом программы на языке ассемблера.
После того, как необходимый для построения выполняемой программы набор объектных файлов получен, необходимо произвести компоновку выполняемой программы. В ОС UNIX компоновщик выполняемых программ называется редактором связей (link editor) и обычно вызывается командой ld. Редактору связей указывается набор объектных файлов и набор библиотек, из которых нужно черпать недостающие для компоновки программы.
Процесс компоновки заключается в следующем. Сначала просматривается набор заданных объектных файлов. Для каждого внешнего имени ищется объектный файл, содержащий определение такого же глобального имени. Если поиск заканчивается успешно, то внешняя ссылка заменяется на ссылку на определение глобального имени. Если в конце этого этапа остаются внешние имена, для которых не удалось найти соответствующего определения глобального имени, то начинается поиск объектных файлов с нужными определениями глобальных имен в указанных библиотеках. Если, в конце концов, удается найти определения для всех внешних имен, все соответствующие объектные файлы собираются вместе и образуют выполняемый файл.
В ОС UNIX имеется несколько стандартных библиотек. В большинстве случаев наиболее важной является библиотека ввода/вывода (stdio). Грамотное использование стандартных библиотек способствует созданию легко переносимых прикладных программ (мы вернемся к обсуждению стандартных библиотек ОС UNIX в четвертой части курса).
Выполняемая программа может быть запущена в интерактивном режиме как команда shell или выполнена в отдельном процессе, образуемом уже запущенной программой.
Любой командный язык семейства shell фактически состоит из трех частей: служебных конструкций, позволяющих манипулировать с текстовыми строками и строить сложные команды на основе простых команд; встроенных команд, выполняемых непосредственно интерпретатором командного языка; команд, представляемых отдельными выполняемыми файлами (более подробно и точно командные языки рассматриваются в пятой части курса).
В свою очередь, набор команд последнего вида включает стандартные команды (системные утилиты, такие как vi, cc и т.д.) и команды, созданные пользователями системы. Для того, чтобы выполняемый файл, разработанный пользователем ОС UNIX, можно было запускать как команду shell, достаточно определить в одном из исходных файлов функцию с именем main (имя main должно быть глобальным, т.е. перед ним не должно указываться ключевое слово static). Если употребить в качестве имени команды имя такого выполняемого файла, командный интерпретатор создаст новый процесс (см. следующий подраздел) и запустит в нем указанную выполняемую программу начиная с вызова функции main.
Тело функции main, вообще говоря, может быть произвольным (для интерпретатора существенно только наличие входной точки в программу с именем main), но для того, чтобы создать команду, которой можно задавать параметры, нужно придерживаться некоторых стандартных правил. В этом случае каждая функция main должна определяться с двумя параметрами - argc и argv. После вызова команды параметру argc будет соответствовать число символьных строк, указанных в качестве аргументов вызова команды, а argv - массив указателей на переменные, содержащие эти строки. При этом имя самой команды составляет первую строку аргументов (т.е. после вызова значение argc всегда больше или равно 1). Код функции main должен проанализировать допустимость заданного значения argc и соответствующим образом обработать заданные текстовые строки.
Например, следующий текст на языке Си может быть использован для создании команды, которая выводит на экран текстовую строку, заданную в качестве ее аргумента:
#include <stdio.h>
main (argc, argv)
int argc;
char *argv[];
{
if (argc != 2)
{ printf("usage: %s your-text\n", argv[0]);
exit;
}
printf("%s\n", argv[1]);
}
Процесс в ОС UNIX - это программа, выполняемая в собственном виртуальном адресном пространстве. Когда пользователь входит в систему, автоматически создается процесс, в котором выполняется программа командного интерпретатора. Если командному интерпретатору встречается команда, соответствующая выполняемому файлу, то он создает новый процесс и запускает в нем соответствующую программу, начиная с функции main. Эта запущенная программа, в свою очередь, может создать процесс и запустить в нем другую программу (она тоже должна содержать функцию main) и т.д.
Управление процессами подробно обсуждается в третьей части курса. Тем не менее кратко опишем здесь общий подход. Для образования нового процесса и запуска в нем программы используются два системных вызова (примитива ядра ОС UNIX) - fork() и exec (имя-выполняемого-файла). Системный вызов fork приводит к созданию нового адресного пространства, состояние которого абсолютно идентично состоянию адресного пространства основного процесса (т.е. в нем содержатся те же программы и данные).
Другими словами, сразу после выполнения системного вызова fork основной и порожденный процессы являются абсолютными близнецами; управление и в том, и в другом находится в точке, непосредственно следующей за вызовом fork. Чтобы программа могла разобраться, в каком процессе она теперь работает - в основном или порожденном, функция fork возвращает разные значения: 0 в порожденном процессе и целое положительное число (идентификатор порожденного процесса) в основном процессе.
Теперь, если мы хотим запустить новую программу в порожденном процессе, нужно обратиться к системному вызову exec, указав в качестве аргументов вызова имя файла, содержащего новую выполняемую программу, и, возможно, одну или несколько текстовых строк, которые будут переданы в качестве аргументов функции main новой программы. Выполнение системного вызова exec приводит к тому, что в адресное пространство порожденного процесса загружается новая выполняемая программа и запускается с адреса, соответствующего входу в функцию main.
В следующем примере пользовательская программа, вызываемая как команда shell, выполняет в отдельном процессе стандартную команду shellls, которая выдает на экран содержимое текущего каталога файлов.
main()
{if(fork()==0) wait(0); /* родительский процесс */
else execl("ls", "ls", 0); /* порожденный процесс */
}
Механизм перенаправления ввода/вывода является одним из наиболее элегантных, мощных и одновременно простых механизмов ОС UNIX. Цель, которая ставилась при разработке этого механизма, состоит в следующем. Поскольку UNIX - это интерактивная система, то обычно программы вводят текстовые строки с терминала и выводят результирующие текстовые строки на экран терминала. Для того, чтобы обеспечить более гибкое использование таких программ, желательно уметь обеспечить им ввод из файла или из вывода других программ и направить их вывод в файл или на ввод другим программам.