Макросы, связанные с потоками
Наиболее интересны два макроса, связанные с потоками :
- получение текущей позиции
int xdr_getpos(xdr_handle)
XDR *xdr_handle; /* handle */
Этот макрос возвращает целое число, указывающее на текущую позицию в потоке.
- позиционирование в потоке
bool_t xdr_setpos(xdr_handle, pos)
XDR *xdr_handle; /* handle */
int pos; /* позиция в потоке */
Этот макрос возвращает TRUE, если позиционирование возможно. С помощью этих макросов можно определить число байтов, требующееся для хранения данных XDR.
ПРОГРАММА 48
/*вычисление числа байтов пpи кодиpовании XDR */ #include <stdoi.h> #include <rpc/rpc.h>
main() { XDR xdrs; /*дескpиптоp XDR */ int pos1, pos2; /*позиции */
/*позиция пеpед кодиpованием xdr */ pos1 = xdr_getpos(&xdrs); /*кодиpование xdr */ .............. /* позиция после кодиpования xdr */ pos2 = xdr_getpos(&xdrs); print("nombre octets dans le flot XDR %d/n", pos2-pos1); }
Маршрутизация
Две машины ведут диалог непосредственно только в случае, если поля "сетевого адреса" их адресов Internet одинаковы (та же физическая сеть). В противном случае, необходимо ввести между ними маршрутизатор. Маршрутизатором называется устройство, подсоединенное по крайней мере к двум сетям, адреса Internet которых различны. Машинную часть адреса Internet можно разделить на адрес базовой сети и машинный адрес в этой базовой сети (subnet). Меж- ду двумя машинами с различными адресами базовых сетей необходимо установить маршрутизатор.
Механизмы кэширования
В операционной системе UNIX ввод-вывод в "блокирующем" режиме осуществляется с помощью кэша, который и позволяет реализовать опережающее чтение ("read ahead") и отсроченную запись ("delayed write"). Что касается клиентов NFS,то в этом случае производительность ввода-вывода позволяют улучшить следящие программы ("де- моны") biod (обычно их четыре) - они обеспечивают опережающее чтение и сохраняют соответствующую информацию в памяти клиента. Кроме того, система запоминает атрибуты файлов и каталогов. Запись выполняется лишь в том случае ,если блок пересылки NFS полон. Если нет доступных следящих программ biod, диалог выполняется непосредственно с одним из серверов nfsd. NFS обеспечивает корректность хранящейся в кэше информации тем, что периодически опрашивает сервер. Сервер NFS выполняет запись непосредственно, без использова- ния механизма кэширования. При чтении, напротив, используется кэш.
Механизмы реализации
XDR использует следующие соглашения :
- единственный формат для представления целых чисел : 32 бита, причем бит с наибольшим весом является младшим (последним по номеру) -Рис. 9.1. ;
- кодирование вещественных чисел в формате IEEE
Этот тип формата неудобен тем, что кодирование приходится выполнять даже тогда, когда в этом нет необходимости,например, если обе машины имеют одно и то же внутреннее представление данных. Однако, время преобразования мало, особенно в сравнении со временем, затрачиваемом на пересылку в сети. По крайней мере, этот подход отличается систематичностью - кодирование всегда выполняется в формат XDR, а декодирование - во внутреннее представление.
Типы данных, определяемые XDR, всегда имеют длину кратную 4 байтам. XDR дополняет закодированное значение нулями, чтобы обеспечить кратность четырем байтам.
Данные, закодированные в формат XDR, не являются типизованными. Это значит, что как передающее, так и принимающее устройство должны знать, данными какого типа они обмениваются. Это, безусловно, ограничение, однако, оно снимает необходи- мость дополнительно кодировать тип данных.
Многооконность
Многооконность - это возможность открыть несколько окон и выполнять в каждом окне свою задачу. Окна организованы иерархически : существует окно, являющееся предком для всех окон ("root window") и всякое окно создается своим окном-родителем. В каждый конкретный момент активно только одно окно - то, на котором сосредоточен "фокус" (т.е. то окно, в которое можно ввести информацию с клавиатуры или с помощью мышки).
МОДЕЛЬ "КЛИЕНТ-СЕРВЕР"
Клиентом называется объект,запрашивающий доступ к службе или ресурсу. Сервер - это объект несущий службу или обладающий ресурсом.
Клиент и сервер могут находиться на одной и той же машине (использование локальных механизмов коммуникации) или на двух разных машинах (использование сетевых средств). В рамках нашего исследования, клиентом и сервером являются два процесса UNIX, связанные между собой через механизм IPC (Interprocess Communication), локальный или сетевой (рис.2.5.).
Рис2.5. Модель клиент-сервер
Клиент и сервер не играют симметричную роль. Процесс-сервер инициализируется и, затем, переходит в состояние ожидания запросов от возможных клиентов. Как правило, процесс-клиент запускается в интерактивном режиме и посылает запросы серверу. Сервер исполняет полученный запрос, причем это может подразумевать диалог с клиентом, а может и нет. Затем сервер вновь переходит в состояние ожидания других клиентов.
Различают два типа процессов-серверов:
- итеративные серверы: процесс-сервер сам обрабатывает ответ. Этот тип сервера используется в случае, если время обработки весьма непродолжительно или если сервер используется единственным клиентом.
- параллельные серверы: процесс-сервер вызывает для обработки вызова клиента другой процесс . Этот процесс создается системным вызовом fork (). Порождающий процесс не блокируется по окончании выполнения порожденного процесса и может, таким образом, ждать другие запросы.
С каждым сервером связан служебный (сервисный) адрес. Клиент посылает запросы по этому адресу. В зависимости от вида осуществляемой обработки данных, раз- личают серверы без состояния (stateless) и серверы с состоянием (statefull). Сервер без состояния не сохраняет о своих клиентах никакой информации. Сервер с состоянием сохраняет информацию о состоянии своих клиентов после каждого запроса. В случае разрыва связи, повторный запуск проще у серверов без состояния, но иногда это может привести к случайным срабатываниям.
Сокеты позволяют,помимо всего прочего, конструировать распределенные прикладные программы в соответствии с моделью "клиент-сервер" (рис. 4.1.).
Рис. 4.1. Модель "клиент-сервер" и сокеты.
Подобно сокетам, TLI позволяет конструировать распределенные прикладные программы в соответствии с моделью "клиент-сервер" (рис. 5.1.).
NFS реализована в соответствии с моделью клиент-сервер. Машина, подсоединенная к сети, является сервером NFS, если она способна предоставить свою файловую систему другим машинам ; говорят, что она "экспортирует" свою файловую систему.
Машина является клиентом NFS, если она использует файловую систему, экспортируемую сервером ; говорят, что она "монтирует" или "импортирует" файловую систему (Рис. 6.1)
Система может быть :
- одновременно клиентом и сервером (станции с жестким диском)
- только клиентом (PC или станции без диска)
- только сервером (центральные IBM)
Один сервер может обслуживать несколько клиентов. Одна машина-клиент способна обращаться к нескольким серверам.
Обычно серверы "экспортируют" свои файловые системы полностью (за исключением нескольких систем,например SunOS версий 4.x,которая позволяет экспортировать отдельные каталоги). Клиенты,напротив,"монтируют" только каталоги.
RFS использует модель клиент-сервер.Машина,подсоединенная к сети, является сервером RFS, если она способна предоставить свою файловую систему другим машинам; говорят, что она "экспортирует" свою файловую систему.
Машина является клиентом RFS, если она использует файловую систему, экспортируемую сервером ; говорят, что она "монтирует" файловую систему.
Рисунок 7.1. иллюстрирует соотношения между компонентами RFS. Имя ресурса связывается с ресурсом сервером, который его экспортирует. Клиент монтирует ресурс, обозначаемый своим именем.
Рисунок 7.1 - Область RFS.
Сервер может экспортировать :
- полностью всю свою файловую систему
- каталоги (которые могут содержать специальные файлы UNIX)
- каталоги, уже смонтированные NFS или RFS (таким образом, машина без диска может быть сервером RFS).
RFS встраивается в ядро UNIX и,одновременно, включает в себя несколько отслеживающих программ. Системные вызовы, предназначенные для периферийных устройств NFS перехватываются ядром и обрабатываются отслеживающими программами.
Motif и Open Look
В настоящее время существуют две развитых среды, позволяющих строить пользовательский интерфейс в X Window :
- Motif - фирмы OSF ;
- Open Look - фирмы UNIX International.
Каждая из этих двух сред предлагает похожую идеологию и сравнимые инструментальные средства :
- руководство по стилю, цель которого обеспечить одинаковый стиль интерфейса для всех прикладных программ (как на Макинтош) ;
- развитые инструментальные средства : XView для Open Look и Xm для Motif.
Кажется, Motif выдвигается в качестве стандарта, обгоняя Open Look.
Мультимашинная организация
Появление сетей, предназначенных для взаимной связи различных компьютеров, привело к разработке средств, а затем и операционных систем, позволяющих осуществлять управление, так называемой, мультимашинной архитектурой (рис.2.3.), то есть совокупности полносоставных компьютеров (процессоры, память, вводы-выводы...), связанных в сеть. В этом случае речь идет о распределенных вычислительных системах.
Рис 2.3.Мультимашинная организация
Следует отметить большое сходство между мультимашинной организацией и архитектурой слабо связанных мультипроцессоров; в обоих структурах процессоры связаны через канал связи, а не через общую память. Различия заключаются в следующем:
- в случае распределенных систем (мультимашинная архитектура) связь между процессорами осуществляется относительно медленно (сеть), а системы независимы;
- в случае параллельных систем (мультипроцессорная архитектура) связь осуществляется быстро (шина), а системы относительно сильно связаны между собой.
Не существует точного определения этих типов архитектуры и этих систем, кроме того, между этими двумя понятиями наблюдается сходство. Распределенные операционные системы, такие как Mach и Chorus могут применяться как при мультимашинной, так и при мультипроцессорной организации. Впрочем, существует несколько вариантов UNIX для мультипроцессоров (на Cray, на Sun...), в которых сосуществуют совершенно различные средства управления распределением по сети и управления связью между процессорами через шину. В данной книге мы рассматриваем использование средств, преназначенных для применения ресурсов, распределенных между различными машинами, доступ к которым возможен через сеть. Мы не рассматриваем ни средства, направленные исключительно на использование мультипроцессорной архитектуры, ни средства, предназначенные для работы в режиме реального времени, так как целью нашего исследования является совместная работа нескольких машин.
Мультиплексирование
Программа poll (), уже рассмотренная в гл. 1, позволяет организовать состояние ожидания по нескольким событиям.
Мультиплексирование с помощью select ()
Здесь мы приводим пример использования сокетов с примитивом select (), рассмотренным в главе 1.
ПРОГРАММА 28 /* использование функции select() для определения, готовыли данные на сокете сервера TCP */ #include "soct.h" #include <sys/time.h> main() { int sock; /* дескриптор исходного сокета */ int nsock; /* дескриптор, полученный с по- мощью accept */ int retour; /* возвращаемое значение */ struct sockaddr_in server;/* адрес сокета */ fd_set readf; /* переменная для select */ fd_set writef; /* переменная для select */ struct timeval to; /* тайм-аут для select */
/* бесконечный цикл ожидания */ for (;;) { /* процесс ждет операцию ввода-вывода на сокете ; одновременно можно ждать и другие операции */ FD_ZERO(&readf); FD_ZERO(&writef); FD_SET(sock, &readf); FD_SET(sock, &writef); to.tv_sec = 10;
retour = select(sock+1, &readf, &writef, 0, &to); /* тайм-аут, если select возвращает нулевое значение */ if (retour == 0) { err_ret("timeout"); continue; } /* в противном случае, ищем соответствующий дескриптор */ if ( (FD_ISSET(sock, &readf)) (FD_ISSET(sock, &writef))) { /* прием связи с сокета */ nsock = accept(sock, (struct sockaddr *) 0, (int *)0); /* обращение к соответствующей службе */ serveur(nsock); /* закрытие текущей связи */ close (nsock);
} else { /* это не сокет; надо проверить все дескрипторы ввода-вывода*/ err_ret("autre descripteur"); } } }
Мультиплексирование ввода-вывода
Используется примитив select () /BSD/ или примитив poll () / System V/. select() предполагает три вида событий: - возможность считывания для данного дескриптора;
- возможность записи для данного дескриптора;
- наступление событий для данного дескриптора, как в случае экспресс-данных для сокета (socket) (cм. главу 4 "Сокеты").
Можно задать значение временной задержки (timeout), по истечении которой ожидание прекращается. Необходимо указать дескрипторы, подлежащие проверке, установив биты в какой-либо переменной. Макросы, позволяющие управлять этими битами через переменную типа fd_set (тип определенный в файле <sys/types.h>: FD_ZERO, FD_SET, FD_CLR, FD_ISSET.
ПРОГРАММА 10 /*Мультиплексиpование с помощью функции select(), обеспечивающее одновpеменное ожидание чтения из стандаpтного ввода и из файла, связанного с дескpиптоpом 4 */ #include <errno.h>l.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/time.h>
main() { struct timeval timeout = (60, 0); /*тайм-аут-60 секунд */ fd_set readfs; /*пеpеменная для select */ char buf[80]; /*буфеp */ int i; /*счетчик циклов*/ #define fdio 0 /*stdin */ #define fdip 4 /*втоpой дескpиптоp */ /*инициализация readfs */ FD_ZERO(&readfs); /*ожидание чтения из дескpиптоpов */ for (;;) { FD_SET(fdio, &readfs); FD_SET(fdip, &readfs); /*select() для stdin.fdip и тайм-аута */ /*пеpвый паpаметp указывает на то, что пpосматpиваются дескpиптоpы с номеpами от 0 до fdip */ switch (select(fdip+1, &readfs, 0, 0, (struct timeval*) &timeout)){ case 0; /*тайм-аут*/ fprintf(stdout, " timeout \n"); exit(1); default: /*поиск соответствующего дескpиптоpа */ if (FD_ISSET(fdio, &readfs)) { /*ввод с теpминала */ for (i = 0; i
read(fdip, &buf[i], 1); if (buf[i] == '\n') break; } } } } } }
Примитив poll () осуществляет операцию того же типа. Дескрипторы и тестируемые события показаны в таблице структур pollfd:
struct pollfd { int fd; /*тестируемый дескриптор*/
short events; /*тестируемые события на fd*/ short revents; /*происшедшее событие на fd*/ };
Кроме того, можно задать значение временной задержки, соот- ветствующее наибольшему времени задержки.
ПРОГРАММА 11 /*Мультиплексиpование с помощью функции poll(), обеспечивающее одновpеменное ожидание чтения из
стандаpтного ввода и из файла, связанного с дескpип- тоpом 4 */ #include
#include
#include
#include
main() { struct pollfd fds [2]; /*массив pollfd*/ char buf[80]; /*буфеp */ int i; /*счетчик циклов*/ #define fdio 0 /*стандаpтный ввод */ #define fdio 4 /*дескpиптоp 4*/ /*ожидание чтения из дескpиптоpов (POLLIN) */ for (;;) {
fds[0].fd = fdio; fds[1].fd = fdip; fds[0].events = pollin; fds[1].events = pollin; fds[0].revents = 0; fds[1].revents = 0; /*poll() для stdin,fdip и тайм-аута */ /*втоpой паpаметp указывает на число пpосматpиваемых дескpиптоpов - вpемя тайм-аута выpажается в милли- секундах */ switch (poll(fds, 2, 60000)) { case 0: /*тайм-аут */ fprintf(stdout, " timeout \n"); exit(1); default: /*ввод с теpминала */ if (fds[0].revents == pollin) { for (i = 0; i
read(fdip, &buf[i], 1); if (buf[i] == '\n') break; } } } } } }
Мультипроцессоры
В целях увеличения вычислительных возможностей и для достижения большего параллелизма по сравнению с мультипрограммированием, предлагаемым операционными системами, на классические монопроцессорные машины с фоннеймановской архитектурой были установлены дополнительные процессоры. Подобная мультипроцессорная архитектура появилась в начале 1960 г.г. (Burroughs 6000 в 1963 г., IBM/360-67 в 1966 г., ...), гораздо раньше, чем были разработаны вычислительные сетеи. Системы, разрабо-анные для мультипроцессорных машин, называются параллельными операционными системами (Parallel Operating Systems). Мультипроцессорные машины подразделяются на два семейства:
- жестко связанные или жестко соединенные мультипроцессоры (tightly coupled), в которых процессоры связаны через общую память (рис.2.1.);
- слабо связанные или слабо соединенные мультипроцессоры (loosely coupled), в которых процессоры связаны через средство связи (как правило, шину), отличное от общей памяти (рис.2.2.).
Необходимо отметить, что эти виды архитектуры могут сочетаться между собой: каждый процессор может обладать локальной памятью и делить с остальными общую память. Кроме того, в настоящее время процессоры обладают одним или двумя уровнями кэширования...
Рис2.1. Жестко связанные мультипроцессоры
Рис 2.2.Слабо связанные процессы
Мультипроцессоры и мультимашины
В данном разделе речь пойдет об аппаратной архитектуре,на базе которой функционируют методы распределенной обработки данных - архитектуре, которую мы называем мультимашинной , чтобы отличить ее от мультипроцессорной архитектуры. Мы увидим, что это отличие не всегда легко заметно, так как технический прогресс ведет к размыванию границ.
Не-блокирующие операции
По умолчанию, операции ввода-вывода, осуществляемые над каким-либо файлом являются блокирующими. Необходимо подождать получения или записи требуемого числа байтов, либо события, указывающего на конец операции. Данную ситуацию можно изменить посредством системных вызовов fcntl () или ioctl (). Операции ввода-вывода, в этом случае, не блокируются и, если эти операции не удовлетворены, посылается сообщение об ошибке. Для определения того,как функционируют примитивы ввода-вывода в режиме отсутствия блокировки, настоятельно рекомендуется изучить документацию предприятия-изготовителя.
ПРОГРАММА 8 /*Блокиpующий ввод-вывод */
#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>
main() { int fd; /*дескpиптоp файла */ int on = 1, off = 0; /*пеpеменная для ioct1()*/ char buf[80]; /*буфеp */
/*используется stdin */ fd = 0;
/*сначала оpганизуется неблокиpующий ввод-вывод, а затем блокиpующий */
#ifdef BSD /*обpаботчик BSD */ /*неблокиpующий ввод-вывод */ fcntl(fd, F_SETFL, FNDELAY|fcntl(fd, F_GETFL, 0)); if (read(fd, buf, sizeof(buf)) < 0) perror("rien a lire"); /*блокиpующий ввод-вывод */ fcntl(fd, F_SETFL,~FNDELAY&fcntl(fd, F_GETFL, 0));
if (read(fd, buf, sizeof(buf)) < 0) perror("erreur"); /*можно также использовать ioct1 */ /*неблокиpующий ввод-вывод */ ioctl(fd, FIONBIO, &on); /*блокиpующий ввод-вывод */ ioctl(fd, FIONBIO, &off); #endif
#ifdef SYS5 /*обpаботчик System V */ /*неблокиpующий ввод-вывод */ fcntl(fd, F_SETFL, O_NDELAY|fcntl(fd, F_GETFL, 0)); if (read(fd, buf, sizeof(buf)) == 0) perror("rien a lire"); /*блокиpующий ввод-вывод */ fcntl(fd, F_SETFL,~O_NDELAY&fcntl(fd, F_GETFL, 0)); if (read(fd, buf, sizeof(buf)) < 0) perror("erreur"); #endif
exit(0); }
Отдельные вызовы сокетов блокируют программу в случае , если удаленный процесс не осуществил ожидаемую операцию: connect (), accept (), write (), read ()... Этой блокировки можно избежать, если объявить сокеты неблокирующими , посредством примитива ioctl () (флаг FIONBIO) или примитива fcntl () (флаг FNDELAY, если речь идет о системе BSD и флаг O_NDELAY в случае System V). В этом случае работа осуществляется следующим образом:
* accept () завершает работу сразу же с ошибкой EWOULDBLOCK;
* connect () завершает работу сразу же немедленно с ошибкой EINPROGRESS;
* recv () или read () или recvfrom возвращают -1 (FIONBIO или FNDELAY) или 0 (O_NDELAY) при отсутствии считываемых данных; в EWOULDBLOCK или EAGAIN выставляется ошибка. Использование неблокирующих операций целесообразно в том случае, когда нет необходимости довести ввода-вывода до конца. В этом случае, конец можно периодически проверять и осущест-влять другую обработку данных до окончания ввода-вывода.
ПРОГРАММА 29 /* неблокирующие операции на сокете клиента */
#include "soct.h" #include <sys/ioctl.h> #include <fcntl.h> #define TAILLEMAXI 1024 client(sock) int sock; /* дескриптор сокета */ { char buf[TAILLEMAXI]; /* буфер */ int i; /* счетчик цикла */ int on = 1, off = 0; /* значение для ioctl() */
/* неблокирующий сокет создается с помощью ioctl() или fcntl() */ /* ioctl(sock, FIONBIO,&on); */ fcntl(sock, F_SETFL, FNDELAY|fcntl(sock, F_GETFL, 0)); /* цикл передачи буферов */ /* обработка ошибок EWOULDBLOCK производится в процедурах
записи */ for (i = 0; i<5; i++) {
writes(sock, buf, TAILLEMAXI); } }
/* запись в сокет буфера, занимающего пос байт ; процедура корректируется, чтобы обеспечить обработку ошибок EWOULDBLOCK */ int writes(sock, pbuf, noc) register int sock; /* дескриптор сокета */ register char *pbuf; /* буфер */ register int noc; /* число байт */ { int nreste, necrit; nreste = noc; while (nreste > 0) { refecriture: necrit = write (sock, pbuf, nreste); if ( (necrit < 0) && (errno = EWOULDBLOCK)) { err_ret("EWOULDBLOCK");
/* повторение при выполнении блокирующей операции */ /* */
fcntl(sock, F_SETFL, ~FNDELAY&fcntl(sock, F_GETFL, 0)); goto refecriture; } if (necrit < 0) return(necrit); nreste -= necrit; pbuf += necrit; } return(noc-nreste); }
Не-блокирующие вызовы
Для использования функций TLI в не-блокирующем режиме можно использовать:
- t_open () с флагом O_NDELAY; - fcntl () (флаг O_NDELAY или O_NONBLOCK).
Функционируют функции следующим образом:
- t_listen () завершает работу немедленно при отсутствии запросов на соединение и посылает ошибку TNODATA;
- t_connect () завершает работу немедленно при невозможности соединения и посылает TNODATA;
- t_rcv () или t_rcvudata () возвращают -1 при отсутствии данных для считывания и посылают TNODATA;
- t_snd () возвращает -1 при невозможности записи (перепол- нение буфера) с ошибкой TFLOW.
Некоторые вспомогательные программы
Номер порта кодируется в двух байтах, адреса Internet в 4-х. Важно, чтобы система-клиент и система-сервер пришли к соглашению по поводу порядка передаваемых байтов. Для этого имеется 4 программы, преобразующие короткие или длинные целые в кодиро-ванные значения по стандартному формату. Две первых программы используются для перехода от локального формата к сетевому, а остальные две - для обратной операции.
#include <sys/types.h>
#include <netinet/in.h>
u_long htonl (hostlong) u_long hostlong;
u_short htons (hostshort) u_short hostshort;
u_long ntohl (netlong) u_long netlong;
u_short ntohs (netshort) u_short netshort;
- Операции с данными
Три программы: bzero (), bcopy () и bcmp () позволяют инициализировать символьную строку нулями, скопировать одну строку в другую и сравнить две строки, соответственно.. В данном случае, речь идет о примитивах BSD. Подобные же примитивы существуют и системах на основе System V: memset (), memcpy () и memcmp ().
В Приложении А.3 показано средство моделирования примитивов BSD посредством примитивов System V.
- Преобразования форматов адресации
Две программы позволяют перейти от адреса Internet в форме символов (состояние,в котором он находился в файле /etc/hosts) к адресу в форме 4-х байтов и наоборот.
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
u_long inet_addr (адрес) char *адрес /*адрес в ASCII-символах*/
char *inet_ntoa (inadresse) struct in_addr inadresse; /*адрес в форме целого*/
Несколько дополнительных программ
- Считывание состояния точки доступа транспортной службы t_getstate () Этот вызов возвращает текущее состояние локальной службы (например: соединение в режиме ожидания, установленное соединение...).
- Считывание и установка опций точки доступа к транспортной службе t_getinfo () Этот примитив позволяет получить информацию о параметрах работы используемых транспортных служб. t_optmgmt () Этот вызов позволяет согласовать, изменить или проконтролировать значения параметров связи между двумя процессами.
Обмен данными между прикладными программами
Два клиента могут обменяться псевдособытиями (с помощью сервера) внутри одного окна ; этот тип события нельзя отфильтровать. С помощью этого механизма можно создавать прикладные программы, которые обмениваются сообщениями (размером до 20 байт), не выводя никакой информации на экран. X Window позволяет двум прикладным программам обмениваться данными в режиме Cut-Paste с помощью механизмов, описанных в руководстве ICCCM (Inter Client Communications Conventions Manual). Преобразование формата информации может быть выполнено посылающим клиентом по запросу принимающего. Обмен осуществляется через сервер.
Обращение к ОС и вызов библиотеки
Ядро UNIX располагает определенным количеством точек входа, с помощью которых процесс может непосредственно задействовать сервисные программы ядра: эти точки входа называются системными вызовами или обращениями к системе). Эти обращения осуществляются на языке Си в форме функций, имеющихся в библиотеке UNIX. Доступ к отдельным сервисным программам обеспечивает не библиотека UNIX, а их собственные библиотеки. Стандартная библиотека Си, независимая от UNIX, играет особую роль: она позволяет осуществлять ввод-вывод с буферизацией (buffer). Например, записанные данные хранятся в буфере до его заполнения, либо до перехода к операции считывания.
Общая память и семафоры
Семафор представляет собой средство синхронизации, позволяющее управлять доступом нескольких процессов к общему ресурсу. Он содержит некое целое значение, служащее счетчиком этого ресурса. Процесс, использующий ресурс, увеличивает значение счетчика, уменьшая его затем при освобождении ресурса. Семафоры UNIX используются следующим образом:
- создать или открыть один или несколько семафоров посредством примитива semget (); - увеличить или уменьшить значение счетчика семафоров примитивом semop ();
- управлять и контролировать эти семафоры с помощью примитива semctl ().
Общая память предлагает нескольким процессам общую область адресации. Ее реализация происходит следующим образом:
- создать или открыть область общей памяти посредством примитива shmget (). При создании указывают размер области;
- связаться с областью памяти с помощью функции shmat (), сообщающей процессу адрес начала памяти;
- использовать область памяти, как если бы она была локальной для процесса;
- управлять и контролировать общую память с помощью примитива shmctl ().
Как правило, общая память и семафоры используются в сочетании, так чтобы правильно управлять доступом к общей памяти.
ПРОГРАММА 17 /*Функция "эхо", использующая pазделяемую память и семафоpы */ /*полный пpогpаммный код содеpжится в паpагpафе 3.1.*/
/*файл mem.h ****************************/ #include "commun.h" #include <sys/ipc.h> #include <sys/sem.h> #include <sys/shm.h> #define MEMKEY 7899 /*ключ, связанный с pазделяемой памя- тью */ #define SEMKEY1 9001 /*ключ, связанный с семафоpом 1 */ #define SEMKEY2 9002 /*ключ, связанный с семафоpом 2 */ #define PERM 0600 /*pазpешение на доступ */
/*файл client.c ****************************/ #include "mem.h"
clientipc() { int memid; /*идентификатоp pазделяемой памяти */ int semclient, semserveur; /*идентификатоpы семафоpов */
/*получение идентификатоpов, связанных с ключами для pазделяемой памяти*/ memid = shmget((key_t) MEMKEY, lbuf, 0); /*получение идентификатоpов, связанных с ключами для семафоpов */ semserveur = semget((key_t) SEMKEY1, 1, 0); semclient = semget((key_t) SEMKEY2, 1, 0); /*обpащение к циклу чтения-записи */ client(memid, semclient, semserveur); }
/*функция пpиема-пеpедачи */ client(memid, semclient, semserveur) int memid; /*идентификатоp pазделяемой памяти*/ int semclient; /*идентификатоp семафоpа */ int semserveur; /*идентификатоp семафоpа */ { char *pbuf; /* указатель на начало pазделяемой памяти */
/*опpеделение адpеса pазделяемой памяти */ pbuf = (char *) shmat(memid, 0, 0); /*цикл пpиема-пеpедачи буфеpов */ for (i=0; i<nbuf; i++) { /*ожидание на семафоpе клиента (освобождаемого сеpвеpом, pазpешающим клиенту писать) */ P(semclient); /*освобождение семафоpа сеpвеpа (pазpешение сеpвеpу читать*/ V(semserveur); /*пpи пpиеме сообщений клиент и сеpвеp меняются pолями */ P(semclient); V(semserveur); } /*для указания сеpвеpу на то, что он должен остановиться, в пеpвый байт буфеpа заносится 0 */ P(semclient); *pbuf = 0; V(semserveur); }
/*файл serveur.c ****************************/ #include "mem.h"
serveuripc() { int memid; /*идентификатоp памяти */ int semclient, semserveur; /*идентификатоp семафоpов */ union semun { int val; struct semid_ds *buf; ushort *array; } semctl_arg; /* стpуктуpа упpавления семафоpом */
/*создание идентификатоpов, связанных с ключом для pазделяемой памяти */ memid = shmget((key_t) MEMKEY, TAILLEMAXI, PERM|IPC_CREAT); /*создание идентификатоpов, связанных с ключами для семафоpов */ semserveur = semget((key_t) SEMKEY1, 1, PERM|IPC_CREAT); semclient = semget((key_t) SEMKEY2, 1, PERM|IPC_CREAT); /*инициализация семафоpов */ semctl_arg.val = 0; semctl(semserveur, 0, SETVAL, semctl_arg); semctl(semclient, 0, SETVAL, semctl_arg); /*обpащение к циклу чтения-записи */ serveur(memid, semclient, semserveur); /*отказ от pазделяемой памяти и семафоpов */ shmctl(memid, IPC_RMID, 0); semctl(semserveur, 1, IPC_RMID, 0); semctl(semclient, 1, IPC_RMID, 0); }
/*функция пpиема-пеpедачи */ serveur(memid, semclient, semserveur) int memid; /*идентификатоp pазделяемой памяти */ int semclient; /*идентификатоp семафоpа */ int semserveur; /*идентификатоp семафоpа */ { /*обpаботка, симметpичная по отношению к клиенту */ ............................................. /*выход, если установлен флаг окончания */ if (*pbuf == 0) { return; } }
/*файл sem.c *****************************/ #include "mem.h"
/*функция, pеализующая опеpации над семафоpами */ static void semcall(sid, op) int sid; /*идентификатоp */ int op; /*опеpация */ { struct sembuf sb; sb.sem_num=0; sb.sem_op=op; sb.sem_flg=0; semop(sid, &sb, 1); }
/*установка семафоpа */ void P(sid) int sid; /*идентификатоp */ { semcall(sid, -1); }
/*сбpос (освобождение) семафоpа */ void V(sid) int sid; /*идентификатоp */ { semcall(sid, 1); }
В то время, как все остальные внутрисистемные IPC требуют копирования внешних данных (принадлежащих, например, одному из файлов) в буфера (buffers), принадлежащие процессу (рис. 3.5.), общая память позволяет процессам непосредственно манипулировать этими данными, без необходимости их копирования (рис. 3.6.). В наибольшей степени различия между общей памятью и остальными IPC проявляются в следующем:
* в случае общей памяти, данные, считываемые процессом, сохраняются вплоть до их изменения посредством записи;
* со всеми остальными IPC, считывание действует разрушающе.
Рис 3.5. Взаимодействие с ядром: канал, именованный канал или сообщение
Рмс 3.6. Взаимодействие с ядром
Общие сведения об IPC System V
IPC System V объединяет следующие объекты:
- файлы сообщений (xxx=msg);
- семафоры (xxx=sem);
- общую память (xxx=shm).
Их реализация обладает следующими общими чертами:
- примитивы создания и открытия объекта предоставляются в форме xxxget (). Они ожидают ключа как одного из параметров вызова и посылают локальный идентификатор. Ключ представляет собой данные типа key_t, известные всем процессам (глобальное имя). Уникальность ключа обеспечивается функцией ftok (). Все остальные примитивы манипулируют объектом через его локальный идентификатор. Если проводить аналогию с файловой системой, ключ соответствует имени, а идентификатор дескриптору файла.
- примитивы управления объектом представляются в форме xxxctl (). Они позволяют изменять его характеристики или уничтожить его;
- каждый объект управляется посредством ядра UNIX.
Очереди сообщений
Сообщение представляет собой некоторое количество данных, которые могут быть посланы от одного процесса другому, в соответствии с очередностью. Сообщения характеризуются типом и длиной. Принимающий процесс может выбрать построение очереди либо по первому из имеющихся сообщений, либо по первому сообщению данного типа. Процесс может посылать сообщения,даже если никакой другой процесс не готов их принять. Посланные сообщения сохраняются после смерти процессов, до их потребления или разрушения очереди. Для использования очередей сообщений имеются следующие примитивы:
- создание или открытие очередей сообщений: msgget ();
- посылка или прием сообщений: msgsnd () и msgrcv ();
- управление или контроль очередями сообщений msgctl ().
Основным недостатком этого механизма является ограниченность размера сообщений.
ПРОГРАММА 16 /*Функция "эхо", использующая файлы сообщений */ /*полный пpогpаммный код содеpжится в паpагpафе 3.1 */
/*файл mes.h****************************/ #include "commun.h" #include <sys/ipc.h> #include <sys/msg.h> #define MKEY1 8001 /*ключ 1: клиент пишет, сеpвеp читает */ #define MKEY2 8002 /*ключ 2: клиент читает, сеp- веp пишет */ #define PERM 0600 /*pазpешение на доступ */ #define NORMAL 1 /*обычное сообщение */ #define FIN 2 /*завеpшающее сообщение */ /*файл client.c ****************************/ #include "mes.h"
clientipc() { int rid, wid; /*идентификатоpы файлов сообщений */ /*получение идентификатоpов, связанных с ключами */ wid = msgget((key_t) MKEY1, 0); rid = msgget((key_t) MKEY2, 0); /*обpащение к циклу чтения-записи */ client(rid, wid); exit(0); }
/*функция пpиема-пеpедачи */ client(rid, wid) int rid; /*идентификатоp файла чтения */ int wid; /*идентификатоp файла записи */ { struct msgbuf { long mtype; char mtext[TAILLEMAXI] } buf; /*цикл пpиема-пеpедачи буфеpов */ for (i=0; i<nbuf; i++) { buf.mtype = NORMAL; retour = msgsnd(wid, &buf, lbuf, 0);
retour = msgrcv(rid, &buf, lbuf, 0, 0); } /*посылка nbgf FIN для остановки сеpвеpа */ buf.mtype = FIN; retour = msgsnd(wid, &buf, 0, 0); }
/*файл serveur.c ****************************/ #include "mes.h"
serveuripc() { int rid, wid; /*идентификатоpы файлов сообщений */
/*создание файлов сообщений */ rid = msgget((key_t) MKEY1, PERM|IPC_CREAT); wid = msgget((key_t) MKEY2, PERM|IPC_CREAT); /*обpащение к циклу чтения-записи*/ serveur(rid, wid); /*удаление файлов сообщений, поскольку пpи завеpшении pаботы пpоцесса они не уничтожаются */ msgct1(rid, IPC_RMID, 0); msgct1(wid, IPC_RMID, 0); }
/*функция пpиема-пеpедачи */ serveur(rid, wid) int rid; /*идентификатоp файла чтения*/ int wid; /*идентификатоp файла записи*/ { /*обpаботка, симметpичная по отношению к клиенту */ .......................................... /*остановка, если оказалось, что тип=FIN */ if (buf.mtype == FIN) return; }
Окна и ресурсы
Если активируется окно, закрытое другим окном, содержимое первого надо восстановить.При этом существует две возможности: либо клиент перерисовывает изображение, либо его восстанавливает сервер. Сервер восстанавливает окно лишь в том случае,если он управляет атрибутом "backing store" и если клиент явно приказал управлять этим атрибутом (не все серверы X позволяют это делать).
Сервер управляет ресурсами (окнами,графическим контекстом ...). К этим ресурсам относятся его собственные ресурсы, а также ресурсы, произведенные его клиентами. Два клиента могут разделять ресурсы : например, работать одновременно в одном окне. Window Manager - клиент X Window - такой же, как и все остальные, и использующий те же механизмы реализации.
Операции поддержки
- Демонтирование каталога сервера #umount каталог_клиент где каталог_клиент : полное имя каталога, начиная с корневого каталога. Для выполнения команды демонтирования, каталог_клиент не должен быть занятым ("busy").
- список ресурсов, экспортируемых сервером, и монтируемых клиентами #showmnt -опции имя_сервера где - имя_сервера : имя машины сервера NFS - опции : могут принимать следующие значения :
- e : выводит список каталогов, экспортируемых сервером (содержимое файла /etc/exports) ;
- d : выводит список каталогов, смонтированных клиентами (содержимое файла /etc/rmtab) ;
- a : выводит полный список монтирований, выполненных кли- ентами, с указанием номера машины.
- Команда rpcinfo #rpcinfo -p имя_сервера Эта команда,которая будет подробно описана в главе 10, выда- ет список список зарегистрированных и функционирующих на сер- вере сервисных функций RPC. Среди них должны находиться nfs и mountd.
- Команда nfsstat #nfsstat -опции - опции могут принимать следующие значения :
- c : информация о клиенте
- s : информация о сервере
- n : информация о NFS
- r : информация о RPC
- z : выдача информации и обнуление счетчиков По умолчанию, используется значение -csnr. Эта команда выводит значения счетчиков, связанных с NFS. Значения счетчиков timeout и retrans клиента следует изучать тща тельно, так как они могут сообщить о неполадках в функциониро- вании сервера. В этом случае, следует изменить значения пара- метров монтирования в соответствии с уже данными указаниями : увеличить значение параметра timeo, уменьшить, в случае необ- ходимости, значения rsize и wsize.
Операции сопровождения
Остановка RFS
Следует ввести команду : #dorfs stop
- Отказ от экспорта ресурса (на сервере)
Следует ввести команду : #unadv имя_ресурса
- Ресурсы, используемые клиентами (на сервере)
Следует ввести команду : #rmntstat [имя_ресурса]
- Демонтирование ресурса
На машине-клиенте следует ввести команду : #umount -d имя_ресурса На сервере следует ввести команду : #fumount имя_ресурса
Команда fuser позволяет определить текущих пользователей ресурса.
Определение параметров сокета
Наиболее интересными опциями являются:
- TCP_NODELAY: запрещает хранение данных в буферах TCP;
- SO_ERROR: пересылает значение переменной so_error, определяемой в файле <sys/socketvar.h>;
- SO_KEEPALIVE: периодическая передача контрольных сообщений на сокет в режиме установления соединения.
Если один из процессов не отвечает, соединение считается прерванным и в переменной so_error возвращается ошибка ETIMEDOUT (см. оп-цию SO_ERROR); - SO_RCVBUF и SO_SNDBUF: определяет размер буферов TCP. Характеристики можно улучшить, взяв буферы большего размера;
- SO_REUSEADDR: позволяет повторно использовать уже использованный сокет-адрес (в частности, номер порта).
ПРОГРАММА 27 /* изменение размера буферов (tampon) TCP ***************/ #include "soct.h" int buffsize = 8192;
main() { int sock; /* дескриптор сокета */ int optval; /* значение опции */ int optlen; /* длина optval */
/* создание сокета */ sock = socket(AF_INET, SOCK_STREAM, 0);
/* считывание и модификация длины буфера TCP */ optlen = sizeof(optval);
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &optval, &optlen); printf("recvbuffsize %d\n", optval); getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &optval, &optlen); printf("sendbuffsize %d\n", optval); setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &buffsize, sizeof(buffsize)); setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &buffsize, sizeof(buffsize)); }
Организация
Каждая глава начинается с введения, в котором представляется ее содержание, и заканчивается резюме, где собраны основные моменты изложенного. В главе 1 представлены основные понятия систем UNIX и TCP/IP.Глава 2 описывает принципы, на базе которых строится распре- деленная обработка данных. Главы с 3по11составляют основное содержание данного пособия: в них подробно рассмотрены средства и программы построения распределенной обработки. В главе 12 рассматриваются перспективные разработки в этой области. И наконец, в главе 13 авторы попытались синтезировать ответ на вопрос: какой архитектуры требует решение конкретных проб- лем пользователя?
ОСНОВНЫЕ ЭЛЕМЕНТЫ СИСТЕМЫ UNIX
В данном разделе нами дается определение основных элементов системы UNIX.
Основные примитивы
Создание сокета
int socket (домен, тип, протокол)
int domain; /*AF_UNIX или AF_INET*/
int type; /*SOCK_STREAM или SOCK_DGRAM*/
int protocole; /*поставить ноль*/
Примитив возвращает используемый сокет-дескриптор для следующих вызовов.
- Binding (связывание)
int bind (sock, localaddr, addrlen)
int sock;/*сокет-дескриптор*/
struct sockaddr *localaddr; /*локальный сокет-адрес*/
int addrlen; /*длина адреса*/
Данный вызов позволяет связать локальный адрес с сокет-дескриптором, создаваемым socket (). Эта операция является необязательной для клиентов, использующих сокет с установлением логического (виртуального) соединения, так как адрес присваивается в момент соединения с сервером в случае, если связыва-ние не произошло. Для сокетов в режиме дейтаграмм, эта операция является необходимой только в случае, если процесс должен получить данные. Для сокетов TCP/IP возможно присвоение номеру порта нулевого значения. Система присваивает номер, который можно получить посредством примитива getsockname ().
- Соединение клиента c сервером
int connect (sock, servaddr, addrlen)
int sock; /*сокет-дескриптор*/
struct sockaddr *servaddr; /*адрес сервера*/
int addrlen; /*длина адреса*/
- установка сервера в режим "прослушивания"
int listen (sock, qlen)
int sock; /*сокет-дескриптор*/
int qlen; /*макс. число необработ. подсоединений*/
Этот примитив указывает, что сервер готов к получению запросов на соединение. Параметр qlen указывает на максимальное число запросов, которое может быть установлено в режим ожидания обработки.
- Согласие сервера на соединение
int accept (sock, addrdistant, addrlen)
int sock; /*сокет-дескриптор*/
struct sockaddr *addrdistant; /*адрес телекоммуник.*/
int *addrlen; /*длина адреса*/
Этот вызов используется сервером для ожидания запросов клиентов. Два последних параметра могут быть установлены в 0, кроме случаев, когда необходимо проверить идентичность клиента. Благодаря этому примитиву сервер дает клиенту понять, что его запрос принят. Примитив accept () возвращает новый сокет-дескриптор, который будет использован для обмена данными с клиентом. Для сервера имеется возможность создания порожденного процесса, кото-рый воспользуется вновь созданным дескриптором, в то время как порождающий процесс вновь перейдет в состояние ожидания соединения (accept ()) на сокете, открытом функцией socket ().
- Примитивы считывания и записи read(), write(), send(), recv(), sendto(), recvfrom()
Вызовы read () и write () используются также, как и для дескриптора файла. Вызовы send () и recv () имеют дополнительный аргумент, позволяющий, кроме всего прочего, посылать экспресс-данные. Вызовы send to () и recv from () используются для сокетов типа SOCK_DGRAM. Два дополнительных параметра позволяют уточ-нить адрес удаленного компьютера в случае sendto() и восстановить этот адрес в случае recvfrom().
- Закрытие соединения int close (sock) int sock; /*сокет-дескриптор*/ Перед закрытием ядро пытается переслать еще не посланные данные.
Функции подразделяются на три группы:
- функции локального управления; они позволяют пользователю транспортной службы получить или освободить точку доступа этой службы, связать адрес с точкой доступа этой службы или выполнить обратную операцию, выделить или освободить область памяти, просмотреть или изменить состояние точки доступа транспортной службы;
- функции в режиме виртуального соединения: они позволяют организовать соединение и вести обмен данными через это соединение;
- функции в режиме отсутствия соединения: они позволяют вести обмен данными без соединения.
Особенности функционирования
- Правила доступа к файлам UNIX Метод управления файлами UNIX неполностью поддерживается не- которыми из операций : например, если первый клиент откроет файл, а второй, в то же самое время, уничтожит этот файл, то запросы на чтение со стороны первого клиента не будут выпол- няться, хотя UNIX будет рассматривать этот файл как правильно открытый и доступный.
Однако, вообще говоря, правила доступа к файлам UNIX поддер- живаются механизмами, реализующими функционирование клиентов UNIX.
- Атрибуты файлов Атрибуты файлов устанавливаются клиентом NFS (дата, время, право доступа, владелец ...)
Мы советуем Вам создать одну и ту же рабочую среду для файлов, как на сервере, так и на клиенте,(в частности одну и ту же маску создания umask), если Вы хотите обеспечить соответствие между двумя системами.
- Одновременный доступ к файлу в режиме записи Два процесса, записывающие данные в один и тот же файл, рис- куют получить несогласованные результаты.
Вообще говоря, файлы, управляемые NFS, лучше использовать только в режиме чтения. Если Вы хотите использовать файл в ре- жиме записи и, при этом, управлять одновременным доступом, воспользуйтесь следящей программой lockd (см. параграф 6.5.).
- Дата и время
Часы и календарь сервера и клиента редко согласуются, а это может привести к определенным трудностям, в том,например, слу- чае, если файл, создаваемый на сервере, используется затем клиентом или наоборот. Эта проблема очень важна для тех инструментальных средств, которые использует дату и время, как make или SCCS.
Решением этой проблемы стал бы механизм синхронизации часов - такой, как rdate в SunOS. Однако, не существует механизма, общего для всех систем, хотя протокол NTP (Network Time Protocol) становится как будто все более и более популярным.
Останов удаленного процесса
Мы убедились в том, что примитивом rexec () можно остановить удаленный процесс. Этот механизм вызывает "тяжелый" останов процесса, кроме случаев, когда полученный сигнал обрабатывается особым образом. Для того, чтобы остановить сервера, можно использовать канал связи между клиентом и сервером. В следующих главах мы рассмотрим это на примерах.
Отладка программ
Отладка программ осуществляется следующим образом:
- надо сначала запрограммировать и проверить клиента и сервер на одной и той же машине, используя локальные средства и полиэкранное отображение;
- затем перенести сервер на другую машину. Кроме того, сервер можно проверить независимо от клиента, используя сервисную программу telnet с опцией номера порта; любой введенный с клавиатуры символ, в этом случае, посылается серверу.
Команда netstat -an позволяет получить информацию о состоянии сокетов, а также номера используемых портов.
Параллельный сервер
Для создания параллельного сервера достаточно создать порожденный процесс, который берет эту услугу на себя. Новый сокет-дескриптор, возвращеный функцией accept (), управляется порожденным процессом. В нижеследующих примерах мы показываем принципы программирования процесса-сервера параллельного типа, реализованного в форме демона (следящей программы). Отметим, в частности, управление сигналом SIGCHLD.
Передача информации между клиентом и сервером в UNIX
NFS встроено в ядро в виде набора следящих программ (демонов). Клиент и сервер обмениваются информацией следующим образом (рисунок 6.3.) :
- для выполнения операции монтирования, клиент NFS обращается к демону rpc.mountd, расположенному со стороны сервера, который посылает назад идентификатор (file handle), ассоциированный со смонтированным каталогом;
- для выполнения файловых операций, клиент NFS посылает идентификатор (file handle) и описание требуемой операции демону nfsd, расположенному со стороны сервера. Этот демон использует для выполнения операции функции ядра.
1- Клиент
2- Команда NFS
3- Клиент NFS
4- Следящая программа mountd
5- Сервер NFS
1: Запрос на монтирование
2: Порождение идентификатора
3: Пересылка идентификатора
4: Чтение и запись с использованием идентификатора
Пользовательский интерфейс
Пользователь манипулирует окнами и активирует прикладные программы с помощью меню,пиктограмм ... Специальный клиент - Window Manager - управляет окнами (создание,перемещение,изменение размера ...).
Понятие дисплея
X Window выводит информацию на дисплей - это понятие объединяет в себе :
- устройство отображения (монитор)
- один или несколько экранов
- клавиатуру и мышку.
Процесс визуализации можно адресовать с помощью имени дисплея - это понятие включает в себя следующие элементы :
- имя машины
- номер устройства отображения
- номер экрана устройства отображения.
Например, имя_сервера:0.0 обозначает дисплей, связанный с первым экраном первого устройства отображения машины имя_сер- вера.
Понятия потока и фильтра XDR
Поток XDR - это последовательность байтов, содержащая данные, представленные в формате XDR. Данные процесса-передатчика преобразуются функциями XDR в представление XDR и заносятся в поток. И наоборот, данные, предназначенные для процесса-приемника, считываются из потока и преобразуются в соответствии с внутренним машинным представлением. Фильтр XDR - это процедура, которая кодирует или декодирует определенный тип данных (целый,с плавающей точкой, массивы ...). Можно комбинировать уже имеющиеся фильтры для создания новых. Самый простой способ создать новый фильтр для сложного типа данных - это использование компилятора RPCGEN. Мы еще вернемся к нему. Фильтры XDR считывают и записывают данные в потоки. Примитивы XDR делятся на две группы :
- примитивы создания и обработки потоков XDR
- примитивы преобразования данных и занесения их в потоки.
- Построение сетевых адресов
Для конструирования адреса Internet по имени машины,можно ис- пользовать функцию gethostbyname (). #include <netdb.h>
struct hostent *gethostbyname (hostname) char *hostname
Функция возвращает указатель структуры hostent:
struct hostent {
char *h_name; /*имя машины*/
char **h_aliases; /*список псевдоимен*/
int h_addrtype; /*AF_INET*/
int h_length; /*4 байта*/
char **h_addr_list;
/*список адресов Internet*/
};
Эта функция находит адрес в файле /etc/hosts, или же использует сервисные программы сервера имен. Сервером имен является демон (следящая программа) UNIX, который обладает или может найти имя и адрес всех машин сети. Наиболее распространенными серверами имен являются NIS (Network Information Service), который ранее назывался YP (Yellow Pages) и BIND (Berkeley Internet Name Service), иначе называемый DNS (Domain Name Service).
ПРОГРАММА 23 /* программа, выдающая на экран адреса Internet, соответст-
вующие именам машин, указанных в качестве параметра */ #include "soct.h" #include <arpa/inet.h>
main(argc, argv) int argc; char **argv; { struct hostent *hostp; /* структура адреса */ long int i; /* счетчик цикла */
/* цикл по аргументам = именам машин */ for (i = 1; i<argc; i++) { if ( (hostp = (structhostent *) gethostbyname(argv[i])) == NULL) printf("**host %s non trouve**\n", argv[i]); else { /* считается, что у машины только один адрес Internet */ printf("host %s adresse : %s \n", hostp->h_name, inet_ntoa(*hostp->h_addr_list)); } } }
Одна из функций позволяет найти имя машины по ее адресу Internet: gethostbyaddr (). Наконец, один из примитивов позволяет найти номер порта сервисной программы, определенной в файле /etc/services: getservbyname ().
#include <netdb.h>
struct servent *getservbyname (servname, protoname) char *servname; /*имя услуги*/
char *protoname; /*TCP или UDP*/
Функция возвращает указатель на структуру servent:
struct servent {
char *s_name; /*имя услуги*/
char **s_aliases; /*список псевдоимен*/
int s_port; /*номер порта*/
char *s_proto; /*используемый протокол*/
};
ПРОГРАММА 24 /* программа, выдающая на экран номера портов, соответствующих именам служб, указанных в качестве параметра */ #include "soct.h" #include <arpa/inet.h>
main(argc, argv) int argc; char **argv; { struct servent *servp; /* структура службы */ long int i; /* счетчик цикла */ /* цикл по аргументам = именам служб */ for (i = 1; i<argc; i++) { if ( (servp = getservbyname(argv[i], "tcp")) == NULL) && (servp = getservbyname(argv[i], "udp")) == NULL) printf("**service %s non trouve**\n", argv[i]); else { /* обратите внимание на номер порта : значение считывается в формате сети ; используйте htonl() */ printf("service %s port: %d protocole: %s \n", servp->s_name, htonl(servp->s_port), servp->s_proto); } } }
- Адрес, связанный с сокетом
int getsockname (sock, адрес, addrlen) int sock; /*сокет-дескриптор*/
struct sockaddr *адрес; /*указатель адреса*/
int *addrlen; /*длина адреса*/
Этот примитив позволяет найти адрес порта, присвоенный сис- темой.
Поток в памяти
Речь идет о потоке, который позволяет кодировать данные в памяти.
Этот тип потока создается с помощью программы xdrstmem_create() :
void xdrstmem_create(xdr_handle, addr, size, op)
XDR *xdr_handle; /* handle */
char *addr; /* адрес в памяти */
int size; /* размер области памяти */
enum xdr_op op; /* XDR_ENCODE или XDR_DECODE */
Данные в формате XDR записываются или считываются из области памяти, начинающейся с адреса addr, размер которой равен size. Размер области памяти должен быть достаточно велик, чтобы вместить данные XDR ; кроме того, размер области должен быть кратен четырем - последнее требование можно обеспечить с помощью макро RNDUP. Аргумент op определяет тип операции, поскольку поток является однонаправленным.
Если выделенной памяти недостаточно для выполнения операции, операция не выполняется. Речь идет о действительно серьезной проблеме, мешающей использовать этот тип потока. Решить эту проблему можно с помощью одного из трех подходов :
- распределить память с большим запасом. Как правило, объем информации, закодированной в протоколе XDR, не превышает десятка байт
- сначала определить размер потока данных, а затем распределить память
- увеличить размер памяти в случае ошибки.
Пример работы с XDR приведен в параграфе, описывающем совместное иcпользование XDR и сокетов. Этот поток можно комбинировать с сокетами UDP в дейтаграммном режиме.
Поток записей
Этот поток позволяет разбивать данные на записи.
Этот тип потока создается с помощью программы xdrrec_create() :
void xdrrec_create(xdr_handle, sendsize, recvsize, iohandle, readproc, writeproc)
XDR *xdr_handle; /* handle */
int sendsize,recvsize; /* размер буферов */
char *iohandle; /* идентификатор */
int (*readproc)(); /* процедура чтения */
int (*writeproc)(); /* процедура записи */
Параметры sendsize и recvsize соответствуют размеру буферов приема и передачи. Фактически, этот тип потока позволяет сохранять в буферах памяти данные, передаваемые процессом-передатчиком процессу-приемнику.
Аргумент iohandle определяет ресурс, который позволяет записывать или считывать данные XDR.
Два последних аргумента - это адреса двух процедур, которые, таким образом, должны быть определены заранее. Если буфер приемника пуст, фильтр XDR вызывает процедуру readproc() для чтения данных. Если буфер передачи полон, фильтр XDR вызывает процедуру writeproc() для записи данных. Этим процедурам передается параметр iohandle. Формат процедур :
int func(iohandle,buf,nbytes)
char *iohandle; /* идентификатор */
char *buf; /* адрес буфера */
int nbytes; /* размер буфера */
iohandle может быть FILE pointer'ом или сокетом TCP или, вообще, любым объектом, позволяющим записывать данные в буфер памяти. Функция возвращает число пересланных байтов или -1 - в случае ошибки.
В отличие от других потоков, поток записей можно использовать как для кодирования, так и для декодирования. Для этого надо соответствующим образом установить значение поля x_op в handle XDR.
Помимо фильтров, для управления потоком записей используются еще три функции библиотеки XDR :
- функция xdrrec_endofrecord()
bool_t xdrrec_endofrecord(xdr_handle,sendnow)
XDR *xdr_handle; /* handle */
bool_t sendnow; /* TRUE или FALSE */
Эта функция указывает на конец записи. Если параметр sendnow установлен в TRUE, происходит принудительная запись содержимого буфера (flush). В противном случае, содержимое буфера будет передано только тогда,когда он заполнится до конца. Значение этого параметра следует установить в TRUE, если приемнику передается последний буфер с данными.
- функция xdrrec_skiprecord() bool_t xdrrec_skiprecord(xdr_handle)
XDR *xdr_handle; /* handle */
Этот примитив используется процессом-приемником. Он необходим для чтения следующей записи. На практике, этим примитивом следует пользоваться каждый раз перед переходом к чтению новой записи, в особенности перед первой попыткой чтения.
- функция xdrrec_eofrecord() bool_t xdrrec_eofrecord(xdr_handle)
XDR *xdr_handle; /* handle */
Этот примитив используется процессом-приемником для определения того, есть ли еще данные в буфере чтения.
ПРОГРАММА 47
/*Использование потока записей для файла */
/*файл fict.h */
#include <stdio.h> #include <rpc/rpc.h> #define FIC "/tmp/ficxdr" /*пpоцедуpа XDR считывания из файла */ readp(); /*файл client.c */ writep();
/*пpоцедуpа XDR записи в файл */ #include "fict.h"
main() { XDR xdrs; /*дескpиптоp XDR */ FILE *fp; /*указатель файла */ int val1=5; /*целое */ int val2=7; /*целое */ float val3=6.89; /*с плавающей точкой */ float val4=5.678; /*с плавающей точкой */
/*откpытие файла на запись */ fp = fopen(FIC, "w"); /*создание потока записей XDR */ xdrrec_create(&xdrs, 0, 0, fp, readp, writep); /*pежим записи */ xdrs.x_op - XDR_ENCODE; /*начинаем писать пеpвую запись */ xdr_int(&xdrs, &val1); xdr_int(&xdrs, &val2); /*отмечаем конец записи, не записывая содеpжимого буфеpа */ xdrrec_endofrecord(&xdrs, FALSE); /*пишем втоpую запись */ xdr_float(&xdrs, &val3); xdr_float(&xdrs, &val4); /*запись содеpжимого буфеpа */ xdrrec_endofrecord(&xdrs, TRUE); }
/*файд serveur.c */ #include "fict.h"
main()
{ XDR xdrs; /*дескpиптоp XDR */ FILE *fp; /*указатель файла */ int val1; /*целое */ int val2; /*целое */ float val3; /*с плавающей точкой */ float val4; /*с плавающей точкой */
/*откpытие файла на чтение */ fp = fopen(FIC, "r"); /*создание потока записей XDR */ xdrrec_create(&xdrs, 0, 0, fp, readp, writep); /*pежим считывания */ xdrs.x_op - XDR_DECODE; /*пеpеходим к пеpвой записи */ xdrrec_skiprecord(&xdrs); /*считываем пеpвую запись*/ xdr_int(&xdrs, &val1); xdr_int(&xdrs, &val2); /*пеpеходим ко втоpой записи */ xdrrec_skiprecord(&xdrs); /*считываем втоpую запись */ xdr_float(&xdrs, &val3); xdr_float(&xdrs, &val4); }
/*файл cdr.c */ /*содеpжит пpоцедуpы readp() и writep() #include <stdio.h>
/*пpоцедуpа считывания из файла */ readp(fp, buf, n) FILE *fp; char *buf; unsigned int n; { int nlu; nlu = fread(buf, 1, n, fp); if (nlu == 0) nlu = -1; return nlu; } /*пpоцедуpа записи в файл */ writer(fp, buf, n) FILE *fp; char *buf; unsigned int n; { int necr; necr = fwrite(buf, 1, n, fp); if (necr == 0) necr= -1; return necr; {
Потоки XDR
Существует три типа потоков XDR : потоки стандартного ввода- вывода, потоки в памяти и потоки записей.
Для управления потоками создается указатель на структуру XDR (XDR handle). Речь идет об объекте, описываемом в файле <rpc/xdr.h>. Этот указатель содержит,в частности, сведения об операциях, выполняемых над потоком XDR :
- XDR_ENCODE : кодирование данных и занесение их в поток
- XDR_DECODE : декодирование данных из потока
- XDR_FREE : освобождение памяти, распределенной под операцию декодирования (как мы увидим в параграфе "Управление памятью", существует и другой, более простой способ выпол- нить эту операцию). Если применить фильтр к потоку, определенному операцией XDR_ ENCODE, данные, перекодированные фильтром в формат XDR, будут записаны в этот поток. Если применить фильтр к потоку, определенному операцией XDR_ DECODE, данные, будут считаны из этого поток и декодированы из формата XDR во внутреннее машинное представление.
Предлагаемый сервис
Система NFS создавалась со следующими намерениями: предоставить пользователю возможность работать с файлами, расположен- ными на чужих машинах так,как если бы они находились на его собственной - не копируя их. При этом отпадает необходимость в пересылке данных что, в частности, позволяет решить проблему управления различными версиями одного и того же продукта,рас- положенными на разных машинах.
NFS разрабатывалась фирмой Sun система,предназначенная для реализации следующих целей :
- независимость от операционных систем
- простота восстановления в случае сбоя сервера
- прозрачный доступ
- сохранение для клиентов UNIX семантики UNIX
- удовлетворительная производительность
Для администратора рабочих станций UNIX,NFS выглядит как расширение локальной команды "монтирования" (mount) на файловые системы, присутствующие в сети.
На рабочих станциях Sun NFS используется для управления станциями без жестких дисков. NFS не управляет одновременным доступом нескольких пользователей к одному и тому же файлу. Это дополнительная функция, реализуемая следящей программой (демоном) LOCKD.
В случае "двоичного" файла, несмотря на прозрачность доступа возникает проблема различного представления данных на разных машинах. NFS не конвертирует данные. Вам следует использовать "форматированные" данные или применить XDR (eXternal Data Representation) или аналогичное программное средство.
RFS обеспечивает прозрачный доступ к удаленным файловым системам и устройствам (кассетным устройствам, магнитофонам, мо- демам, принтерам),а также к специальным файлам UNIX (pipe).
RFS разрабатывалась фирмой AT&T со следующими целями :
- независимость по отношению к транспортной сети ;
- сохранение семантики UNIX при обращении к файлам ;
- прозрачный доступ к файлам и периферийным устройствам ;
- несколько уровней безопасности : пароль, выбор клиентов, таблицы соответствия, стандартные права доступа UNIX ;
- удовлетворительная производительность.
RFS управляет одновременным доступом к файлам со стороны нескольких пользователей. В сети используются примитивы управ- ления замками UNIX (lockf(),fcntl()).
Каждый пользователь RFS должен принадлежать некоторой области. Область - это административная единица, которая управляет именами ресурсов, а также обеспечивает безопасность доступа к этим ресурсам. Управление именами берет на себя машина области,именуемая первичным сервером имен, которую поддерживает, в случае необходимости вторичный сервер имен. Первичный сервер имен выполняет следующие функции :
- запоминает имена и адреса всех ресурсов области ;
- в случае необходимости хранит имена и адреса других областей ;
- обеспечивает безопасность доступа к ресурсам области.
Вторичный сервер имен (можно объявить один или несколько вторичных серверов - но это не обязательно) обеспечивает функцию замены первичного сервера в случае, если последний попал в аварию.
Идентифицируя ресурс его именем, RFS позволяет администраторам станций-клиентов обращаться к ресурсам даже в том случае, если они не знают, где именно данный ресурс находится. В частности, если ресурс физически недоступен, его можно заменить другим ресурсом с тем же именем - причем это обстоятельство будет скрыто от клиентов.
Возможность разделения именованного канала позволяет исполь- зовать этот механизм для коммуникации между отделенными процессами.
XDR позволяет описывать и представлять данные способом, независящим от конкретной машины, и таким образом позволяет обмениваться ими.
Какие существуют альтернативы общему формату XDR ?
- можно передавать данные только в формате ASCII. Этот метод громоздок, значительно увеличивает объем передаваемых данных и опасен тем, что при преобразовании данных можно потерять точность.
- преобразование от случая к случаю, по меренеобходимости. При этом необходимо иметь столько программ преобразования,сколько существует форматов представления данных. На рабочих станциях UNIX очень часто используется формат IEEE, который, таким образом, может служить основой для универсального представления.
XDR - это одновременно, язык описания данных и средство их представления. XDR - не зависит не только от аппаратных структур, но и от языков программирования. Таким образом, данные, сформированные программой, написанной на Фортране и выполненной на ЭВМ Cray, могут быть считаны и обработаны программой, написанной на Си и выполненной на рабочей станции Sun и наоборот.
Предложения
Мы будем рады, если читатели смогут послать нам свои коммен- тарии, критические замечания и предложения по обычной, либо электронной почте:
Bertrand DUPOUY ENST 46, rue Barrault 75364 Paris Cedex 13 dupouy@eole.enst.fr
Michel Gabassi EDF Direction des Etudes et Recherches 1, avenue du General de Gaulle 92141 Clamart Cedex gab@cli53an.edf.fr
Предоставляемые услуги
Термин "сокет" (socket) обозначает одновременно библиотеку сетевых интерфейсов и оконечное устройство канала связи (точку связи), через которое процесс может передавать или получать данные. Эта точка связи представлена переменным целым значением, аналогичным дескриптору файла. Сокет-интерфейс представляет собой совокупность примитивов, позволяющих управлять обменом данными между процессами, независимо от того, протекают эти процессы на одной машине или нет. Сокет-библиотека маскирует интерфейс и механизмы транспортного уровня: один сокет-вызов преобразуется в несколько транспортных запросов. Сокеты позволяют осуществить доступ к сети как к файлу.
TLI представляет собой библиотеку функций, позволяющих двум удаленным программам вступить в связь между собой и обмениваться данными. Как указывает его название, TLI представляет собой интерфейс "транспортного" уровня. Можно выбирать между TLI-надстройкой над транспортными уровнями OSI и TCP/UDP. Интерфейс TLI маскирует особые механизмы, реализуемые транспортной службой. Прикладные программы, таким образом, оказываются независимыми от транспортного уровня, при условии использования механизма транспортного отбора ("network selection") и определения адресов ("name-to-address mapping"). Эти механизмы включены в UNIX System V Release 4. Использование TLI - надстройки над транспортными уровнями OSI (в режиме виртуального соединения и в режиме отсутствия соединения) позволяет воспользоваться всеми потенциальными возможностями этих стандартизованных уровней:
- согласование качества услуг;
- запрос на соединение;
- отказ в соединении;
- передача обычных и экспресс-данных;
- синхронизованное рассоединение...
Преимущества и недостатки такого подхода
Преимущество подобной библиотеки утилит заключается в легкости реализации поставляемых программ (применяемых непосредственно на Фортране). Это позволяет осуществить распределенную обработку данных, не прибегая к манипулированию сокетами и XDR. Однако, использование подобной библиотеки становится весьма проблематичным, как только появляется необходимость в переносе более сложных типов данных (структур, например). Кроме того, существует ограничение и по характеристикам: действительно, передача данных эффективна при каждом вызове процедуры из библиотеки (то есть, не существует механизма буферизации).
Применение
Как уже было сказано, использование программных продуктов SQL прозрачно для пользователя и, особенно, для разработчика. Однако, может случиться так, что ему нужно будет знать о существовании сети для оптимизации характеристик: целесообразно, например, перегруппировать запросы с целью уменьшения числа передач.
Написанные на языке Си, намеренно
Написанные на языке Си, намеренно упрощенные, примеры были проверены на машинах Sun Sparcstation (4/65 и 4/330) в системе SunOS версия 4.1. и HP 9000 375 в HP-UX версия 7.0. Обработка ошибок, возвращаемых примитивами, чаще всего опус- калась, для того, чтобы не утяжелять код. По этой же причине большая часть примеров приведена не полностью.
сокетов (см. параграф 4.3.5.) с заменой reads() и writes() на readt() и writet(), определенные в файле tli.c */ ...................... }
/* файл serveur.c #include "tli.h"
serveuripc() { int fd; /* дескриптор TLI int nfd; /* дескриптор TLI struct t_call *callptr; /* структура TLI struct t_bind *reg; /* структура TLI struct sockaddr_in serv_addr; /* адрес сервера
/* создание точки входа в транспортный уровень */ fd = t_open("/dev/tcp", O_RDWR, NULL); /* присваивание значения адресу и связывание bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_PORT); req = (struct t_bind *) t_alloc(fd, T_BIND, T_ALL); req->addr.len = sizeof(serv_addr); req->addr.maxlen = sizeof(serv_addr); req->addr.buf = (char *) &serv_addr; req->qlen = 1; t_bind(fd, req, NULL);
/* распределение структуры t_call, используемой функциями t_listen() и t_accept() */ callptr = (struct t_call *) t_alloc(fd, T_CALL, T_ADDR); /* бесконечный цикл ожидания привходящих связей for (;;) { t_listen(fd, callptr); /* получение новой точки входа в транспортный уровень, используемой для обмена */ nfd = accept_call(fd, callptr); /* обращение к службе "эхо" serveur(nfd); /* закрытие используемой точки входа t_close(nfd); } }
/* прием запроса на связь. Возвращает новую точку входа или - 1 в случае ошибки */ int accept_call(lfd, callptr) int lfd ; /* дескриптор TLI struct t_call *callptr; /* структура TLI { int nfd ; /* дескриптор TLI /* открытие новой точки входа в транспортный уровень nfd = t_open("/dev/tcp", O_RDWR, NULL); /* ей присваивается какой-нибудь адрес t_bind(nfd, NULL, NULL); /* связывание старой и новой точек входа */ /* в нашем примере (итеративный сервер) можно было бы сохранить текущий дескриптор lfd для обмена данными с клиентом */ if (t_accept(lfd, nfd, callptr) < 0) { if (t_errno == TLOOK) { /* если ошибка : связь разорвана ? */ t_rcvdis(lfd, NULL); /* тогда закрываем точку входа */ t_close(nfd); return(-1); } err_sys(" t_accept echoue"); } return(nfd); }
/* функция приема-передачи serveur(nfd) int nfd; /* дескриптор TLI */ { /* обработка, идентичная обработке для сокетов с заменой reads() и writes() на readt() и writet() */ ....................... /* результатом операции может быть отрицательное значение, если клиент разорвал связь */ if (rval < 0) { /* выход, если получено T_DISCONNECT */ if (t_look(nfd) == T_DISCONNECT) return; err_sys ("pb lecture TLI "); } }
/* файл tli.c *****************************/ /* содержит процедуры, используемые, как клиентом, так и сервером : * writet() : запись в точку входа блоками размером по 4К (как показывает тест (на Sun OS 4.1.) нельзя послать блок данных, размер которого превосходит это значение). * readt() : считывает информацию из точек входа до тех пор, пока не считает требуемое количество символов. * err_sys() : выводит на экран сообщение об ошибке и кончает работу. */ #include <stdio.h> #include <tiuser.h> /* запись буфера, состоящего из пос байт в точку входа */ int writet(fd, pbuf, noc) register int fd; /* дескриптор TLI */ register char *pbuf; /* буфер */ register int noc; /* число записываемых байт */ { /*то же, что и writes(), но write() заменяется на t_snd()*/ ........... }
/* считывание буфера, состоящего из пос байт из точки входа int readt(fd, pbuf, noc) register int fd; /* дескриптор TLI */ register char *pbuf; /* буфер */ register int noc; /* число считываемых байт */ { /* то же, что и reads() (Глава 4), но read() заменяется на t_snd() */ ........... }
/* процедура обработки ошибок */ err_sys(mes) char *mes; /* сообщение пользователя */ { t_error(mes); exit(1); } - Модификация предыдущего примера с использованием примити- вов read () и write () на сервере. На сервере создается модуль Stream tirdwr, позволяющий ис- пользовать вызовы read (), write () и close (). Эта операция осуществляется в процедуре accept_call (), вследствие этого измененной. ПРОГРАММА 41 /* модификация процедуры accept_call(), обеспечивающая возможность использования read(),write() и close() */ /* прием запроса на связь: создание дескриптора файла для этой связи. Возвращает или новый дескриптор или, в случае сбоя, -1. */ accept_call(lfd, callptr) int lfd; /* дескриптор TLI */
struct t_call *callptr; /* структура TLI */ { int nfd; /* дескриптор TLI */ /* начало такое же, как и в предыдущем примере */ ............................. /* выполняется "push" для модуля "tirwdr" для нового потока, чтобы обеспечить использование read() и write(). Надо сначала выполнить "pop" для модуля "timod" /* ioct1(nfd, I_POP, (char *)0);
ioct1(nfd, I_PUSH, "tirdwr"); return(nfd); /* возвращает новый дескриптор */ }
Принципы
Вызовы сокетов заменяются примитивами более высокого уровня, доступным, в частности, без адаптации, из программ,написанных на Фортране. Несмотря на свое сходство с доступом к файлам, сокет-интерфейс требует манипуляций со структурой данных, которые непривычны для программистов, работающих на Фортране. Обмен двоичными данными требует использования идентичного формата данных на всех машинах. Проще всего здесь использовать XDR (eXternal Data Representation), о котором мы поговорим в главе 9.
Принципы применения
При создании сокета, указывается в какой области происходит работа: UNIX (AF_UNIX), TCP/IP (AF_INET), X25, DECNET, APPLETALK... Каждой области соответствует свой тип протокола. Кроме того, задается тип, определяющий свойства коммуникации:
- SOCK_DGRAM: сообщения посылаются в форме дейтаграмм. Связанный с ним протокол связи нe является таким надежным (нарушается последовательность, возможны потери данных) в реэиме без установления логического соединения, как UDP в области AF_INET;
- SOCK_STREAM: посылаются потоки байтов, понятие "сообщения" не вводится. Используемый протокол связи надежен, с установлением виртуального соединения, как TCP в области AF_INET;
- SOCK_ RAW: обеспечивает доступ к протоколам самого низкого уровня, таким как IP в области AF_INET, либо реализует новые протоколы.
Если используются сокеты над UDP размер передаваемых данных ограничен несколькими килобайтами, от 2 Кб до 8 Кб, в зависи-мости от системы. Сокет-интерфейс можно использовать для связи между двумя процессами на одной машине. В этом случае необходимо указать, что работа производится в области AF_UNIX. Вызовы сокетов для области AF_UNIX те же, что и для области AF_INET; меняются только структуры, связанные с адресами. Данное сходство вызо-вов позволяет достаточно легко переходить от локальных задач к сетевым и обратно.
Использование в режиме с установлением виртуального соединения
Клиент :
- создает сокет;
- подсоединяется к серверу, предоставляя адрес удаленного сокета (адрес Internet сервера и номер сервисного порта). Это соединение автоматически присваивает клиенту номер порта;
- осуществляет считывание или запись на сокет;
- закрывает сокет.
Сервер:
- создает сокет;
- связывает сокет-адрес (адрес Internet и номер порта) с сервисной программой: "binding";
- переводит себя в состояние "прослушивания" входящих соединений;
- для каждого входящего соединения:
- принимает соединение (создается новый сокет с теми же характеристиками, что и исходный;
- считывает и записывает на новый сокет;
- закрывает новый сокет.
На рис. 4.4. показаны примитивы, используемые для сокетов типа SOCK_STREAM.
Рис. 4.4. Использование сокетов с установлением логического соединения.
Некоторые вызовы способные заблокировать программу : Клиент:
- connect () до того, как сервер осуществит accept ();
- write () при переполнении буфера передачи;
- read () до того, как будет получен хотя бы один символ вследствие операции записи, осуществленной сервером.
Сервер:
- accept () до того, как клиент осуществит connect ();
- read () до того, как будет получен хотя бы один символ, вследствие операции записи, осуществленной клиентом;
- write () при переполнении буфера передачи.
Библиотека TLI требует особого редактирования связей с помощью программы (-lnsl). Различные структуры и постоянные описаны во включаемом файле /usr/include/tiuser.h. Применение библиотеки различается, в зависимости от того, в каком режиме используется транспортная служба (с установлением соединения и без такового).
Присвоение номеров портов
Зарезервировано определенное число номеров; это номера в диапазоне от 1 до 1023, отведенные для стандартных служб. Для сервера номер порта либо закреплен в программном коде, либо считывается в файле /etc/services, либо присваивается системой в момент bind (). Для клиента номер порта либо зафиксирован в его коде, либо считывается из файла /etc/services, либо присваивается систе-мой в момент connect (). Если номер порта сервера хранится в /etc/services, файл /etc /services клиента должен содержать идентичный номер, что при-водит к возникновению проблемы соответствия. Можно дать системе возможность выбора номера порта для сер-вера и восстановить этот номер в процессе-клиенте. Для этого можно использовать функции popen () или rexec (). Пример такого решения мы увидим в разделе 4.3.5. Другое решение состоит в том, что в программах фиксируется номер порта, идентичный для клиента и для сервера, при условии, что этот номер не используется уже другой сервисной программой. Проверить, что был ли присвоен номер, можно справив-шись в таблице /etc/services (номера портов, установленных для определенного числа служб ) и в результате выполнения команды rpcinfo -p (номера портов, динамически присвоенных серверам RPC).
Процесс
Процесс является активным элементом, управляемым системой UNIX: программа плюс контекст. Процесс вводится в виде элемента в таблицу (usr/include/sys/proc.h), связанную с определенной структурой (U-structure), определяющей все ресурсы, используемые процессом.
Процесс "демон"
Демон (следящая программа) UNIX представляет собой процесс, работающий в фоновом режиме, он функционирует постоянно и не связан с каким-либо терминалом. Существует несколько способов создать демон :
- запустить его при запуске системы, включив его в файл /etc /rc. В этом случае, в программе необходимо создать процесс, окончание которого на ожидается, для того, чтобы не блокировать командный файл запуска;
- выполнить его с помощью файла crontab, что позволяет периодически контролировать его демоном cron;
- выполнить его в качестве фоновой задачи из shell, программно создав процесс, окончания которого не ожидается.
Для правильного кодирования демона необходимо соблюдать не- которые правила (см. пример в главе 4 "Сокеты"):
- закрыть все дескрипторы открытых файлов;
- выйти в корневой каталог дерева файловой системы;
- установить маску создания файлов; - отсоединиться от контрольного терминала, создав собственную группу процессов;
- игнорировать сигналы ввода-вывода;
- правильно управлять сигналом SIGCLD, для того, чтобы возможные порожденные процессы не оставались в состоянии "зомби".
Программа
Программа UNIX представляет собой исполняемый файл (с инструкциями).
Программные каналы (pipes)
Системный вызов pipe () создает два дескриптора файлов: один позволяет процессу-производителю отправить байты по каналу, а другой позволяет процессу-потребителю взять их (рис. 3.3.).
Рис 3.3. Считывание и запись в канале
Использование каналов аналогично использованию файлов (создание, считывание, запись...), однако считывание разрушительно (любой считываемый символ выводится из канала), а операции типа позиционирования запрещены. Этот механизм используется только между порождающим и порожденным (созданным функцией fork ()) процессами, или между двумя процессами, имеющими общего предка, что ограничивает его применение. Каналы являются однонаправленными. Если, при обмене, каждый процесс является одновременно и производителем, и потребителем (рис. 3.4.), необходимо использовать два канала.
Рис 3.4. Считывание и запись в двух каналах
Данные записываются в область памяти ограниченного размера, управляемую ядром. Считывание по каналу блокируется до тех пор, пока в него не будет помещен хотя бы один символ. Запись блокируется при переполнении канала. Процедуры read () и write (), в нижеследующем примере, поз- воляют обойти это ограничение: процесс закольцовывается по считыванию и записи до тех пор, пока не будет передано нужное число байтов.
ПРОГРАММА 13
/*Функция "эхо", использующая пpогpаммные каналы ******/ /*полный пpогpаммный код содеpжится в паpагpафе 3.1.*/
/*файл client.c ****************************/ #include "commun.h"
clientipc() { int pipe1[2]; /*дескpиптоpы pipe1 */ int pipe2[2]; /*дескpиптоpы pipe2 */ int retour; /*значение возвpата*/ char arg1 [10]; /*пpомежуточный буфеp */ char arg2 [10]; /*пpомежуточный буфеp */ /*создание двух каналов для двухстоpоннего взаимодействия */ if (pipe(pipe1) < 0 pipe(pipe2) < 0) err_sys("creation pipe"); /*с помощью fork() создается пpоцесс-сеpвеp */ switch (fork()) { case 0: /*поpожденный пpоцесс */ /*закpываются неиспользуемые дескpиптоpы */ close(pipe1[1]); close(pipe2[0]); /*выполняется пpогpамма-сеpвеp */ sprintf(arg1, "%d", pipe1[0]); sprintf(arg2, "%d", pipe2[1]); retour = exec1("serveur", "serveur", arg1, arg2, (char *) 0); default: /*поpождающий пpоцесс */ /*закpываются неиспользуемые дескpиптоpы */ close(pipe1[0]); close(pipe2[1]); /*вызов пpоцедуpы-клиента */ client(pipe2[0], pipe1[1]); close(pipe1[1]); close(pipe2[0]); /*ожидание конца выполнения поpожденного пpоцесса */ wait(&retour); exit(0); } }
/*функция пpиема-пеpедачи */ client(rfd, wfd) int rfd; /*дескpиптоp канала чтения */ int wfd; /*дескpиптоp канала записи */ { /*посылка значения длины буфеpов */ retour = writep(wfd, &lbuf, sizeof(lbuf)); /*цикл пpиема-пеpедачи буфеpов */ for (i=0; i<nbuf; i++) { retour = writep(wfd, buf, lbuf);
retour = readp(rfd, buf, lbuf); } } /*файл serveur.c ***************************/ #include "commun.h"
main(argc, argv) int argc; char **argv; { int rfd; /*дескpиптоp канала чтения*/ int wfd; /*дескpиптоp канала записи*/ /*восстановление значений, пеpеданных чеpез паpаметpы*/ rfd = atoi(argv[1]); wfd = atoi(argv[2]); /*вызов функции пpиема-пеpедачи */ serveur(rfd, wfd); /*закpытие дескpиптоpов и выход */ close(rfd); close(wfd); exit(0); }
/*функция пpиема-пеpедачи */ serveur(rfd, wfd) int rfd; /*дескpиптоp канала чтения */ int wfd; /*дескpиптоp канала записи */ { /*обpаботка, симметpичная по отношению к клиенту */ ............................................. /*если в качестве значения возвpата пpи чтении подучен 0 - это значит, что сеpвеp кончил свою pаботу */ if (retour == 0) return; }
/*файл pip.c ****************************/ /*общие для клиента и сеpвеpа пpоцедуpы, позволяющие читать и писать, не обpащая внимания на огpаничения, связанные с pазмеpом канала */
/*чтение из канала буфеpа, занимающего пос байт */ int readp(dpipe, pbuf, noc) register int dpipe; /*дескpиптоp канала */ register chr *pbuf; /*буфеp */ register int noc; /*число считываемых байт */ { int nreste, nlit; nreste = noc; while (nreste >0) { nlit = read(dpipe, pbuf, nreste); if (nlit < 0) return(nlit); else if (nlit == 0) break; nreste -= nlit; pbuf += nlit; } return(noc-nreste); }
/*запись в канал буфеpа, занимающего пос байт */ int writep(dpipe, pbuf, noc) register int dpipe; /*дескpиптоp канала */ register chr * pbuf;/*буфеp */ register int noc;/*число считываемых байт */ { int nreste, necrit; nreste = noc; while (nreste > 0) { necrit = write(dpipe, pbuf, nreste); if (necrit < 0) return(necrit); nreste -= necrit; pbuf += necrit; } return(noc-nreste); }
Стандартная библиотека ввода-вывода предлагает функцию popen (соmmande), которая создает канал между текущим процессом и порожденным процессом, созданным для выполнения программы commande. Функция popen () возвращает FILE pointer, используемый при считывании или записи, в зависимости от параметра вызова. Между двумя процессами возможны следующие диалоги:
- порожденный процесс записывает результат commande через стандартный вывод, а порожденный процесс считывает его с помощью FILE pointer.
- процесс-родитель записывает результат commande на FILE pointer, а порожденный процесс считывает его через стандартный ввод.
Программа popen () позволяет считывать и записывать данные в буферном режиме, то есть данные передаются только при заполнении буфера или при переходе к обратной операции (от считывания к записи, от записи к считыванию).
ПРОГРАММА 14 /*Функция пpиема-пеpедачи инфоpмации, связывающая клиента с сеpвеpом и использующая функцию popen() : popen () создает только один канал, что не позволяет pеализовать функцию "эхо" /*полный пpогpаммный код содеpжится в паpагpафе 3.1 */
/*файл client.c ****************************/ #include "commun.h"
clientipc() { char commande [50]; /*буфеp*/ FILE *fp;/*указатель файла, возващаемый функцией pooen() /*с помощью popen() создается пpоцесс-сеpвеp. Сеpвеpу пеpедается в качестве паpаметpа значение длины буфе- pов. Опция w указывает на то, что клиент записывает данные, котоpые впоследствии должен считать сеpвеp*/ sprintf(commande, "serveur %s", argv[2]); fp = popen(commande, "w"); /*обpащение к пpоцедуpе-клиенту */ client(fp); pclose(fp); exit(0); }
/*функция пеpедачи */ client(rwfd) FILE *rwfd; /*указатель файла вывода*/ { /*цикл пеpедачи буфеpов */ for (i=0; i<nbuf; i++) { retour = fwrite(buf, 1, lbuf, rwfd); } /*пеpедачи флага для остановки сеpвеpа */ buf[0] = 0; retour = fwrite(buf, 1, 1, rwfd); }
/*файл serveur.c ********************************/ #include "commun.h"
main(argc, argv) int argc; char ** argv; { int lbuf; /*длина буфеpов */ /*восстановление значения длины буфеpов */ lbuf = atoi(argv[1]); /*обpащение к пpоцедуpе пpиема */ serveur(lbuf); }
/*функция пpиема */ serveur(lbuf) int lbuf; /*длина буфеpов */ { /*цикл пpиема буфеpов из файла stdin */ for (;;) { ret = fread(buf, 1, lbuf, stdin); /*остановка, если получен флан окончания */ if (buf[0] == 0) exit(0); } }
FILE pointer можно связать с дескриптором файла канала посредством примитива fdopen (). Таким образом, каналы можно использовать в буферном режиме.
Считывание и запись можно сделать неблокирующим, или использовать в асинхронном режиме, как мы уже видели в главе 1.
Производительность
Для того, чтобы эксплуатировать систему было удобно, необходимо размещать серверы Х на мощных рабочих станциях, обладающих памятью соответствующего размера. Сэкономить при этом можно на терминалах Х. Что касается сети, механизмы асинхронной пересылки и хранения информации в буферах вносят свой вклад в повышение производительности системы.
Протокол
NFS использует RPC (Remote Procedure Call) для управления диалогами между клиентами и серверами и XDR (eXternal Data Representation) для обмена данными,связанными с протоколом (но не для данных пользователя !)
Что касается нижних уровней протокола, то в принципе NFS можно надстроить над TCP. Однако, все текущие версии из соображений эффективности используют UDP.
NFS - это протокол без сохранения состояния : сервер не отслеживает действия клиента. Если сервер останавливается, клиент продолжает посылать запросы до тех пор, пока сервер не будет перезапущен (речь идет о текущей возможности, сервер может остановиться и после некоторой задержки). Это позволяет очень просто перезапустить сервер NFS. С другой стороны, при этом, сервер должен обновлять файлы после каждой операции записи, чтобы свести к минимуму риск потери данных. При этом производительность сильно падает для операций записи, в отличие от операций чтения,использующих кэш. Кроме того, сервер, не сохраняющий состояние, не позволяет полностью поддерживать семантику UNIX. NFS не управляет специ- альными файлами и не способен блокировать файлы и части файлов.
1. Клиент
2. Обращение к системе
3. Интерфейс VFS
4. Система локальныхфайлов UNIX
4. Клиент NFS
5. RPC и XDR
6. Сервер
7. NFS сервер
8. Сеть
9. Данные
RFS разрабатывался так, чтобы быть независимым от транспортного протокола, при условии, что транспорт происходит в режиме коммутации пакетов (например, TCP). Внутри RFS использует механизм STREAMS. RFS использует протокол с сохранением состояния : сервер запоминает то,что сделал клиент. Благодаря этой возможности RFS полностью поддерживает семантику файловой системы UNIX и в частности возможность управления периферийными устройствами, а также, множественным доступом к файлам и записям (замки на фай- лах и записях). В файле (/usr/nserve/fmaster) сохраняется имя первичного и вторичного серверов имен каждого сервера и каждого клиента. Это позволяет избежать механизма "широковещания" Ethernet.
Сократить пересылку информации в сети позволяют механизмы кэширования.
Рис. 7.2. - Внутренние механизмы RFS.
1 - клиент
2 - Обращение к системе
3 - FFS (File System Switch - Переключатель файловой системы)
4 - Локальная файловая система UNIX
5 - Клиент RFS
6 - Модули STREAMS
7 - Сервер
8 - сервер RFS
9 - периферийное устройство
10 - Сеть
11 - Данные