Самодел 2 (1114717), страница 7
Текст из файла (страница 7)
first =’A’;
last =’Z’;
} else {
first =’a’;
last =’z’;
}
for (ch = first; ch <= last; ch++)
write(1,&ch,1);
_exit(0);
В этом случае после выхода из if родительский процесс запишет stdout строчку «ABCD…YZ» и завершится, а сыновний – строчку «abcd…yz» и завершится. Если при порождении возникла ошибка, то у нас так и останется один процесс, который выведет строчку «abcd…yz».
Семейство системных вызовов exec()
Для замены тела процесса на содержимое файла для исполнения используется системный вызов exec(). После этого вызова данный процесс начинает выполнять другую программу, передавая управление на точку ее входа.
#include <unistd.h>
int execl (const char *path, char *arg0, …, char *argn, 0);
path – имя файла, содержащего исполняемый код программы
arg0 – имя файла, содержащего вызываемую на выполнение программу
arg1,…,argn – аргументы программы, передаваемые ей при вызове
Возвращается: при удачном завершении 0, в случае ошибки -1 (происходит возврат к первоначальной программе в точку после вызова exec()). Заметим, что выполнение “нового” тела происходит в рамках уже существующего процесса. То есть после вызова exec() сохраняется идентификатор процесса, и идентификатор родительского процесса, таблица дескрипторов файлов, приоритет, и большая часть других атрибутов процесса. Фактически происходит замена сегмента кода и сегмента данных.
П
ример:
#include <unistd.h>
int main(int argc, char **argv)
{
…
/*тело программы*/
…
execl(“/bin/ls”, ”ls”, ”-l”,(char*)0); /* или execlp(“ls”, ”ls”, ”-l”,(char*)0); */
printf(“это напечатается в случае неудачного обращения к предыдущей функции, к примеру, если не был найден файл ls \n”);
…
}
В этом примере выполнялся некоторый процесс. После вызова exec() его тело полностью заменилось на тело программы ls, если она была найдена. Процесс закончит свое существование после окончания работы ls. Если произойдет ошибка, процесс продолжит свое выполнение с точки, сразу после exec(), в данном случае выведет строку.
Пример. Вызов программы компиляции
int main(int argc, char **argv)
{
char *pv[]={“cc”, “-o”, “ter”, “ter.c”, (char*)0};
…
/*тело программы*/
…
execv (“/bin/cc”, pv);
…
}
Если exec() не вернет ошибку, то процесс запустит cc так, что он преобразуется в процесс с телом cc и массивом **argv, значения которого равны значениям массива **pv.
И
спользование схемы fork-exec
int main(int argc, char **argv)
{
if (fork() == 0) {
execl(“/bin/echo”, ”echo”,”это”,”сообщение один”,NULL);
printf(“ошибка”);
}
if (fork() == 0) {
execl(“/bin/echo”,”echo”,”это”,”сообщение два”,NULL);
printf(“ошибка”);
}
if(fork() == 0)
{
execl(“/bin/echo”,”echo”,”это”,”сообщение три”,NULL);
printf(“ошибка”);
}
printf(“процесс-предок закончился”)
}
Процесс порождает трех сыновей, в каждом из которых тело заменяется телом программы echo, выдающей на экран соответствующее сообщение, или выводится «ошибка», если exec() не был выполнен. Сам процесс-отец пишет, что он завершился, после чего завершается.
Завершение процесса
Процесс может завершиться в результате одного из событий:
-
Системный вызов _exit() (при этом процесс-сын может передать процессу отцу параметры)
-
Выполнение оператора return, входящего в состав функции main()
-
Получение сигнала (сигнал – программный аналог прерывания)
-
Ошибка (на самом деле она инициирует сигнал)
Завершение процесса по _exit():
#include <unistd.h>
void _exit(int status);
status – код возврата программы, имеющий (как правило) значение: 0 при успешном завершении, не 0 при неудаче (возможно, номер варианта).
Сам вызов никогда не завершается неудачно, поэтому у него нет возвращаемого значения.
-
Освобождается сегмент кода и сегмент данных процесса
-
Закрываются все открытые дескрипторы файлов
-
Если у процесса имеются потомки, их предком назначается процесс с идентификатором 1
-
Освобождается большая часть контекста процесса (кроме статуса завершения и статистики выполнения)
-
Процессу-предку посылается сигнал SIGCHLD
Получение информации о завершении своего потомка
Процесс-предок имеет возможность получить информацию о завершении потомка с помощью системного вызова wait():
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
status – по завершению содержит:
-
в старшем байте – код завершения процесса-потомка (пользовательский код завершения процесса)
-
в младшем байте – индикатор причины завершения процесса-потомка, устанавливаемый ядром ОС Unix (системный код завершения процесса)
Возвращается: PID завершенного процесса или –1 в случае ошибки или прерывания
-
Приостановка родительского процесса до завершения (остановки) какого-либо из потомков
-
После передачи информации о статусе завершения предку, все структуры, связанные с процессом – «зомби», освобождаются, удаляется запись о нем из таблицы процессов
Пример. Использование системного вызова wait()
#include <stdio.h>
int main(int argc, char **argv)
{
int i;
for (i=1; i<argc; i++) {
int status;
if(fork()>0) { wait(&status); printf(“process-father\n”); continue; }
execlp(argv[i], argv[i], 0);
exit();
}
}
Процесс-предок запускает по очереди процессы, имена исполняемых файлов которых были заданы в командной строке при запуске. При этом запустив одного сына, он ожидает пока он завершится, после чего выдает строчку «process-father» и продолжает выполнять цикл for.
Если процесс хочет получить информацию о завершении каждого из своих потомков, он должен несколько раз обратиться к вызову wait().
Если процесс не интересуется информацией status, он может передать в качестве аргумента вызову wait() NULL-указатель.
Если к моменту вызова wait() один из потомков данного процесса уже завершился, перейдя в состояние зомби, то выполнение родительского процесса не блокируется, и wait() сразу же возвращает информацию об этом завершенном процессе. Если же к моменту вызова wait() у процесса нет потомков, системный вызов сразу же вернет –1. Также возможен аналогичный возврат из этого вызова, если его выполнение будет прервано поступившим сигналом.
Еще один пример:
int main(int argc, char **argv)
{
if ((fork()) == 0) { execl(“/bin/echo”,”echo”,”this is”,”string 1”,0); exit(); }
if ((fork()) == 0) { execl(“/bin/echo”,”echo”,”this is”,”string 2”,0); exit(); }
printf(“process-father is waiting for children\n”);
while(wait() != -1);
printf(“all children terminated\n”);
exit();
}
Процесс запускает два процесса, которые должна выдать на экран по строчке (неизвестно, какой из них сделает это раньше). Сам процесс после этого приостанавливается, ожидая завершения обоих сыновних процессов, потом сообщает нам об этом, печатая соответствующую строчку, и завершается.
wait() может работать в синхронном/асинхронном режимах. Если существуют сыновние процессы, и ни один из них не завершился, или нет событий, то при синхронной работе wait() родительский процесс будет приостановлен до тех пор, пока сын не завершится или не произойдет событие. При асинхронной работе – wait() вернет -1 и возвратится к выполнению процесса-предка.
Ж
изненный цикл процессов в ОС UNIX
Пример блокировки – wait() в синхронном режиме, чтение данных извне и момент, когда их уже нужно использовать.
Зомби – уже завершившийся процесс, но занятые им ресурсы еще не освобождены (процесс ожидает принятия сигнала SIGCHLD отцом).
Формирование процессов 0 и 1
Начальная загрузка
Когда мы включаем компьютер, начинается аппаратная загрузка: грузится то, что зашито в неизменяемой памяти (вообще говоря, изменяемой, но не оперативно). Эта программа знает перечень системных устройств, их приоритеты, где находится программный загрузчик ОС (например, смотрит приоритеты (пусть это будут floppy, HD, CD_ROM) и проверяет является ли каждый из них системным пока не найдет таковое). Если аппаратный загрузчик добрался до программного загрузчика, он считывает нулевой блок, который по умолчанию содержит точку входа загрузчика ОС. Там может быть выбор ОС для загрузки. Далее рассматривается файловая система выбранной ОС, где находится загрузчик ядра или ядро. Стартует ядро. Такова упрощенная схема загрузки.
Начальная загрузка – это загрузка ядра системы в основную память и ее запуск.
В случае ОС UNIX:
-
Чтение нулевого блока системного устройства аппаратным загрузчиком
-
Поиск и считывание в память файла /unix
-
Запуск на исполнение файла /unix
Инициализация системы
-
Установка системных часов
-
Формирование диспетчера памяти
-
Формирование значения некоторых структур данных (например, таблицы процессов)
-
Инициализация процесса с номером “0” (ядро)
-
Нулевой процесс не имеет кодового сегмента
-
Нулевой процесс существует в течение всего времени работы системы
-
Создание ядром первого процесса
-
Копируется процесс “0” (то есть нет области кода) - нестандартно
-
Создание области кода процесса “1”
-
Копирование в область кода процесса “1” программы, реализующей системный вызов exec(), который необходим для выполнения программы /etc/init – тоже нестандартно
Выполнение программ диспетчера
-
Запуск exec() для замены команды процесса “1” кодом из файла /etc/init (система включает однопользовательский режим, но т.к. теперь есть консоль, то возможен переход в многопользовательский режим)
-
Подключение интерпретатора команд к системной консоли
С
оздание многопользовательской среды
Init – это диспетчер, который отслеживает многопользовательскую работу. Существуют зарегистрированные терминалы, некоторые из которых включены. Для последних init активизирует getty. getty запрашивает и проверяет логин и пароль. В случае их правильности getty завершается, и начинается сеанс работы с системой (особый файл), запускается shell. Конец работы shell’а – это конец файла, поставленный либо как exit (logout), либо Ctrl+D. После завершения работы shell’а init порождает новый getty.
Планирование
О
сновные задачи планирования
Планирование:
-
очереди процессов на начало обработки
-
- распределения времени ЦП между процессами
-
- свопинга
-
- обработки прерываний
-
- очереди запросов на обмен
Планирование очереди процессов на начало обработки
На данном этапе определяется уровень многопроцессности системы.
Дисциплина обслуживания очереди :
- простейшая – FIFO
- по приоритету
- с учетом предполагаемого времени выполнения процесса, объема операций ввода/вывода и так далее.
Планирование распределения времени ЦП между процессами
Квант времени – непрерывный период процессорного времени.
Приоритет процесса – числовое значение, показывающее степень привилегированности процесса при использовании ресурсов ВС (в частности, времени ЦП).
Две задачи:
– определение величины кванта и
– стратегия обслуживания очереди готовых к выполнению процессов.
2 подхода:
1. Если величина кванта не ограничена – невытесняющая стратегия планирования времени ЦП (применяется в пакетных системах). Процесс запускается на процессор, потом управление предаётся самому процессу. Величина кванта сверху не регламентируется. Можно прервать процесс, чтобы дать возможность запуститься другим процессам. Пример: Netware file-server.
2. Вытесняющие стратегии величина кванта ограничена. (ОС сама насильственно определяет).