учебники, программирование, основы, введение в,

 

Мобильное программирование приложений реального времени

Основные идеи, понятия и объекты
В рамках решаемой стандартом POSIX общей задачи обеспечения мобильности приложений на уровне исходных текстов можно выделить подзадачу мобильного программирования систем, к которым предъявляются требования работы в реальном масштабе времени. (Разумеется, стандарт не может и не должен преодолевать зависимость числовых параметров реализации от свойств аппаратной платформы.)
В качестве средств для решения этой подзадачи в стандарте POSIX-2001 присутствует целый ряд идей, понятий, объектов и функций, которые можно сгруппировать в следующие предметные области:

  • одношаговое порождение процессов;
  • сигналы реального времени;
  • часы и таймеры;
  • средства межпроцессного взаимодействия;
  • объекты в памяти;
  • удержание процессов в памяти;
  • приоритетное планирование;
  • асинхронный ввод/вывод;
  • рекомендательные интерфейсы.

Одношаговое порождение процессов (в противоположность традиционной для ОС Unix двухшаговой модели fork()/exec()) основано на применении функций posix_spawn() и posix_spawnp(). Оно в любом случае менее тяжеловесно (хотя и тяжеловеснее порождения потоков управления), но особенно актуально для аппаратных конфигураций, в которых отсутствует поддержка виртуальной памяти и, как следствие, реализация функции fork() проблематична.
Под сигналами реального времени понимается не нарушающее совместимости расширение общего механизма сигналов, повышающее детерминированность за счет постановки сигналов, асинхронно доставленных приложению, в очередь.
Основные понятия, ассоциированные с часами и таймерами, были рассмотрены в курсе [1]. В настоящем курсе мы сосредоточимся на необязательных элементах стандарта POSIX-2001, специфичных для реального времени.
В качестве средств межпроцессного взаимодействия в реальном времени в стандарт POSIX-2001 включены очереди сообщений, семафоры и разделяемые сегменты памяти (см. также курс [1], где детально анализируется другой класс средств межпроцессного взаимодействия с аналогичными названиями).
Чтобы время доступа к объекту было по возможности минимальным и не превышало заданной величины, этот объект делают резидентным в физической памяти. Применительно к процессам подобный подход реализуется посредством удержания в памяти страниц, принадлежащих прикладному процессу.
Достижению той же цели – минимизации времени (а также унификации) доступа – служит механизм объектов в памяти. Стандартом POSIX-2001 предусмотрено три вида таких объектов:

  • файлы, отображенные в память;
  • объекты в разделяемой памяти (они же – упомянутые выше как средства межпроцессного взаимодействия разделяемые сегменты памяти);
  • объекты в типизированной памяти.

С помощью функции mmap() строится отображение объекта в памяти на группу страниц из адресного пространства вызывающего процесса, так что доступ к адресам из заданного диапазона выливается в обращение к ассоциированному объекту (например, к файлу).
Объекты в разделяемой памяти характеризуются тем, что их можно параллельно отобразить в адресное пространство нескольких процессов.
Объект в типизированной памяти представляет собой комбинацию типизированных пула и порта памяти. Типизированный пул – это область памяти с однородными операционными характеристиками. Пулы могут быть вложенными. Аппаратный доступ к содержимому пула осуществляется через порт. Объект в типизированной памяти идентифицируется именем из пространства имен типизированной памяти.
Отметим, что объект в памяти не обязательно должен быть резидентным в физической памяти.
Согласно стандарту POSIX-2001, приоритет – это неотрицательная целочисленная величина, ассоциированная с процессом или потоком управления. Допустимый диапазон приоритетов определяется применяемой политикой планирования. Большие величины представляют более высокие приоритеты.
Приоритетное планирование (или планирование, основанное на приоритетах) – это средство усиления детерминированности, позволяющее приложениям определять порядок, в соответствии с которым потоки управления, готовые к выполнению, получают доступ к процессорным ресурсам.
Средства асинхронного ввода/вывода позволяют прикладным процессам ставить в очередь команды ввода/вывода данных и получать асинхронные уведомления о завершении выполнения этих команд.
Рекомендательные интерфейсы – это средство проинформировать операционную систему о поведении мобильного приложения, чтобы ОС могла принять меры для оптимизации его обслуживания. Например, если прогнозируется последовательный доступ к файлу, ОС может осуществлять опережающее чтение, чтобы совместить во времени ввод и обработку данных.

Одношаговое порождение процессов
При стандартизации средств одношагового порождения процессов преследовались две основные цели:

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

В качестве дополнительных целей объявлены:

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

Еще одна дополнительная цель – простота интерфейса. Семейство exec*() насчитывает шесть членов; для posix_spawn*() хватило двух с единым списком аргументов в духе execve() и небольшими отличиями в трактовке имени файла с образом нового процесса (напоминающими разницу между execve() и execvp()).
Более точно: для одношагового порождения процессов служат функции posix_spawn() и posix_spawnp() (см
#include <spawn.h>

int posix_spawn (pid_t *restrict pid,
const char *restrict path,
const posix_spawn_file_actions_t
*file_actions,
const posix_spawnattr_t *restrict attrp,
char *const argv [restrict],
char *const envp [restrict]);
int posix_spawnp (pid_t *restrict pid,
const char *restrict file,
const posix_spawn_file_actions_t
*file_actions,
const posix_spawnattr_t *restrict attrp,
char *const argv [restrict],
char *const envp [restrict]);
Листинг 3.1. Описание функций одношагового порождения процессов. Аргументами, отличающими posix_spawn() и posix_spawnp() от функций семейства exec*() являются pid, file_actions, attrp. Опишем их назначение.
По указателю pid (если он отличен от NULL) возвращается идентификатор успешно порожденного процесса.
Аргументы file_actions и attrp отвечают за контроль сущностей, наследуемых при одношаговом порождении процессов. Чтобы достичь перечисленных выше целей, функции posix_spawn() и posix_spawnp() контролируют шесть видов наследуемых сущностей:

  • файловые дескрипторы;
  • идентификатор группы процессов;
  • идентификаторы пользователя и группы процесса;
  • параметры планирования;
  • маску сигналов;
  • способ обработки сигналов, игнорируемых родительским процессом.

Контроль файловых дескрипторов, в первую очередь стандартных ввода, вывода и протокола, который осуществляется с помощью аргумента file_actions, позволяет порожденному процессу получить доступ к потокам данных, открытым или даже порожденным родительским процессом, без встраивания в исходный текст или передачи в качестве аргументов main() конкретных имен файлов или номеров дескрипторов.
Как правило, все открытые дескрипторы родительского процесса остаются таковыми и в порожденном, за исключением тех, у которых установлен флаг FD_CLOEXEC. Кроме того, если значение аргумента file_actions отлично от NULL, до обработки флагов FD_CLOEXEC принимается во внимание указуемый объект типа posix_spawn_file_actions_t, который содержит действия по закрытию, открытию и/или дублированию файловых дескрипторов. Для формирования объектов типа posix_spawn_file_actions_t служат функции posix_spawn_file_actions_init(), posix_spawn_file_actions_addclose(), posix_spawn_file_actions_addopen() и posix_spawn_file_actions_adddup2(); функция posix_spawn_file_actions_destroy() ликвидирует подобный объект (см.
#include <spawn.h>

int posix_spawn_file_actions_init
(posix_spawn_file_actions_t *file_actions);

int posix_spawn_file_actions_destroy
(posix_spawn_file_actions_t *file_actions);

int posix_spawn_file_actions_addclose
(posix_spawn_file_actions_t *file_actions,
int fildes);

int posix_spawn_file_actions_addopen
( posix_spawn_file_actions_t *restrict file_actions,
int fildes, const char *restrict path, int oflag,
mode_t mode);

int posix_spawn_file_actions_adddup2
(posix_spawn_file_actions_t *file_actions, int fildes,
int newfildes);
Листинг 3.2. Описание функций формирования и ликвидации объектов типа posix_spawn_file_actions_t. Функция posix_spawn_file_actions_addclose() добавляет дескриптор fildes к числу закрываемых перед началом выполнения порожденного процесса. Функция posix_spawn_file_actions_addopen() предписывает открыть дескриптор fildes, как если бы был выполнен вызов open (path, oflag, mode). Наконец, функция posix_spawn_file_actions_adddup2() специфицирует дублирование дескриптора fildes в newfildes (close (newfildes); fcntl (fildes, F_DUPFD, newfildes)). Таким образом, функции posix_spawn() и posix_spawnp(), отправляясь от набора открытых дескрипторов родительского процесса, выполняют действия, заданные аргументом file_actions, и получают набор дескрипторов, открытых в порождаемом процессе, то есть родительский процесс берет на себя согласование по файловым дескрипторам с независимо созданным новым образом процесса.
Отметим, что с помощью функции posix_spawn_file_actions_addopen() удобно перенаправлять ввод/вывод порожденного процесса.
За контроль других сущностей, наследуемых при одношаговом порождении процессов, отвечает атрибутный объект , заданный аргументом attrp. Для формирования и опроса подобных объектов служат функции, показанные на листингах  
#include <spawn.h>

int posix_spawnattr_init (
posix_spawnattr_t *attr);

int posix_spawnattr_destroy (
posix_spawnattr_t *attr);

int posix_spawnattr_getflags (
const posix_spawnattr_t
*restrict attr,
short *restrict flags);

int posix_spawnattr_setflags (
posix_spawnattr_t *attr,
short flags);

int posix_spawnattr_getpgroup (
const posix_spawnattr_t
*restrict attr,
pid_t *restrict pgroup);

int posix_spawnattr_setpgroup (
posix_spawnattr_t *attr,
pid_t pgroup);
Листинг 3.3. Описание функций формирования и опроса атрибутных объектов порождаемых процессов. #include <spawn.h>
#include <sched.h>

int posix_spawnattr_getschedparam (
const posix_spawnattr_t *restrict attr,
struct sched_param *restrict schedparam);

int posix_spawnattr_setschedparam (
posix_spawnattr_t *restrict attr,
const struct sched_param
*restrict schedparam);

int posix_spawnattr_getschedpolicy (
const posix_spawnattr_t *restrict attr,
int *restrict schedpolicy);

int posix_spawnattr_setschedpolicy (
posix_spawnattr_t *attr, int schedpolicy);
Листинг 3.4. Описание функций опроса и установки параметров и политики планирования в атрибутных объектах порождаемых процессов.
#include <spawn.h>
#include <signal.h>
int posix_spawnattr_getsigdefault (
const posix_spawnattr_t *restrict attr,
sigset_t *restrict sigdefault);

int posix_spawnattr_setsigdefault (
posix_spawnattr_t *restrict attr,
const sigset_t *restrict sigdefault);

int posix_spawnattr_getsigmask (
const posix_spawnattr_t *restrict attr,
sigset_t *restrict sigmask);

int posix_spawnattr_setsigmask (
posix_spawnattr_t *restrict attr,
const sigset_t *restrict sigmask);
Листинг 3.5. Описание функций опроса и установки подразумеваемой обработки и маски сигналов в атрибутных объектах порождаемых процессов.

Флаговые атрибуты определяют, какие из контролируемых наследуемых сущностей в порождаемом процессе должны быть изменены. Допустимы следующие флаги.
POSIX_SPAWN_RESETIDS
Установить действующие идентификаторы пользователя и группы по соответствующим реальным идентификаторам родительского процесса. (Если у файла с образом нового процесса взведены биты ПДИП и/или ПДИГ , то, независимо от состояния флага POSIX_SPAWN_RESETIDS, действующие идентификаторы наследуются у этого файла, а не у родительского процесса.)
POSIX_SPAWN_SETPGROUP
Установить идентификатор группы процессов по соответствующему атрибуту объекта, на который указывает аргумент attrp. При нулевом значении этого атрибута порожденный процесс выделяется в новую группу с идентификатором, равным его (процесса) идентификатору.
POSIX_SPAWN_SETSIGDEF
Установить подразумеваемый способ обработки сигналов, заданных в атрибутном объекте. Отметим, что это существенно только для сигналов, игнорируемых родительским процессом.
POSIX_SPAWN_SETSIGMASK
Установить начальную маску сигналов по атрибутному объекту.
POSIX_SPAWN_SETSCHEDPARAM
Установить параметры планирования по атрибутному объекту.
POSIX_SPAWN_SETSCHEDULER
Установить политику и параметры планирования по атрибутному объекту (независимо от состояния флага POSIX_SPAWN_SETSCHEDPARAM).
Если значение аргумента attrp равно NULL, используются подразумеваемые значения атрибутов.
Все характеристики нового процесса, на которые не воздействуют аргументы attrp и file_actions, устанавливаются так, как если бы применялось двухшаговое порождение fork()/exec(). Будут ли при одношаговом порождении выполняться обработчики разветвления процессов, зарегистрированные с помощью функции atfork(), зависит от реализации.
При библиотечной реализации функций posix_spawn() и posix_spawnp() некоторые ошибки могут быть выявлены только после порождения процесса. В таком случае родительский процесс может узнать о них, анализируя с помощью макросов WIFEXITED, WEXITSTATUS (см. курс [1]) значение stat_val, возвращаемое функциями wait() и/или waitpid(). Предлагается, чтобы статус «аварийного завершения до начала реального выполнения» равнялся 127. Это не очень естественно и удобно, но иного выхода не видно.
В целом средства одношагового порождения процессов позволяют достичь сформулированных выше целей. Они просты, но обладают достаточной выразительной силой и, несомненно, способны заменить двухшаговое порождение по крайней мере в половине типичных случаев.
Чтобы лучше понять семантику одношагового порождения процессов, приведем фрагмент возможной библиотечной реализации функции posix_spawn(), представленной в четвертой, информационной части стандарта POSIX-2001 (см.
Листинг 3.6. Фрагмент возможного содержимого файла spawn.h. Листинг 3.7. Фрагмент возможной библиотечной реализации функции posix_spawn().Если для порожденного процесса нужно особым образом установить характеристику, не принадлежащую к числу описанных выше контролируемых сущностей (например, значение переменной окружения), приходится перед обращением к posix_spawn() сохранить и переустановить ее, а затем восстановить прежнее значение Правда, при этом необходимо, чтобы все потоки управления, выполняющиеся в рамках родительского процесса, были устойчивы к подобным манипуляциям.
/* Запуск процесса с произвольным идентификатором     */
/* пользователя                     */

uid_t old_uid;
uid_t new_uid = ...;

old_uid = getuid ();
setuid (new_uid);

posix_spawn (...);

setuid (old_uid);
Листинг 3.8. Пример установки характеристики порожденного процесса, не принадлежащей к числу контролируемых стандартными средствами. На показан пример перенаправления стандартных ввода и вывода порождаемого процесса с помощью формирования и использования объекта типа posix_spawn_file_actions_t. В данном случае стандартный вывод (дескриптор 1) направляется в файл outfile, а стандартный ввод (дескриптор 0) отождествляется с открытым ранее дескриптором socket_pair [1]. Попутно обеспечивается закрытие в новом процессе дескрипторов socket_pair [0] и socket_pair [1].
posix_spawn_file_actions_t file_actions;

posix_spawn_file_actions_init (
&file_actions);
posix_spawn_file_actions_addopen (
&file_actions, 1, "outfile", ...);
posix_spawn_file_actions_adddup2 (
&file_actions, socket_pair [1], 0);
posix_spawn_file_actions_addclose (
&file_actions, socket_pair [0]);
posix_spawn_file_actions_addclose (
&file_actions, socket_pair [1]);

posix_spawn (..., &file_actions, ...);
posix_spawn_file_actions_destroy (
&file_actions);
Листинг 3.9. Пример перенаправления стандартных ввода и вывода порождаемого процесса. Любопытно сопоставить реальные накладные расходы на одношаговое и двухшаговое порождение процессов. Программа, порождающая с помощью функции posix_spawn() практически пустые процессы, показана на. На приведены данные о времени ее работы, полученные с помощью команды time -p. Полученные результаты практически не отличаются от измеренного ранее времени двухшагового порождения. Это означает, что в используемой нами версии ОС Linux функции posix_spawn() и posix_spawnp() реализованы как библиотечные, а их применение выигрыша в эффективности в данном случае не дает (но по соображениям «мобильной потенциальной эффективности» их все равно есть смысл использовать).
#include <spawn.h>
#include <stdio.h>
#include <sys/wait.h>
#include <errno.h>

#define N  10000

int main (void) {
char *s_argv [] = {"dummy", NULL};
char *s_env [] = {NULL};

  int i;

  for (i = 0; i &lt; N; i++) {
if ((errno = posix_spawn (
NULL, "./dummy", NULL, NULL, s_argv,
s_env)) != 0) {
perror ("POSIX_SPAWN");
return (errno);
}
(void) wait (NULL);
}

  return 0;
}
Листинг 3.10. Пример программы, порождающей в цикле практически пустые процессы с помощью функции posix_spawn().real 34.37
user 12.01
sys 22.07
Листинг 3.11. Возможные результаты измерения времени работы программы, порождающей в цикле практически пустые процессы с помощью функции posix_spawn().

Сигналы реального времени
Приложениям реального времени требуются средства надежного, детерминированного, асинхронного извещения (уведомления) о событиях. Теоретически для этого можно было бы разработать совершенно новый механизм, допускающий исключительно эффективную реализацию, однако авторы стандарта POSIX-2001 не пошли по такому пути по той простой причине, что уже имеется механизм сигналов, который обладает почти всеми необходимыми свойствами. «Почти», потому что он не решает следующих проблем.

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

Чтобы устранить перечисленные недостатки, механизм сигналов был расширен, а в стандарт была введена необязательная часть, получившая название «сигналы реального времени» (Realtime Signals Extension, RTS).
Номера сигналов реального времени лежат в диапазоне от SIGRTMIN до SIGRTMAX. Всего таких сигналов должно быть не меньше, чем RTSIG_MAX. Обеспечивается ли поведение в реальном времени для других сигналов, зависит от реализации.
«Жизненный цикл» сигналов реального времени состоит из четырех фаз:

  • генерация;
  • ожидание;
  • доставка;
  • обработка.

Сигналы реального времени могут генерироваться при срабатывании высокоточных таймеров, завершении операции асинхронного ввода/вывода, поступлении межпроцессного сообщения, выполнении функции sigqueue() и т.д.
На фазе генерации сигналов центральную роль играет определенная в заголовочном файле <signal.h> структура типа sigevent, которая, согласно стандарту, должна содержать по крайней мере следующие поля.

int sigev_notify;  /* Способ уведомления */
int sigev_signo;   /* Номер сигнала     */

union sigval sigev_value;
/* Значение сигнала     */

void (*) (union sigval) 
sigev_notify_function;    
/* Функция  уведомления  */

(pthread_attr_t *)sigev_notify_attributes;
/* Атрибуты  уведомления */
Значение поля sigev_notify определяет способ уведомления об асинхронном событии.
Константа SIGEV_NONE означает отсутствие уведомления (событие проходит незамеченным).
Константа SIGEV_SIGNAL предписывает сгенерировать сигнал с номером sigev_signo. Если для этого сигнала установлен флаг SA_SIGINFO, он (сигнал) ставится в очередь к процессу. Тем самым обеспечивается надежная доставка уведомлений о событиях.
Константа SIGEV_THREAD задает в качестве механизма уведомления вызов функции.
Чтобы обеспечить дифференциацию между сигналами одного типа, с ними ассоциируется значение, которое задается при генерации сигнала либо в явном виде как отдельный аргумент соответствующей функции, либо как значение поля sigev_value структуры-аргумента типа sigevent.
Значение сигнала реального времени может быть целым числом или указателем, поскольку объединение типа sigval должно определяться со следующими полями:
int  sival_int;
/* Значение сигнала – целое число */
void *sival_ptr;
/* Значение сигнала – указатель   */
Если для многопотоковой программы в качестве способа уведомления о наступлении события избран вызов функции (константа SIGEV_THREAD в поле sigev_notify), то указатель на эту функцию извлекается из поля sigev_notify_function, а ее аргументом служит значение сигнала. Эта функция выполняется как стартовая для вновь созданного обособленного потока управления с атрибутным объектом *sigev_notify_attributes.
При подобном способе уведомления сигналы как таковые не генерируются, а уведомляющую функцию не следует путать с функцией обработки сигнала, которая вызывается в ином контексте и с другими аргументами.
После того, как сигнал сгенерирован функцией sigqueue() или иным способом, позволяющим задать определяемое приложением значение, наступает фаза ожидания. Сигналы одного типа ставятся в очередь к процессу в порядке генерации.
Сигналы, сгенерированные с помощью вызова функции kill() или в результате наступления такого события, как аппаратное прерывание, срабатывание будильника или ввод управляющего символа с терминала, для которых реализация не обеспечивает постановку в очередь, никак на эту очередь не влияют.
При наличии нескольких неблокированных ждущих сигналов реального времени, их доставка процессу производится в порядке возрастания номеров. Тем самым поддерживается ранжированная по приоритетам доставка уведомлений, а роль приоритета играет номер сигнала.
На фазе обработки уведомления об асинхронном событии используется структура типа siginfo_t. Для сигналов реального времени она включает, помимо описанных в курсе [1], дополнительный элемент
union sigval si_value; 
/* Значение сигнала */
в который переносится значение из поля sigev_value структуры типа sigevent.
Подразумеваемые действия при обработке сигнала реального времени (SIG_DFL) состоят в аварийном завершении процесса.
Простейший способ сгенерировать сигнал реального времени – обратиться к функции sigqueue() (см).
#include <signal.h>
int sigqueue (pid_t pid, int signo,
const union sigval value);
Листинг 3.12. Описание функции sigqueue().Функция sigqueue() посылает сигнал с номером signo и значением value процессу, идентификатор которого задан аргументом pid. Права на посылку сигнала определяются так же, как и для функции kill(); аналогично kill(), при нулевом значении signo сигнал не посылается, а проверяется существование процесса с идентификатором pid.
Функция sigqueue() завершается немедленно, без какого-либо ожидания. Если для сигнала signo установлен флаг SA_SIGINFO и в наличии достаточно ресурсов, сигнал ставится в очередь к процессу pid. Если флаг SA_SIGINFO не установлен, сигнал посылается процессу-получателю по крайней мере один раз, но, возможно, без ассоциированного с ним значения.
Если процесс посылает сигнал сам себе, он (сигнал) будет доставлен вызывающему потоку управления до выхода из функции sigqueue().
Дождаться доставки сигнала реального времени можно с помощью функций sigwaitinfo() и sigtimedwait() (см.), являющихся аналогами рассмотренной в курсе [1] функции sigwait().
#include <signal.h>

int sigwaitinfo (
const sigset_t *restrict set,
siginfo_t *restrict info);

int sigtimedwait (
const sigset_t *restrict set,
siginfo_t *restrict info,
const struct timespec *restrict timeout);
Листинг 3.13. Описание функций sigwaitinfo() и sigtimedwait().
Данные функции возвращают в качестве нормального результата номер полученного сигнала, входящего в набор set. Кроме того, если значение аргумента info отлично от NULL, заполняются поля si_signo (номер сигнала), si_code (источник сигнала) и, возможно, si_value (значение, если оно ассоциировано с сигналом) указуемого объекта структурного типа siginfo_t. Разумеется, полученный сигнал изымается из очереди ждущих, а соответствующие ресурсы освобождаются.
Функция sigtimedwait() отличается тем, что ограничивает время ожидания сигнала заданным интервалом. Если аргумент timeout является пустым указателем, поведение функции не специфицировано. Если реализация поддерживает монотонные часы (CLOCK_MONOTONIC), они и будут использоваться для контроля времени ожидания.
Если несколько потоков управления ждут один сигнал, он достанется кому-то одному из них.

Для приложений реального времени, функционирующих на аппаратных конфигурациях с ограниченными ресурсами, может оказаться полезной возможность (распространяющаяся на все сигналы) выполнять функции обработки сигналов не на основном, а на альтернативном стеке. Специфицировать использование альтернативного стека можно с помощью флага SA_ONSTACK, установленного в поле sa_flags структуры типа sigaction при обращении к функции sigaction(). Функция sigaltstack() (см) позволяет установить и/или опросить характеристики альтернативного стека.
#include <signal.h>
int sigaltstack (const stack_t *restrict ss,
    stack_t *restrict oss);
Согласно стандарту POSIX-2001, структурный тип stack_t должен содержать по крайней мере следующие поля.
void   *ss_sp;    /* Адрес стека  */
size_t ss_size;   /* Размер стека */
int    ss_flags;  /* Флаги        */
Если значение аргумента ss отлично от NULL, то заданные им характеристики альтернативного стека возымеют действие после возврата из функции sigaltstack(). Поле ss_flags определяет состояние нового стека. Флаг SS_DISABLE в этом поле по сути означает отказ от альтернативного стека; в таком случае значения ss_sp и ss_size игнорируются.
Альтернативный стек располагается в диапазоне адресов от ss_sp до (но не включая) ss_sp + ss_size. Стандарт не специфицирует, с какого конца и в каком направлении растет стек.
Если значением аргумента oss служит непустой указатель, то после возврата из функции sigaltstack() в указуемую структуру типа stack_t будут помещены прежние значения характеристик альтернативного стека. В частности, в поле ss_flags будет отражено состояние стека, которое могут характеризовать по крайней мере следующие флаги.
SS_ONSTACK
Этот флаг означает, что в данный момент процесс выполняется на альтернативном стеке обработки сигналов. Попытки модифицировать стек в то время, как на нем происходит выполнение, трактуются как ошибочные.
SS_DISABLE
Этот флаг означает, что в данный момент альтернативный стек обработки сигналов отключен.
Константа SIGSTKSZ задает подразумеваемый, а MINSIGSTKSZ – минимально допустимый размер альтернативного стека для функции обработки сигналов. Обычно, чтобы учесть системные накладные расходы, нужно сложить потребности приложения и MINSIGSTKSZ.
Естественно, все заботы по контролю за переполнением и исчерпанием альтернативного стека возлагаются на приложение.
На показана типичная схема определения альтернативного стека.
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
        . . .
stack_t sighstk;
    . . .

if ((sighstk.ss_sp = malloc(
SIGSTKSZ)) == NULL) {
perror ("malloc (SIGSTKSZ)");
/* Аварийное завершение */
}
sighstk.ss_size = SIGSTKSZ;
sighstk.ss_flags = 0;
if (sigaltstack (&sighstk,
(stack_t *) NULL) != 0) {
perror ("SIGALTSTACK");
. . .
}
. . .
Обработка сигналов (не обязательно реального времени) нередко сочетается с нелокальными переходами. В таких случаях могут оказаться полезными функции sigsetjmp() и siglongjmp() (смОни аналогичны функциям setjmp() и longjmp() с единственным содержательным отличием: если значение аргумента savemask функции sigsetjmp() отлично от нуля, маска сигналов вызывающего потока управления сохраняется как часть его окружения и восстанавливается после выполнения siglongjmp().
#include <setjmp.h>

int sigsetjmp (sigjmp_buf env,
int savemask);

void siglongjmp (sigjmp_buf env, int val);
Здесь сигналы реального времени служат средством межпотокового взаимодействия. В качестве значений сигналов передаются данные для захвата и освобождения вилок. В данном случае сигналы реального времени обрабатываются синхронно, посредством функции sigwaitinfo(), в рамках специально выделенного потока управления (что, конечно, нечестно). Разумеется, ожидаемые таким способом сигналы предварительно должны быть блокированы.
Для информирования философов о том, что нужные вилки захвачены и можно приступать к еде, применяются обычные сигналы, обрабатываемые асинхронно.
В качестве небольшой тонкости обратим внимание на значение второго аргумента (оно отлично от нуля) в вызове функции sigsetjmp(). Таким образом обеспечиваетсявосстановление маски сигналов после нелокального перехода из функции обработки сигнала SIG_PHIL. Если этого не сделать, сигнал SIG_PHIL останется блокированным и второй раз философ вилок уже не получит (точнее, он не узнает о том, что вилки для него захвачены).


Часы и таймеры
Стандартом POSIX-2001 предусмотрены средства создания таймеров для процессов, которые будут генерировать уведомления о наступлении заданного момента в виде рассмотренных выше сигналов реального времени. При этом разрешающая способность общесистемных часов реального времени CLOCK_REALTIME и монотонных часов CLOCK_MONOTONIC, а также основанных на них сервисов времени должна быть не хуже, чем заданное конфигурационной константой _POSIX_CLOCKRES_MIN значение 20 мсек (1/50 секунды). Естественно, реализация может обеспечивать более высокую разрешающую способность.
Если определена конфигурационная константа _POSIX_CPUTIME, значит, реализация дополнительно поддерживает для процессов часы процессорного времени с идентификатором типа clockid_t и значением CLOCK_PROCESS_CPUTIME_ID.
Если определена константа _POSIX_THREAD_CPUTIME, то аналогичные часы с идентификатором CLOCK_THREAD_CPUTIME_ID поддерживаются и для потоков управления.
Функция clock_getcpuclockid() позволяет выяснить идентификатор часов процессорного времени для заданного (отличного от вызывающего) процесса, а функция pthread_getcpuclockid() – аналогичный идентификатор для заданного потока управления текущего процесса. В принципе, это позволяет мобильным образом строить системы мониторинга выполнения независимо разработанных приложений, выявляя узкие места и ситуации перерасхода процессорного времени, что очень важно по крайней мере для выполнения требований мягкого реального времени. Правда, возможность создания таймеров на основе полученных идентификаторов объявлена в стандарте POSIX-2001 как зависящая от реализации.
Для приложений реального времени важна возможность использования не только процессорных, но и монотонных часов. В основном из этих соображений в стандарте POSIX-2001 присутствует расширенный аналог рассмотренной в курсе [1] функции nanosleep() – clock_nanosleep() (см..
#include <time.h>
int clock_nanosleep (clockid_t clock_id,
int flags, const struct timespec *rqtp,
struct timespec *rmtp);
Листинг 3.19. Описание функции clock_nanosleep().
Аргумент rqtp задает момент времени (по часам с идентификатором clock_id), до наступления которого приостанавливается выполнение текущего потока управления. Если в аргументе flags установлен флаг TIMER_ABSTIME, этот момент трактуется как абсолютный, в противном случае – как относительный.
Разумеется, «наносон» может быть прерван доставкой обрабатываемого сигнала. Если при этом значение аргумента rmtp отлично от NULL, а момент возобновления выполнения задан как относительный, то в указуемую структуру типа timespec помещается «недоспанное» время.
Отметим, что функция clock_nanosleep() полностью аналогична nanosleep(), если не устанавливать флаг TIMER_ABSTIME, а в качестве часов использовать общесистемные часы реального времени CLOCK_REALTIME.
Возможность приостановки выполнения до наступления абсолютного момента времени полезна, например, для периодических процессов, когда, после завершения всех операций текущего периода, нужно заснуть до начала следующего периода. Для nanosleep() в таком случае необходимо узнать текущее время и вычесть его из расчетного времени возобновления выполнения; clock_nanosleep() с флагом TIMER_ABSTIME позволяет сразу задать время возобновления. Более сложное вычисление аргумента функции nanosleep() плохо не столько само по себе, сколько из-за возможного вытеснения потока управления с процессора перед самым вызовом nanosleep(); в результате поток позже заснет и, соответственно, проснется позже запланированного момента времени. (Нетрудно видеть, что это общая проблема относительных таймеров.)
Понятно также, почему «недоспанное» время возвращается, только если момент возобновления выполнения задан как относительный. Чтобы «доспать» после обработки сигнала, абсолютный момент времени перевычислять не нужно, а вот в качестве относительного как раз пригодится значение, на которое указывает аргумент rmtp.
Естественно, попытка использования в clock_nanosleep() часов процессорного времени вызывающего потока управления считается ошибкой.
Следующая программа иллюстрирует типичную схему применения функции clock_nanosleep() для организации периодических процессов.
В качестве несложного упражнения читателю предлагается объяснить, почему в функции tmspc_add() наносекунды суммируются после секунд, а также почему длительность первой итерации оказывается несколько больше, чем у последующих.
Для организации периодических процессов можно воспользоваться не только часами, но и таймерами. Чтобы создать для процесса таймер, генерирующий уведомления о наступлении заданного момента в виде сигнала реального времени, следует обратиться к функции timer_create() (см).
#include <signal.h>
#include <time.h>
int timer_create (clockid_t clockid,
struct sigevent *restrict evp,
timer_t *restrict timerid);
Таймер создается на основе часов с идентификатором clockid; идентификатор таймера (уникальный в пределах вызывающего процесса) записывается по указателю timerid. Разумеется, сразу после создания таймер оказывается в невзведенном состоянии.
Аргумент evp, указывающий на структуру типа sigevent, определяет характер уведомлений о срабатывании таймера. Если его значение равно NULL, то в качестве способа уведомления принимается SIGEV_SIGNAL, при срабатывании генерируется сигнал реального времени с подразумеваемым номером и значением, равным идентификатору таймера.
Если реализация поддерживает часы процессорного времени, то подобные часы для вызывающего процесса (потока управления) могут обслуживать создаваемый таймер.

Для удаления таймера служит функция timer_delete() (см.). Взведенный таймер перед удалением автоматически разряжается.
#include <time.h>
int timer_delete (timer_t timerid);
Листинг 3.23. Описание функции timer_delete().
Для выполнения содержательных действий с таймерами служат функции timer_gettime(), timer_settime() и timer_getoverrun() (см.).
#include <time.h>

int timer_gettime (timer_t timerid,
struct itimerspec *value);

int timer_settime (timer_t timerid,
int flags,
const struct itimerspec *restrict value,
struct itimerspec *restrict ovalue);

int timer_getoverrun (timer_t timerid);
Листинг 3.24. Описание функций timer_gettime(), timer_settime() и timer_getoverrun().
Согласно стандарту POSIX-2001, структура типа itimerspec содержит по крайней мере следующие поля.
struct timespec it_interval;    
/* Интервал таймера     */
struct timespec it_value;     
/* Текущие показания таймера     */
/* (ведется обратный отсчет)     */
Значение поля it_value (если оно отлично от нуля) показывает время, оставшееся до срабатывания таймера; нулевое значение it_value свидетельствует о том, что таймер разряжен. После срабатывания таймер запускается вновь с начальным значением поля it_value, равным it_interval, если последнее отлично от нуля; в таком случае мы имеем периодический таймер.
Функция timer_gettime() запоминает текущие характеристики таймера с заданным идентификатором в структуре, на которую указывает аргумент value.
Функция timer_settime() взводит или снимает таймер со взвода, устанавливая новые характеристики, пользуясь значением аргумента value, и сохраняет старые по указателю ovalue (если он отличен от NULL).
Если в аргументе flags установлен флаг TIMER_ABSTIME, таймер взводится как абсолютный, в противном случае – как интервальный.
Если при обращении к timer_settime() заданный абсолютный момент времени уже прошел, таймер тут же срабатывает.
Округление заданного времени с учетом разрешающей способности часов всегда производится в большую сторону; таймер может сработать немного позже, но никак не раньше.
В любой момент времени в очереди к процессу может находиться не более одного сигнала о срабатывании таймера. Число избыточных срабатываний следует получать как результат функции timer_getoverrun(). Верхней границей упомянутого числа является конфигурационная константа DELAYTIMER_MAX.
Отметим, что наличие функции timer_getoverrun() позволяет избавиться от неопределенно большого расхода ресурсов на постановку в очередь сигналов реального времени, которые процесс не успевает обрабатывать.
Применим таймер для контроля длительности обеда философов (см. Листинг 3.25. Пример реализации обеда философов с использованием сигналов реального времени и таймера.
Обратим внимание на применение вызова функции end_ph_dinner() в качестве способа уведомления о срабатывании таймера. Существенно, что эта функция выполняется в контексте отдельного потока; в противном случае с терминированием потоков-философов могли возникнуть проблемы.
Отметим также использование деструктора индивидуальных данных, ассоциированных с ключом phil_key. По сути они играют роль обработчиков завершения потоков-философов; возможно, так их и следовало оформить, но тогда могло возникнуть нежелательное взаимодействие с нелокальными переходами. Кроме того, их можно было сделать более содержательными, позаботившись об освобождении ресурсов (вилок), захваченных философом, но в данном конкретном случае это не требуется.
Читателю предлагается самостоятельно объяснить, почему важно, что философы нумеруются с единицы, а не с нуля.
Довольно тонким вопросом является терминирование извне потоков управления. В разных операционных системах оно реализовано по-разному. В принципе, согласно стандарту POSIX-2001, если запрос на терминирование поступает, когда поток приостановлен в точке терминирования (каковой является, например, функция sleep()), его «будят» и он приступает к обработке запроса. Если, однако, между пробуждением и планируемым началом обработки происходит событие, которого поток ожидал, стандарт не специфицирует, будет ли обработка все же начата, или возобновится нормальное выполнение, а запрос останется ждущим (до достижения потоком следующей точки терминирования). Такого рода оговорки до некоторой степени оправдывают реализацию, при которой поступление запроса прерывает ожидание, но вместо немедленного терминирования происходит возврат из функции с результатом, свидетельствующим об ошибке, и значением errno, равным EINTR. Чтобы сделать приложение мобильным по отношению к подобным вариациям, в программе проверяется статус завершения функций sleep() и sigwaitinfo() с соответствующей реакцией на ошибки.
Возможные результаты выполнения приведенной программы показаны на Если сравнить их с результатами варианта без таймера, можно увидеть ряд любопытных различий на завершающей стадии обеда.
Листинг 3.26. Возможные результаты выполнения программы, реализующей обед философов с использованием сигналов реального времени и таймера.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика