QNX/UNIX: Анатомия параллелизма - Цилюрик Олег Иванович
В этом приложении предпринята попытка обрисовать специфику, характерную для организации обмена сообщениями в QNX 6, особенно проявляющуюся при межузловом обмене, и поделиться практическими решениями, учитывающими эту специфику.
Организация обмена сообщениями на основе «семейных» процессов
Рассмотрим, как можно организовать обмен сообщениями между потоками, принадлежащими процессам, связанным «родственными узами». Для простоты изложения, чтобы в дальнейшем не формулировать «поток, принадлежащий процессу», будем рассматривать однопоточные процессы и говорить (в традициях QNX 4) «обмен сообщениями между процессами».
Итак, пусть некий родительский процесспорождает на другом узле дочерний процесс. Под порождением будем подразумевать «запуск с узла», то есть запуск процесса, выполняемый утилитой onс опцией -f. Для порождения используем функцию spawn():
char* args[] = { "/net/904-3/home/ZZZ/bin/TestChild", NULL};
...
spawn("/home/ZZZ/bin/TestChild", 0, NULL, &InhProc, args, NULL);
Рассмотрим вначале проблемы, стоящие перед дочерним процессом при его желании связаться с родительским. Как уже указывалось, для создания соединения ему необходимо «знать» триаду соединения.
Пусть процессу-клиенту известно символьное имя узла, на котором функционирует искомый адресат. (При описываемой здесь «семейной» архитектуре разрабатываемой системы символьные имена обычно жестко определены и поэтому зачастую просто записываются в конфигурационном файле, откуда процессы всегда могут получить символьное имя нужного узла.) Для того чтобы получить дескриптор узла по известному имени, можно вызвать функцию netmgr_strtond(), которая преобразует имя узла в его дескриптор.
Однако вызов этой функции дочерним процессом приводит к неожиданному (по крайней мере, так было со мной...) на первый взгляд результату: функция возвращает дескриптор узла «с точки зрения» родительского процесса! Иными словами, нулевой дескриптор приписывается не узлу, на котором «живет» дочерний процесс, а узлу с родительским процессом. Другие дескрипторы тоже соотносятся с именами узлов так, как это выполняется на узле с родителем — порожденный процесс, унаследовав от родителя текущую рабочую и корневую директории, тем самым остался, если можно так выразиться, душой на своей исторической родине.
В [4] (глава Дмитрия Алексеева «Утилита on») этот вопрос был достаточно хорошо освещен и было сказано, что для решения проблемы следует перед порождением процесса вызвать функцию chroot()с именем узла, на котором процесс будет порожден (и при необходимости «обратным» вызовом chroot()с именем узла процесса-родителя после вызова spawn()). Это позволяет порожденному процессу обрести новую корневую директорию на том узле, где он выполняется. И тогда вызовы netmgr_strtond()будут возвращать дескрипторы узлов именно с точки зрения того узла, на котором функционирует порожденный процесс.
Далее, если дочерний процесс запущен на удаленном узле, то вызванная им функция getppid()в качестве родительского процесса возвращает совсем даже не идентификатор «фактического» родителя, а идентификатор процесса io-net, что, может быть, формально и верно, но по существу это издевательство (особенно для «мигрантов» с QNX 4, где они получали идентификатор виртуального канала и обращались с ним как с обычным идентификатором процесса). Итак, для того чтобы порожденный процесс знал свое «отчество», проще всего родителю передать свой идентификатор дочернему процессу при его рождении в параметре списка argv[].
Я бы рекомендовал также не уповать на то, что дескрипторы каналов, создаваемые процессами-адресатами отправляемых сообщений, всегда будут равны 1. По ряду причин это далеко не всегда так. И для надежности лучше просто передавать этот дескриптор в аргументах при порождении процесса.
Таким образом, родительский процесс при порождении дочернего процесса должен передать ему в списке аргументов свой идентификатор и дескриптор созданного канала и косвенно, посредством вызова chroot(), имя узла, на котором дочерний процесс запускается. После этого порожденный процесс способен правильно собрать триаду, необходимую для выполнения MsgSend().
Теперь обсудим проблемы, стоящие перед родительским процессом. Если мы хотим отсылать сообщения с родительского процесса на порожденный, то два из трех членов триады родительский процесс может легко получить: дескриптор узла — с помощью функции netmgr_strtond(), а идентификатор порожденного процесса возвращается функцией spawn(). Но вот с дескриптором канала опять появляется риск «не угадать». Кроме того, если родитель породит дочерний процесс и немедленно после этого попытается подсоединиться к каналу, который должен создать этот процесс, то, вероятнее всего, функция ConnectAttach()вернет -1, поскольку порожденный процесс еще не успеет к тому времени создать канал. Значит, понадобится цикл на N попыток с паузой в ожидании открытия.
Не проще ли тогда просто выполнить синхронизацию? То есть родительскому процессу дождаться сообщения от дочернего процесса, которое будет означать, что порожденный процесс выполнил все необходимые действия по своему развертыванию и в частности создал канал. И теперь совершенно естественно передать в этом синхронизирующем сообщении дескриптор созданного канала. После принятия сообщения родительский процесс имеет все необходимые ему данные для выполнения функции отсылки сообщения MsgSend().
При подобной иерархической структуре системы по типу «родитель-ребенок» общение между порожденными процессами, если таковое требуется, обеспечивается с помощью родительского процесса. Породив один из процессов и получив от него дескриптор канала, родительский процесс может при порождении еще одного процесса передать ему полную триаду «старшего» дочернего процесса, позволяющую новому процессу установить с ним соединение.
Ниже приводится образец кода, реализующего этот подход. Обратите внимание на значение аргумента index, задаваемое в вызовах функции ConnectAttach()равным _NTO_SIDE_CHANNEL. В примерах из [1], книги, безусловно, основополагающей для любого программиста под QNX 6, для упрощения изложения это значение устанавливается в 0. Однако значение, равное _NTO_SIDE_CHANNEL, гарантирует, что возвращаемое функцией значение идентификатора соединения будет взято не из того же пространства, из которого выделяются файловые дескрипторы; в противном случае возникают проблемы, достаточно определенно обрисованные в описании функции ConnectAttach(), приведенном в технической документации QNX.
Пример кода родительского процесса
#include <stdio.h>
#include <stdlib.h>
#include <string.h>