Текст книги "Операционная система UNIX"
Автор книги: Андрей Робачевский
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 22 (всего у книги 39 страниц)
Разработчики системы межпроцессного взаимодействия BSD UNIX руководствовались рядом соображений:
Во-первых, взаимодействие между процессами должно быть унифицировано, независимо от того, выполняются ли они на одном компьютере или на разных хостах сети. Наиболее оптимальная реализация межпроцессного взаимодействия, удовлетворяющего этому требованию, должна иметь модульную структуру и базироваться на общей подсистеме поддержки сети UNIX. При этом могут быть использованы различные схемы адресации объектов, их расположение, протоколы передачи данных и т.д. В этой связи было введено понятие коммуникационный домен (communication domain), описывающее набор обозначенных характеристик взаимодействия.
Для обозначения коммуникационного узла, обеспечивающего прием и передачу данных для объекта (процесса), был предложен специальный объект – сокет (socket). Сокеты создаются в рамках определенного коммуникационного домена, подобно тому как файлы создаются в рамках файловой системы. Сокеты имеют соответствующий интерфейс доступа в файловой системе UNIX, и так же как обычные файлы, адресуются некоторым целым числом – дескриптором. Однако в отличие от обычных файлов, сокеты представляют собой виртуальный объект, который существует, пока на него ссылается хотя бы один из процессов.
Во-вторых, коммуникационные характеристики взаимодействия должны быть доступны процессам в некоторой унифицированной форме. Другими словами, приложение должно иметь возможность затребовать определенный тип связи, например, основанный на виртуальном канале (virtual circuit) или датаграммах (datagram), причем эти типы должны быть согласованы для всех коммуникационных доменов. Все сокеты условно можно разделить на несколько типов, в зависимости от предоставляемых коммуникационных характеристик. Полный набор этих характеристик включает:
□ Упорядоченную доставку данных
□ Отсутствие дублирования данных
□ Надежную доставку данных
□ Сохранение границ сообщений
□ Поддержку передачи экстренных сообщений
□ Предварительное установление соединения
Например, каналы, рассмотренные ранее, обеспечивают только первые три характеристики. При этом данные имеют вид сплошного потока, вычленение сообщений из которого должно при необходимости быть обеспечено взаимодействующими приложениями.
Поддержка передачи экстренных сообщений предполагает возможность доставки данных вне нормального потока. Как правило, это сообщения, связанные с некоторыми срочными событиями, требующими немедленной реакции.
Взаимодействие с предварительным установлением соединения предполагает создание виртуального канала между источником и получателем данных. Это избавляет от необходимости идентифицировать передающую сторону в каждом пакете данных. Идентификация происходит на начальном этапе установления связи и затем сохраняется для всех пакетов, принадлежащих данному виртуальному каналу.
В BSD UNIX реализованы следующие основные типы сокетов:
□ Сокет датаграмм (datagram socket), через который осуществляется теоретически ненадежная, несвязная передача пакетов.
□ Сокет потока (stream socket), через который осуществляется надежная передача потока байтов без сохранения границ сообщений. Этот тип сокетов поддерживает передачу экстренных данных.
□ Сокет пакетов (packet socket), через который осуществляется надежная последовательная передача данных без дублирования с предварительным установлением связи. При этом сохраняются границы сообщений.
□ Сокет низкого уровня (raw socket), через который осуществляется непосредственный доступ к коммуникационному протоколу.
Наконец, для того чтобы независимые процессы имели возможность взаимодействовать друг с другом, для сокетов должно быть определено пространство имен. Имя сокета имеет смысл только в рамках коммуникационного домена, в котором он создан. Если для IPC System V используются ключи, то имена сокетов представлены адресами.
Итак, сокеты являются коммуникационным интерфейсом взаимодействующих процессов. Конкретный характер взаимодействия зависит от типа используемых сокетов, а коммуникационный домен, в рамках которого создан сокет, определяет базовые свойства этого взаимодействия. В табл. 3.6 приведены типы сокетов и их названия.
Таблица 3.6. Типы сокетов в системе BSD UNIX
SOCK_DGRAM | Сокет датаграмм |
SOCK_STREAM | Сокет потока |
SOCK_SEQPACKET | Сокет пакетов |
SOCK_RAW | Сокет низкого уровня |
Для создания сокета процесс должен указать тип сокета и коммуникационный домен, в рамках которого будет использоваться сокет. Поскольку коммуникационный домен может поддерживать использование нескольких протоколов, процесс может также указать конкретный коммуникационный протокол для взаимодействия. Если таковой не указан, система выберет наиболее подходящий из списка протоколов, доступных для данного коммуникационного домена. Если же в рамках указанного домена создание сокета данного типа невозможно, т.е. отсутствует соответствующий коммуникационный протокол, запрос процесса завершится неудачно.
Для создания сокета используется системный вызов socket(2)[44]44
Поскольку сокеты являются неотъемлемой частью BSD UNIX, в системах этой ветви функции, связанные с этими объектами, в частности socket(2) и рассмотренные ниже, представляют собой системные вызовы. В UNIX ветви System V интерфейс сокетов сохранен для совместимости, но имеет совершенно отличную от принятой в BSD архитектуру (основанную на подсистеме STREAMS). Поэтому все его функции являются библиотечными и описываются, соответственно в разделе 3 электронного справочника. Однако, оставляя пальму первенства в этом вопросе за BSD UNIX, в этом разделе будем считать эти функции системными вызовами и связывать с ними раздел 2 справочника man(1M).
[Закрыть], имеющий следующий вид:
#include
#include
int socket(int domain, int type, int protocol);
Здесь аргумент domain
определяет коммуникационный домен, type
– тип сокета, a protocol
– используемый протокол (может быть не указан, т.е. приравнен 0). В случае успеха системный вызов возвращает положительное целое число, аналогичное файловому дескриптору, которое служит для адресации данного сокета в последующих вызовах.
По существу коммуникационный домен определяет семейство протоколов (protocol family), допустимых в рамках данного домена. Возможные значения аргумента domain
включают:
AF_UNIX | Домен локального межпроцессного взаимодействия в пределах единой операционной системы UNIX. Внутренние протоколы. |
AF_INET | Домен взаимодействия процессов удаленных систем. Протоколы Internet (TCP/IP). |
AF_NS | Домен взаимодействия процессов удаленных систем. Протоколы Xerox NS. |
Поскольку домен и семейство протоколов определяют адресное пространство взаимодействия (допустимые адреса и их формат), то в названиях доменов присутствует префикс AF (от address family – семейство адресов). Допустимыми также являются названия с префиксом PF (protocol family) PF_UNIX
, PF_INET
и т.д.
Заметим, что домен может не поддерживать определенные типы сокетов. Для сравнения в табл. 3.7 приведены два основных коммуникационных домена – внутренний домен UNIX, предназначенный для взаимодействия процессов одной операционной системы, и домен TCP/IP, используемый в сетевых распределенных приложениях.
Таблица 3.7. Поддержка различных типов сокетов в доменах
Тип сокета | ||
SOCK_STREAM | Да | Да |
SOCK_DGRAM | Да | Да |
SOCK_SEQPACKET | Нет | Нет |
SOCK_RAW | Нет | Да |
Также допустимы не все комбинации типа сокета и используемого коммуникационного протокола (если таковой явно указан в запросе). Так для домена AF_INET
возможны следующие комбинации:
SOCK_STREAM | IPPROTO_TCP (TCP) |
SOCK_DGRAM | IPPROTO_UDP (UDP) |
SOCK_RAW | IPPROTO_ICMP (ICMP) |
SOCK_RAW | IPPROTO_RAW (IP) |
Указанные протоколы принадлежат семейству сетевых протоколов TCP/IP и будут подробно рассмотрены в главе 6.
Создание сокета не означает создания коммуникационного узла. Для однозначной идентификации сокета его необходимо позиционировать в пространстве имен данного коммуникационного домена. В общем случае каждый коммуникационный канал определяется двумя узлами – источником и получателем данных, и может быть охарактеризован пятью параметрами:
1. Коммуникационным протоколом
2. Локальным адресом
3. Локальным процессом
4. Удаленным адресом
5. Удаленным процессом
Как правило, адрес определяет операционную систему (или хост сети), а процесс – конкретное приложение, получающее или передающее данные. Однако конкретные значения и формат этих параметров определяются коммуникационным доменом.
Поскольку при создании сокета указывается только один параметр – коммуникационный протокол, прежде чем передача данных между взаимодействующими процессами станет возможной необходимо указать четыре дополнительных параметра для коммуникационного канала. Очевидно, что взаимодействующие стороны должны делать это согласованно, используя либо заранее определенные адреса, либо договариваясь о них в процессе установления связи. Процедура установки этих параметров существенным образом зависит и от типа создаваемого канала, определяемого типом используемого сокета и коммуникационного протокола.
Иллюстрация взаимодействия между процессами при виртуальном коммуникационном канале с предварительным установлением связи приведена на рис. 3.21, а взаимодействие, основанное на датаграммах без установления связи показано на рис. 3.22.
Рис. 3.21. Взаимодействие между процессами при создании виртуального канала (с предварительным установлением соединения)
Рис. 3.22. Взаимодействие между процессами, основанное на датаграммах (без предварительного установления соединения)
Как видно из рисунков, фактической передаче данных предшествует начальная фаза связывания (binding) сокета, когда устанавливается дополнительная информация, необходимая для определения коммуникационного узла. Связывание может быть осуществлено с помощью системного вызова bind(2):
#include
#include
int bind(int sockfd, struct sockaddr *localaddr, int addrlen);
Здесь sockfd
является дескриптором сокета, полученным при его создании; аргумент localaddr
определяет локальный адрес, с которым необходимо связать сокет; параметр addrlen
определяет размер адреса. Заметим, что речь идет о связывании с локальным адресом, в общем случае определяющим два параметра коммуникационного канала (коммуникационный узел): локальный адрес и локальный процесс.
Как уже обсуждалось, адрес сокета зависит от коммуникационного домена, в рамках которого он определен. В общем случае адрес определяется следующим образом (в файле
struct sockaddr {
u_short sa_family;
char sa_data[14];
}
Поле sa_family
определяет коммуникационный домен (семейство протоколов), a sa_data
– содержит собственно адрес, формат которого определен для каждого домена.
Например, для внутреннего домена UNIX адрес выглядит следующим образом (определен в
struct sockaddr_un {
short sun_family; /* ==AF_UNIX */
char sun_path[108];
};
Поскольку в данном домене взаимодействующие процессы выполняются под управлением одной операционной системы на одном и том же хосте, коммуникационный узел может быть однозначно определен одним параметром – локальным процессом. В качестве адреса в домене UNIX используются имена файлов.
В отличие от локального межпроцессного взаимодействия, для сетевого обмена данными необходимо указание как локального процесса, так и хоста, на котором выполняется данный процесс. Для домена Internet (семейство протоколов TCP/IP) используется следующий формат адреса (определен в файле
struct sockaddr_in {
short sin_ family; /* ==AF_INET */
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[0];
}
Адреса этого домена (IP-адреса) будут рассмотрены подробнее в главе 6. Пока лишь заметим, что адрес хоста представляет собой 32-разрядное целое число sin_addr
, а процесс (приложение) адресуется 16-разрядным номером порта sin_port
.
На рис. 3.23 показаны рассмотренные форматы адресов сокетов.
Рис. 3.23. Адреса сокетов
Итак, связывание необходимо для присвоения сокету локального адреса и, таким образом, для определения коммуникационного узла. Можно выделить три случая использования для этого функции bind(2):
1. Сервер регистрирует свой адрес. Этот адрес должен быть заранее известен клиентам, желающим "общаться" с сервером. Связывание необходимо, прежде чем сервер будет готов к приему запросов от клиентов.
2. При взаимодействии без предварительного установления связи и создания виртуального канала клиент также должен предварительно зарегистрировать свой адрес. Этот адрес должен быть уникальным в рамках коммуникационного домена. В случае домена UNIX об этом должно позаботиться само приложение. Этот адрес не должен быть заранее известен серверу, поскольку запрос всегда инициирует клиент, автоматически передавая вместе с ним свой адрес. Полученный адрес удаленного узла затем используется сервером для мультиплексирования сообщений, отправляемым различным клиентам.
3. Даже в случае взаимодействия с использованием виртуального канала клиент может пожелать зарегистрировать собственный адрес, не полагаясь при этом на систему.
Назначение адреса для клиента также можно выполнить с помощью системного вызова connect(2), устанавливающего связь с сервером и автоматически связывающего сокет клиента с локальным коммуникационным узлом. Вызов connect(2) имеет вид:
#include
#include
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
Характер этого вызова предполагает создание виртуального канала и, таким образом, используется для предварительного установления связи между коммуникационными узлами. В этом случае клиенту нет необходимости явно связывать сокет с помощью системного вызова bind(2). Локальный узел коммуникационного канала указывается дескриптором сокета sockfd
, для которого система автоматически выбирает приемлемые значения локального адреса и процесса. Удаленный узел определяется аргументом servaddr
, который указывает на адрес сервера, a addrlen
задает его длину.
Вызов connect(2) может также применяться и клиентами, использующими без создания виртуального канала. В этом случае connect(2) не вызывает фактического соединения с сервером, а является удобным способом сохранения параметров адресата (сервера), которому будут направляться датаграммы. При этом клиент будет избавлен от необходимости указывать адрес сервера при каждом отправлении данных.
Следующие два вызова используются сервером только при взаимодействии, основанном на предварительном создании виртуального канала между сервером и клиентом.
Системный вызов listen(2) информирует систему, что сервер готов принимать запросы. Он имеет следующий вид:
#include
#include
int listen(int sockfd, int backlog);
Здесь параметр sockfd
определяет сокет, который будет использоваться для получения запросов. Предполагается, что сокет был предварительно связан с известным адресом. Параметр backlog
указывает максимальное число запросов на установление связи, которые могут ожидать обработки сервером.[45]45
Если в момент получения запроса на установление связи очередь ожидающих запросов достигла своего максимального значения, вызов connect(2) клиента завершится с ошибкой ECONNREFUSED
для домена UNIX (AF_UNIX
). Для других доменов результат зависит от того, поддерживает ли протокол повторную передачу запроса. Например, протокол TCP (домен AF_INET
) будет передавать повторные запросы, пока число запросов в очереди не уменьшится, либо не произойдет тайм-аут, определенный для протокола. В последнем случае вызов клиента завершится с ошибкой ETIMEDOUT
.
[Закрыть]
Фактическую обработку запроса клиента на установление связи производит системный вызов
#include
#include
int accept(int sockfd, struct sockaddr *clntaddr,
int* addrlen);
Вызов accept(2) извлекает первый запрос из очереди и создает новый сокет, характеристики которого не отличаются от сокета sockfd
, и таким образом завершает создание виртуального канала со стороны сервера. Одновременно accept(2) возвращает параметры удаленного коммуникационного узла – адрес клиента clntaddr
и его размер addrlen
. Новый сокет используется для обслуживания созданного виртуального канала, а полученный адрес клиента исключает анонимность последнего. Дальнейший типичный сценарий взаимодействия имеет вид:
sockfd = socket(...);
Создать сокет
bind(sockfd, ...);
Связать его с известным локальным адресом
listen(sockfd, ...);
Организовать очередь запросов
for(;;) {
newsockfd = accept(sockfd, ...);
Получить запрос
if (fork() == 0) {
Породить дочерний процесс
close(sockfd);
Дочерний процесс
...
exit(0);
} else
close(newsockfd);
Родительский процесс
}
В этом сценарии, в то время как дочерний процесс обеспечивает фактический обмен данными с клиентом, родительский процесс продолжает "прослушивать" поступающие запросы, порождая для каждого из них отдельный процесс-обработчик. Очередь позволяет буферизовать запросы на время, пока сервер завершает вызов accept(2) и затем создает дочерний процесс. Заметим, что новый сокет newsockfd
, полученный в результате вызова accept(2), адресует полностью определенный коммуникационный канал: протокол и полные адреса обоих узлов – клиента и сервера. Напротив, для сокета sockfd
определена только локальная часть канала. Это позволяет серверу продолжать использовать sockfd
для «прослушивания» последующих запросов.
Наконец, если для сокетов потока при приеме и передаче данных могут быть использованы стандартные вызовы read(2) и write(2), то сокеты дата– грамм должны пользоваться специальными системными вызовами (эти вызовы также доступны для сокетов других типов):
#include
#include
int send(int s, const char *msg, int len, int flags);
int sendto(int s, const char *msg, int len, int flags,
const struct sockaddr* toaddr, int tolen);
int recv(int s, char *buf, int len, int flags);
int recvfrom(int s, char *buf, int len, int flags,
struct sockaddr* fromaddr, int* fromlen);
Функции send(2) и sendto(2) используются для передачи данных удаленному узлу, а функции recv(2) и recvfrom(2) – для их приема. Основным различием между ними является то, что функции send(2) и recv(2) могут быть использованы только для «подсоединенного» сокета, т.е. после вызова connect(2).
Все эти вызовы используют в качестве первого аргумента дескриптор сокета, через который производится обмен данными. Аргумент msg
содержит сообщение длиной len
, которое должно быть передано по адресу toaddr
, длина которого составляет tolen
байтов. Для функции send(2) используется адрес получателя, установленный предшествовавшим вызовом connect(2). Аргумент buf
представляет собой буфер, в который копируются полученные данные.
Параметр flags
может принимать следующие значения:
MSG_OOB | Передать или принять экстренные данные вместо обычных |
MSG_PEEK | Просмотреть данные, не удаляя их из системного буфера (последующие операции чтения получат те же данные) |
В заключение приведем пример использования сокетов для организации межпроцессного взаимодействия. Поскольку в данном разделе не затрагиваются сетевые вопросы, то и сокеты, которые будут использованы в примере, принадлежат домену UNIX. Как и в предыдущих примерах, функциональность нашей распределенной системы не отличается разнообразием: клиент посылает серверу сообщение «Здравствуй, Мир!», а сервер отправляет его обратно клиенту, который после получения выводит сообщение на экран.
В примере использованы сокеты датаграмм, которые в домене UNIX практически не отличаются от сокетов потока. В качестве адреса сервера предлагается имя файла ./echo.server (мы полагаем, что в системе запущен только один сервер из данного каталога). Предполагается, что клиенты заранее знают этот адрес. Сервер связывает созданный сокет с этим локальным адресом и таким образом регистрируется в системе. Начиная с этого момента он готов к получению и обработке сообщений. Сервер начинает бесконечный цикл, ожидая сообщений от клиентов, блокируясь на вызове recvfrom(2). При получении сообщения сервер отправляет его обратно, вызывая sendto(2).
Сервер:
#include
#include
#include
#define MAXBUF 256 char
buf[MAXBUF];
main() {
struct sockaddr_un serv_addr, clnt_addr;
int sockfd;
int saddrlen, caddrlen, max caddrlen, n;
/* Создадим сокет */
if ((sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0} {
printf(«Невозможно создать сокетn»);
exit(1);
}
/* Свяжем сокет с известным локальным адресом. Поскольку адрес
в домене UNIX представляет собой имя файла, который будет
создан системным вызовом bind(2), сначала удалим файл с этим
именем в случае, если он сохранился от предыдущего запуска
сервера */
unlink(«./echo_server»);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sun_family = AF_UNIX;
strcpy(serv_addr.sun_path, «./echo.server»);
saddrlen =
sizeof(serv_addr.sun_family) + strlen(serv_addr.sun_path);
if (bind(sockfd, (struct sockaddr*)&serv_addr,
saddrlen) < 0) {
printf(«Ошибка связывания сокета с адресомn»);
exit(1);
}
/* Теперь запустим бесконечный цикл чтения сообщений от
клиентов и отправления их обратно */
max_caddrlen = sizeof(clnt_addr);
for(;;) {
caddrlen = max_caddrlen;
n = recvfrom(sockfd, buf, MAXBUF, 0,
(struct sockaddr*)&clnt_addr, &caddrlen);
if (n < 0) {
printf(«Ошибка приемаn»);
exit(1);
}
/* Благодаря вызову recvfrom(2), мы знаем адрес клиента,
от которого получено сообщение. Используем этот адрес
для передачи сообщения обратно отправителю */
if (sendto(sockfd, buf, n, 0,
(struct sockaddr*)&clnt_addr, caddrlen) != n) {
printf(«Ошибка передачиn»);
exit(1);
}
}
}
Клиент создает сокет датаграмм и связывает его со своим уникальным адресом. Уникальность адреса определяется уникальностью имени файла. Поскольку одновременно могут работать несколько клиентов, возникает задача выполнения условия уникальности. Для этого мы используем функцию mktemp(3C), позволяющую по заданному шаблону /tmp/clnt.XXXX и на основании идентификатора текущего процесса получить уникальное имя, заменяя соответствующим образом символы 'X'. Связывание сокета позволяет при отправлении сообщения неявно указать его «адрес отправителя», так что серверу не составляет труда отправить сообщение обратно.
Клиент:
#include
#include
#include < sys/un.h>
char *msg = «Здравствуй, Мир!n»;
#define MAXBUF 256
char buf[MAXBUF];
main() {
struct sockaddr_un serv_addr, clnt_addr;
int sockfd;
int saddrlen, caddrlen, msglen, n;
/* Установим адрес сервера, с которым мы будем обмениваться
данными. Для этого заполним структуру данных sockaddr_un,
которую будем использовать при отправлении данных серверу
с помощью вызова sendto(). Значение адреса известно
по предварительной договоренности */
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sun_family = AF_UNIX;
strcpy(serv_addr.sun_path, «./echo.server»);
saddrlen = sizeof(serv_addr.sun_family) +
strlen(serv_addr.sun_path);
/* Создадим сокет датаграмм */
if ((sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
printf(«Невозможно создать сокетn»);
exit(1);
}
/* Необходимо связать сокет с некоторым локальным адресом,
чтобы сервер имел возможность возвратить посланное сообщение.
Этот адрес должен быть уникальным в пределах коммуникационного
домена – т.е. данной операционной системы. Для обеспечения
этого условия, воспользуемся функцией mktemp(3C), которая
возвращает уникальное имя, основанное на представленном
шаблоне и идентификаторе нашего процесса PID */
bzero(&clnt_addr, sizeof(clnt_addr));
clnt_addr.sun_family = AF_UNIX;
strcpy(clnt_addr.sun_path, «/tmp/clnt.XXXX»);
mktemp(clnt_addr.sun_path);
caddrlen =
sizeof(clnt addr.sun_family) + strlen(clnt_addr.sun_path);
if (bind(sockfd, (struct sockaddr*)&clnt_addr,
caddrlen) < 0) {
printf(«Ошибка связывания сокетаn»);
exit(1);
}
/* Итак, отправляем сакраментальное приветствие */
msglen = strlen(msg);
if (sendto(sockfd, msg, msglen, 0,
(struct sockaddr*)&serv addr, saddrlen) != msglen) {
printf(«Ошибка передачи сообщенияn»);
exit(1);
}
/* Прочитаем эхо*/
if ((n = recvfrom(sockfd, buf, MAXBUF, 0, NULL, 0)) < 0) {
printf(«Ошибка получения сообщенияn»);
exit(1);
}
/* И выведем его на экран */
printf(«Эхо: %sn», buf);
/* Уберем за собой */
close(sockfd);
unlink(clnt_addr.sun_path);
exit(0);
}