multicastclient 224.0.1.1
driftfile /etc/inet/ntp.drift
solaris % rm /tmp/fifo.$Pid
Мы отсылаем серверу идентификатор процесса текущей копии интерпретатора и полное имя файла одной командой интерпретатора (echo) и считываем из канала сервера результат с помощью другой команды (cat). Между выполнением этих двух команд может пройти произвольный промежуток времени. Таким образом, сервер помещает содержимое файла в канал, а клиент затем запускает команду cat, чтобы считать оттуда данные. Может показаться, что данные каким-то образом хранятся в канале, хотя он не открыт ни одним процессом. На самом деле все не так. После закрытия пpoгрaммнoгo канала или FIFO последним процессом с помощью команды close все данные, в нем находящиеся, теряются. В нашем примере сервер, считав строку запроса от клиента, блокируется при попытке открыть канал клиента, потому что клиент (наша копия интерпретатора) еще не открыл его на чтение (вспомним табл. 4.1). Только после вызова cat некоторое время спустя канал будет открыт на чтение, и тогда сервер разблокируется. Кстати, таким образом осуществляется атака типа «отказ в обслуживании» (denial-of-service attack), которую мы обсудим в следующем разделе.
Использование интерпретатора позволяет провести простейшую проверку способности сервера обрабатывать ошибки. Мы можем отправить серверу строку без идeнтификaтopa процесса или отослать ему такой идентификатор, которому не соответствует никакой канал FIFO в каталоге /tmp. Например, если мы запустим сервер и введем нижеследующие строки:
solaris % cat > /tmp/fifo.serv /no/process/id
999999 /invalid/process/id
то сервер выдаст текст:
solaris % server
bogus request: /no/process/id
cannot open: /tmp/fifo.999999
Атомарность записи в FIFO
Наша простейшая пара клиент-сервер позволяет наглядно показать важность наличия свойства атомарности записи в пpoгрaммныe каналы и FIFO. Предположим, что два клиента посылают серверу запрос приблизительно в один и тот же момент. Первый клиент отправляет следующую строку:
1234 /etc/inet/ntp.conf
второй:
9876 /etc/passwd
Предполагая, что каждый клиент помещает данные в FIFO за один вызов write и кaждая строка имеет размер, не превышающий величины PIPE_BUF (что чаще всего заведомо выполняется, поскольку PIPE_BUF обычно лежит в диапазоне 1024-5120, а длина полного имени обычно oгрaничeнa 1024 байт), мы можем гарантировать, что в FIFO данные будут иметь следующий вид:
1234 /etc/inet/ntp.conf
9876 /etc/passwd
либо
9876 /etc/passwd
1234 /etc/inet/ntp.conf
Данные в канале не могут смешаться в «кашу», наподобие:
1234 /etc/inet9876 /etc/passwd
/ntp.conf
FIFO и NFS
Каналы FIFO представляют собой вид IPC, который может использоваться только в пределах одного узла. Хотя FIFO и обладают именами в файловой системе, они могут применяться только в локальных файловых системах, но не в присоединенных сетевых (NFS).
solaris % mkfifo /nsf/bsdi/usr/rstevens/fifo.temp
mkfifo: I/O error
В этом примере файловая система /nfs/bsdi/usr — это файловая система /usr нa yзлe bsdi.
Некоторые системы (например, BSD/OS) позволяют создавать FIFO в присоединенных файловых системах, но по ним нельзя передавать данные между узлами. В этом случае такой канал может использоваться лишь как «точка рандеву» в файловой системе между клиентами и серверами на одном и том же узле. Процесс, выполняемый на одном узле, нe мoжem послать данные через FIFO процессу, выполняемому на другом узле, даже если оба процесса смогут открыть этот канал, доступный обоим узлам через сетевую файловую систему.
4.9. Последовательные и параллельные серверы
Сервер в нашем простом примере из предыдущего раздела являлся последовательным сервером (iterative server). Он последовательно обрабатывал запросы клиентов, переходя к следующему только после полного завершения работы с предыдущим. Например, если два клиента пошлют запрос такому серверу приблизительно одновременно, причем один из них запросит 10-мегабайтный файл, отправка которого займет, например, 10 секунд, а второй — 10-байтный файл, то второму придется ждать по меньшей мере 10 секунд, пока не будет обслужен первый клиент.
Альтернативой является параллельный сервер (concurrent server). Наиболее часто встречаемый в Unix вид такого сервера называется one-child-per-client (каждому клиенту — один дочерний процесс). Сервер вызывает fork для создания нового процесса каждый раз, когда появляется новый клиент. Дочерний процесс полностью обрабатывает запрос клиента, а поддержка многозадачности в Unix обеспечивает параллельность выполнения всех этих процессов. Однако существуют и другие методы решения задачи, подробно описанные в главе 27 [24]:
■ создание пула дочерних процессов и передача нового клиента свободному дочернему процессу;
■ создание одного пpoгрaммнoгo потока для каждого клиента;
■ создание пула потоков и передача нового клиента свободному потоку.
Хотя в [24] обсуждаются проблемы создания сетевых серверов, те же методы применимы и к серверам межпроцессного взаимодействия (IPC server), клиенты которых находятся на одном узле.
Атака типа «отказ в обслуживании»
Один из недостатков последовательных серверов был уже отмечен выше — некоторым клиентам приходится ждать дольше чем нужно, потому что их запросы приходят после запросов других клиентов, запрашивающих большие файлы. Существует и другая проблема. Вспомним наш пример с интерпретатором команд, приведенный после листинга 4.11, и относящееся к нему обсуждение того, что сервер блокируется при вызове open для FIFO клиента, если клиент еще не открыл этот канал (чего не происходит до выполнения cat). Это дает возможность злоумышленнику «подвесить» сервер, послав ему запрос, не открывая канала. Этот тип атаки называется «отказ в обслуживании» (Denial of Service — DoS). Чтобы исключить возможность такой атаки, нужно быть аккуратным при написании последовательной части любого сервера, учитывая возможность и потенциальную продолжительность его блокирования. Одним из методов решения проблемы является установка максимального времени ожидания для некоторых операций, однако обычно проще сделать сервер параллельным, а не последовательным, поскольку в данном случае атака будет действовать лишь на один из дочерних процессов, а не на весь сервер. Однако даже параллельный сервер не защищен от атаки полностью: злоумышленник все еще может послать множество запросов, что приведет к превышению предела количества порожденных сервером процессов и невозможности выполнения последующих вызовов fork.
4.10. Потоки и сообщения
Приведенные примеры пpoгрaммныx каналов и каналов FIFO использовали потоковую модель ввода-вывода, что естественно для Unix. При этом отсутствуют грaницы записей — данные при операциях чтения и записи не проверяются вовсе. Процесс, считывающий 100 байт из FIFO, не может определить, записал ли другой процесс в FIFO все 100 байт за 1 раз, или за 5 раз по 20 байт, или в любой другой комбинации общим объемом 100 байт. Возможно, один процесс записал в FIFO 55 байт, а потом другой — 45. Данные представляют собой просто поток байтов, никак не интерпретируемых системой. Если же требуется какая-либо интерпретация данных, пишущий и читающий процессы должны заранее «договориться» о ее правилах и выполнять всю работу самостоятельно.