Процедура загрузки
Выяснив, что представляет собой программа, давайте рассмотрим процедуру ее загрузки в оперативную память компьютера (многие из обсуждаемых далее концепций, впрочем, в известной мере применимы и к прошивке программы в ПЗУ).
Для начала предположим, что программа была заранее собрана в некий единый самодостаточный объект, называемый загрузочным или загружаемым модулем. В ряде операционных систем программа собирается в момент загрузки из большого числа отдельных модулей, содержащих ссылки друг на друга, но об этом ниже.
Для того чтобы не путаться, давайте будем называть программой ту часть загрузочного модуля, которая содержит исполняемый код. Результат загрузки программы в память будем называть процессом или, если нам надо отличать загруженную программу от процесса ее исполнения, образом процесса. К образу процесса иногда причисляют не только код и данные процесса (подвергнутые преобразованию как в процессе загрузки, так и в процессе работы программы), но и системные структуры данных, связанные с этим процессом. В старой литературе процесс часто называют задачей.
В системах с виртуальной памятью каждому процессу обычно выделяется свое адресное пространство, поэтому мы иногда будем употреблять термин процесс и в этом смысле. Впрочем, во многих системах значительная часть адресных пространств разных процессов перекрывается – это используется Для реализации разделяемого кода и данных.
В рамках одного процесса может исполняться один или несколько потоков или нитей управления. Это понятие будет подробнее разбираться в Главе 8.
Некоторые системы предоставляют и более крупные структурные единицы, чем процесс. Например, в системах семейства Unix существуют группы процессов, которые используются для реализации логического объединения и объединяют не только код и данные процесса (подвергнутые преобразованию как в процессе загрузки, так и в процессе работы программы), но и системные структуры данных, связанные с этим процессом. В старой литературе процесс часто называют задачей.
В системах с виртуальной памятью каждому процессу обычно выделяется свое адресное пространство, поэтому мы иногда будем употреблять термин процесс и в этом смысле. Впрочем, во многих системах значительная часть адресных пространств разных процессов перекрывается – это используется Для реализации разделяемого кода и данных.
В рамках одного процесса может исполняться один или несколько потоков или нитей управления. Это понятие будет подробнее разбираться в Главе 8.
Некоторые системы предоставляют и более крупные структурные единицы, чем процесс. Например, в системах семейства Unix существуют группы процессов, которые используются для реализации логического объединения процессов в задания (job). Ряд систем имеют также понятие сессии – совокупности всех заданий, которые пользователь запустил в рамках одного сеанса работы. Впрочем, соответствующие концепции часто плохо определены, а их смысл сильно меняется от одной ОС к другой, поэтому мы практически не будем обсуждать эти понятия.
В более старых системах и в старой литературе называют результат загрузки задачей, а процессами – отдельные нити управления.
Однако в наиболее распространенных ныне ОС семейств Unix и Win32, принято задачу называть процессом, а процесс – нитью (tread).
Этой терминологии мы и будем придерживаться, кроме тех случаев, когда будем обсуждать примеры из жизни ОС, в которой принята иная терминология.
Создание процессов в 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(1, 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 */ }
Но вернемся к способам загрузки программ.