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


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. Когда в очередь помещаются различные не заблокированные процессом (потоком) сигналы в диапазоне SIGRTMINSIGRTMAX, то сигналы с меньшими номерами доставляются обработчику из 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]. Мы же построим приложение, реализующее его основную идею:

Приоритеты сигналов реального времени

#include <stdlib.h>

#include <stdio.h>

#include <iostream.h>

#include <signal.h>

#include <unistd.h>

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.

Изменим временной порядок возбуждения сигналов - от сигналов с меньшими номерами к сигналам с большими номерами:

Назад Дальше