Основы операционных систем

Изучение теорий операционных систем. Исследование принципов построения и особенностей проектирования современных ОС. Сущность виртуальной памяти и алгоритма синхронизации. Рассмотрение операционной и файловой системы, система управления вводом-выводом.

Рубрика Программирование, компьютеры и кибернетика
Вид книга
Язык русский
Дата добавления 12.01.2010
Размер файла 2,6 M

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

Послать сигнал (не имея полномочий суперпользователя) можно только процессу, у которого эффективный идентификатор пользователя совпадает с эффективным идентификатором пользователя для процесса, посылающего сигнал.

Аргумент pid описывает, кому посылается сигнал, а аргумент sig - какой сигнал посылается. Этот системный вызов умеет делать много разных вещей, в зависимости от значения аргументов:

Если pid > 0 и sig > 0, то сигнал номером sig (если позволяют привилегии) посылается процессу с идентификатором pid.

Если pid = 0, a sig > 0, то сигнал с номером sig посылается всем процессам в группе, к которой принадлежит посылающий процесс.

Если pid = -1, sig > 0 и посылающий процесс не является процессом суперпользователя, то сигнал посылается всем процессам в системе, для которых идентификатор пользователя совпадает с эффективным идентификатором пользователя процесса, посылающего сигнал.

Если pid = -1, sig > 0 и посылающий процесс является процессом суперпользователя, то сигнал посылается всем процессам в системе, за исключением системных процессов (обычно всем, кроме процессов с pid = 0 и pid = 1).

Если р!с! < 0, но не -1, в1д > 0, то сигнал посылается всем процессам из группы, идентификатор которой равен абсолютному значению аргумента р1с! (если позволяют привилегии).

Если значение Б1д = 0, то производится проверка на ошибку, а сигнал не посылается, так как все сигналы имеют номера > 0. Это можно использовать для проверки правильности аргумента р!с! (есть ли в системе процесс или группа процессов с соответствующим идентификатором).

Возвращаемое значение

Системный вызов возвращает 0 при нормальном завершении и -1 при ошибке.

Изучение особенностей получения терминальных сигналов текущей и фоновой группой процессов

Возьмем тривиальную программу 12-13-1.с, в которой процесс порождает ребенка, и они оба зацикливаются, и на ее основе проиллюстрируем сказанное выше:

/* Тривиальная программа для иллюстрации понятий группа процессов, сеанс, фоновая группа и т. д. */ #include <unistd.h> int main(void){

(void)fork();

while(1);

return 0;

}

Для этого будем использовать команду ps с опциями -е и j, которая позволяет получить информацию обо всех процессах в системе и узнать их идентификаторы, идентификаторы групп процессов и сеансов, управляющий терминал сеанса и к какой группе процессов он приписан. Набрав команду "ps -е j" (обратите внимание на наличие пробела между буквами е и j!!!) мы получим список всех процессов в системе. Колонка P1D содержит идентификаторы процессов, колонка PGID -- идентификаторы групп, к которым они принадлежат, колонка SID -- идентификаторы сеансов, колонка TTY-- номер соответствующего управляющего терминала, колонка TPGID (может присутствовать не во всех версиях UNIX, но в Linux она есть) -- к какой группе процессов приписан управляющий терминал.

Наберите тривиальную программу, откомпилируйте ее и запустите на исполнение (лучше всего из-под оболочки Midnight Commander -- mc). Запустив команду "ps -е j " с другого экрана, проанализируйте значения идентификаторов группы процессов, сеансов, прикрепления управляющего терминала, текущей и фоновой групп. Убедитесь, что тривиальные процессы относятся к текущей группе сеанса. Проверьте реакцию текущей группы на сигналы SIGINT -- нажатие клавиш <CTRL> и <С> -- и SIGQUIT - нажатие клавиш <CTRL> и <4>.

Запустите теперь тривиальную программу в фоновом режиме, например командой "a.out &". Проанализируйте значения идентификаторов группы процессов, сеансов, прикрепления управляющего терминала, текущей и фоновой групп. Убедитесь, что тривиальные процессы относятся к фоновой группе сеанса. Проверьте реакцию фоновой группы на сигналы SIGINT -- нажатие клавиш <CTRL> и <С>, и SIGQUIT - нажатие клавиш <CTRL> и <4>. Ликвидируйте тривиальные процессы с помощью команды kill.

Изучение получения сигнала SIGHUP процессами при завершении лидера сеанса

Возьмите снова тривиальную программу из предыдущего раздела и запустите ее на исполнение из-под Midnight Commander в текущей группе. Проанализировав значения идентификаторов группы процессов, сеансов, прикрепления управляющего терминала, текущей и фоновой групп, ликвидируйте лидера сеанса для тривиальных процессов. Убедитесь, что все процессы в текущей группе этого сеанса прекратили свою работу.

Запустите тривиальную программу в фоновом режиме. Снова удалите лидера сеанса для тривиальных процессов. Убедитесь, что фоновая группа продолжает работать. Ликвидируйте тривиальные процессы.

Системный вызов signal(). Установка собственного обработчика сигнала

Одним из способов изменения поведения процесса при получении сигнала в операционной системе UNIX является использование системного вызова signal().

Системный вызов signaJO

Прототип системного вызова

include <signal.h> void (*signal (int sig,

void (*handler) .(int.))) Unt);

Описание системного вызова

Системный вызов signal Служит для изменения реакции процесса ив какой-либо сигнал.Хотя прототип системного вызова выглядит дово Приведенное выше описание можно (жевсюутштьс5>щют* обрезал:

функция signal, возвращающая указатель на функцию с одним параметром типа int, которая ничего не возвращает, и имеющая два параметра: параметр sig-mna int и параметр handler, служащий указателем на ничего не возвращающую функцию с одним параметром типа int. Параметр sig - это номер сигнала, обработку кстсрсто предстоит измени^^ Параметр handler описывает новый способ обработки сигнала - это может быть указатель на пользовательскую функцию - обработчик сигнала, специальное значение SIGJDFL или специальное значение S IG_IGN. Специальное значение S IG.IGN используется для того, чтобы процесс игнорировал поступившие сигналы с номером sig, специальное значение SIGJDFL -для восстановления реакции процесса на этот сигнал по умолчанию.

Возвращаемое значение

Системный вызов возвращает указатель на старый способ сюработки сигнала, значение которого можно использовать для восстановления старого способа в случае необходимости.

Этот системный вызов имеет два параметра: один из них задает номер сигнала, реакцию процесса на который требуется изменить, а второй определяет, как именно мы собираемся ее менять. Для первого варианта реакции процесса на сигнал (см. раздел «Понятие сигнала. Способы возникновения сигналов и виды их обработки») -- его игнорирования -- применяется специальное значение этого параметра -- sig_ign. Например, если требуется игнорировать сигнал sigint, начиная с некоторого места работы программы, в этом месте программы мы должны употребить конструкцию

(void) signal(sigint, sig_ign);

Для второго варианта реакции процесса на сигнал -- восстановления его обработки по умолчанию -- применяется специальное значение этого параметра -- sig_dfl. Для третьего варианта реакции процесса на сигнал в качестве значения параметра подставляется указатель на пользовательскую функцию обработки сигнала, которая должна иметь прототип вида

void *handler(int);

Ниже приведен пример скелета конструкции для пользовательской обработки сигнала sighup: void * my handler (int nsig) {

<обработка сигнала>

}

int main() {

(void)signal(SIGHUP, my_handler);

}

В качестве значения параметра в пользовательскую функцию обработки сигнала (в нашем скелете -- параметр nsig) передается номер возникшего сигнала, так что одна и та же функция может быть использована для обработки нескольких сигналов.

Прогон программы, игнорирующей сигнал SIGINT

Рассмотрим следующую программу -- 12--13-2.с:

/* Программа, игнорирующая сигнал SIGINT */ #include <signal.h> int main(void){

/* Выставляем реакцию процесса на сигнал SIGINT

на игнорирование */

(void)signal(SIGINT, SIG_IGN);

/*Начиная с этого места, процесс будет игнорировать возникновение сигнала SIGINT */ while(1) ; return 0;

}

Эта программа не делает ничего полезного, кроме переустановки реакции на нажатие клавиш <CTRL> и <С> на игнорирование возникающего сигнала и своего бесконечного зацикливания. Наберите, откомпилируйте и запустите эту программу, убедитесь, что на нажатие клавиш <CTRL> и <С> она не реагирует, а реакция на нажатие клавиш <CTRL> и <4> осталась прежней.

Модификация предыдущей программы для игнорирования сигналов SIGINT и SIGQUIT

Модифицируйте программу из предыдущего раздела так, чтобы она перестала реагировать и на нажатие клавиш <CTRL> и <4>. Откомпилируйте и запустите ее, убедитесь в отсутствии ее реакций на внешние раздражители. Снимать программу придется теперь с другого терминала командой kill.

Прогон программы с пользовательской обработкой

сигнала SIGINT

Рассмотрим теперь другую программу -- 12-- 13-З.с:

/* Программа с пользовательской обработкой сигнала SIGINT */

#include <signal.h> #include <stdio.h>

/* Функция my_handler - пользовательский обработчик сигнала */

void my_handler(int nsig){

printf("Receive signal %d, CTRL-C pressed\n", nsig);

}

int main(void){

/* Выставляем реакцию процесса на сигнал SIGINT */

(void)signal(SIGINT, my_handler);

/*Начиная с этого места, процесс будет печатать

сообщение о возникновении сигнала SIGINT */

while(1);

return 0;

}

Эта программа отличается от программы из раздела «Прогон программы, игнорирующей сигнал SIGINT» тем, что в ней введена обработка сигнала SIGINT пользовательской функцией. Наберите, откомпилируйте и запустите эту программу, проверьте ее реакцию на нажатие клавиш <CTRL> и <С> и на нажатие клавиш <CTRL> и <4>.

Модификация предыдущей программы для пользовательской обработки сигналов SIGINT и SIGQUIT

Модифицируйте программу из предыдущего раздела так, чтобы она печатала сообщение и о нажатии клавиш <CTRL> и <4>. Используйте одну и ту же функцию для обработки сигналов SIGINT и SIGQUIT. Откомпилируйте и запустите ее, проверьте корректность работы. Снимать программу также придется с другого терминала командой kill.

Восстановление предыдущей реакции на сигнал

До сих пор в примерах мы игнорировали значение, возвращаемое системным вызовом signal (). На самом деле этот системный вызов возвращает указатель на предыдущий обработчик сигнала, что позволяет восстанавливать переопределенную реакцию на сигнал. Рассмотрим пример программы 12--13-4.с, возвращающей первоначальную реакцию на сигнал SIGINT после 5 пользовательских обработок сигнала:

/* Программа с пользовательской обработкой сигнала SIGINT, возвращающаяся к первоначальной реакции на этот сигнал после 5 его обработок*/ #include <signal.h> #include <stdio.h>

int i=0; /* Счетчик числа обработок сигнала */ void (*р)(int); /* Указатель, в который будет занесен адрес предыдущего обработчика сигнала */ /* Функция my_handler - пользовательский обработчик

сигнала */ void my_handler(int nsig){

printf("Receive signal %d, CTRL-C pressed\n", nsig);

i = i+1;

/* После 5-й обработки возвращаем первоначальную реакцию на сигнал */

if(i == 5) (void)signal(SIGINT, p);

}

int main(void){

/* Выставляем свою реакцию процесса на сигнал SIGINT, запоминая адрес предыдущего обработчика */ р = signal(SIGINT, my_handler);

/*Начиная с этого места, процесс будет 5 раз печатать .сообщение о возникновении сигнала SIGINT */ while(1) ; return 0;

}

Наберите, откомпилируйте программу и запустите ее на исполнение.

Сигналы SIGUSR1 HSIGUSR2. Использование сигналов для синхронизации процессов

В операционной системе UNIX существует два сигнала, источниками которых могут служить только системный вызов kill () или команда kill -- это сигналы SIGUSR1 и SIGUSR2. Обычно их применяют для передачи информации о происшедшем событии от одного пользовательского процесса другому в качестве сигнального средства связи.

В материалах семинара 5 (раздел «Написание, компиляция и запуск программы для организации двунаправленной связи между родственными процессами через pipe»), когда рассматривалась связь родственных процессов через pipe, речь шла о том, что pipe является однонаправленным каналом связи, и что для организации связи через один pipe в двух направлениях необходимо задействовать механизмы взаимной синхронизации процессов. Организуйте двустороннюю поочередную связь процесса-родителя и процесса-ребенка через pipe, используя для синхронизации сигналы SIGUSR1 и SIGUSR2, модифицировав программу из раздела. «Прогон программы для организации однонаправленной связи между родственными процессами через pipe» семинара 5.

Задача повышенной сложности: организуйте побитовую передачу целого числа между двумя процессами, используя для этого только сигналы SIGUSR1 И SIGUSR2.

При реализации нитей исполнения в операционной системе Linux (см. семинары 6--7, начиная с раздела «Понятие о нити исполнения (thread) в UNIX. Идентификатор нити исполнения. Функция pthread_self()») сигналы SIGUSR1 и SIGUSR2 используются для организации синхронизации между процессами, представляющими нити исполнения, и процессом-координатором в служебных целях. Поэтому пользовательские программы, применяющие в своей работе нити исполнения, не могут задействовать сигналы SIGUSR1 и SIGUSR2.

Завершение порожденного процесса. Системный вызов waitpid(). Сигнал SIGCHLD

В материалах семинаров 3--4 (раздел «Завершение процесса. Функция exit()») при изучении завершения процесса говорилось о том, что если процесс-ребенок завершает свою работу прежде процесса-родителя, и процесс-родитель явно не указал, что он не заинтересован в получении информации о статусе завершения процесса-ребенка, то завершившийся процесс не исчезает из системы окончательно, а остается в состоянии закончил исполнение (зомби-процесс) либо до завершения процесса-родителя, либо до того момента, когда родитель соблаговолит получить эту информацию.

Для получения такой информации процесс-родитель может воспользоваться системным вызовом waitpid () или его упрощенной формой wait (). Системный вызов waitpid () позволяет процессу-родителю синхронно получить данные о статусе завершившегося процесса-ребенка либо блокируя процесс-родитель до завершения процесса-ребенка, либо без блокировки при его периодическом вызове с опцией WNOHANG. Эти данные занимают 16 бит и в рамках нашего курса могут быть расшифрованы следующим образом:

* Если процесс завершился при помощи явного или неявного вызова

функции exit (), то данные выглядят так (старший бит находится

О

слева)

15

Младшие 8 бит системного вызова

0x00

exit()

* Если процесс был завершен сигналом, то данные выглядят так (старший бит находится слева)

15

0x00

і

Номер сигнала

Признак создания

Каждый процесс-ребенок при завершении работы посылает своему процессу-родителю специальный сигнал ЭЮСНЬО, на который у всех процессов по умолчанию установлена реакция «игнорировать сигнал». Наличие такого сигнала совместно с системным вызовом waitpid{) позволяет организовать асинхронный сбор информации о статусе завершившихся порожденных процессов процессом-родителем.

Системные вызовы wait() и wartpid()

Прототипы системных вызовов

#include <sys/types.h> #include <wait.h>

pid_t waitpid(pid_t pid, int ^status, int options); pid_t wait(int ^status);

Описание системных вызовов

Это описание не является полным описанием системных вызовов, а адаптировано применительно к нашему курсу. Для получения полного описания обращайтесь к UNIX Manual.

Системный вызов waitpid () блокирует выполнение текущего процесса до тех пор, пока либо не завершится порожденный им процесс, определяемый значением параметра pid, либо текущий процесс не получит сигнал, для которого установлена реакция по умолчанию «завершить процесс» или реакция обработки пользовательской функцией. Если порожденный процесс, заданный параметром pid, к моменту системного вызова находится в состоянии се ь°1а " -ь , то системный вызов возвращается немедленно без блокирования текущего процесса.

Параметр pid определяет порожденный процесс, завершения которого дожидается процесс-родитель, следующим образом:

Если pid > 0 ожидаем завершения процесса с идентификатором pid.

Если pid = 0, то ожидаем завершения любого порожденного процесса в группе, к которой принадлежит процесс-родитель.

Если pid = -1, то ожидаем завершения любого порожденного процесса.

Если pid < 0, но не -1, то ожидаем завершения любого порожденного процесса из группы, идентификатор которой равен абсолютному значению параметра pid.

Параметр options в нашем курсе может принимать два значения: 0 и WNOHANG, Значение WNOHANG требует немедленного возврата из вызова без блокировки текущего процесса в любом случае.

Если системный вызов обнаружил завершившийся порожденный процесс из числа специфицированных параметром pid, то этот процесс удаляется из вычислительной системы, а по адресу, указанному в параметре status, сохраняется информация о статусе его завершения. Параметр status может быть задан равным NULL, если эта информация не имеет для нас значения.

При обнаружении завершившегося процесса системный вызов возвращает его идентификатор. Если вызов был сделан с установленной опцией WNOHANG, и порожденный процесс, специфицированный параметром pid, существует, но еще не завершился, системный вызов вернет значение 0. Во всех остальных случаях он возвращает отрицательное значение. Возврат из вызова, связанный с возникновением обработанного пользователем сигнала, может быть в этом случае идентифицирован по значению системной переменной ermo == EINTR, и вызов может быть сделан снова.

Системный вызов wait является синонимом для системного вызова waitpid со значениями параметров pid = -1, options = 0.

Используя системный вызов signal (), мы можем явно установить игнорирование этого сигнала (SIG_IGN), тем самым проинформировав систему, что нас не интересует, каким образом завершатся порожденные процессы. В этом случае зомби-процессов возникать не будет, но и применение системных вызовов wait () и waitpid() будет запрещено.

Прогон программы для иллюстрации обработки сигнала SIGCHLD

Для закрепления материала рассмотрим пример программы 12--13-5с асинхронным получением информации о статусе завершения порожденного процесса:

/* Программа с асинхронным получением информации о

статусе двух завершившихся порожденных процессов */

ttinclude <sys/types.h>

#include <unistd.h>

#include <waith>

#include <signal.h>

#include <stdio.h>

/* Функция my_handler - обработчик сигнала SIGCHLD */ void my_handler(int nsig){

int status;

pid_t pid;

/* Опрашиваем статус завершившегося процесса и одновременно узнаем его идентификатор */ if((pid = waitpid(-l, &status, 0)) < 0){

/* Если возникла ошибка - сообщаем о ней и

продолжаем работу */

printf("Some error on waitpid errno = %d\n", errno); } else {

/* Иначе анализируем статус завершившегося процесса */

if ((status & Oxff) == 0) {

/* Процесс завершился с явным или неявным вызовом функции exit() */

printf("Process %d was exited with status %d\n", pid, status » 8); } else if ((status & OxffOO) == 0){

/* Процесс был завершен с помощью сигнала */

printf("Process %d killed by signal %d %s\n", pid, status &0x7f,(status & 0x80) ? "with core file" : "without core file");

}

}

}

int main(void){ pid_t pid;

/* Устанавливаем обработчик для сигнала SIGCHLD */ (void) signal(SIGCHLD, my_handler); /* Порождаем Child 1 */ if((pid = fork()) < 0){

printf("Can\'t fork child l\n");

exit(1) ; } else if (pid == 0){

/* Child 1 - завершается с кодом 200 */

exit(200);

}

/* Продолжение процесса-родителя - порождаем Child 2 */ if((pid = fork()) < 0){

printf("Can\'t fork child 2\n");

exit(1) ; } else if (pid == 0){

/* Child 2 - циклится, необходимо удалять с

помощью сигнала! */

while(1);

}

/* Продолжение процесса-родителя - уходим в цикл */ while(1) ; return 0;

В этой программе родитель порождает два процесса. Один из них завершается с кодом 200, а второй зацикливается. Перед порождением процессов родитель устанавливает обработчик прерывания для сигнала SIGCHLD, а после их порождения уходит в бесконечный цикл. В обработчике прерывания вызывается waitpid () для любого порожденного процесса. Так как в обработчик мы попадаем, когда какой-либо из процессов завершился, системный вызов не блокируется, и мы можем получить информацию об идентификаторе завершившегося процесса и причине его завершения. Откомпилируйте программу и запустите ее на исполнение. Второй порожденный процесс завершайте с помощью команды kill с каким-либо номером сигнала. Родительский процесс также будет необходимо завершать командой kill.

Возникновение сигнала SIGPIPE при попытке записи в pipe или FIFO, который никто не собирается читать

В материалах семинара 5 (раздел «Особенности поведения вызовов read() и write() для pip'a») при обсуждении работы с pip'aMH и FIFO мы говорили, что для них системные вызовы read () и write () имеют определенные особенности поведения. Одной из таких особенностей является получение сигнала SIGPIPE процессом, который пытается записывать информацию в pipe или в FIFO в том случае, когда читать ее уже некому (нет ни одного процесса, который держит соответствующий pipe или FIFO открытым для чтения). Реакция по умолчанию на этот сигнал -- прекратить работу процесса. Теперь мы уже можем написать корректную обработку этого сигнала пользователем, например, для элегантного прекращения работы пишущего процесса. Однако для полноты картины необходимо познакомиться с особенностями поведения некоторых системных вызовов при получении процессом сигналов во время их выполнения.

По ходу нашего курса мы представили читателям ряд системных вызовов, которые могут во время выполнения блокировать процесс. К их числу относятся системный вызов open () при открытии FIFO, системные вызовы read() и write () при работе с pip'aMH и FIFO, системные вызовы msgsnd () и msgrcv () при работе с очередями сообщений, системный вызов semop () при работе с семафорами и т. д. Что произойдет с процессом, если он, выполняя один из этих системных вызовов, получит какой-либо сигнал? Дальнейшее поведение процесса зависит от установленной для него реакции на этот сигнал:

Если реакция на полученный сигнал была «игнорировать сигнал» (независимо оттого, установлена она по умолчанию или пользователем с помощью системного вызова signal ()), то поведение процесса не изменится.

Если реакция на полученный сигнал установлена по умолчанию и заключается в прекращении работы процесса, то процесс перейдет в состояние закончил исполнение.

Если реакция процесса на сигнал заключается в выполнении пользовательской функции, то процесс выполнит эту функцию (если он находился в состоянии ожидание, он попадет в состояние готовность и затем в состояние исполнение) и вернется из системного вызова с констатацией ошибочной ситуации (некоторые системные вызовы позволяют операционной системе после выполнения обработки сигнала вновь вернуть процесс в состояние ожидания). Отличить такой возврат от действительно ошибочной ситуации можно с помощью значения системной переменной errno, которая в этом случае примет значение EINTR (для вызова write и сигнала SIGPIPE соответствующее значение в порядке исключения будет EPIPE). После этого краткого обсуждения становится до конца ясно, как корректно обработать ситуацию «никто не хотел прочитать» для системного вызова write (). Чтобы пришедший сигнал SIGPIPE не завершил работу нашего процесса по умолчанию, мы должны его обработать самостоятельно (функция-обработчик при этом может быть и пустой!). Но этого мало. Поскольку нормальный ход выполнения системного вызова был нарушен сигналом, мы вернемся из него с отрицательным значением, которое свидетельствует об ошибке. Проанализировав значение системной переменной errno на предмет совпадения со значением EPIPE, мы можем отличить возникновение сигнала SIGPIPE от других ошибочных ситуаций (неправильные значения параметров и т. д.) и грациозно продолжить работу программы.

Понятие о надежности сигналов. POSIX-функции для работы с сигналами

Основным недостатком системного вызова signal () является его низкая надежность.

Во многих вариантах операционной системы UNIX установленная при его помощи обработка сигнала пользовательской функцией выполняется только один раз, после чего автоматически восстанавливается реакция на сигнал по умолчанию. Для постоянной пользовательской обработки сигнала необходимо каждый раз заново устанавливать реакцию на сигнал прямо внутри функции-обработчика.

В системных вызовах и пользовательских программах могут существовать критические участки, на которых процессу недопустимо отвлекаться на обработку сигналов. Мы можем выставить на этих участках реакцию «игнорировать сигнал» с последующим восстановлением предыдущей реакции, но если сигнал все-таки возникнет на критическом участке, то информация о его возникновении будет безвозвратно потеряна.

Наконец, последний недостаток связан с невозможностью определения количества сигналов одного и того же типа, поступивших процессу, пока он находился в состоянии готовность. Сигналы одного типа в очередь не ставятся! Процесс может узнать о том, что сигнал или сигналы определенного типа были ему переданы, но не может определить их количество. Этот недостаток мы можем проиллюстрировать, слегка изменив программу с асинхронным получением информации о статусе завершившихся процессов, рассмотренную нами ранее в разделе «Изучение особенностей получения терминальных сигналов текущей и фоновой группой процессов». Пусть в новой программе 12--13-6.с процесс-родитель порождает в цикле пять новых процессов, каждый из которых сразу же завершается со своим собственным кодом, после чего уходит в бесконечный цикл:

/* Программа для иллюстрации ненадежности сигналов */

#include <sys/types.h>

#include <unistd.h>

#include <waith>

#include <signal.h>

#include <stdio.h>

/* Функция my_handler - обработчик сигнала SIGCHLD */ void my_handler(int nsig){

int status;

pid_t pid;

/* Опрашиваем статус завершившегося процесса и одновременно узнаем его идентификатор */ if((pid = waitpid(-l, &status, 0)) < 0){

/* Если возникла ошибка - сообщаем о ней и

продолжаем работу */

printf("Some error on waitpid errno = %d\n", errno); } else {

/* Иначе анализируем статус завершившегося процесса */ if ( (status & Oxff) ==0) {

/* Процесс завершился с явным или неявным

вызовом функции exit() */

printf("Process %d was exited with status %d\n", pid, status >> 8); } else if ((status & OxffOO) == 0){

/* Процесс был завершен с помощью сигнала */ printf("Process %d killed by signal %d %s\n", pid, status &0x7f,(status & 0x80) ? "with core file" : "without core file");

}

}

}

int main(void){ pid_t pid; int i;

/* Устанавливаем обработчик для сигнала SIGCHLD */ (void) signal(SIGCHLD, my_handler);

/* В цикле порождаем пять процессов-детей */ for (i=0; i

if((pid = fork()) < 0){

printf("Can\'t fork child %d\n" , i) ; exit(1); } else if (pid == 0) {

/* Child i - завершается с кодом 200 + i */ exit(200 + i) ;

}

/* Продолжение процесса-родителя - уходим на новую итерацию */

}

/* Продолжение процесса-родителя - уходим в цикл */ while(1) ; return 0 ;

}

Сколько сообщений о статусе завершившихся детей мы ожидаем получить? Пять! А сколько получим? It depends... Откомпилируйте, прогоните и посчитайте.

Последующие версии System Y и BSD пытались устранить эти недостатки собственными средствами. Единый способ более надежной обработки сигналов появился с введением POSIX-стандарта на системные вызовы UNIX. Набор функций и системных вызовов для работы с сигналами был существенно расширен и построен таким образом, что позволял временно блокировать обработку определенных сигналов, не допуская их потери. Однако проблема, связанная с определением количества пришедших сигналов одного типа, по-прежнему остается актуальной. (Надо отметить, что подобная проблема существует на аппаратном уровне и для внешних прерываний. Процессор зачастую не может определить, какое количество внешних прерываний с одним номером возникло, пока он выполнял очередную команду.)

Рассмотрение POSIX-сигналов выходит за рамки нашего курса. Желающие могут самостоятельно просмотреть описания функций и системных вызовов sigemptyset ( ) , sigfillset ( ) , sigaddsetO, sigdelset(), sigismember ( ), sigaction(), sigprocmask ( ), sigpending (), sigsuspend () в UNIX Manual.

Задача повышенной сложности: модифицируйте обработку сигнала в программе из этого раздела, не применяя POSIX-сигналы, так, чтобы процесс-родитель все-таки сообщал о статусе всех завершившихся процессов-детей.

Семинары 14-15. Семейство протоколов TCP/IP. Сокеты (sockets) в UNIX и основы работы с ними

Краткая история семейства протоколов TCP/IP. Общие сведения об архитектуре семейства протоколов TCP/IP. Уровень сетевого интерфейса. Уровень Internet. Протоколы IP, ICMP, ARP, RARP. Internet-адреса. Транспортный уровень. Протоколы TCP и UDP, UDP- и ТСР-сокеты (sockets). Адресные пространства портов. Понятие encapsulation. Уровень приложений/программ. Использование модели клиент -- сервер при изучении сетевого программирования. Организация связи между удаленными процессами с помощью датаграмм. Сетевой порядок байт. Функции htons (), htonl (), ntohs (), ntohl (). Функции преобразования IP-адресов inet_ntoa (), inet_aton(). Функция bzero(). Системные вызовы socket (), bind (), sendto (), recvf rom(). Организация связи между процессами с помощью установки логического соединения. Системные вызовы connect (), listen(), accept (). Использование интерфейса сокетов для других семейств протоколов. Файлы типа «сокет».

Ключевые слова: семейство протоколов TCP/l Р, уровень сетевого интерфейса, уровень Internet, транспортный уровень, уровень приложений/процессов, протоколы сетевого интерфейса, IP, ICMP, ARP, RARP, TCP, UDP, МАС-адрес, IP-адрес, порт, сокет, адрес сокета, encapsulation, датаграмма, виртуальное (логическое соединение), UDP-сокет, ТСР-сокет, пассивный (слушающий) ТСР-сокет, присоединенный ТСР-сокет, трехэтапное рукопожатие, не полностью установленное соединение, полностью установленное соединение, функции htons(), htonl(), ntohs(), ntohl(), inet_ntoa(), inet_aton(), bzero(), системные вызовы socket(), bind(), sendto (), recvfrom(), connect () , listen(), accept (} , UNIX Domain протоколы, файлы типа «сокет».

Краткая история семейства протоколов TCP/IP

Мы приступаем к последней теме наших семинарских и практических занятий -- введению в сетевое программирование в операционной системе UNIX.

Все многообразие сетевых приложений и многомиллионная всемирная компьютерная сеть выросли из четырехкомпьютерной сети ARPANET, созданной по заказу Министерства Обороны США и связавшей вычислительные комплексы в Стэндфордском исследовательском институте, Калифорнийском университете в Санта-Барбаре, Калифорнийском университете в Лос-Анджелесе и университете Юты. Первая передача информации между двумя компьютерами сети ARPANET состоялась в октябре 1969 года, и эту дату принято считать датой рождения нелокальных компьютерных сетей. (Необходимо отметить, что дата является достаточно условной, так как первая связь двух удаленных компьютеров через коммутируемые телефонные линии была осуществлена еще в 1965 году, а реальные возможности для разработки пользователями ARPANET сетевых приложений появились только в 1972 году.) Эта сеть росла и почковалась, закрывались ее отдельные части, появлялись ее гражданские аналоги, они сливались вместе, и в результате «что выросло -- то выросло».

При создании ARPANET был разработан протокол сетевого взаимодействия коммуникационных узлов -- Network Control Protocol (NCP), осуществлявший связь посредством передачи датаграмм (см. лекцию 14, раздел «Связь с установлением логического соединения и передача данных с помощью сообщений»). Этот протокол был предназначен для конкретного архитектурного построения сети и базировался на предположении, что сеть является статической и настолько надежной, что компьютерам не требуется умения реагировать на возникающие ошибки. По мере роста ARPANET и необходимости подключения к ней сетей, построенных на других архитектурных принципах (пакетные спутниковые сети, наземные пакетные радиосети), от этого предположения пришлось отказаться и искать другие подходы к построению сетевых систем. Результатом исследований в этих областях стало появление семейства протоколов TCP/IP, на базе которого обеспечивалась надежная доставка информации по неоднородной сети. Это семейство протоколов до сих пор занимает ведущее место в качестве сетевой технологии, используемой в операционной системе UNIX. Именно поэтому мы и выбрали его для практической иллюстрации общих сетевых решений, изложенных в лекции 14.

Общие сведения об архитектуре семейства протоколов TCP/IP

Семейство протоколов TCP/IP построено по «слоеному» принципу, подробно рассмотренному ранее (лекция 14, раздел «Многоуровневая модель построения сетевых вычислительных систем»). Хотя оно и имеет многоуровневую структуру, его строение отличается от строения эталонной модели OSI, предложенной стандартом ISO. Это и неудивительно, так как основные черты семейства TCP/IP были заложены до появления эталонной модели и во многом послужили толчком для ее разработки. В семействе протоколов TCP/IP можно выделить четыре уровня:

Уровень сетевого интерфейса.

Уровень Internet.

Транспортный уровень.

Уровень приложений/процессов.

Соотношение уровней модели 081/180 и уровней семейства ТСР/1Р приведено на рисунке 14-15.1.

Модель 081/180

Модель ТСР/ІР

Уровень приложений

Уровень представления данных

Уровень приложений/процессов

Сеансовый уровень

Транспортный уровень

Транспортный уровень

Сетевой уровень

Сетевой уровень

Канальный уровень

Уровень сетевого интерфеса

Физический уровень

Рис. 14-15.1. Соотношение моделей 081/180 и ТСР/1Р

На каждом уровне семейства ТСР/1Р присутствует несколько протоколов. Связь между наиболее употребительными протоколами и их принадлежность уровням изображены на рисунке 14-15.2.

Давайте кратко охарактеризуем каждый уровень семейства.

Уровень сетевого интерфейса

Уровень сетевого интерфейса составляют протоколы, которые обеспечивают передачу данных между узлами связи, физически напрямую соединенными друг с другом, или, иначе говоря, подключенными к одному сегменту сети, и соответствующие физические средства передачи данных. К этому уровню относятся протоколы Ethernet, Token Ring, SLIP, PPP и т. д. и такие физические средства как витая пара, коаксиальный кабель, оптоволоконный кабель и т. д. Формально протоколы уровня сетевого интерфейса не являются частью семейства TCP/IP, но существующие стандарты определяют, каким образом должна осуществляться передача данных семейства TCP/IP с использованием этих протоколов. На уровне сетевого интерфейса в операционной системе UNIX обычно функционируют драйверы различных сетевых плат.

Передача информации на уровне сетевого интерфейса производится на основании физических адресов, соответствующих точкам входа сети в узлы связи (например, физических адресов сетевых карт). Каждая точка входа имеет свой уникальный адрес -- МАС-адрес (Media Access Control), физически зашитый в нее на этапе изготовления. Так, например, каждая сетевая плата Ethernet имеет собственный уникальный 48-битовый номер.

Уровень Internet. Протоколы IP, ICMP, ARP, RARP. Internet-адреса

Из многочисленных протоколов уровня Internet мы перечислим только те, которые будут в дальнейшем упоминаться в нашем курсе:

ICMP -- Internet Control Message Protocol. Протокол обработки ошибок и обмена управляющей информацией между узлами сети.

IP -- Internet Protocol. Это протокол, который обеспечивает доставку пакетов информации для протокола ICMP и протоколов транспортного уровня TCP и UDP.

ARP -- Address Resolution Protocol. Это протокол для отображения адресов уровня Internet в адреса уровня сетевого интерфейса.

RARP -- Reverse Address Resolution Protocol. Этот протокол служит для решения обратной задачи: отображения адресов уровня сетевого интерфейса в адреса уровня Internet.

Два последних протокола используются не для всех сетей; только некоторые сети требуют их применения.

Уровень Internet обеспечивает доставку информации от сетевого узла отправителя к сетевому узлу получателя без установления виртуального соединения с помощью датаграмм и не является надежным.

Центральным протоколом уровня является протокол IP. Вся информация, поступающая к нему от других протоколов, оформляется в виде IP-пакетов данных (IP datagrams). Каждый IP-пакет содержит адреса компьютера отправителя и компьютера получателя, поэтому он может передаваться по сети независимо от других пакетов и, возможно, по своему собственному маршруту. Любая ассоциативная связь между пакетами, предполагающая знания об их содержании, должна осуществляться на более высоком уровне семейства протоколов.

IP-уровень семейства TCP/IP не является уровнем, обеспечивающим надежную связь, так как он не гарантирует ни доставку отправленного пакета информации, ни то, что пакет будет доставлен без ошибок. IP вычисляет и проверяет контрольную сумму, которая покрывает только его собственный 20-байтовый заголовок для пакета информации (включающий, например, адреса отправителя и получателя). Если IP-заголовок пакета при передаче оказывается испорченным, то весь пакет просто отбрасывается. Ответственность за повторную передачу пакета тем самым возлагается на вышестоящие уровни.

IP-протокол, при необходимости, осуществляет фрагментацию и де-фрагментацию данных, передаваемых по сети. Если размер IP-пакета слишком велик для дальнейшей передачи по сети, то полученный пакет разбивается на несколько фрагментов, и каждый фрагмент оформляется в виде нового IP-пакета с теми же адресами отправителя и получателя.

Фрагменты собираются в единое целое только в конечной точке своего путешествия. Если при дефрагментации пакета обнаруживается, что хотя бы один из фрагментов был потерян или отброшен, то отбрасывается и весь пакет целиком.

Уровень Internet отвечает за маршрутизацию пакетов. Для обмена информацией между узлами сети в случае возникновения проблем с маршрутизацией пакетов используется протокол ICMP. С помощью сообщений этого же протокола уровень Internet умеет частично управлять скоростью передачи данных - он может попросить отправителя уменьшить скорость передачи.

Поскольку на уровне Internet информация передается от компьютера-отправителя к компьютеру-получателю, ему требуются специальные IP-адреса компьютеров (а точнее, их точек подсоединения к сети -- сетевых интерфейсов) -- удаленные части полных адресов процессов (см. лекцию 14, раздел «Удаленная адресация и разрешение адресов»). Мы будем далее работать с IP версии 4 (IPv4), которая предполагает наличие у каждого сетевого интерфейса уникального 32-битового адреса. Когда разрабатывалось семейство протоколов TCP/IP, казалось, что 32 битов адреса будет достаточно для всех нужд сети, однако не прошло и 30 лет, как выяснилось, что этого мало. Поэтому была разработана версия 6 для IP (IPv6), предполагающая наличие 128-битовых адресов. С точки зрения сетевого программиста IPv6 мало отличается от IPv4, но имеет более сложный интерфейс передачи параметров, поэтому для практических занятий был выбран IPv4.

Все IP-адреса версии 4 принято делить на 5 классов. Принадлежность адреса к некоторому классу определяют по количеству последовательных едениц в старших битах адреса (см. рис. 14-15.3). Адреса классов А, В и С используют собственно для адресации сетевых интерфейсов. Адреса класса D применяются для групповой рассылки информации (multicast addresses) и далее нас интересовать не будут. Класс Е (про который во многих книгах по сетям забывают) был зарезервирован для будущих расширений.

Каждый из IP-адресов классов А--С логически делится на две части: идентификатор или номер сети и идентификатор или номер узла в этой сети. Идентификаторы сетей в настоящее время присваиваются локальным сетям специальной международной организацией -- корпорацией Internet по присвоению имен и номеров (ICANN). Присвоение адреса конкретному узлу сети, получившей идентификатор, является заботой ее администратора. Класс А предназначен для небольшого количества сетей, содержащих очень много компьютеров, класс С -- напротив, для большого количества сетей с малым числом компьютеров. Класс В занимает среднее положение. Надо отметить, что все идентификаторы сетей классов А и В к настоящему моменту уже задействованы.

7 бит

24 бита

номер сети

номер узла

Спасе А

14 бит

16 бит

номер сети

номер узла

Спасе В

21 бит

8 бит

номер сети

номер узла

Спасе С

28 бит

для группового вещания

Сласс D

28 бит

зарезервировано

Сласс Е

31

Рис. 14-15.3. Классы IP-адресов

Любая организация, которой был выделен идентификатор сети из любого класса, может произвольным образом разделить имеющееся у нее адресное пространство идентификаторов узлов для создания подсетей.

Допустим, что вам выделен адрес сети класса С, в котором под номер узла сети отведено 8 бит. Если нужно присвоить IP-адреса 100 компьютерам, которые организованы в 10 Ethernet-сегментов по 10 компьютеров в каждом, можно поступить по-разному. Можно присвоить компьютерам номера от 1 до 100, игнорируя их принадлежность к конкретному сегменту -- воспользовавшись стандартной формой IP-адреса. Или же можно выделить несколько младших бит из адресного пространства идентификаторов узлов для идентификации сегмента сети, например 4 бита, а для адресации узлов внутри сегмента использовать оставшиеся 4 бита. Последний способ получил название адресации с использованием подсетей (см. рис. 14-15.4).

Запоминать четырехбайтовые числа для человека достаточно сложно, поэтому принято записывать IP-адреса в символической форме, переводя значение каждого байта в десятичный вид по отдельности и разделяя полученные десятичные числа в записи точками, начиная со старшего байта: 192.168.253.10.

Допустим, что мы имеем дело с сегментом сети, использующим Ethernet на уровне сетевого интерфейса и состоящим из компьютеров, где применяются протоколы TCP/IP на более высоких уровнях. Тогда у нас в сети есть два вида адресов: 48-битовые физические адреса Ethernet (МАС-адреса) и 32-битовые IP-адреса. Для нормальной передачи информации необходимо, чтобы Internet-уровень семейства протоколов, обращаясь к уровню сетевого интерфейса, знал, какой физический адрес соответствует данному IP-адресу и наоборот, т. е. умел «разрешать адреса». В очередной раз мы сталкиваемся с проблемой разрешения адресов, которая в различных постановках разбиралась в материалах лекций. При разрешении адресов могут возникнуть две сложности:

Если мы знаем IP-адреса компьютеров, которым или через которые мы хотим передать данные, то каким образом Internet уровень семейства протоколов TCP/IP сможет определить соответствующие им МАС-адреса? Эта проблема получила название address resolution problem (проблема разрешения адресов).

Для решения второй проблемы один или несколько компьютеров в сегменте сети должны выполнять функции RARP-сервера и содержать набор физических адресов для рабочих станций и соответствующих им IP-адресов. Когда рабочая станция с операционной системой, сгенерированной без назначения IP-адреса, начинает свою работу, она получает МАС-адрес от сетевого оборудования и рассылает соответствующий RARP-запрос, содержащий этот адрес, всем компьютерам сегмента сети. Только RARP-сервер, содержащий информацию о соответствии указанного физического адреса и выделенного IP-адреса, откликается на данный запрос и отправляет ответ, содержащий IP-адрес.

Транспортный уровень. Протоколы TCP и UDP. TCP- и UDP-сокеты. Адресные пространства портов. Понятие encapsulation

Мы не будем вдаваться в детали реализации протоколов транспортного уровня, а лишь кратко рассмотрим их основные характеристики. К протоколам транспортного уровня относятся протоколы TCP и UDP.

Протокол TCP реализует потоковую модель передачи информации, хотя в его основе, как и в основе протокола UDP, лежит обмен информацией через пакеты данных. Он представляет собой ориентированный на установление логической связи (connection-oriented), надежный (обеспечивающий проверку контрольных сумм, передачу подтверждения в случае правильного приема сообщения, повторную передачу пакета данных в случае неполучения подтверждения в течение определенного промежутка времени, правильную последовательность получения информации, полный контроль скорости передачи данных) дуплексный способ связи между процессами в сети. Протокол UDP, наоборот, является ненадежным способом связи, ориентированным на передачу сообщений (дата-грамм). От протокола IP он отличается двумя основными чертами: использованием для проверки правильности принятого сообщения контрольной суммы, насчитанной по всему сообщению, и передачей информации не от узла сети к другому узлу, а от отправителя к получателю.

На лекции 14 (раздел «Полные адреса. Понятие сокета (socket) Пусть у нас есть бездисковые рабочие станции или рабочие станции, на которых операционные системы сгенерированы без назначения IP-адресов (это часто делается, когда один и тот же образ операционной системы загружается на ряд компьютеров, например, в учебных классах). Тогда при старте операционной системы на каждом таком компьютере операционная система знает только МАС-адреса, соответствующие данному компьютеру. Как можно определить, какой Internet-адрес был выделен данной рабочей станции? Эта проблема называется reverse address resolution problem (обратная проблема разрешения адресов).

Первая задача решается с использованием протокола ARP, вторая -с помощью протокола RARP.

Протокол ARP позволяет компьютеру разослать специальное сообщение по всему сегменту сети, которое требует от компьютера, имеющего содержащийся в сообщении IP-адрес, откликнуться и указать свой физический адрес. Это сообщение поступает всем компьютерам в сегменте сети, но откликается на него только тот, кого спрашивали. После получения

) мы говорили, что полный адрес удаленного процесса или промежуточного объекта для конкретного способа связи с точки зрения операционных систем определяется парой адресов: <числовой адрес компьютера в сети, локальный адрес>.

Такая пара получила название socket (гнездо, панель), так как по сути дела является виртуальным коммуникационным узлом (можно представить себе виртуальный разъем или ящик для приема/отправки писем), ведущим от объекта во внешний мир и наоборот. При непрямой адресации сами промежуточные объекты для организации взаимодействия процессов также именуются сокетами.

Поскольку уровень Internet семейства протоколов TCP/IP умеет доставлять информацию только от компьютера к компьютеру, данные, полученные с его помощью, должны содержать тип использованного протокола транспортного уровня и локальные адреса отправителя и получателя. И протокол TCP, и протокол UDP используют непрямую адресацию.

Для того чтобы избежать путаницы, мы в дальнейшем термин «сокет» будем употреблять только для обозначения самих промежуточных объектов, а полные адреса таких объектов будем называть адресами сокетов.

Для каждого транспортного протокола в стеке TCP/IP существуют собственные сокеты: UDP- сокеты и ТСР-сокеты, имеющие различные адресные пространства своих локальных адресов -- портов. В семействе протоколов TCP/IP адресные пространства портов представляют собой положительные значения целого 16-битового числа. Поэтому, говоря о локальном адресе сокета, мы часто будем использовать термин «номер порта». Из различия адресных пространств портов следует, что порт 1111 TCP -- это совсем не тот же самый локальный адрес, что и порт 1111 UDP. О том, как назначаются номера портов различным сокетам, мы поговорим позже.

Итак, мы описали иерархическую систему адресации, используемую в семействе протоколов TCP/IP, которая включает в себя несколько уровней:

Физический пакет данных, передаваемый по сети, содержит физические адреса узлов сети (МАС-адреса) с указанием на то, какой протокол уровня Internet должен использоваться для обработки передаваемых данных (поскольку пользователя интересуют только данные, доставляемые затем на уровень приложений/процессов, то для него это всегда IP).

IP-пакет данных содержит 32-битовые IP-адреса компьютера-отправителя и компьютера-получателя, и указание на то, какой вышележащий протокол (TCP, UDP или еще что-нибудь) должен использоваться для их дальнейшей обработки.


Подобные документы

  • Основные понятия об операционных системах. Виды современных операционных систем. История развития операционных систем семейства Windows. Характеристики операционных систем семейства Windows. Новые функциональные возможности операционной системы Windows 7.

    курсовая работа [60,1 K], добавлен 18.02.2012

  • Понятие виртуальной памяти, ее реализация. Особенности страничной организации по требованию. Этапы обработки ситуации отсутствия страницы в памяти. Стратегии (алгоритмы) замещения страниц. Особенности некоторых операционных систем: Windows NT и Solaris.

    презентация [2,2 M], добавлен 24.01.2014

  • Изучение особенностей операционной системы, набора программ, контролирующих работу прикладных программ и системных приложений. Описания архитектуры и программного обеспечения современных операционных систем. Достоинства языка программирования Ассемблер.

    презентация [1,3 M], добавлен 22.04.2014

  • Общая характеристика преимуществ взаимодействующих процессов: модульность, ускорение вычислений. Знакомство с основами современных операционных систем. Анализ особенностей использования общего почтового ящика, рассмотрение способов создания и удаления.

    презентация [1,6 M], добавлен 24.01.2014

  • Характеристика сущности, назначения, функций операционных систем. Отличительные черты их эволюции. Особенности алгоритмов управления ресурсами. Современные концепции и технологии проектирования операционных систем, требования, предъявляемые к ОС XXI века.

    курсовая работа [36,4 K], добавлен 08.01.2011

  • Операционная система - программа, которая загружается при включении компьютера. Способы реализации интерфейса и классификация операционных систем. Организация файловой системы, типы файлов и их наименования. Понятие каталога, атрибуты файловой системы.

    реферат [16,6 K], добавлен 25.02.2011

  • Важность операционной системы для мобильных устройств. Популярность операционных систем. Доля LINUX на рынке операционных систем. История OS Symbian, BlackBerry OS, Palm OS. Отличия смартфона от обычного мобильного телефона. Учет ограничений по памяти.

    презентация [477,3 K], добавлен 01.12.2015

  • Основные понятия операционных систем. Современное оборудование компьютера. Преимущества и недостатки операционной системы Linux. Функциональные возможности операционной системы Knoppix. Сравнительная характеристика операционных систем Linux и Knoppix.

    реферат [1,5 M], добавлен 17.12.2014

  • Основные понятия операционных систем. Синхронизация и критические области. Сигналы и взаимодействие между процессами. Управление памятью. Драйверы устройств. Особенности современных операционных систем. Центральный процессор, микросхемы часов и таймеров.

    учебное пособие [1,2 M], добавлен 24.01.2014

  • Использование операционных систем Microsoft Windows. Разработка операционной системы Windows 1.0. Возможности и характеристика последующих версий. Выпуск пользовательских операционных систем компании, доработки и нововведения, версии Windows XP и Vista.

    реферат [23,3 K], добавлен 10.01.2012

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.