Текст книги "QNX/UNIX: Анатомия параллелизма"
Автор книги: Олег Цилюрик
Соавторы: Владимир Зайцев,Егор Горошко
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 10 (всего у книги 23 страниц) [доступный отрывок для чтения: 10 страниц]
Традиционная обработка сигнала
В этой части изложения мы рассмотрим традиционные модели перехвата сигналов и установки для них собственных обработчиков (в том числе и игнорирование или восстановление стандартной обработки по умолчанию). Термин «традиционный» здесь означает, что мы бегло рассмотрим обработку сигналов применительно к процессам и стандартным сигналам UNIX (не сигналам реального времени), то есть в том изложении, как она традиционно рассматривается в литературе по UNIX (и здесь сигнал воспринимается, конечно же, единственным потоком приложения, а не процессом, но в этом случае различие не принципиально). Позже мы рассмотрим модель обработки сигналов реального времени и расширим ее на многопоточные приложения.
«Старая» модель обработки сигналаВ ранних версиях UNIX была принята единственная модель обработки сигналов, основанная на функции signal(), которая подразумевает семантику так называемых «ненадежных сигналов», принятую в этих ОС. Позже эта модель была подвержена радикальной критике, вскрывшей ее «ненадежность». Данная модель сохранена для совместимости с ранее разработанным программным обеспечением. Она обладает существенными недостатками, основными из которых являются:
• процесс не может заблокировать сигнал, то есть отложить получение сигнала на период выполнения критических участков кода;
• каждый раз при получении сигнала его диспозиция устанавливается на действие по умолчанию, и при необходимости продолжить обработку поступающих сигналов требуется повторно восстанавливать требуемый обработчик.
Вот пример ( файл s2.cc) использования этой модели в коде, который уже стал иллюстративным образцом и кочует из одного источника в другой:
Ненадежная модель реакции на сигнал
#include
#include
#include
// обработчик сигнала SIGINT
static void handler(int signo) {
// восстановить обработчик:
signal(SIGINT, handler);
cout << «Получен сигнал SYSINT» << endl;
}
int main() {
// устанавливаются диспозиции сигналов:
signal(SIGINT, handler);
signal(SIGSEGV, SIG_DFL);
signal(SIGTERM, SIG_IGN);
while(true) pause();
}
Примечание
Макросы
SIG_DFLиSIG_IGNопределяются так:
#define SIG_ERR (( void(*)(_SIG_ARGS))-1 )
#define SIG_DFL (( void(*)(_SIG_ARGS))0)
#define SIG_IGN (( void(*)(_SIG_ARGS))1)
#define SIG_HOLD (( void(*)(_SIG_ARGS))2)где
_SIG_ARGS– это фактически типint.SIG_DFLиSIG_IGNустанавливают диспозиции сигнала «по умолчанию» и «игнорировать» соответственно, а оSIG_HOLDмы будем отдельно говорить позже.
Выполнение этой программы вам будет не так просто прекратить: на комбинацию завершения [Ctrl+C] она отвечает сообщением о получении сигнала... и все. Воспользуемся для этого посылкой программе опять же сигнала, но из другого процесса (другого экземпляра командного интерпретатора). Смотрим PID запущенного процесса:
# pidin
...
220//86 1 /s2 10 r STOPPED
...
И посылаем процессу сигнал завершения:
# kill -9 2207786или kill -SIGKILL 2207786
Таким же образом, как показано командой kill, мы будем посылать сигналы процессам «извне» и в описываемых далее тестах, не останавливаясь подробно, как это происходит, в том числе и для сигналов реального времени (41…56).
Предыдущий пример можно переписать ( файл s4.cc) для обеспечения часто требуемой на практике защиты от немедленного прерывания выполнения по [Ctrl+C], чтобы дать программе возможность выполнить все требуемые операции по завершению (сбросить буферы данных на диск, закрыть файлы, сокеты и другие используемые объекты):
#include
#include
#include
#include
static void handler(int signo) {
cout << «Saving data ... wait.r» << flush;
sleep(2); // здесь выполняются все завершающие действия!
cout << " " << flush;
exit(EXIT_SUCCESS);
}
int main() {
signal(SIGINT, handler);
signal(SIGSEGV, SIG_DFL);
signal(SIGTERM, SIG_IGN);
while (true) pause();
}
Оператор ожидания pause()при поступлении сигналов завершается с возвратом -1, а переменная errnoустанавливается в EINTR. Этот оператор дает нам еще один способ (файл s3.cc) неявного (без явной установки обработчиков) использования сигналов:
#include
#include
#include
int main(void) {
alarm(5);
cout << «Waiting to die in 5 seconds ...» << endl;
pause();
return EXIT_SUCCESS;
}
Описываемая модель обработки сигналов обладает рядом недостатков, считается устаревшей и, более того, как было показано, не обеспечивает надежную обработку сигналов. Тем не менее эту модель достаточно широко применяют в простых случаях, например при необходимости установить тайм-аут для некоторой операции. Вот как, к примеру, устанавливается тайм-аут ожидания установления соединения в TCP/IP-клиенте [9]:
void alarm_handler(int sig) { return; }
int main() {
...
signal(SIGALRM, alarm_handler); alarm(5);
int rc = connect( ... );
alarm(0);
if (rc < 0 && errno == EINTR)
cout << «Истек тайм-аут» << endl, exit(EXIT_FAILURE);
...
}
Здесь уместно напомнить немаловажное обстоятельство, связанное с сигналами, которое обделяется вниманием во многих руководствах по программированию: большинство блокирующих вызовов API ( connect(), delay(), wait(), waitid()и многие другие) будут разблокированы при получении блокированным потоком любого сигнала. Такие вызовы API, как pause()и sigwait(), вообще предназначены только для выполнения пассивной блокировки до момента поступления сигнала. Многие их них возвращают значение или устанавливают в качестве кода системной ошибки errnoзначение EINTR, специально отведенное для отражения такого результата завершения, как прерывание поступившим извне сигналом. Мы неоднократно будем использовать это обстоятельство в тексте примеров программного кода, например:
if (delay(100) != 0)
В данном случае учитываем, что функция delay()возвращает нереализованный остаток «заказанного» ей ожидания, который может быть ненулевым только при прерывании этого ожидания сигналом извне (нулевое значение соответствует «естественному» истечению времени задержки).
В более поздней («новой») модели обработки сигналов (называемой еще моделью надежных сигналов) используются не единичные сигналы, а наборы сигналов – тип sigset_t.
Примечание
POSIX требует, чтобы в реализации тип
sigset_tопределялся таким образом, чтобы он мог «вместить» все определенные в системе сигналы; для QNX это число равно 64. Определение типаsigset_tв QNX, как и большинство фундаментальных для системы определений, находится в заголовочном файле:
struct { long bits[2]; }Понятно, что в этом случае тип
sigset_t– это битовая маска, но на практике знание представления этого типа не имеет никакой ценности для программиста, так как все операции над ним выполняются набором специальных операций, так что совершенно обоснованно этот тип можно считать абстрактным.
Для формирования сигнальных наборов определяется набор специальных операций:
• sigemptyset(sigset_t *set)– инициализирует набор set, исключая из него все сигналы;
• sigfillset(sigset_t *set)– инициализирует набор set, включая в него все сигналы;
• sigaddset(sigset_t *set, int signo)– добавляет в инициализированный набор setединичный сигнал signo;
• sigdelset(sigset_t *set, int signo)– удаляет из инициализированного набора setединичный сигнал signo.
В качестве signoв функциях добавления и удаления единичных сигналов используется символическая константа, соответствующая сигналу (такая как SIGINT), либо численное значение сигнала, но в этом случае код становится зависимым от системы. Легко увидеть, что, пользуясь совокупностью этих 4-х операций, можно сформировать любой произвольный набор сигналов. Например:
sigset_t sig;
sigemptyset(&sig);
sigaddset(&sig, SIGPOLL);
sigaddset(&sig, SIGALRM);
Этот фрагмент кода формирует сигнальный набор, состоящий из двух сигналов: SIGPOLLи SIGALRM.
Диспозиция обработки каждого сигнала в этой модели устанавливается функцией:
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
где signo– номер (имя) сигнала, для которого устанавливается диспозиция;
act– определение нового обработчика сигнала;
oact– структура (если указано не NULL), где будет сохранено описание ранее установленного обработчика (например, для последующего восстановления реакции).
Структура описания обработчика sigactionопределена так (мы исключили из определения часть структуры, предназначенную для компилятора Watcom, QNX 4.X):
struct sigaction {
#define sa_handler un._sa_handler
#define sa_sigaction un._sa_sigaction
union {
void (*_sa_handler)(_SIG_ARGS);
void (*_sa_sigaction)(int, siginfo_t*, void*);
} un;
int sa_flags;
sigset_t sa_mask;
};
Примечание
Это определение по форме, но не по содержанию отличается от описания, показанного в POSIX и используемого во многих традиционных UNIX [5] (обратите внимание на изменение порядка следования полей маски и флагов; это может стать преградой для прямой инициализации структуры в стиле C++ из соображений переносимости):
struct sigaction {
/* указатель на функцию обработчика сигнала */
void (*sa_handler)(int);
/* сигналы, блокирующиеся во время обработки */
sigset_t sa_mask;
/* флаги, влияющие на поведение сигнала */
int sa_flags;
/* указатель на функцию обработчика сигнала */
void (*sa_sigaction)(int, siginfo_t*, void*);
};Определения
#defineв первых строках описания – это обычная в QNX практика переопределения имен для компиляторов, «не понимающих» анонимных (неименованных) объединений (union). Легко видеть, что даже размеры структур в этих двух определениях (QNX и POSIX) будут отличаться, что подсказывает необходимость соблюдения здесь особой тщательности при использовании.
Первое поле sa_handlerопределяет обработчик, устанавливаемый для сигнала в традиционной модели. Это может быть:
• SIG_DFL– восстановить обработчик сигнала, принятый по умолчанию (определения SIG_DFLи SIG_IGNсм. в предыдущем разделе);
• SIG_IGN– игнорировать данный сигнал;
• адрес функции-обработчика, устанавливаемой как реакция на поступление этого сигнала. Эта функция будет выполняться при поступлении сигнала signo, и в качестве аргумента вызова она получит значение signo(одна функция может выступать как обработчик целой группы сигналов). Управление будет передано этой функции, как только процесс получит сигнал, какой бы участок кода при этом ни выполнялся. После возврата из функции управление будет возвращено в ту точку, в которой выполнение процесса было прорвано.
Второе поле sa_maskдемонстрирует первое применение набора сигналов: сигналы, установленные в sa_mask, будут блокироваться на время выполнения обработчика sa_handler(при вызове sa_handlerи сам сигнал signoбудет неявно добавлен в набор sa_mask, поэтому его можно не указывать явно). Это не значит, что поступившие в это время сигналы будут игнорироваться и теряться, просто их обработка будет отложена до завершения работы обработчика sa_handler. [29]29
Все это и делает механизм обработки более надежным по сравнению с более ранним механизмом, который описывался выше.
[Закрыть]
Поле sa_flagsможет использоваться для изменения характера реакции на сигнал signo. Возможны следующие значения поля флагов:
• SA_RESETHAND– после выполнения функции обработчика будет восстановлен обработчик по умолчанию ( SIG_DFL, что соответствует духу модели «ненадежных сигналов» и позволяет воспроизводить ее поведение);
• SA_NOCLDSTOP– используется только для сигнала SIGCHLD; флаг указывает системе не генерировать для родительского процесса SIGCHLDот порожденных процессов, которые завершаются посредством SIGSTOP.
• SA_SIGINFO– при этом будет использована обработка сигналов на базе очереди сигналов (модель сигналов реального времени). По умолчанию используется простая обработка: результат воздействия нескольких сигналов определяется последним поступившим. В случае установки этого флага будет использована расширенная форма обработчика sa_sigaction(при этом поле sa_handlerне будет использоваться) [30]30
Спецификация XSI требует, чтобы процесс использовал либо поле sa_handler, либо поле sa_sigaction, но не оба поля одновременно (в случае «классической» структуры sigaction, см. выше). Реализация QNX за счет объединения двух обработчиков под одним unionобеспечивает это требование автоматически, хотя определения при этом становятся несколько более громоздкими.
[Закрыть]. Обработчику будет передаваться дополнительная информация о сигнале – структура siginfo_t(его номер, PID пославшего сигнал процесса, действующий идентификатор пользователя этого процесса). Эта весьма объемная структура будет очень кратко рассмотрена ниже. [31]31
Модель очереди сигналов введена главным образом для обеспечения сигналов реального времени и будет рассмотрена ниже.
[Закрыть]Ее описание вынесено в отдельный заголовочный файл и может быть изучено там.
Приведем несколько небольших и самых простых примеров использования модели надежных сигналов.
Модель надежных сигналов
1. Перехватчик сигнала SIGINT(реакция на пользовательский ввод [Ctrl+C]) [32]32
Инициализации, используемые в примерах вида sigaction act = { &catchint, 0, (sigset_t)0};, будут зависимыми от системы из-за описанных ранее различий определения struct sigactionв разных ОС UNIX.
[Закрыть]( файл s8.cc):
void catchint(int signo) {
cout << "SIGINT: signo = " << signo << endl;
}
int main() {
static struct sigaction act = { &catchint, 0, (sigset_t)0 };
// запрещаем любые сигналы на время обработки SIGINT:
sigfillset(&(act.sa_mask));
// до этого вызова реакцией на Ctrl+C будет завершение задачи:
sigaction(SIGINT, &act, NULL);
for (int i = 0; i < 20; i++)
sleep(1), cout << "Cycle # " << i << endl;
}
Результатом нормального (без вмешательства оператора) выполнения приложения будет последовательность из 20 циклов секундных ожиданий, но если в процессе этих ожиданий пользователь пытается прервать работу процесса по [Ctrl+C], то он получит вывод, подобный следующему:
...
Cycle # 10
... здесь пользователь пытается прервать программу
SIGINT: signo = 2
Cycle # 11
...
2. Запрет прерывания выполнения программы с терминала. Для этого достаточно заменить строку инициализации структуры sigactionна:
static struct sigaction act = { SIG_IGN, 0, (sigset_t)0 };
Можно проигнорировать сразу несколько сигналов (прерывающих выполнение программы с клавиатуры):
sigaction(SIGINT, &act, NULL );
sigaction(SIGQUIT, &act, NULL);
Далее остановимся еще на одном вызове API-сигналов, который широко используется в этой и последующих моделях обработки (сигналы реального времени, реакция в потоках):
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
Этот вызов позволяет прочитать текущее значение (если setустановлено в NULL, то параметр howигнорируется) или переустановить сигнальную маску для текущего потока. Параметры вызова:
• set– это то значение, в соответствии с которым корректируется сигнальная маска процесса;
• how– указывает, какое именно действие переустановки сигнальной маски требуется осуществить:
• SIG_BLOCK– добавить сигналы, указанные в set к маске процесса (заблокировать реакцию на эти сигналы);
• SIG_UNBLOCK– сбросить указанные set сигналы в сигнальной маске;
• SIG_SETMASK– переустановить сигнальную маску процесса на значение, указанное в set.
• oset– значение, в котором будет сохранено значение маски, предшествующее вызову (старое значение).
Как и большинство сигнальных функций, данная функция возвращает нулевое значение в результате успешного выполнения и -1 в случае неудачи, при этом код ошибки устанавливается в errno.
Именно эта функция снимает одно из самых существенных ограничений, свойственных модели «ненадежных сигналов», – позволяет заблокировать реакцию на сигналы при выполнении критических участков кода и восстановить ее при завершении выполнения этих участков.
Модель сигналов реального времени
Сигналы реального времени были добавлены в POSIX относительно недавно (1996 г.). Эта новая модель в различных ОС UNIX реализуется с разной степенью полноты и с отклонениями от спецификаций, и QNX не исключение. Модель еще до конца не отработана, поэтому возможны сюрпризы (и сейчас они будут).
Модель сигналов реального времени, которую специфицирует POSIX, устанавливается флагом SA_SIGINFO(который уже упоминался выше) при вызове sigaction(). В нижеследующем перечислении того, что предусматривает эта модель, мы излагаем в первую очередь качественную картину происходящего, предлагаемую POSIX, кое-где уточняя ее конкретными данными реализации QNX (артефакты в поведении QNX будут отдельно отмечены позже):
1. Сигналы, называемые сигналами реального времени, могут принимать значения между SIGRTMINи SIGRTMAX. Количество таких сигналов определяется в системе константой RTSIG_MAX, которая должна быть не менее 8 (POSIX). В QNX: SIGRTMIN= 41, SIGRTMAX= 56.
2. Обработка сигналов реального времени строится на основе очереди. Если сигнал порожден N раз, то он должен быть и N раз получен адресатом (в описываемых ранее моделях это не так, в них процесс получает только единичный экземпляр сигнала). Повторные экземпляры одного и того же сигнала в модели реального времени доставляются обработчику в порядке FIFO.
3. Помимо 8-битного кода с сигналом реального времени ассоциируется 32-битное значение ( si_value, мы им займемся позже), заполняемое отправителем и доставляемое получателю (что позволяет «различать» экземпляры сигналов в очереди, о которой говорилось выше).
4. Для работы с сигналами реального времени добавлено несколько новых функций. В частности, в этой модели для отправки сигнала некоторому процессу используется sigqueue()вместо kill().
Эти два вызова определяются очень близкими формами:
int kill(pid_t pid, int signo);
int sigqueue(pid_t pid, int signo, const union sigval value);
Примечание
Как мы вскоре увидим, эти две синтаксические формы одного и того же вызова отличаются лишь тем, помещают ли они в сигнал указанное значение или оставляют его нулевым. Если процесс устанавливает обработку сигнала на основании очереди, он будет получать почти одинаковым образом сигналы, посланные обоими вызовами. Разница «почти» состоит в том, что получатель на основании анализа поля
si_codeвsiginfo_tв состоянии отличить, каким вызовом ему был послан сигнал.
Примечание
При ошибке выполнения
sigqueue()(код возврата -1) могут устанавливаться (в errno) следующие коды ошибок:•
EAGAIN– недостаточно ресурсов для помещения сигнала в очередь;•
EINVAL– недопустимое значениеsignoили неподдерживаемый сигнал;•
ENOSYS– вызовsigqueue()не поддерживается реализацией (возможно, версией);•
EPERM– у процесса недостаточно привилегий для посылки сигнала принимающему процессу;•
ESRCH– несуществующий PID процесса получателя.Последний случай особо интересен, так как при указании в качестве номера сигнала
signo = 0реальная посылка сигнала не производится, но устанавливается код ошибки. Это простейший и эффективный способ выяснить, выполняется ли в системе процесс с заданным PID.
5. Когда в очередь помещаются различные не заблокированные процессом (потоком) сигналы в диапазоне SIGRTMIN… SIGRTMAX, то сигналы с меньшими номерами доставляются обработчику из FIFO-очереди раньше сигналов с большими номерами (то есть сигналы с меньшими номерами имеют более высокий приоритет).
6. Обработчик для сигналов реального времени устанавливается с флагом SA_SIGINFO, а функция обработчика объявляется теперь с другим прототипом:
void func(int signo, siginfo_t* info, void* context);
Обработчик имеет больше параметров и получает больше информации. POSIX требует, чтобы тип siginfo_tсодержал как минимум:
typedef struct {
int si_signo;
int si_code;
union sigval si_value; /* целое или указатель от отправителя */
} siginfo_t;
В QNX sigvalопределяется так (подобное определение дают и другие ОС UNIX):
union sigval {
int sival_int;
void *sival_ptr;
};
Это 32-битное значение предназначено для посылки совместно с сигналом данных для получателя, которые, как видно из синтаксиса определения sigval, могут быть целочисленным значением или указателем неспецифицированного типа.
7. Поле si_codeтипа siginfo_t, передаваемое получателю, определяет природу возбуждения сигнала:
• SI_ASINCIO– сигнал порожден завершением операций асинхронного ввода/вывода, запущенного одной из функций POSIX aio_*();
• SI_MESGQ– сигнал возбуждается при помещении сообщения в пустую очередь сообщений UNIX;
• SI_QUEUE– сигнал был отправлен функцией sigqueue()(в этом разделе нас интересуют только такие сигналы);
• SI_TIMER– сигнал был порожден по истечении установленного времени интервального таймера;
• SI_USER– сигнал был отправлен функцией kill().
8. Допускается, что при возбуждении сигнала еще каким-либо механизмом (сверх перечисленных, что может определяться специфическими особенностями ОС) значение si_codeможет отличаться от перечисленных. Однако значение поля si_valueсчитается актуальным только в тех случаях, когда si_codeимеет одно из значений: SI_ASINCIO, SI_MESGQ, SI_QUEUE, SI_TIMER.
9. Согласно POSIX сигналы, обработчики для которых также устанавливаются с флагом SA_SIGINFO, но не входящие в диапазон сигналов реального времени, например стандартные сигналы UNIX, могут обрабатываться как на основе помещения их в очередь, так и без ее использования; выбор оставляется на усмотрение разработчика ОС.
Мы перечислили основные требования POSIX к модели обработки сигналов реального времени. Дополнения, отличия и специфические структуры данных QNX будут рассмотрены немного позже.
Весьма доходчивый пример для проверки и иллюстрации обработки сигналов реального времени приведен У. Стивенсом [2]. Мы же построим приложение, реализующее его основную идею: [33]33
Повторить приложение У. Стивенса в QNX в чистом виде не удастся – оно аварийно завершится по сигналу. Тонкий анализ этого факта интересен сам по себе, но он выходит за рамки нашего рассмотрения. Мы обращаем внимание на это обстоятельство, чтобы лишний раз сделать акцент на достаточно ощутимых отличиях реализаций QNX от схем POSIX (или того, как эти схемы понимаются в других ОС).
[Закрыть]
Приоритеты сигналов реального времени
#include
#include
#include
#include
#include
static void handler(int signo, siginfo_t* info, void* context) {
cout << "received signal " << signo << " code = " << info->si_code <<
" val = " << info->si_value.sival_int << endl;
}
int main(int argc, char *argv[]) {
cout << «signal SIGRTMIN=» << (int)SIGRTMIN
<< « – signal SIGRTMAX=» << (int)SIGRTMAX << endl;
int opt, val, beg = SIGRTMAX, num = 3,
fin = SIGRTMAX – num, seq = 3;
// обработка параметров запуска:
while ((opt = getopt(argc, argv, «b:e n»)) != -1) {
switch(opt) {
case 'b': // начальный сигнал серии
if (sscanf(optarg, «%i», &val) != 1)
perror(«parse command line failed»), exit(EXIT_FAILURE);
beg = val;
break;
case 'e': // конечный сигнал серии
if (sscanf(optarg, «%i», &val) != 1)
perror(«parse command line failed»), exit(EXIT_FAILURE);
fin = val;
break;
case 'n': // количество сигналов в группе посылки
if (sscanf(optarg, «%i», &val) != 1)
perror(«parse command line failed»), exit(EXIT_FAILURE);
seq = val;
break;
default:
exit(EXIT_FAILURE);
}
}
num = fin – beg;
fin += num > 0 ? 1 : -1;
sigset_t sigset;
sigemptyset(&sigset);
for (int i = beg; i != fin; i += (num > 0 ? 1 : -1))
sigaddset(&sigset, i);
pid_t pid;
// вот здесь ветвление на 2 процесса
if (pid – fork() == 0) {
// дочерний процесс, здесь будут приниматься посланные сигналы
sigprocmask(SIG_BLOCK, &sigset, NULL);
for (int i = beg; i != fin; i += (num > 0 ? 1 : -1)) {
struct sigaction act, oact;
sigemptyset(&act.sa_mask);
act.sa_sigaction = handler;
// вот оно – реальное время!
act.sa_flags = SA_SIGINFO;
if (sigaction(i, &act, NULL) < 0) perror("set signal handler: ");
}
cout << «CHILD: signal mask set» << endl;
sleep(3); // пауза для посылки сигналов родителем
cout << «CHILD: signal unblock» << endl;
sigprocmask(SIG_UNBLOCK, &sigset, NULL);
sleep(3); // пауза для приема всех сигналов
exit(EXIT_SUCCESS);
}
// родительский процесс: отсюда будут посылаться сигналы
sigprocmask(SIG_BLOCK, &sigset, NULL);
// пауза для установки обработчиков дочерним процессом
sleep(1);
union sigval value;
for (int i = beg, i != fin; i += (num > 0 ? 1 : -1)) {
for (int j = 0; j < seq; j++) {
value.sival_int = j;
sigqueue(pid, i, value);
cout << "signal sent: " << i << " with val = " << j << endl;
}
}
cout << "PARENT: finished!' << endl;
exit(EXIT_SUCCESS);
}
Идея этого теста крайне проста:
• Создаются два процесса, один из которых (родительский) посылает серию последовательных (по номерам) сигналов, а второй (дочерний) должен их принять и обработать.
• Начальный и конечный номера сигналов в серии могут быть переопределены ключами -bи -есоответственно.
• Посылается не одиночный сигнал, а их повторяющаяся группа; размер группы повторений может быть переопределен ключом – n.
• В качестве значения, передаваемого с каждым сигналом, устанавливается последовательный номер его посылки в группе.
Таким образом, мы можем изменять последовательность сигналов на передаче и наблюдать последовательность их доставки к принимающему процессу. Запустим полученное приложение и сразу же командой pidinпосмотрим его состояние:
1077295 1 ./s5 10r NANOSLEEP
1081392 1 ./s5 10r NANOSLEEP
Это то, что мы и предполагали получить. Рассмотрим теперь результат выполнения приложения со значениями сигналов по умолчанию (сигналы 56...54, именно в порядке убывания, в каждой группе посылается 3 сигнала):
# ./s5
signal SIGRTMIN=41 – signal SIGRTMAX=56
CHILD: signal mask set
signal sent: 56 with val = 0
signal sent: 56 with val = 1
signal sent: 56 with val = 2
signal sent: 55 with val = 0
signal sent: 55 with val = 1
signal sent: 55 with val = 2
signal sent: 54 with val = 0
signal sent: 54 with val = 1
signal sent: 54 with val = 2
PARENT: finished!
# CHILD: signal unblock
received signal 56 code = -2 val = 0
received signal 56 code = -2 val = 1
received signal 56 code = -2 val = 2
received signal 55 code = -2 val = 0
received signal 55 code = -2 val = 1
received signal 55 code = -2 val = 2
received signal 54 code = -2 val = 0
received signal 54 code = -2 val = 1
received signal 54 code = -2 val = 2
Первый сюрприз, который нас ожидает, – это общее количество сигналов реального времени, выводимое программой в первой строке. Документация (HELP QNX) утверждает:
There are 24 POSIX 1003.1b realtime signals, including:
SIGRTMIN – First realtime signal.
SIGRTMAX – Last realtime signal.
Здесь есть некоторое несоответствие: тест дает значения констант SIGRTMIN= 41 и SIGRTMAX= 56, а общее количество сигналов = 16 (POSIX, как вы помните, требует минимум 8). «Потерявшиеся» сигналы (24 – 16 = 8), очевидно, и есть те сигналы из диапазона 32…40, которые выходят за пределы общих UNIX-сигналов (1…31), но не отнесены к диапазону сигналов реального времени (41…56).
Но гораздо больший сюрприз состоит в порядке доставки сигналов из очереди FIFO принимающему процессу. Документация об этом сообщает (выделено нами):
The POSIX standard includes the concept of queued realtime signals. QNX Neutrino supports optional queuing of any signal, not just realtime signals. The queuing can be specified on a signal-by-signal basis within a process. Each signal can have an associated 8-bit code and a 32-bit value.
This is very similar to message pulses described earlier. The kernel takes advantage of this similarity and uses common code for managing both signals and pulses. The signal number is mapped to a pulse priority using SIGMAX – signo. As a result, signals are delivered in priority order with lower signal numbers having higher priority. This conforms with the POSIX standard, which states that existing signals have priority over the new realtime signals.
Изменим временной порядок возбуждения сигналов – от сигналов с меньшими номерами к сигналам с большими номерами:
# ./s5 -b54 -e56 -n2
signal SIGRTMIN=41 – signal SIGRTMAX=56
CHILD: signal mask set
signal sent: 54 with val = 0
signal sent: 54 with val = 1
signal sent: 55 with val = 0
signal sent: 55 with val = 1
signal sent: 56 with val = 0
signal sent: 56 with val = 1
PARENT: finished!
# CHILD: signal unblock
received signal 56 code = -2 val = 0
received signal 56 code = -2 val = 1
received signal 55 code = -2 val = 0
received signal 55 code = -2 val = 1
received signal 54 code = -2 val = 0
received signal 54 code = -2 val = 1
Независимо от порядка отправки сигналов порядок доставки их из очереди принимающему процессу сохраняется от старших номеров сигналов, которые являются более приоритетными, к младшим. А это в точности противоположно тому, что обещает документация, и соответствует картине, которую У. Стивенс наблюдал в ОС Sun Solaris 2.6 и которую он же комментирует словами: «Похоже, что в реализации Solaris 2.6 есть ошибка».
Выполним ту же задачу, но теперь не относительно сигналов диапазона реального времени, а относительно стандартных UNIX-сигналов. Выберем для этого произвольный диапазон сигналов ( ):
#define SIGVTALRM 28 /* virtual timer expired */
#define SIGPROF 29 /* profiling timer expired */
#define SIGXCPU 30 /* exceeded cpu limit */
#define SIGXFSZ 31 /* exceeded file size limit */
Посмотрим результат в этом случае:
# ./s5 -b28 -e31 -n2
signal SIGRTMIN=41 – signal SIGRTMAX=56
CHILD: signal mask set
signal sent: 28 with val = 0
signal sent: 28 with val = 1
signal sent: 29 with val = 0
signal sent: 29 with val = 1
signal sent: 30 with val = 0
signal sent: 30 with val = 1
signal sent: 31 with val = 0
signal sent: 31 with val = 1
PARENT finished!
# CHILD signal unblock
received signal 31 code = -2 val = 0
received signal 31 code = -2 val = 1
received signal 30 code = -2 val = 0
received signal 30 code = -2 val = 1
received signal 29 code = -2 val = 0
received signal 29 code = -2 val = 1
received signal 28 code = -2 val = 0
received signal 28 code = -2 val = 1
QNX при установке обработчика с флагом SA_SIGINFOиспользует модель работы реального времени не только относительно сигналов диапазона SIGRTMIN... SIGRTMAX, но и для всего множества стандартных UNIX-сигналов. Это расширение, впрочем, не противоречит приведенному выше утверждению POSIX, что такое решение факультативно (см. пункт 9 описания модели сигналов реального времени) и оставляется на усмотрение разработчика ОС.
Любопытным может оказаться и рассмотрение реакции на сигналы, которые никак не определены в QNX (в исследуемый диапазон для сравнения включены как неопределенные, так и определенные сигналы системы):
# ./s5 -b39 -e41 -n2
signal SIGRTMIN=41 – signal SIGRTMAX=56
CHILD: signal mask set
signal sent: 39 with val = 0
signal sent: 39 with val = 1
signal sent: 40 with val = 0
signal sent: 40 with val = 1
signal sent: 41 with val = 0
signal sent: 41 with val = 1
PARENT finished!
# CHILD: signal unblock
received signal 41 code = -2 val = 0
received signal 41 code = -2 val = 1
received signal 40 code = -2 val = 0
received signal 40 code = -2 val = 1
received signal 39 code = -2 val = 0
received signal 39 code = -2 val = 1
Для них реакция никак не отличается от реакции на другие сигналы, что, впрочем, неудивительно, учитывая замечание в цитировавшемся выше фрагменте документации о том, что возбуждение сигнала и посылка импульса (сообщения) микроядра в QNX – одно и то же, и обрабатываются они единым механизмом.




























