QNX/UNIX: Анатомия параллелизма - Олег Цилюрик 21 стр.


sigprocmask(SIG_BLOCK, &sig, NULL);

cout << "Process " << getpid() << ", waiting for signal " << SIGNUM << endl;

// установка обработчика (для дочерних потоков)

struct sigaction act;

act.sa_mask = sig;

act.sa_sigaction = handler;

act.sa_flags = SA_SIGINFO;

if (sigaction(SIGNUM, &act, NULL) < 0) perror("set signal handler: ");

const int thrnum = 3;

for (int i = 0; i < thrnum; i++) {

threcord threc = { 0, false };

pthread_create(&threc.tid, NULL, threadfunc, (void*)i);

tharray.push_back(three);

}

pause();

// сюда мы попадаем после ^C для завершающих операций...

tharray.erase(tharray.begin(), tharray.end());

cout << "Clean vector" << endl;

}

Это приложение, в отличие от предыдущих, построено уже с использованием специфики С++, в нем используется контейнерный класс vector из библиотеки STL (Standard Template Library). Может быть множество вариаций на подобную тему. Приведенное нами приложение (как одна из вариаций) только подтверждает, что принятая в QNX модель достаточна для описания самых неожиданных потребностей. Логика работы приложения крайне проста: получая сигнал, поток блокирует повторную реакцию на этот сигнал, после чего возбуждает дубликат полученного сигнала от своего имени.

Примечание

Показанное приложение в значительной степени искусственно и неэффективно. Мы приводим его здесь не как образец того, "как нужно делать", а только как иллюстрацию гибкости возможностей, предоставляемых в области параллельного программирования. При некоторой изобретательности можно заставить программу вести себя согласно вашим капризам, какими бы изощренными они ни оказались.

Запускаем полученное приложение:

# s10

Process 2089006, waiting for signal 41

После чего с другого терминала пошлем приложению ожидаемый им сигнал, например командой:

# kill -41 2089006

Посылаем этот сигнал несколько раз (в данном случае 3) и получаем вывод от приложения:

SIG = 41; TID = 4

SIG = 41; TID = 2

SIG = 41; TID = 3

SIG = 41; TID = 3

SIG = 41; TID = 4

SIG = 41; TID = 2

SIG = 41; TID = 2

SIG = 41; TID = 3

SIG = 41; TID = 4

^C

Clean vector

Видно, что реакция на каждый сигнал возбуждается несколько раз (по числу потоков), каждый раз выполняясь в контексте разного потока (TID). Интересно и изменение порядка активизации потоков от сигнала к сигналу, то есть потоки в очереди ожидающих "перетасовываются" при поступлении каждого сигнала.

Примечание

В приложение добавлена реакция на ^C (сигнал SIGINT):

• начиная с некоторой сложности приложений, их завершению должна обязательно предшествовать некоторая последовательность действий; в данном случае мы условно показываем очистку вектора состояний потоков;

• реакция на SIGINT выполнена в "ненадежной" манере в смешении с моделью очереди сигналов для SIGRTMIN, что показывает возможность смешанного применения всех моделей в рамках одного приложения; все определяется требованиями и вопросами удобства.

Как мы уже видели, тот факт, что обработчик сигнала выполняется в контексте потока, который разблокировал реакцию на этот сигнал (независимо от того, в момент выполнения какого потока приходит сигнал), позволяет реализовать в обработчике сигнала обработку любой сложности в интересах этого потока. Для этого лишь требуется разместить все области данных, запрашиваемые в этой обработке, не в стеке потока (объявленные как локальные переменные потоковой функции), а в области собственных данных потока, которые мы детально рассмотрели ранее. Схематично это можно показать в коде так:

• Положим, нам нужно уведомлять о некоторых событиях N потоков.

Будем использовать для этого сигналы SIGRTMINSIGRTMIN + (N - 1):

for (int i = SIGRTMIN, i < SIGRTMIN + N; i++) {

pthread_create(NULL, NULL, threadfunc, (void*)(i));

}

• При запуске N потоков (из главного потока) потоковые функции, помимо устанавливания своих индивидуальных сигнальных масок (в точности так, как это показано выше в листинге "Чередование потоковых сигналов"), размещают экземпляры собственных потоковых данных:

class DataBlock {

~DataBlock(void) {...}

};

static pthread_key_t key;

static pthread_once_t once = PTHREAD_ONCE_INIT;

static void destructor(void* db) { delete (DataBlock*)db; }

static void once_creator(void) {

pthread_key_create(&key, destructor);

}

void* threadfunc(void* data) {

// надлежащим образом маскируем сигналы

// ...

// это произойдет только в первом потоке из N

pthread_once(&once, once_creator);

DataBlock* pdb = new DataBlock(...);

pthread_setspecific(key, pdb);

// Теперь поток может пользоваться данными *pdb, как и локальными!

// цикл ожидания приходящих сигналов:

while (true) pause();

}

Все потоки используют один и тот же обработчик для всех сигналов; он выполняет одни и те же действия, но над различными объектами данных. Над каким объектом данных выполнить действие, обработчик "узнает" из контекста потока, в котором он выполняется:

static void handler(int signo, siginfo_t* info, void* context) {

DataBlock* pdb = (DataBlock*)pthread_getspecific(key);

// выполняем действия для своего потока ...

}

• Теперь, например из главного потока процесса (главный поток выбран для простоты - источником сигнала может быть произвольный поток, даже не этого процесса), требуемое действие вызывается возбуждением соответствующего сигнала:

sigqueue(getpid(), SIGRTMIN + K, val);

Это только скелетная схема, но на ее основе можно строить развитые протоколы обработки данных (пример взят из работоспособного приложения).

За пределы POSIX: сигналы в сети

А теперь, "на закуску", посмотрим справочную информацию по системной команде kill (послать сигнал). Вы, должно быть, помните, что в QNX есть дополнительная возможность получить справку по любой команде системы, используя команду # use <имя-команды>. Более того, вы можете и в любое свое приложение встроить возможность получения интерактивной справки. Как это происходит, описано в [4]. Итак:

# use kill

kill - terminate or signal processes (POSIX)

kill [-signal_name|-signal_number] pid ...

kill -l

Options:

-signal_name Symbolic name of signal to send

-signal_number Integer representing a signal type

-l List symbolic signal names

-n node Kill processes on the specified node.

(/bin/kill only)

Where:

Valid signal names are:

SIGNULL SIGHUP SIGINT SIGQUIT SIGILL SIGTRAP

SIGIOT SIGABRT SIGEMT SIGFPE SIGKILL SIGBUS

SIGSEGV SIGSYS SIGPIPE SIGALRM SIGTERM SIGUSR1

SIGUSR2 SIGCHLD SIGPWR SIGWINCH SIGURG SIGPOLL SIGSTOP SIGTSTP

SIGCONT SIGVTALARM SIGTTIN SIGTTOU

Note:

kill is also available as a shell builtin

Здесь нас ожидает сюрприз, который мы выделили в показанном фрагменте жирным шрифтом. И говорит эта строка о том, что в системе QNX сигнал может посылаться процессу, работающему на любом узле сети QNET. И это совершенно естественно, если вспомнить промелькнувшее выше замечание из технической документации, что сигнал в QNX - это пульс, то есть один из видов сообщений микроядра.

Таким образом, системная команда QNX kill (именно системная - /bin/kill, в отличие от встроенной формы kill командных интерпретаторов, которые строго следуют традициям UNIX, как и предупреждает выделенная нами строка) имеет возможность посылать сигналы любому процессу в сети. Тем не менее при рассмотрении прототипов вызовов kill() и sigqueue() мы не находим и следа параметра, предоставляющего возможность определить удаленный процесс. Тогда каким образом это делает команда kill? Совершенно верно: используя вызов native QNX API, который выглядит так (этот вызов, как и многие другие, имеет две формы, вторая из которых является безопасной в много- поточной среде):

#include <sys/neutrino.h>

int SignalKill(uint32_t nd, pid_t pid,

int tid, int signo, int code, int value);

int SignalKill_r(uint32_t nd, pid_t pid, int tid, int signo,

int code int value);

где nd - дескриптор сетевого узла QNET, на котором будут разыскиваться pid и tid. Для посылки сигнала локальному процессу (потоку) можно для nd указать 0, но лучше - определенную системой константу ND_LOCAL_NODE.

Примечание

Дескриптор узла в сети QNET - понятие относительное; он может быть получен, например, вызовом netmgr_strtond(). Но и здесь не все так просто:

• Дескриптор, соответствующий, скажем, узлу "host", полученный на узле "А", может иметь значение N, но дескриптор того же узла, полученный на узле "В", будет иметь уже значение M, то есть дескриптор узла - это "дескриптор сетевого узла X, как он видится с сетевого узла Y".

• Тот же дескриптор узла "host" может быть определен как имеющий значение N, но уже через несколько секунд он может "сменить" свое значение на M, то есть значения, полученные netmgr_strtond(), должны использоваться немедленно...

Эти и другие сложности относятся к особенностям программного использования QNET и требуют отдельного обстоятельного обсуждения. Однако они не являются предметом нашего текущего рассмотрения.

pid - PID процесса, которому направляется сигнал, pid может иметь и отрицательное значение, при этом положительное значение (-pid) идентифицирует группу процессов EGID, и сигнал будет отправлен всем процессам группы. При нулевом значении pid сигнал будет отправлен всем процессам группы процесса отправителя.

tid - 0 или TID потока, которому направляется сигнал. При указании tid сигнал будет доставляться только указанному потоку, а при tid = 0 - всем потокам процесса. Дальнейшая судьба сигнала в обоих случаях зависит от маскирования сигнала в потоке, как мы рассматривали ранее.

signo - номер сигнала (с ним неоднократно встречались выше).

code и value - код и значение, ассоциированные с сигналом (их мы тоже встречали при рассмотрении модели сигналов реального времени).

Как и обычно, внешнее различие (для программиста) основной формы SignalKill() и формы, безопасной в многопоточной среде, SignalKill_r() состоит в том, что:

SignalKill() возвращает -1 в случае ошибки, а код ошибки заносится в errno; любой другой возврат является индикатором успешного выполнения;

SignalKill_r() возвращает EOK в случае успеха, а в случае ошибки возвращается отрицательный код ошибки (тот же, который основная форма заносит в errno, но со знаком минус).

Возможны следующие коды ошибок, возвращаемые этими вызовами:

EINVAL - недопустимое числовое значение signo;

ESRCH - несуществующий адресат (pid или tid);

EPERM - процесс не имеет достаточных прав для посылки сигнала;

EAGAIN - недостаточно ресурсов ядра для выполнения запроса.

Для того чтобы получить работающий пример использования этой возможности, возьмите любой из приводившихся выше примеров, разнесите процессы по сетевым узлам и определите "целеуказание" в процессе-отправителе.

Простейшим примером и демонстрацией удаленной реакции в сети может быть следующая последовательность действий:

• Производим запуск задачи на удаленном узле, например:

# on -f <host> raqc

• После чего, выполнив ряд операций в запущенной программе, прекращаем ее работу по [Ctrl+C] с локального терминала.

Интересно оценить далеко идущие последствия этого "маленького" расширения стандартной POSIX-схемы работы с сигналами:

• На технике "сетевых сигналов" может быть построена целая система уведомлений сетевых составляющих компонент единой программной прикладной системы.

• Именно "уведомлений" (но не синхронизации с наследованием приоритетов, влияющей на общую систему диспетчеризации составляющих частей и т.п.): посылка сигнала является неблокирующей операцией (не требует ответа), а прием сигнала не сопровождается наследованием (или любым изменением) приоритетов.

• Такое "сигнальное" взаимодействие, записанное в формальной POSIX-семантике (но, по сути, осуществляющее механизмы, далеко выходящие за POSIX), может оказаться гораздо проще в записи и понимании, чем при использовании низкоуровневых механизмов обмена сообщениями (пульсами).

4. Примитивы синхронизации

ОС QNX Neutrino предоставляет широкий набор элементов синхронизации выполнения потоков, как в рамках одного процесса, так и разных. Это практически полный спектр примитивов, описываемых как базовым стандартом POSIX, так и всеми его расширениями реального времени. Тем не менее при работе со всеми этими примитивами не покидает ощущение, что некоторые из них являются органичными для самой ОС (мьютекс, условная переменная), в то время как другие - достаточно громоздкая надстройка над базовыми механизмами, реализуемая, главным образом, в угоду POSIX.

Примечание

К сожалению, и техническая документация QNX [8], и фундаментальная книга Р. Кертена [1] написаны по одной схеме: все, что касается примитивов синхронизации, введенных более поздними расширениями POSIX (барьеры, жесткая блокировка (sleepon), спинлок, блокировки чтения-записи), описывается детально и сопровождается обстоятельными примерами кода, а вот базовые понятия, такие как pthread_mutex_t, sem_t (да и pthread_cond_t, по существу), описаны лишь качественно, "на пальцах", в иллюстративных рассказах об алгоритме пользования ванной комнатой и кухней (термин bathroom встречается намного чаще, чем pthread_mutex_t). Мы попытаемся по возможности компенсировать этот перекос.

Хотелось бы обратить внимание на интересный факт. В POSIX-варианте API QNX представлен большой набор разнообразных средств синхронизации: мьютексы, условные переменные, семафоры, барьеры, блокировки чтения/записи, ждущие блокировки, спинлоки. Однако в родном native API QNX из всего этого многообразия мы видим всего три элемента синхронизации: мьютекс, семафор и условная переменная. И это при том, что условная переменная не является самостоятельным средством синхронизации и применяется как расширение функциональных возможностей мьютекса!

Это означает, что все многообразие средств синхронизации, предоставляемое в POSIX API QNX, строится исключительно с применением этих минимальных средств синхронизации. Действительно, анализ заголовочных файлов системы показывает, что все дополнительные средства синхронизации, появляющиеся в POSIX API, строятся с использованием только мьютекса и условной переменной. Можно сказать, что пара мыотекс-условная переменная с одной стороны и семафор с другой являются независимыми базисами, каждый из которых позволяет построить практически любой, сколь угодно специфический элемент синхронизации, удовлетворяющий потребностям, которые могут возникнуть при построении вашей системы. Мы проиллюстрируем эту мысль позже, при рассмотрении особенностей применения мьютексов.

Назад Дальше