Системное программирование. Учебное пособие
.pdf
1.2. Модульная разработка программ
Система (среда) программирования – система, образуемая языком программирования, компилятором или интерпретатором программ, представленных на этом языке, соответствующей документацией, а также вспомогательными средствами для подготовки программ к форме, пригодной для выполнения.
При разработке программ, а тем более – сложных, используется принцип модульности, разбиения сложной программы на составные части, каждая из которых может подготавливаться отдельно. Модульность является основным инструментом структурирования программного обеспечения, облегчающим его разработку, отладку и сопровождение.
1.2. Модульная разработка программ
Программный модуль – программа (или функционально завершенный фрагмент программы), предназначенная(ый) для хранения, трансляции, объединения с другими программными модулями и загрузки в оперативную память.
При выборе модульной структуры должны учитываться следующие основные соображения:
•функциональность (модуль должен выполнять законченную функцию);
•слабая связность (модуль должен иметь минимум связей с другими модулями, связь через глобальные переменные и области памяти нежелательна);
•специфицируемость (входные и выходные параметры модуля должны четко формулироваться).
Программа пишется в виде исходного модуля.
Исходный модуль – программный модуль на исходном языке, обрабатываемый транслятором и представляемый для него как целое, достаточное для проведения трансляции.
Следующим этапом является трансляция.
Трансляция – преобразование программы, представленной на одном языке, в другое ее представление. Под ним, как правило, понимают другой язык программирования. Иначе говоря, мы должны получить программу, равносильную по смыслу первой, или, как еще говорят, се-
11
1. Введение в системное программирование
мантически эквивалентную ей. Однако, как правило, выходным языком транслятора является машинный язык целевой вычислительной машины, т. е. тот, на котором программа будет выполняться.
Машинный язык – язык программирования, предназначенный для представления программы в форме, позволяющей выполнять ее непосредственно на вычислительной машине.
Трансляторы – общее название для программ, осуществляющих трансляцию. Их иногда подразделяют на компиляторы и интерпретаторы – в зависимости от исходного языка программы, которую они обрабатывают. Компиляторы обычно работают с языками высокого уровня. Ассемблеры, как правило, работают с языками ассемблера.
Язык ассемблера – язык программирования, который представляет собой символьную форму машинного языка с рядом возможностей, характерных для языка высокого уровня (обычно включает в себя макросредства).
Язык высокого уровня – как известно, язык программирования, понятия и структура которого удобны для восприятия человеком.
Объектный модуль – программный модуль, получаемый в результате компиляции исходного модуля.
Поскольку результатом компиляции является модуль на языке, близком к машинному, в нем уже не остается признаков того языка, на каком был написан программный модуль. Это дает принципиальную возможность создавать программы из модулей, написанных на разных языках. Специфика исходного языка, однако, может сказываться на физическом представлении базовых типов данных, способах обращения к процедурам/функциям и т. п. В целях совместимости разноязыковых модулей должны выдерживаться общие соглашения. Большая часть объектного модуля – это команды и данные машинного языка именно
втой форме, в какой они будут существовать во время выполнения программы. Однако программа в общем случае состоит из многих модулей. Поскольку компилятор обрабатывает только один конкретный модуль, он не может должным образом обработать те части этого модуля, в которых производятся обращения к данным или процедурам, определенным
вдругом модуле. Такие обращения называются внешними ссылками. Те места в объектном модуле, где содержатся внешние ссылки, преобразуются в некоторую промежуточную форму, подлежащую дальнейшей
12
1.2. Модульная разработка программ
обработке. Говорят, что объектный модуль представляет собой программу на машинном языке с неразрешенными внешними ссылками.
Разрешение внешних ссылок выполняется на следующем этапе подготовки, который обеспечивается так называемым редактором связей (или компоновщиком). Редактор связей соединяет, т. е. компонует, все объектные модули, входящие в программу. Поскольку компоновщик «видит» все компоненты программы, он имеет возможность обработать те места в объектных модулях, которые содержат внешние ссылки. Результатом работы компоновщика является загрузочный модуль.
Загрузочный модуль – это программный модуль, представленный в форме, пригодной для загрузки в оперативную память для выполнения.
Загрузочный модуль сохраняется в виде файла на внешней памяти. Для выполнения программа должна быть перенесена (загружена) в оперативную память. Иногда при этом требуется некоторая дополнительная обработка (например, настройка адресов в программе на ту область физической памяти, в которую программа загрузилась). Эта функция выполняется загрузчиком, который обычно является частью ОС.
Возможен также вариант, когда компоновка выполняется при каждом запуске программы на выполнение и совмещается с загрузкой. Это делает связывающий загрузчик. Вариант связывания при запуске более расходный, так как затраты на связывание тиражируются при каждом запуске. Однако он обеспечивает:
•большую гибкость в сопровождении, так как позволяет менять отдельные объектные модули программы, не меняя остальных модулей;
•экономию внешней памяти, поскольку объектные модули, используемые во многих программах, не копируются в каждый загрузочный модуль, а хранятся в одном экземпляре.
Вариант интерпретации подразумевает прямое исполнение исходного модуля.
Интерпретация – реализация смысла некоторого синтаксически законченного текста, представленного на конкретном языке.
Интерпретатор читаетизисходногоочереднуюпрограммнуюконструкцию, переводит его в машинный язык и выполняет. Все затраты на подготовку тиражируются при каждом выполнении, следовательно, интерпретируемая программа в общем случае оказывается менее эф-
13
1. Введение в системное программирование
фективной, чем компилируемая. Однако интерпретация обеспечивает удобство разработки, гибкость в сопровождении и переносимость.
Не обязательно подготовка программы должна вестись на той же вычислительной машине и в той же ОС, в которых программа будет выполняться. Системы, обеспечивающие подготовку программ в среде, отличной от целевой, называются кросс-системами. В ней может выполняться вся подготовка или ее отдельные этапы:
•трансляция;
•редактирование связей;
•отладка.
Типовое применение кросс-систем – для тех случаев, когда целевая вычислительная машина просто не имеет ресурсов, необходимых для подготовки программ. Таковыми, например, являются встроенные системы. Программные средства, обеспечивающие отладку программы на целевой системе, можно также рассматривать как частный случай кросс-системы.
1.3. Обязанности системных программистов
Приведем неполный перечень обязанностей системных программистов согласно утвержденному профессиональному стандарту «Системный программист» [6]:
•создание модулей системного программного обеспечения;
•создание встраиваемого программного обеспечения и драйверов устройств;
•созданиесистемныхутилитикомпонентоперационныхсистем;
•создание средств разработки программ.
Каквидно,профессиясистемногопрограммистанеутратилаактуальность и, следовательно, достойна быть предметом нашего изучения.
Для разработки мы будем пользоваться GNU Toolchain – набором пакетов программ, необходимых для компиляции и генерации выполняемого кода из исходных текстов [7]. Являются стандартным средством разработки программ и ядра ОС Linux. Набор инструментов содержит следующие пакеты программ:
• заголовочные файлы ядра Linux;
14
Контрольные вопросы и задания
•binutils (компоновщик ld и другие программы);
•GNU Compiler Collection (он же gcc) – набор компиляторов;
•стандартная библиотека языка С – GNU/libc;
•GNU make;
•autotools.
Контрольные вопросы и задания
1.Опишите отличия прикладного, промежуточного и системного программного обеспечения.
2.К какому классу ПО относятся операционные системы?
3.К какому классу ПО относятся среды программирования?
4.Возможна ли трансляция с одного языка программирования высокого уровня на другой язык программирования высокого уровня?
5.Возможна ли компиляция с одного языка программирования низкого уровня на другой язык программирования низкого уровня?
6.За счет чего в общем случае компилируемые программы оказываются быстрее интерпретируемых?
7.Возможна ли кросс-компиляция программ?
8.Всегда ли готовы объектные модули к загрузке с последующим выполнением?
9.Зачем выполняется компоновка объектных модулей в исполняемый модуль?
10.Подготовьте эссе на тему обязанностей системного програм-
миста.
15
2. МНОГОЗАДАЧНОСТЬ В ОПЕРАЦИОННЫХ СИСТЕМАХ
Программа, которая не взаимодействует с внешним миром, вряд ли может сделать что-то полезное. Вывести сообщение на экран, прочитать данные из файла, установить сетевое соединение – это примеры действий, которые программа не может совершить без помощи ОС. В Linux прикладной программный интерфейс ядра организован через системные вызовы. Системный вызов (syscall) можно рассматривать как функцию, которую для клиента выполняет ОС. Примеры наиболее популярных системных вызовов мы будем приводить в этой и последующей главах.
2.1. Многозадачность: процессы
Выполняющийся экземпляр программы называется процессом. Если на экране отображаются два терминальных окна, то, скорее всего, одна и та же терминальная программа запущена дважды – просто ей соответствуют два процесса. В каждом окне работает интерпретатор команд, и это еще один процесс. Когда пользователь вводит команду в интерпретатор, соответствующая ей программа запускается в виде процесса. По завершении работы программы управление вновь передается процессу интерпретатора.
В некоторых случаях в рамках одного приложения создаются несколько процессов, чтобы оно могло выполнить группу действий одновременно. Это повышает надежность приложения и предоставляет возможность использования уже написанных программ.
Большинство рассматриваемых далее функций управления процессами доступно и в других UNIX-подобных ОС. Обычно имеет смысл взглянуть на файл unistd.h, однако никогда не помешает проверить это в документации.
Каждый процесс в Linux помечается уникальным 16-битным идентификатором (PID). Идентификатор назначается последовательно по мере создания процессов.
16
2.1.Многозадачность: процессы
Увсех процессов, кроме init, имеется также родительский процесс. Процесс init в этой иерархии процессов является корневым элементом. У каждого процесса, за исключением init, есть идентификатор предка (PPID).
Работая с идентификаторами процессов в программах на языке C, следует объявлять соответствующие переменные как имеющие тип pid_t, который определен наряду с другими в файле sys/types.h. Программа может узнать PID своего процесса с помощью системного вызова getpid, а PPID – с помощью системного вызова getppid. Эти вызовы используются в программе, листинг которой приведен далее:
#include <stdio.h>
#include <unistd.h> int main()
{
printf("The process ID is %d\n", getpid()); printf("The parent process ID is %d\n", getppid()); return 0;
}
При каждом вызове наша программа сообщает о разных идентификаторах, поскольку всякий раз запускается новый процесс. Тем не менее если программа вызывается из одной и той же командной оболочки, то идентификатор родителя оказывается одинаковым.
Для вывода информации о процессах обычно используется стандартная утилита ps.
Для уничтожения процесса предназначена команда kill, которая в качестве одного из параметров получает PID уничтожаемого процесса
исигнал SIGTERM.
ВLinux поддерживается целая система сигналов, с помощью которых могут взаимодействовать процессы. Некоторые из этих сигналов приведены далее:
Имя |
Значение |
Эффект |
SIGHUP |
1 |
Зависание |
SIGINT |
2 |
Прерывание от клавиатуры |
SIGKILL |
9 |
Удаление процесса |
SIGTERM |
15 |
Прерывание процесса |
17
2. Многозадачность в операционных системах
SIGSTOP |
19, 23 |
Приостановка процесса |
SIGBUS |
7 |
Ошибка шины |
SIGSEGV |
11 |
Ошибка памяти |
SIGFPE |
8 |
Ошибка при работе |
|
|
с плавающей точкой |
SIGABRT |
6 |
Системный вызов abort |
SIGСHLD |
17 |
Приостановка дочернего |
|
|
процесса |
Сигналы SIGKILL и SIGSTOP нельзя перехватить, заблокировать или игнорировать.
2.2. Создание процессов
Принято говорить о двух способах создания процессов. Первый из них относительно прост, однако применяется сравнительно редко, поскольку неэффективен и связан с риском для безопасности ОС. Второй способ сложнее, но избавлен от недостатков первого. Рассмотрим их более подробно.
Функция system определена в стандартной библиотеке языка C и позволяет вызывать из программы системную команду таким образом, как если бы она была запущена из командной строки. Фактически эта функция запускает стандартный интерпретатор команд и передает ему команду для выполнения. В программе, чей листинг приведен далее, вызывается команда, отображающая содержимое корневого каталога:
#include <stdio.h>
#include <unistd.h> int main()
{
int returnValue = system("ls -l /"); return returnValue;
}
Функция system возвращает код завершения указанной команды. Если по каким-то причинам интерпретатор команд не может быть запущен, то возвращается значение 127, а при возникновении других ошибок – значение (–1).
18
2.2. Создание процессов
Эта функция подвержена тем же ограничениям безопасности, что и системный интерпретатор. Рассчитывать на какую-то конкретную версию оболочки и на одинаковое поведение для пользователей с разными привилегиями не приходится. В большинстве сценариев предпочтительнее создавать процессы с помощью функций fork и exec.
В Linux с помощью функции fork создается дочерний процесс, являющийся полной копией своего родителя. Затем необходимо воспользоваться одной из функций семейства exec. Они заставляют требуемый процесс перестать быть экземпляром одной программы и превратиться в экземпляр другой программы.
Вызывая fork, программа создает дубликат, называемый дочерним процессом. Родитель продолжает выполнять программу с той точки, где была вызвана fork. То же самое делает дочерний процесс.
Очевидно, что родительский и дочерний процессы отличаются друг от друга значениями идентификаторов PID. Выше мы показали, как программа может узнать, в каком процессе она находится. Сама функция fork реализует несколько иной подход. Она возвращает разные значения в родительском и дочернем процессах. Родитель получает идентификатор своего потомка, а дочернему процессу возвращается нулевое значение. Процессов с нулевым PID в системе нет, поэтому родительский процесс легко разбирается в ситуации.
Далее приведен код ветвления программы с помощью fork:
#include <stdio.h> #include <unistd.h>
#include <sys/types.h> int main()
{
pid_t childPid;
printf("The main program PID is %d\n", getpid()); childPid = fork();
if(0 != childPid)
{
printf("This is parent process, its PID is %d\n", getppid()); printf("The child's PID is %d\n", childPid);
}
else
19
2. Многозадачность в операционных системах
{
printf("This is child process, its PID is %d\n", getppid());
}
return 0;
}
Здесь true-ветвь оператора if выполняется родителем, а false – дочерним процессом.
Функции семейства exec заменяют программу, выполняющуюся в текущем процессе, другой. Когда программа вызывает функцию exec, ее выполнение немедленно прекращается и начинает работу новая программа.
Функции из семейства exec отличаются друг от друга по своим возможностям и способу вызова:
•функции execvp и execlp принимают в качестве аргумента имя программы и ищут ее в каталогах, определенных переменной окружения PATH. Всем остальным функциям нужно передавать полное имя программы;
•execv, execvp и execve принимают список аргументов в виде массива указателей на строки, оканчивающегося NULL-указателем;
•execl, execlp и execle принимают список аргументов переменного размера;
•execve и execle в качестве дополнительного аргумента принимают массив переменных среды. Он содержит строковые указатели
иоканчивается пустым указателем. Каждая строка должна иметь вид «ПЕРЕМЕННАЯ = Значение».
Поскольку функции exec заменяют одну программу другой, то они возвращают значения только в том случае, если вызов программы невозможен из-за ошибки.
Список аргументов программы аналогичен аргументам командной строки, указываемым при запуске в интерактивном режиме.
Программа, чей код приведен далее, отображает содержимое корневого каталога, однако в отличие от приводившегося ранее кода команда выполняется напрямую, а не через интерпретатор командной строки:
#include <stdio.h>
#include <stdlib.h>
20
