
- •Сравнение характеристик разных ос.
- •Задание
- •Вопросы коллоквиума.
- •«Наследники» Создание дочерних процессов
- •Задание
- •Вопросы коллоквиума
- •«Трубы» Использование программных каналов
- •Задание
- •Вопросы коллоквиума
- •«На деревню дедушке» Использование очереди сообщений
- •Задание
- •Вопросы коллоквиума
- •«Вспомнить всё» Использование семафоров и разделяемой памяти
- •Задание
- •Вопросы коллоквиума
«ОSы»
Сравнение характеристик разных ос.
Скорость математических вычислений с плавающей запятой определяется не только разрядностью шины ЭВМ. Большое значение имеет также поддержка операционной системой всех возможностей аппаратуры, процент накладных расходов на промежуточные действия и рациональный алгоритм программы.
Последнее требование особенно важно при динамическом распределении памяти, т.к. штатные средства защиты памяти операционных систем не дают полной гарантии безошибочной работы. Известно, что у каждой программы есть сегменты кода, стека и данных. Любое видоизменение сегмента кода чревато программным сбоем, непосредственное использование стека не менее опасное занятие, сегмент данных находиться в полном распоряжении программиста.
Задание
Для сравнения вычислительной мощности операционных систем Windows95 и Linux необходимо написать на языке Си программу циклического вычисления собственной трансцендентной функции и с помощью секундомера оценить время её исполнения например:
for(i=0;i<300000;i++)
result=i*3+sin(i)-sqrt(i/15);
Затем добавить в цикл строку вывода результата на экран, а потом в файл. Занести времена исполнения в сводную таблицу и сделать вывод об относительной производительности подсистем компьютера (центрального процессора, видеосистемы и дискового массива), как в рамках одной операционной системы, так и разных.
Для оценки реакции разных операционных систем на преднамеренное нарушение защиты памяти написать следующие программы:
бесконечного цикла захвата памяти, используя функции malloc, calloc и распределяя в каждом цикле от 32 до 64 кБ памяти, для исчерпания сегмента данных;
бесконечного рекурсивного вызова собственной функции. Функция должна помещать в стек значительный объем формальных параметров, до полного заполнения сегмента стека;
записи произвольных значений в ячейки 1-го Мбайта, с целью определения доступности критичных для системы областей памяти;
выхода вперед и назад за границу статического и динамически распределенного массива, для проверки реакции ОС на эту частую ошибку.
Все программы компилируются в режиме эмуляции DOS и в терминале Linux, а исполняются:
в режиме эмуляции DOS;
в окне DOS Windows95;
терминале Linux;
в окне терминала X Windows.
Занести результаты работы в отчет. Сделать выводы о количестве доступной памяти и надежности изучаемой операционной системы.
Сделать общий вывод о характеристиках сравниваемых операционных системах.
Вопросы коллоквиума.
Что такое «куча».
Сегмент стека предназначен.
Чем отличается сегментная модель памяти от линейной.
Где встречается понятие «граница в 64 к».
Для чего используется swap–файл.
«Наследники» Создание дочерних процессов
Любой UNIX-процесс состоит как минимум из сегмента кода, сегмента данных и сегмента стека. В стеке хранятся аргументы функций, переменные и адреса возврата всех функций, активных в процессе в каждый данный момент времени.
Все процессы в UNIX-системе, кроме самого первого (процесс с ID равным 0), который создается программой начальной загрузки системы, создаются с помощью системного вызова fork. После завершения системного вызова fork и родительский, и порожденный процессы возобновляют свое выполнение.
Когда процесс создается функцией fork, он получает копии сегментов текста, данных и стека своего родительского процесса. Еще он получает таблицу дескрипторов файлов, которая содержит ссылки на те же открытые файлы, что имеются и у родительского процесса. Это позволяет им совместно пользоваться одним указателем на каждый открытый файл. Кроме того, процессу присваиваются следующие атрибуты, которые либо наследуются от родительского процесса, либо устанавливаются ядром:
Реальный идентификатор владельца (rUID): идентификатор пользователя, который создал родительский процесс. С помощью этого атрибута ядро следит за тем, кто и какие процессы создает в системе.
Реальный идентификатор группы (rGID): идентификатор группы, к которой принадлежит пользователь, создавший родительский процесс. С помощью этого атрибута ядро следит за тем, какая группа и какие процессы порождает в системе.
Эффективный идентификатор пользователя (eUID): обычно совпадает с rUID, за исключением случая, когда у файла, который был выполнен для создания процесса, установлен флаг set-UID (с помощью команды или вызова chmod). В этом случае eUID процесса становится равным UID файла. Это позволяет процессу обращаться к файлам и создавать новые файлы, пользуясь такими же привилегиями, как и у владельца выполняемой программы.
Эффективный идентификатор группы владельца (eGID): обычно совпадает с rGID, за исключением случая, когда у файла, который был выполнен для создания процесса, установлен флаг set-GID. В этом случае eGID процесса становится равным GID файла. Это позволяет процессу обращаться к файлам и создавать файлы, пользуясь привилегиями группы, к которой относится программный файл.
Текущий каталог: ссылка (номер индексного дескриптора) на рабочий файл-каталог.
Корневой каталог: ссылка (номер индексного дескриптора) на корневой каталог.
Обработка сигналов: параметры обработки сигналов. Сигнальная маска: маска, которая показывает, какие сигналы подлежат блокированию.
Значение umask: маска, которая используется при создании файлов для установки необходимых прав доступа к ним.
Значение nice: значение приоритета процесса.
Управляющий терминал: управляющий терминал процесса.
Следующие атрибуты у родительского и порожденного процессов разные:
Идентификатор процесса (PID): целочисленный идентификатор, уникальный для процесса во всей операционной системе.
Идентификатор родительского процесса (PPID): идентификационный номер родительского процесса.
Блокировки файлов: набор блокировок файлов, созданных родительским процессом, порожденным процессом не наследуется.
После системного вызова fork родительский процесс может посредством системного вызова wait или waitpid приостановить свое выполнение до завершения порожденного процесса или продолжать выполнение независимо от порожденного процесса.
Процесс завершает выполнение при помощи системного вызова _exit. Аргументом этого вызова является код статуса завершения процесса. По соглашению, нулевой код статуса завершения означает, что процесс завершил выполнение успешно, а ненулевой свидетельствует о неудаче.
С помощью системного вызова ехес процесс может выполнить другую программу. Если вызов выполняется успешно, ядро заменяет существующие сегменты текста, данных и стека процесса новым набором, который представляет собой новую подлежащую выполнению программу. Тем не менее процесс остается прежним (поскольку идентификатор процесса и идентификатор родительского процесса одинаковы), и его таблица дескрипторов файлов и открытые каталоги остаются теми же.
Когда программа, вызванная посредством ехес, заканчивает выполнение, она завершает процесс. Код статуса завершения этой программы сообщается родителю данного процесса с помощью функции wait.
Функции fork и ехес, как правило, используются вместе для порождения подпроцесса, предназначенного для выполнения другой подпрограммы. Например, shell в UNIX выполняет каждую команду пользователя путем вызова fork и ехес, и затребованная команда выполняется в порожденном процессе. Прототип функции fork имеет следующий вид:
#include <sys/stdtypes.h>
#include <sys/types.h>
pid_t fork( void )
Функция fork не принимает аргументов и возвращает значение типа pid_t (определяемое в <sys/types.h>). Этот вызов может давать один из следующих результатов:
Успешное выполнение. Создается порожденный процесс, и функция возвращает идентификатор этого порожденного процесса родительскому. Порожденный процесс получает от fork нулевой код возврата.
Неудачное выполнение. Порожденный процесс не создается, а функция присваивает переменной errno код ошибки и возвращает значение -1.
При успешном вызове fork создается порожденный процесс. Как порожденный процесс, так и родительский планируются ядром UNIX для выполнения независимо, а очередность запуска этих процессов зависит от реализации ОС. По завершении вызова fork выполнение обоих процессов возобновляется. Возвращаемое значение вызова fork используется для того, чтобы определить, является ли процесс порожденным или родительским. Таким образом, родительский и порожденный процессы могут выполнять различные задачи одновременно.
Системный вызов _exit завершает процесс. В частности, этот вызов освобождает сегмент данных вызывающего процесса, сегмент стека и U-области и закрытие всех открытых дескрипторов файлов. Однако запись в таблице процессов для этого процесса остается нетронутой, с тем чтобы там регистрировался статус завершения процесса, а также отображалась статистика его выполнения (например, информация об общем времени выполнения, количестве пересланных блоков ввода-вывода данных и т.д.). Родительский процесс может выбрать хранящиеся в записи таблицы процессов данные посредством системного вызова wait. Этот вызов освобождает также запись в таблице процессов, относящуюся к порожденному процессу.
Если процесс создает порожденный процесс и заканчивается до завершения последнего, то ядро назначит процесс init в качестве управляющего для порожденного процесса (это второй процесс, создаваемый после начальной загрузки ОС UNIX. Его идентификатор всегда равен 1). После завершения порожденного процесса соответствующая запись в таблице процессов будет уничтожена процессом init. Прототип функции _exit имеет следующий вид:
#indude <unistd.h>
void _exit(int exit_code);
Целочисленный аргумент функции _exit — это код завершения процесса. Родительскому процессу передаются только младшие 8 битов этого кода. Код завершения 0 означает успешное выполнение процесса, а ненулевой код — неудачное выполнение. В некоторых UNIX-системах в заголовке <stdio.h> определяются символические константы EXIT_SUCCESS и EXIT_FAILURE, которые могут быть использованы как аргументы функций _exit, соответствующие успешному и неудачному завершению.
Функция _exit никогда не выполняется неудачно, поэтому возвращаемого значения для нее не предусмотрено. Библиотечная С-функция exit является оболочкой для _exit. В частности, exit сначала очищает буферы и закрывает все открытые потоки вызывающего процесса. Затем она вызывает все функции, которые были зарегистрированы посредством функции atexit (в порядке, обратном порядку их регистрации), и, наконец, вызывает _exit для завершения процесса.
Родительский процесс использует системный вызов wait для перехода в режим ожидания завершения порожденного процесса и для выборки его статуса завершения (присвоенного порожденным процессом с помощью функции _exit). Кроме того, эти вызовы освобождают ячейку таблицы процессов порожденного процесса с тем, чтобы она могла использоваться новым процессом. Прототип этой функций выглядят так:
#include <sys/wait.h>
pid_t wait ( int* status_p );
Функция wait приостанавливает выполнение родительского процесса до тех пор, пока ему не будет послан сигнал, либо пока один из его порожденных процессов не завершится или не будет остановлен (а его статус еще не будет сообщен). Если порожденный процесс уже завершился или был остановлен до вызова wait, функция wait немедленно возвратится со статусом завершения порожденного процесса (его значение содержится в аргументе status_p), a возвращаемым значением функции будет PID порожденного процесса. Если, однако, родительский процесс не имеет порожденных процессов, завершения которых он ожидает, или был прерван сигналом при выполнении wait, функция возвращает значение -1, а переменная errno будет содержать код ошибки. Следует отметить, что если родительский процесс создал более одного порожденного, функция wait будет ожидать завершения любого из них.
Приведенные ниже прототипы функций, возвращают атрибуты процессов:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid (void); pid_t getppid (void);
pid_t getpgrp (void); uid_t getuid (void); uid_t geteuid (void);
gid_t getgid (void); gid_t getegid (void);
Необходимо отметить, что в старых версиях UNIX все перечисленные функции возвращают значения типа int. Типы данных pid_t, uid_t и gid_t определяются в заголовке <sys/types.h>.
Функции getpid и getppid возвращают идентификаторы текущего и родительского процессов соответственно. Для этих системных вызовов аргументы не требуются.
Функция getpgrp возвращает идентификатор группы вызывающего процесса. Каждый процесс в системах UNIX принадлежит к группе процессов. Когда пользователь входит в систему, регистрационный процесс становится лидером сеанса и лидером группы процессов. Идентификатор сеанса и идентификатор группы процесса для лидера сеанса совпадают с PID данного процесса. Если регистрационный процесс создает для выполнения заданий новые порожденные процессы, эти процессы будут принадлежать к тому же сеансу и группе, что и регистрационный процесс. Если регистрационный процесс переводит определенные задания в фоновый режим, то процессу, связанному с каждым фоновым заданием, будет присвоен другой идентификатор группы. Если фоновое задание выполняется более чем одним процессом, то процесс, создавший все остальные процессы с целью выполнения данного фонового задания, станет лидером этой группы процессов.
Системные вызовы getuid и getgid возвращают соответственно реальный идентификатор владельца и реальный идентификатор группы вызывающего процесса. Реальные идентификаторы группы и владельца являются идентификаторами лица, создавшего этот процесс.
Системные вызовы geteuid и getegid возвращают значения атрибутов eUID и eGID вызывающего процесса. Эти идентификаторы используются ядром для определения прав вызывающего процесса на доступ к файлам. Такие атрибуты присваиваются также идентификаторам группы и владельца для файлов, создаваемых процессом. В нормальных условиях эффективный идентификатор пользователя процесса совпадает с его реальным идентификатором. Однако в случае, когда установлен бит смены идентификатора пользователя set-UID выполняемого файла, эффективный идентификатор владельца процесса будет равен идентификатору владельца исполняемого файла. Это дает процессу такие же права доступа, какими обладает пользователь, владеющий исполняемым файлом. Аналогичный механизм применим и к эффективному идентификатору группы. Так, эффективный идентификатор группы процесса будет отличаться от реального идентификатора группы, если установлен бит смены идентификатора группы set-GID соответствующего исполняемого файла.
Флаги set-UID и set-GID исполняемого файла можно изменить с помощью команды chmod.
Каждый процесс может пребывать в двух фазах: системной (внутри тела системного вызова - его выполняет для нас ядро операционной системы) и пользовательской (внутри кода самой программы). Время, затраченное процессом в каждой фазе, может быть измеряно системным вызовом times(). Кроме того, этот вызов позволяет узнать суммарное время, затраченное порожденными процессами. Системный вызов times() заполняет структуру:
struct tms {
clock_t tms_utime;
clock_t tms_stime;
clock_t tms_cutime;
clock_t tms_cstime;
};
и возвращает значение
#include <sys/times.h>
struct tms time_buf;
clock_t real_time = times(&time_buf);
Все времена измеряются в "тиках" - некоторых долях секунды. В современных системах эта цифра имеет значение 100. В следующей таблице приведены данные о содержимом полей структуры tms.
Поле |
Смысл поля |
tms_utime |
время, затраченное процессом в пользовательской фазе |
tms_stime |
время, затраченное процессом в системной фазе |
tms_cutime |
время, затраченное порожденными процессами в пользовательской фазе: сумма всех tms_utime и tms_cutime порожденных процессов |
tms_cstime |
время, затраченное порожденными процессами в системной фазе: сумма всех tms_stime и tms_cstime порожденных процессов |
real_time - время, соответствующее астрономическому времени системы. Имеет смысл измерять только разность времен.
В приведенном ниже примере исполнимый файл ./father создаёт трех (./son) сыновей и выводит время работы. «Дети» выводят свои и «папины» идентификаторы.
/*father.c*/
#include<stdio.h>
#include <sys/times.h>
#include<sys/types.h>
#include <time.h>
#include <math.h>
#include <iostream.h>
int main(int c, char**v)
{
int i,st;
float j;
clock_t start_time, elapsed_time,real_time;
struct tms time_start, time_end;
real_time = times(&time_start);
printf("папа pid=%d деда pid=%d\n",getpid(),getppid());
for(i=0;i<3;i++)
{
if(fork()==0)
execvp("./son",NULL);
else
wait(&st);
}
real_time = times(&time_end);
time_end.tms_utime-=time_start.tms_utime;
time_end.tms_stime-=time_start.tms_stime;
time_end.tms_cutime-=time_start.tms_cutime;
time_end.tms_cstime-=time_start.tms_cstime;
printf("ядерное время процесса=%d время процесса=%d \n
ядерное время детей=%d время детей=%d \n",time_end.tms_stime, time_end.tms_utime, time_end.tms_cstime, time_end.tms_cutime);
exit(0);
}
/*son.c*/
#include<stdio.h>
#include <time.h>
int main(int c, char**v)
{
int i,st;
printf("сына pid=%d папа pid=%d\n",getpid(),getppid());
exit(0);
}