Методические указания к лабораторной работе
Обычно в ОС UNIX доступны несколько интерпретаторов. Наиболее распространены Bourne-shell (или просто - shell), C-shell, Korn-shell. В идейном плане все эти интерпретаторы близки и в дальнейшем речь будет идти о стандартном Shell (/bin/sh).
Работая на командном языке, пользователь может вводить переменные, присваивать им значения, выполнять простые команды, строить составные команды, управлять потоком выполнения команд, объединять последовательность команд в процедуры (командные файлы). На уровне командного языка доступны такие свойства системы как соединение процессов через программный канал, направление стандартного ввода/вывода в конкретные файлы, синхронное и асинхронное выполнение команд.
Если указанный интерпретатору файл является текстовым и содержит команды командного языка (командный файл) и при этом имеет разрешение на выполнение (помечен "х"), Shell-интерпретатор интерпретирует и выполняет команды этого файла. Другой способ вызова командного файла - использование команды sh (вызов интерпретатора), в котором первым аргументом указывается имя командного файла.
Коротко перечислим средства группирования команд и перенаправления ввода/вывода:
cmd1 arg ...; cmd2 arg ...; ... cmdN arg ... - последовательное выполнение команд;
cmd1 arg ...& cmd2 arg ...& ... cmdN arg ... - асинхронное выполнение команд;
cmd1 arg ... && cmd2 arg ... - зависимость последующей команды от предыдущей таким образом, что последующая команда выполняется, если предыдущая выдала нулевое значение;
cmd1 arg ... cmd2 arg ... - зависимость последующей команды от предыдущей таким образом, что последующая команда выполняется, если предыдущая выдала ненулевое значение;
cmd > file - стандартный вывод направлен в файл file;
cmd >> file - стандартный вывод направлен в конец файла file;
cmd < file - стандартный ввод выполняется из файла file;
cmd1 | cmd2 - конвейер команд, в котором стандартный вывод команды cmd1 направлен на стандартный вход команды cmd2.
Интерфейс между пользовательской программой и внешним устройством (или между двумя пользовательскими программами) в ОС UNIX осуществляется в рамках единой структуры данных, называемой файлом ОС UNIX.
Всякий файл ОС UNIX в соответствие с его типом может быть отнесен к одной из следующих четырех групп: обычные файлы, каталоги, специальные файлы, каналы.
Обычный файл представляет собой совокупность блоков диска, входящих в состав файловой системы ОС UNIX. В указанных блоках может быть произвольная информация.
Каталоги представляют собой файлы особого типа, отличающиеся от обычных прежде всего тем, что осуществить запись в них может только ядро ОС UNIX, в то время как доступ по чтению может получить любой пользовательский процесс, имеющий соответствующие полномочия. Каждый элемент каталога состоит из двух полей: поля имени файла и поля, содержащего указатель на описатель файла, где хранится вся информация о файле: дата создания, размер, код защиты, имя владельца и т.д. В любом каталоге содержится, по крайней мере, два элемента, содержащие в поле имени файла имена "." и "..". Элемент каталога, содержащий в поле имени файла контекст ".", в поле ссылки содержит ссылку на описатель файла, описывающий этот каталог. Элемент каталога, содержащий в поле имени файла контекст "..", в поле ссылки содержит ссылку на описатель файла, в котором хранится информация о родительском каталоге текущего каталога.
Специальные файлы - это некоторые файлы, каждому из которых ставится в соответствие свое внешнее устройство, поддерживаемое ОС UNIX и имеющее специальную структуру. Его нельзя использовать для хранения данных, как обычный файл или каталог. В то же время над специальным файлом можно производить те же операции, что и над обычным файлом: открывать, вводить и/или выводить информацию и т.д. Результат применения любой из этих операций зависит от того, какому конкретному устройству соответствует обрабатываемый специальный файл, однако в любом случае будет осуществлена соответствующая операция ввода-вывода на внешнее устройство, которому соответствует выбранный специальный файл.
Четвертый вид файлов - каналы, будет рассмотрен отдельно в последующих лабораторных работах.
Для получения информации о типе файла необходимо воспользоваться системными вызовами stat (fstat), описанными в предыдущей лабораторной работе. Поле st_mode содержит флаги, описывающие файл. Флаги несут следующую информацию:
В представленной ниже таблице 1 приведены системные функции ОС UNIX для работы с файловой системой.
Таблица 1
Основным назначением системы управления вводом-выводом ОС UNIX является создание интерфейса между программой и внешним усройством компьютера. Поскольку любая операция ввода-вывода осуществляется как операция ввода-вывода в файл, то логическая структура программного интерфейса, реализуемого системой управления вводом-выводом, не зависит ни от типа данных, ни от типа внешнего устройства компьютера.
При осуществлении операций ввода-вывода в файл, специфицированный пользовательским дескриптором файла, ОС UNIX ставит в соответствие используемому системному вызову последовательность программных запросов к аппаратуре компьютера с помощью целого ряда связанных наборов данных, структура которых поддерживается самой ОС UNIX, ее файловой системой и системой управления вводом-выводом. Основным из упомянутых наборов можно считать таблицу описателей файлов.
Таблица описателей файлов представляет собой хранящуюся в оперативной памяти компьютера структуру данных, элементами которой являются копии описателей файлов, по одной на каждый файл ОС UNIX, к которому была осуществлена попытка доступа. При выполнении операции открытия файла в ОС UNIX сначала по полному имени файла определяется элемент каталога, где в поле имени содержится имя файла, для которого производится операция открытия файла. В найденном элементе каталога из поля ссылки извлекается порядковый номер описателя файла. Затем описатель файла с соответствующим номером копируется в оперативную память, в ее область, называемую таблицей описателей файлов (если он до этого там отсутствовал).
С таблицей описателей файлов тесно связана другая структура данных, называемая таблицей файлов. Каждый элемент таблицы файлов содержит информацию о режиме открытия файла, специфицированным при открытии файла, а также информацию о положении указателя чтения-записи. При каждом открытии файла в таблице файлов появляется новый элемент.
Один и тот же файл ОС UNIX может быть открыт несколькими не связанными друг с другом процессами, при этом ему будет соответствовать один элемент таблицы описателей файлов и столько элементов таблицы файлов, сколько раз этот файл был открыт. Однако из этого правила есть одно исключение: оно касается случая, когда файл, открытый процессом, потом открывается процессом-потомком, порожденным с помощью системного вызова fork(). Пpи возникновении такой ситуации опеpации откpытия файла, осуществленной пpоцессом-потомком, будет поставлен в соответствие тот из существующих элементов таблицы файлов (в том числе положение указателя чтения-записи), котоpый в свое вpемя был поставлен в соответствие опеpации откpытия этого файла, осуществленной пpоцессом-предком.
Для порождения нового процесса (процесс-потомок) используется системный вызов fork(). Формат вызова:
В предыдущей лабораторной работе были рассмотрены различные программные средства, связанные с созданием и управлением процессами в рамках ОС UNIX. Данная лабораторная работа предполагает комплексное их использование при решении задачи синхронизации процессов и их взаимодействия посредством программных каналов.
Кратко перечислим состав системных вызовов, требуемых для выполнения данной лабораторной работы:
Создание, завершение процесса, получение информации о процессе, - fork(), exit(), getpid(), getppid();
Синхронизация процессов - signal(), kill(), sleep(), alarm(), wait(), pause();
Создание информационного канала и работа с ним - pipe(), read(), write().
При выполнении операции перенаправления ввода-вывода важным моментом является наследование пользовательских дескрипторов, осуществляемое с помощью системных вызовов dup() и fcntl().
Системный вызов dup() обрабатывает свой единственный параметр как пользовательский дескриптор открытого файла и возвращает целое число, которое может быть использовано как еще один пользовательский дескриптор того же файла. С помощью копии пользовательского дескриптора файла к нему может быть осуществлен доступ того же типа и с использованием того же значения указателя записи-чтения, что и с помощью оригинального пользовательского дескриптора файла.
Системный вызов fcntl(), имеющий формат
Механизм IPC (Inter-Process Communication Facilities) включает:
средства, обеспечивающие возможность синхронизации процессов при доступе к совместно используемым ресурсам (семафоры - semaphores);
средства, обеспечивающие возможность посылки процессом сообщений другому произвольному процессу (очереди сообщений - message queries);
средства, обеспечивающие возможность наличия общей для процессов памяти (сегменты разделяемой памяти - shared memory segments).
Наиболее общим понятием IPC является ключ, хранимый в общесистемной таблице и обозначающий объект межпроцессного взаимодействия, доступный нескольким процессам. Обозначаемый ключом объект может быть очередью сообщений, набором семафоров или сегментом разделяемой памяти. Ключ имеет тип key_t, состав которого зависит от реализации и определяется в файле <sys/types.h>. Ключ используется для создания объекта межпроцессного взаимодействия или получения доступа к существующему объекту. Обе операции выполняются посредством операции get. Результатом операции get является его целочисленный идентификатор, который может использоваться в других функциях межпроцессного взаимодействия.
I. Семафоры.
Для работы с семафорами поддерживаются три системных вызова:
semget() для создания и получения доступа к набору семафоров;
semop() для манипулирования значениями семафоров (это тот системный вызов, который позволяет процессам синхронизоваться на основе использования семафоров
semctl() для выполнения разнообразных управляющих операций над набором семафоров
Прототипы перечисленных системных вызовов описаны в файлах
#include <sys/ipc.h>
#include <sys/sem.h>
Системный вызов semget() имеет следующий синтаксис:
Существует две модели взаимодействия между процессами в сети: модель соединений с протоколом TCP (Transmission Control Protocol), и модель дейтаграмм с протоколом UDP (User Datagram Protocol). В данной лабораторной работе используется первая из названных моделей.
Ниже приводятся основные шаги и необходимые системные вызовы для выполнения основных этапов при работе с сокетами в режиме TCP-соединения.
1. Адресация и создание сокета
Совокупная информация об адресе, порте программы-адресата (абонента), модели соединения, протоколе взаимодействия составляет т.н. сокет (конечная абонентская точка), формально представляющий собой структуру данных. Существует несколько видов сокетов:
обобщенный сокет (generic socket), определяется в файле <sys/socket.h>:
struct sockaddr {
u_char sa_family; /* Семейство адресов (домен) */
char sa_data[]; }; /* Адрес сокета */
Сокеты для связи через сеть, определяется в файле <netinet/in.h>:
struct sockaddr_in {
u_char sin_len; /* Длина поля sockaddr_in (для FreeBSD) */
u_char sin_family; /* Семейство адресов (домен) */
u_short sin_port; /* Номер порта */
struct in_addr sin_addr; /* IP-адрес */
char sin_zero[8]; }; /* Поле выравнивания */
где struct in_addr {
n_int32_t s_addr}.
Создается сокет при помощи системного вызова socket().
#include <sys/socket.h>
int socket (int domain, int type, int protocol);
Параметр domain - домен связи, в котором будет использоваться сокет (значение AF_INET - для домена Internet (соединение через сеть), AF_UNIX - домен, если процессы находятся на одном и том же компьютере);
Параметр type определяет тип создаваемого сокета (значение SOCK_STREAM - для режима соединений, SOCK_DGRAM - для режима дейтаграмм);
Параметр protocol определяет используемый протокол (в случае protocol = 0 по умолчанию для сокета типа SOCK_STREAM будет использовать протокол TCP, а сокета типа SOCK_DGRAM - протокол UDP).
При программировании TCP-соединения должны быть созданы сокеты (системный вызов socket()) и в программе сервера и в программе клиента, при этом в обеих программах сокеты связываются с адресом машины, на которую будет установлена программа сервера. Но, если в программе сервера для определения IP-адреса в структуре сокета может быть использована переменная INADDR_ANY, то в программе клиента для занесения в структуру сокета IP-адреса машины сервера необходимо использовать системный вызов inet_addr().
Shell- переменные могут хранить строки текста. Правила формирования их имен аналогичны правилам задания имен переменных в обычных языках программирования. При необходимости присвоить Shell-переменной значение, содержащее пробелы и другие специальные знаки, оно заключается в кавычки. При использовании Shell-переменной в выражении ее имени должен предшествовать знак $. В последовательности символов те из них, которые составляют имя, должны быть выделены в { } или " ". Кроме того интерпретатор Shell автоматически присваивает значения пяти своим переменным:
$? - значение, возвращаемое последней выполняемой командой;
$$ - идентификационный номер процесса Shell;
$! - идентификационный номер фонового процесса, запускаемого интерпретатором Shell последним;
$# - число аргументов, переданных в Shell;
$- - флаги, переданные в Shell.
Для отмены специальных символов ($,|,пробел и т.д.) в Shell-программах существуют следующие правила:
если символу предшествует обратная косая черта, то его специальный символ отменяется;
отменяется специальный смысл всех символов, вошедших в последовательность, заключенную в апострофы.
При вызове Shell-программ им могут передаваться параметры. Соответствующие аргументы в Shell-программах идентифицируются $1, $2, $3 и т.д. Кроме того, переменная $0 соответствует имени выполняемой Shell-программы, а переменная $# - числу аргументов в команде.
Shell-интерпретатор дает возможность выполнять подстановку результатов выполнения команд в Shell-программах. Если команда заключена в одиночные обратные кавычки, то интерпретатор Shell выполняет эту команду и подставляет вместо нее полученный результат.
Наиболее важные команды для составления Shell-программ:
команда echo выводит в выходной поток значения своих аргументов;
команда expr выполняет арифметические действия над своими аргументами;
команда eval обеспечивает дополнительный уровень подстановки своих аргументов, а затем их выполнение;
команда test с соответствующими ключами проверяет необходимое условие;команда sleep служит для реализации задержки.
Программные конструкции Shell-программ:
Тpетий набоp данных называется таблицей откpытых файлов пpоцесса. Каждому пpоцессу в ОС UNIX сpазу после поpождения ставится в соответствие таблица откpытых файлов пpоцесса. Если, в свою очеpедь, указанный пpоцесс поpождает новый пpоцесс, напpимеp, с помощью системного вызова fork(), то пpоцессу-потомку ставится в соответствие таблица откpытых файлов пpоцесса, котоpая в пеpвый момент функциониpования пpоцесса-потомка пpедставляет собой копию таблицы откpытых файлов пpоцесса-пpедка.
В pезультате каждый элемент таблицы откpытых файлов пpоцесса содеpжит указатель местоположения соответствующего элемента таблицы файлов, котоpая в свою очеpедь, содеpжит ссылку на элемент таблицы описателей файла. Если пользовательский дескpиптоp файла использовать для индексации элементов таблицы откpытых файлов пpоцесса, то получим логическую схему системы упpавления файлами (вводом-выводом).
Лабоpатоpная pабота пpедполагает написание пpогpаммы, показыващей действия системы упpавления вводом-выводом пpи выполнении некотоpых действий с файлами. Пpогpамма должна демонстpиpовать динамику фоpмиpования таблиц и их изменений в пpоцессе указанных в ваpианте задания событий.
При этом при выполнении тех заданий, где требуется демонстрировать создание таблиц описателей файлов, информацию о файле необходимо получать с помощью системных вызовов stat (fstat), поскольку именно информация, хранящаяся в описателе файла, в основном и помещается системным вызовом stat (fstat) в стуктуру, специфицированную его вторым выходным параметром.
Полученную информацию из структуры stat, дополненную именем файла и следует в лабораторных работах трактовать в качестве таблицы описателей файлов.
В тех заданиях, где требуется отслеживать динамику создания и модификации таблиц файлов и таблиц открытых файлов процесса, эти таблицы должны программно моделироваться при возникновении событий, указанных в заданиях лабораторной работы. Никаких действий по созданию процессов в программах выполнять не требуется.
Структура элемента таблицы файлов в программах лабораторной работы (упрощенный вариант) должен иметь вид:
Сетевые вызовы
inet_addr() и
inet_ntoa() выполняют преобразования IP- адреса из формата текстовой строки "x.y.z.t" в структуру типа
in_addr и обратно.
#include <arpa/inet.h>
in_addr_t inet_addr (const char *ip_address);
char * inet_ntoa(const struct in_addr in);
Для того чтобы процесс мог ссылаться на адрес своего компьютера, файле
<netinet/in.h> определена переменная
INADDR_ANY, содержащая локальный адрес компьютера в формате
in_addr_t.
2. Связывание
Системный вызов
bind() связывает сетевой адрес компьютера с идентификатором сокета.
#include<sys/types.h>
#include<sys/socket.h>
int bind (int sockfd, const struct sockaddr *address, size_t add_len);
sockfd - дескриптор файла сокета, созданным с помощью вызова socket(),
address - указателем на обобщенную структуру адреса сокета, к которой преобразуется структура sockaddr_in, в случае передачи данных через сеть.
size_t add_len - размер указанной структуры адреса сокета.
В случае успешного завершения вызова
bind() он возвращает значение 0. В случае ошибки, например, если сокет для этого адреса уже существует, вызов
bind() возвращает значение -1. Переменная
errno будет иметь при этом значение
EADDRINUSE.
Oперация связывания выполняется только в программе сервера.
3. Включение приема TCP-соединений
После выполнения связывания с адресом и перед тем, как какой-либо клиент сможет подключиться к созданному сокету, сервер должен включить прием соединений посредством системного вызова
listen().
#include<sys/socket.h>
int listen (int sockfd, int queue_size);
sockfd - дескриптор файла сокета, созданным с помощью вызова socket(),
queue_size - число запросов на соединение с сервером, которые могут стоять в очереди.
Данная операция выполняется только в программе сервера.
4. Прием запроса на установку TCP-соединения
Когда сервер получает от клиента запрос на соединение, он создает новый сокет для работы с новым соединением. Первый же сокет используется только для установки соединения. Дополнительный сокет для работы с соединением создается при помощи вызова
accept(), принимающего очередное соединение.
#include<sys/types.h>
#include<sys/socket.h>
int accept (int sockfd, struct sockaddr *address, size_t *add_len);
sockfd - дескриптор сокета, для которого ведется прием соединений;
address - указатель на обобщенную структуру адреса сокета с информацией о клиенте; так как связь использует соединение адрес клиента знать не обязательно и допустимо задавать параметр address значением NULL;
add_len - размер структуры адреса, заданной параметром address, если значение address не равно NULL.
Возвращаемое значение соответствует идентификатору нового сокета, который будет использоваться для связи. До тех пор, пока от клиента не поступил запрос на соединение, процесс, выдавший системный вызов
accept() переводится в состояние ожидания.
Данная операция выполняется только в программе сервера.
5. Подключение клиента
Для выполнения запроса на подключение к серверному процессу клиент использует системный вызов
connect().
#include<sys/types.h>
#
include<sys/socket.h>
int connect (int csockfd, const struct sockaddr *address, size_t add_len);
сsockfd - дескриптор файла сокета клиента, созданным с помощью вызова socket();
address - указателем на обобщенную структуру адреса сокета, к которой преобразуется структура sockaddr_in, в случае передачи данных через сеть;
size_t add_len - размер указанной структуры адреса сокета.
В случае успешного завершения вызова
connect() он возвращает значение 0. В случае ошибки, системный вызов
connect() возвращает значение -1, а переменная
errno идентифицирует ошибку.
Данная операция выполняется только в программе клиента.
6. Пересылка данных
Для сокетов типа
SOCK_STREAM дескрипторы сокетов, полученные сервером посредством вызова
accept() и клиентом с помощью вызова
socked(), могут использоваться для чтения или записи. Для этого могут использоваться обычные вызовы
read() и
write(), либо специальные системные вызовы
send() и
recv(), позволяющие задавать дополнительные параметры пересылки данных по сети. Синхронизация данных при работе с сокетом аналогична передаче данных через программный канал.
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv (int sockfd, void *buffer, size_t length, int flags);
ssize_t send (int sockfd, const void *buffer, size_t length, int flags);
socfd - дескриптор сокета, через который читаются или записываются данные;
buffer - буфер, в который они помещаются или откуда отсылаются через сокет;
length - размер буфера;
flags - поле дополнительных опций при получении или передаче данных.
В случае успешного чтения/записи системные вызовы
send() и
recv() возвращают число прочитанных/отосланных байт, или -1 в случае ошибки; в случае разорванной связи (клиент разорвал TCP-соединение) вызов
recv() (или
read()) возвращают нулевое значение; если процесс пытается записать данные через разорванное TCP-соединение посредством
write() или
send(), то он получает сигнал
SIGPIPE, который можно обработать, если предусмотрена обработка данного сигнала.
В случае
flags = 0 вызовы
send() и
recv() полностью аналогичны системным вызовам
read() и
write().
Возможные комбинациями констант параметра
flags системнного вызова
send():
MSG_PEEK Процесс может просматривать данные, не "получая" их;
MSG_OOB Обычные данные пропускаются. Процесс принимает только срочные данные, например, сигнал прерывания;
MSG_WAITALL Возврат из вызова recv произойдет только после получения всех данных.
Возможные комбинациями констант параметра
flags системного вызова
recv():
MSG_OOB Передать срочные (out of band) данные;
MSG_DONTROUTE При передаче сообщения игнорируются условия маршрутизации протокола более низкого уровня. Обычно это означает, что сообщение посылается по прямому, а не по самому быстрому маршруту (самый быстрый маршрут не обязательно прямой и может зависеть от текущего распределения нагрузки сети).
Данные операции выполняются и в программе сервера и в программе клиента.
7. Закрытие TCP-соединения
Закрываются сокеты так же, как и обычные дескрипторы файлового ввода/вывода, - при помощи системного вызова
close(). Для сокета
SOCK_STREAM ядро гарантирует, что все записанные в сокет данные будут переданы принимающему процессу.
Данные операции выполняются и в программе сервера и в программе клиента.
Содержание раздела