Выяснив, что представляет собой программа, давайте рассмотрим
процедуру ее загрузки в оперативную память компьютера (многие из обсуждаемых
далее концепций, впрочем, в известной мере применимы и к прошивке программы
в ПЗУ).
Для начала предположим, что программа была заранее собрана в некий единый
самодостаточный объект, называемый загрузочным
или загружаемым модулем. В ряде операционных
систем программа собирается в момент загрузки из большого числа отдельных
модулей, содержащих ссылки друг на друга, но об этом ниже.
Для того чтобы не путаться, давайте будем называть программой
ту часть загрузочного модуля, которая содержит исполняемый код. Результат
загрузки программы в память будем называть процессом
или, если нам надо отличать загруженную программу от процесса ее исполнения,
образом процесса. К образу процесса иногда
причисляют не только код и данные процесса (подвергнутые преобразованию
как в процессе загрузки, так и в процессе работы программы), но и системные
структуры данных, связанные с этим процессом. В старой литературе процесс
Создание процессов в Unix
В системах семейства Unix новые процессы создаются системным вызовом fork.
Этот вызов создает два процесса, образы которых в первый момент полностью
идентичны, у них различается только значение, возвращенное вызовом fork.
Типичная программа, использующая этот вызов, выглядит так, как представлено
в примере 3.1.
При этом каждый из процессов имеет свою копию всех локальных и статических
переменных. На процессорах со страничным диспетчером памяти физического
копирования не происходит. Изначально оба процесса используют одни и те
же страницы памяти, а дублируются только те из них, которые были изменены.
На системах, не имеющих страничного или сегментного диспетчера памяти,
fork требует копирования адресных пространств, что приводит к большим
накладным расходам, да и просто не всегда возможно.
Если мы хотим запустить другую программу, то мы должны исполнить системный
вызов из семейства exec. Вызовы этого семейства различаются только способом
передачи параметров. Все они прекращают исполнение текущего образа процесса
и создают новый процесс с новым виртуальным адресным пространством, но
с тем же идентификатором процесса. При этом у нового процесса будет тот
же приоритет, будут открыты те же файлы (это часто используется), и он
унаследует ряд других важных характеристик.
Несколько неожиданное, но тем не менее верное описание действия exec —
это замена образа процесса в рамках того же самого процесса.
Запуск другой программы в UNIX выглядит примерно так, как представлено
в примере 3.2.
Программа в примере 3.2 запускает командный интерпретатор /bin/sh, известный
как Bourne shell, приказывает ему исполнить команду Is -1 и перенаправляет
стандартный вывод этой команды в файл ls.log.
Техника программирования, основанная на fork/exec, несколько отличается
от принятой во многих других современных системах, в том числе Win32,
где при создании нового процесса мы сразу же указываем программу, которую
он будет исполнять.
Пример 3.1. Создание процесса в системах семейства Unix
;nt pid; /* Идентификатор порожденного процесса */
switch(pid = fork())
I
case 0: /* Порожденный процесс */
break; case -1: /Ошибка */
perror("Cannot fork");
extt(l) ; default: /* Родительский процесс */
/* Здесь мы можем ссылаться на порожденный процесс,
* используя значение pid */
Пример 3.2. Создание процесса и замена программы в системах семейства Unix
int pid; /* Идентификатор порожденного процесса */
switch (pid = fork () )
{
case 0: /* Порожденный процесс */
dup2(l, open("Is.log", 0_WRONLY I 0_CREAT)); /* Перенаправить
открытый файл #1 * (stdout) в файл Is.log */
execl("/bin/sh", "sh", "-c", "Is",
"-1", 0);
/* Сюда мы попадаем только при ошибке! */
/* fall through */ case -1: /* Ошибка */
perror("Cannot fork or exec");
exit(1); default: /* Родительский процесс */
/* Здесь мы можем ссылаться на порожденный
* процесс, используя значение pid */
}
Но вернемся к способам загрузки программ.