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

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

Разбить на страницы
Показывать лекцию целиком

Основные идеи, понятия и объекты

В рамках решаемой стандартом 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() (см. листинг 3.1).

    #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]);

    Аргументами, отличающими 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() ликвидирует подобный объект (см. листинг 3.2).

    #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);

    Функция 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. Для формирования и опроса подобных объектов служат функции, показанные на листингах 3.3, 3.4 и 3.5.

    #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);
    #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);
    #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);

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

    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 и 3.7).

    typedef struct {
        short posix_attr_flags;
        pid_t posix_attr_pgroup;
        sigset_t posix_attr_sigmask;
        sigset_t posix_attr_sigdefault;
        int posix_attr_schedpolicy;
        struct sched_param posix_attr_schedparam;
    } posix_spawnattr_t;
    
    typedef char *posix_spawn_file_actions_t;
    
    int posix_spawn (pid_t *pid, const char *path,
        const posix_spawn_file_actions_t *file_actions,
        const posix_spawnattr_t *attrp, char *const argv [],
        char *const envp []);
    int posix_spawn (pid_t *pid,
        const char *path,
        const posix_spawn_file_actions_t *file_actions,
        const posix_spawnattr_t *attrp,
        char *const argv [],
        char *const envp []) {
    
        /* Создадим новый процесс */
            if ((*pid = fork()) == (pid_t) 0) {
            /* Порожденный процесс */
            /* Позаботимся о группе процессов */
                if (attrp->posix_attr_flags  POSIX_SPAWN_SETPGROUP) {
                /* Изменим унаследованную группу */
                    if (setpgid (0, attrp->posix_attr_pgroup) != 0) {
                /* Неудача */
                exit (127);
                }
            }
    
            /* Позаботимся о действующих идентификаторах     */
            /* пользователя и группы     */
                if (attrp->posix_attr_flags  POSIX_SPAWN_RESETIDS) {
                /* В данном случае неудачи быть не может */
                setuid (getuid ());
                setgid (getgid ());
            }
    
            /* Позаботимся о подразумеваемом способе     */
            /* обработки сигналов     */
                if (attrp->posix_attr_flags  POSIX_SPAWN_SETSIGDEF) {
                struct sigaction deflt;
                sigset_t all_signals;
                int s;
    
                deflt.sa_handler = SIG_DFL;
                deflt.sa_flags = 0;
    
                sigfillset (all_signals);
    
                /* Цикл по всем сигналам */
                for (s = 0; sigismember (all_signals, s); s++) {
                    if (sigismember (attrp->posix_attr_sigdefault, 
                            s)) {
                        if (sigaction (s, deflt, NULL) == -1) {
                            exit (127);
                        }
                    }
                }
            }
    
            /* Проконтролируем остальные атрибуты     */
            /* . . .                         */
    
            /* Подменим образ процесса */
            execve (path, argv, envp);
            exit (127);
        } else {
            /* Родительский (вызывающий) процесс */
            if (*pid == (pid_t) (-1))
                return errno;
            return 0;
        }
    }

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

    /* Запуск процесса с произвольным идентификатором     */
    /* пользователя                     */
    
    uid_t old_uid;
    uid_t new_uid = ...;
    
    old_uid = getuid ();
    setuid (new_uid);
    
    posix_spawn (...);
    
    setuid (old_uid);

    На листинге 3.9 показан пример перенаправления стандартных ввода и вывода порождаемого процесса с помощью формирования и использования объекта типа 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);

    Любопытно сопоставить реальные накладные расходы на одношаговое и двухшаговое порождение процессов. Программа, порождающая с помощью функции posix_spawn() практически пустые процессы, показана на листинге 3.10. На листинге 3.11 приведены данные о времени ее работы, полученные с помощью команды 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 < 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;
    }
    real 34.37
    user 12.01
    sys 22.07

    Сигналы реального времени

    Приложениям реального времени требуются средства надежного, детерминированного, асинхронного извещения (уведомления) о событиях. Теоретически для этого можно было бы разработать совершенно новый механизм, допускающий исключительно эффективную реализацию, однако авторы стандарта 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  sigval_int; 
        /* Значение сигнала – целое число */
    void *sigval_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() (см. листинг 3.12).

    #include <signal.h>
    int sigqueue (pid_t pid, int signo, 
        const union sigval value);

    Функция sigqueue() посылает сигнал с номером signo и значением value процессу, идентификатор которого задан аргументом pid. Права на посылку сигнала определяются так же, как и для функции kill() ; аналогично kill(), при нулевом значении signo сигнал не посылается, а проверяется существование процесса с идентификатором pid.

    Функция sigqueue() завершается немедленно, без какого-либо ожидания. Если для сигнала signo установлен флаг SA_SIGINFO и в наличии достаточно ресурсов, сигнал ставится в очередь к процессу pid. Если флаг SA_SIGINFO не установлен, сигнал посылается процессу-получателю по крайней мере один раз, но, возможно, без ассоциированного с ним значения.

    Если процесс посылает сигнал сам себе, он ( сигнал ) будет доставлен вызывающему потоку управления до выхода из функции sigqueue().

    Дождаться доставки сигнала реального времени можно с помощью функций sigwaitinfo() и sigtimedwait() (см. листинг 3.13), являющихся аналогами рассмотренной в курсе [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);

    Данные функции возвращают в качестве нормального результата номер полученного сигнала, входящего в набор set. Кроме того, если значение аргумента info отлично от NULL, заполняются поля si_signo (номер сигнала ), si_code (источник сигнала ) и, возможно, si_value (значение, если оно ассоциировано с сигналом ) указуемого объекта структурного типа siginfo_t. Разумеется, полученный сигнал изымается из очереди ждущих, а соответствующие ресурсы освобождаются.

    Функция sigtimedwait() отличается тем, что ограничивает время ожидания сигнала заданным интервалом. Если аргумент timeout является пустым указателем, поведение функции не специфицировано. Если реализация поддерживает монотонные часы ( CLOCK_MONOTONIC ), они и будут использоваться для контроля времени ожидания.

    Если несколько потоков управления ждут один сигнал, он достанется кому-то одному из них.

    Для приложений реального времени, функционирующих на аппаратных конфигурациях с ограниченными ресурсами, может оказаться полезной возможность (распространяющаяся на все сигналы ) выполнять функции обработки сигналов не на основном, а на альтернативном стеке. Специфицировать использование альтернативного стека можно с помощью флага SA_ONSTACK, установленного в поле sa_flags структуры типа sigaction при обращении к функции sigaction(). Функция sigaltstack() (см. листинг 3.14) позволяет установить и/или опросить характеристики альтернативного стека.

    #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.

    Естественно, все заботы по контролю за переполнением и исчерпанием альтернативного стека возлагаются на приложение.

    На листинге 3.15 показана типичная схема определения альтернативного стека.

    #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() (см. листинг 3.16). Они аналогичны функциям setjmp() и longjmp() с единственным содержательным отличием: если значение аргумента savemask функции sigsetjmp() отлично от нуля, маска сигналов вызывающего потока управления сохраняется как часть его окружения и восстанавливается после выполнения siglongjmp().

    #include <setjmp.h>
    
    int sigsetjmp (sigjmp_buf env, 
        int savemask);
    
    void siglongjmp (sigjmp_buf env, int val);

    В качестве примера использования сигналов реального времени приведем еще один вариант реализации обеда философов (см. листинг 3.17).

    /* * * * * * * * * * * * * * * * * * * * * * * */
    /* Многопотоковый вариант обеда философов     */
    /* с использованием сигналов реального времени     */
    /* * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    #include <setjmp.h>
    #include <pthread.h>
    #include <time.h>
    #include <errno.h>
    
    /* Число обедающих философов */
    #define QPH             5
    
    /* Время (в секундах) на обед */
    #define FO             15
    
    /* Длительность еды */
    #define ernd             (rand () % 3 + 1)
    
    /* Длительность разговора */
    #define trnd             (rand () % 5 + 1)
    
    /* Номер сигнала, используемого для захвата и освобождения вилок */
    #define SIG_FORK     SIGRTMIN
    
    /* Номер сигнала, используемого для информирования философа */
    #define SIG_PHIL     SIGINT
    
    static pthread_t pt_id [QPH];     /* Массив идентификаторов    */
                                    /* потоков - философов     */
    static int fork_busy [QPH] = {0, };    /* Состояние вилок     */
    static int phil_req [QPH] = {0, };    /* Невыполненные     */
                                                /* заявки на вилки     */
    static sigjmp_buf phil_env [QPH];     /* Массив буферов для     */
                                                /* нелокальных переходов    */
    
    static pid_t pid_wt;                 /* Идентификатор процесса,*/
                                                /* контролирующего вилки    */
    
    static pthread_key_t phil_key;     /* Ключ индивидуальных     */
                                                /* данных потоков-философов    */
    
    /* * * * * * * * * * * * * * * * * * * */
    /* Функция обработки сигнала SIG_PHIL     */
    /* * * * * * * * * * * * * * * * * * * */
    static void phil_eat (int signo) {
        int no; /* Номер философа, которому достался сигнал */
    
        no = (int) pthread_getspecific (phil_key);
        if ((no > 0)  (no <= QPH)) {
            siglongjmp (phil_env [no – 1], signo);
        }
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * */
    /* Попытка выполнить заявку на захват вилок     */
    /* от философа номер no, если она есть     */
    /* * * * * * * * * * * * * * * * * * * * * * */
    static void fork_lock (int no) {
        if (phil_req [no – 1] != 0) {
            /* Заявка есть.     */
            /* Вилки свободны?    */
            if ((fork_busy [no – 1] == 0)  (fork_busy 
                    [no % QPH] == 0)) {
                /* Выполним заявку */
                fork_busy [no – 1] = fork_busy [no % QPH] = 1;
                phil_req [no – 1] = 0;
                (void) pthread_kill (pt_id [no – 1], SIG_PHIL);
            }
        }
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Стартовая функция потока, обслуживающего заявки на     */
    /* захват и освобождение вилок.     */
    /* Заявка передается в виде значения, ассоциированного     */
    /* с сигналом signo.             */
    /* Значение no > 0 запрашивает захват вилок для философа     */ 
    /* с номером no, no < 0 – освобождение вилок философа -no    */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    void *start_waiter (void *signo) {
        siginfo_t sinfo;    /* Структура для получения данных     */
                                    /* о сигнале     */
        int no;                 /* Номер философа, приславшего заявку    */
        sigset_t s_sgno;    /* Маска ожидаемых сигналов     */
    
        pid_wt = getpid ();
    
        /* Сформируем маску ожидаемых сигналов */
        if ((sigemptyset (s_sgno) != 0) || (sigaddset (s_sgno, 
                (int) signo) != 0)) {
            perror ("SIGEMPTYSET/SIGADDSET");
            return (NULL);
        }
    
        while (1) {
            if (sigwaitinfo (s_sgno, sinfo) != (int) signo) {
                return (NULL);
            } else {
                /* Поступила заявка.     */
                /* Посмотрим, что от нас хотят    */
                if ((no = sinfo.si_value.sival_int) > 0) {
                /* Заявка на захват вилок.   */
                /* Запомним ее ...           */
                phil_req [no – 1] = 1;
                /* ... и попробуем выполнить */
                fork_lock (no);
                } else {
                /* Освобождение вилок */
                no = -no;
                fork_busy [no – 1] = fork_busy [no % QPH] = 0;
                /* Попробуем выполнить заявки от соседей */
                fork_lock (no % QPH + 1);
                fork_lock (no == 1 ? QPH : (no – 1));
                }
            } /* Другие сигналы нас не интересуют */
        } /* while (1) */
    }
    
    /* * * * * * * * * * * * * * * * * * */
    /* Стартовая функция потока-философа.     */
    /* Аргумент – номер философа     */
    /* * * * * * * * * * * * * * * * * * */
    void *start_phil (void *no) {
        int fo;         /* Время до конца обеда     */
        int t;         /* Время очередного отрезка еды или беседы     */
        time_t tbe;    /* Время, когда философу понадобились вилки     */
        union sigval sval;     /* Значение посылаемого сигнала:     */
                                    /* (int) no – заказ вилок     */
                                    /* -(int) no – освобождение вилок     */
    
        /* Запомним значение аргумента в качестве     */
        /* индивидуальных данных потока     */
        (void) pthread_setspecific (phil_key, no);
    
        /* Подготовка к обеду */
        fo = FO;
    
        if (sigsetjmp (phil_env [(int) no – 1], 1) != 0) {
            /* Сюда придем после нелокального перехода     */
            /* из обработчика сигнала SIG_PHIL.     */
            /* Философ просил вилки и получил их     */
            printf ("Философ %d ест\n", (int) no);
            t = ernd; sleep (t);
            /* Нужно вычесть времена еды и ожидания вилок */
            fo -= (int) (time ((time_t *) NULL) – tbe) + t;
            /* Отдает вилки */
            sval.sigval_int = -((int) no);
            (void) sigqueue (pid_wt, SIG_FORK, sval);
        }
    
        while (fo > 0) {
            printf ("Философ %d беседует\n", (int) no);
            t = trnd; sleep (t); fo -= t;
    
                    /* Пытается взять вилки */
            tbe = time ((time_t *) NULL);
            sval.sival_int = (int) no;
            (void) sigqueue (pid_wt, SIG_FORK, sval);
    
                    /* Пока вилки заняты, приходится беседовать... */
            printf ("Философ %d беседует в ожидании вилок\n", 
                        (int) no);
            sleep (fo);
            fo = 0;
        } /* while */
        printf ("Философ %d закончил обед\n", (int) no);
    
        return (NULL);
    }
    
    /* * * * * * * * * * */
    /* Организация обеда */
    /* * * * * * * * * * */
    int main (void) {
        int no;                         /* Номер философа     */
        struct sigaction sact;     /* Структура для обработки     */
                                        /* обычных сигналов     */
        pthread_t pt_wt;             /* Идентификатор потока,     */
                                            /* управляющего вилками     */
    
        /* Блокируем сигнал SIG_FORK */
        if ((sigemptyset (sact.sa_mask) == 0) 
                (sigaddset (sact.sa_mask, SIG_FORK) == 0)) {
            (void) pthread_sigmask (SIG_BLOCK, sact.sa_mask, 
                (sigset_t *) NULL);
        }
    
        /* Установим для сигнала SIG_FORK флаг SA_SIGINFO */
        sact.sa_flags = SA_SIGINFO;
        sact.sa_sigaction = (void (*) (int, siginfo_t *, 
                                        void *)) SIG_DFL;
        (void) sigaction (SIG_FORK, sact, 
            (struct sigaction *) NULL);
    
        /* Установим реакцию на сигнал SIG_PHIL */
        sact.sa_handler = phil_eat;
        sigemptyset (sact.sa_mask);
        sact.sa_flags = 0;
        (void) sigaction (SIG_PHIL, sact, 
            (struct sigaction *) NULL);
    
        /* Создадим поток, захватывающий и освобождающий вилки */
        if ((errno = pthread_create (pt_wt, NULL, 
                start_waiter, (void *) SIG_FORK)) != 0) {
            perror ("PTHREAD_CREATE-1");
            return (errno);
        }
    
        /* Создадим ключ индивидуальных данных */
        if ((errno = pthread_key_create (phil_key, 
                (void (*) (void *)) NULL)) != 0) {
            perror ("PTHREAD_KEY_CREATE");
            return (errno);
        }
    
        /* Все – к столу */
        for (no = 1;  no <= QPH; no++) {
            if ((errno = pthread_create (pt_id [no – 1], NULL, 
                    start_phil, (void *) no)) != 0) {
                perror ("PTHREAD_CREATE");
                return (no);
            }
        }
    
        /* Ожидание завершения обеда */
        for (no = 1; no <= QPH; no++) {
            (void) pthread_join (pt_id [no – 1], NULL);
        }
    
        (void) pthread_key_delete (phil_key);
    
        /* Завершим поток, контролирующий вилки */
        (void) pthread_cancel (pt_wt);
        (void) pthread_join (pt_wt, NULL);
    
        return 0;
    }

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

    Для информирования философов о том, что нужные вилки захвачены и можно приступать к еде, применяются обычные сигналы, обрабатываемые асинхронно.

    В качестве небольшой тонкости обратим внимание на значение второго аргумента (оно отлично от нуля) в вызове функции sigsetjmp(). Таким образом обеспечивается восстановление маски сигналов после нелокального перехода из функции обработки сигнала SIG_PHIL. Если этого не сделать, сигнал SIG_PHIL останется блокированным и второй раз философ вилок уже не получит (точнее, он не узнает о том, что вилки для него захвачены).

    Возможные результаты выполнения приведенной программы показаны на листинге 3.18.

    Философ 1 беседует
    Философ 2 беседует
    Философ 3 беседует
    Философ 4 беседует
    Философ 5 беседует
    Философ 4 беседует в ожидании вилок
    Философ 4 ест
    Философ 2 беседует в ожидании вилок
    Философ 2 ест
    Философ 3 беседует в ожидании вилок
    Философ 4 беседует
    Философ 1 беседует в ожидании вилок
    Философ 5 беседует в ожидании вилок
    Философ 5 ест
    Философ 2 беседует
    Философ 3 ест
    Философ 5 беседует
    Философ 1 ест
    Философ 2 беседует в ожидании вилок
    Философ 4 беседует в ожидании вилок
    Философ 3 беседует
    Философ 4 ест
    Философ 5 беседует в ожидании вилок
    Философ 1 беседует
    Философ 2 ест
    Философ 2 беседует
    Философ 4 закончил обед
    Философ 1 беседует в ожидании вилок
    Философ 5 ест
    Философ 2 беседует в ожидании вилок
    Философ 2 ест
    Философ 3 беседует в ожидании вилок
    Философ 3 закончил обед
    Философ 1 закончил обед
    Философ 5 закончил обед
    Философ 2 закончил обед

    Часы и таймеры

    Стандартом 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() (см. листинг 3.19).

    #include <time.h>
    int clock_nanosleep (clockid_t clock_id, 
        int flags, const struct timespec *rqtp,
        struct timespec *rmtp);

    Аргумент 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() часов процессорного времени вызывающего потока управления считается ошибкой.

    Следующая программа (см. листинг 3.20) иллюстрирует типичную схему применения функции clock_nanosleep() для организации периодических процессов.

    /* * * * * * * * * * * * * * * * * * * */
    /* Организация периодических процессов     */
    /* с помощью функции clock_nanosleep()     */
    /* * * * * * * * * * * * * * * * * * * */
    
    #define _XOPEN_SOURCE 600
    
    #include <time.h>
    #include <stdio.h>
    #include <unistd.h>
    
    /* * * * * * * * * * * * * * * * * * * * */
    /* Сложение двух структур типа timespec     */
    /* * * * * * * * * * * * * * * * * * * * */
    static void tmspc_add (struct timespec *a1, 
            struct timespec *a2, struct timespec *res) {
        res->tv_sec =     a1->tv_sec  + a2->tv_sec +
            (a1->tv_nsec + a2->tv_nsec) / 1000000000;
        res->tv_nsec = (a1->tv_nsec + a2->tv_nsec) % 1000000000;
    }
    
    /* * * * * * * * * * * * * * * * * * * */
    /* Организация периодического процесса     */
    /* * * * * * * * * * * * * * * * * * * */
    int main (void) {
        struct timespec t_bp;        /* Время начала очередного     */
                                            / *периода выполнения     */
        struct timespec prd = {1, 250000000}; /* Период     */
                                                     /* выполнения: 1.25 сек     */
        clockid_t clk_id = CLOCK_REALTIME; /* Идентификатор     */
                                                /* используемых часов     */
        struct timespec t_tmp;
        int i;
    /* Запомним время начала выполнения */
        (void) clock_gettime (clk_id, t_bp);
        printf ("Начало выполнения: %ld сек %ld нсек\n", 
                        t_bp.tv_sec, t_bp.tv_nsec);
    
        for (i = 0; i < 8; i++) {
            /* Содержательные действия.     */
            /* Предполагается, что они укладываются в период     */
            sleep (1);
    
            /* Доспим до конца периода */
            tmspc_add (t_bp, prd, t_bp);
            (void) clock_nanosleep (clk_id, TIMER_ABSTIME, 
                t_bp, NULL);
    
            (void) clock_gettime (clk_id, t_tmp);
            printf ("Конец периода: %ld сек %ld нсек\n", 
                        t_tmp.tv_sec, t_tmp.tv_nsec);
        }
    
        return 0;
    }

    На листинге 3.21 показаны возможные результаты выполнения приведенной программы.

    Начало выполнения: 1079080828 сек 194254000 нсек 
    Конец периода: 1079080829 сек 460021000 нсек 
    Конец периода: 1079080830 сек 710023000 нсек 
    Конец периода: 1079080831 сек 960020000 нсек 
    Конец периода: 1079080833 сек 210021000 нсек 
    Конец периода: 1079080834 сек 460023000 нсек 
    Конец периода: 1079080835 сек 710021000 нсек 
    Конец периода: 1079080836 сек 960094000 нсек 
    Конец периода: 1079080838 сек 210022000 нсек

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

    Для организации периодических процессов можно воспользоваться не только часами, но и таймерами. Чтобы создать для процесса таймер, генерирующий уведомления о наступлении заданного момента в виде сигнала реального времени, следует обратиться к функции timer_create() (см. листинг 3.22).

    #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() (см. листинг 3.23). Взведенный таймер перед удалением автоматически разряжается.

    #include <time.h>
    int timer_delete (timer_t timerid);

    Для выполнения содержательных действий с таймерами служат функции timer_gettime(), timer_settime() и timer_getoverrun() (см. листинг 3.24).

    #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);

    Согласно стандарту 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).

    /* * * * * * * * * * * * * * * * * * * * * * * */
    /* Многопотоковый вариант обеда философов     */
    /* с использованием сигналов реального времени     */
    /* и таймера для контроля длительности обеда     */
    /* * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    #include <setjmp.h>
    #include <pthread.h>
    #include <time.h>
    #include <errno.h>
    
    /* Число обедающих философов */
    #define QPH                 5
    
    /* Время (в секундах) на обед */
    #define FO                 15
    
    /* Длительность еды */
    #define ernd                 (rand () % 3 + 1)
    
    /* Длительность разговора */
    #define trnd                 (rand () % 5 + 1)
    
    /* Номер сигнала, используемого для захвата     */
    /* и освобождения вилок         */
    #define SIG_FORK         SIGRTMIN
    
    /* Номер сигнала, используемого таймером     */
    /* для окончания обеда             */
    #define SIG_DEND         (SIGRTMIN + 1)
    /* Номер сигнала, используемого     */
    /* для информирования философа     */
    #define SIG_PHIL         SIGINT
    
    static pthread_t pt_id [QPH];    /* Массив идентификаторов    */
                                                /* потоков-философов     */
    static int fork_busy [QPH] = {0, }; /* Состояние вилок    */
    static int phil_req [QPH] = {0, }; /* Невыполненные     */
                                                /* заявки на вилки     */
    static sigjmp_buf phil_env [QPH];     /* Массив буферов для    */
                                                /* нелокальных переходов    */
    
    static pid_t pid_wt;     /* Идентификатор процесса,     */
                                    /* контролирующего вилки     */
    
    static pthread_key_t phil_key;     /* Ключ индивидуальных     */
                                            /* данных потоков-философов     */
    
    /* * * * * * * * * * * * * * * * * * */
    /* Функция обработки сигнала SIG_PHIL     */
    /* * * * * * * * * * * * * * * * * * */
    static void phil_eat (int signo) {
        int no; /* Номер философа, которому достался сигнал */
    
        no = (int) pthread_getspecific (phil_key);
        if ((no > 0)  (no <= QPH)) {
            siglongjmp (phil_env [no – 1], signo);
        }
    }
    
    /* * * * * * * * * * * * * * * * * * */
    /* Деструктор индивидуальных данных     */
    /* потоков-философов     */
    /* * * * * * * * * * * * * * * * * * */
    static void phil_destructor (void *no) {
        printf ("Философ %d закончил обед\n", (int) no);
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * * */
    /* Функция, вызываемая при срабатывании таймера, */
    /* контролирующего длительность обеда     */
    /* * * * * * * * * * * * * * * * * * * * * * ** */
    static void end_ph_dinner (union sigval dummy) {
        int i;
    
        for (i = 0; i < QPH; i++) {
            (void) pthread_cancel (pt_id [i]);
        }
    }
    
    /* * * * * * * * * * * * * * * * * * * * * */
    /* Попытка выполнить заявку на захват вилок     */
    /* от философа номер no, если она есть     */
    /* * * * * * * * * * * * * * * * * * * * * */
    static void fork_lock (int no) {
        if (phil_req [no – 1] != 0) {
            /* Заявка есть.    */
            /* Вилки свободны? */
            if ((fork_busy [no – 1] == 0)  (fork_busy 
                    [no % QPH] == 0)) {
                /* Выполним заявку */
                fork_busy [no – 1] = fork_busy [no % QPH] = 1;
                phil_req [no – 1] = 0;
                (void) pthread_kill (pt_id [no – 1], SIG_PHIL);
            }
        }
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Стартовая функция потока, обслуживающего заявки на     */
    /* захват и освобождение вилок.     */
    /* Заявка передается в виде значения, ассоциированного    */
    /* с сигналом signo.             */
    /* Значение no > 0 запрашивает захват вилок для философа */
    /* с номером no, no < 0 – освобождение вилок философа -no     */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    void *start_waiter (void *signo) {
        siginfo_t sinfo;     /* Структура для получения данных    */
                                    /* о сигнале     */
        int no;                 /* Номер философа, приславшего     */
                                    /* заявку     */
        sigset_t s_sgno;     /* Маска ожидаемых сигналов     */
    
        pid_wt = getpid ();
    /* Сформируем маску ожидаемых сигналов */
        if ((sigemptyset (s_sgno) != 0) || 
                (sigaddset (s_sgno, (int) signo) != 0)) {
            perror ("SIGEMPTYSET/SIGADDSET");
            return (NULL);
        }
    
        while (1) {
            if (sigwaitinfo (s_sgno, sinfo) != (int) signo) {
                return (NULL);
            } else {
                /* Поступила заявка.     */
                /* Посмотрим, что от нас хотят */
                if ((no = sinfo.si_value.sigval_int) > 0) {
        /* Заявка на захват вилок.   */
        /* Запомним ее ...           */
        phil_req [no – 1] = 1;
        /* ... и попробуем выполнить */
        fork_lock (no);
                } else {
                /* Освобождение вилок */
                no = -no;
                fork_busy [no – 1] = fork_busy [no % QPH] = 0;
                /* Попробуем выполнить заявки от соседей */
                fork_lock (no % QPH + 1);
                fork_lock (no == 1 ? QPH : (no – 1));
                }
            } /* Другие сигналы нас не интересуют */
        } /* while (1) */
    }
    
    /* * * * * * * * * * * * * * * * * * */
    /* Стартовая функция потока-философа.     */
    /* Аргумент – номер философа     */
    /* * * * * * * * * * * * * * * * * * */
    void *start_phil (void *no) {
        union sigval sval; /* Значение посылаемого сигнала:     */
                                    /* (int) no – заказ вилок     */
                                    /* -(int) no – освобождение вилок    */
    
        /* Запомним значение аргумента в качестве     */
        /* индивидуальных данных потока     */
        (void) pthread_setspecific (phil_key, no);
    if (sigsetjmp (phil_env [(int) no – 1], 1) != 0) {
            /* Сюда придем после нелокального перехода    */
            /* из обработчика сигнала SIG_PHIL.     */
            /* Философ просил вилки и получил их      */
            printf ("Философ %d ест\n", (int) no);
            if (sleep (ernd) != 0) {
                return (NULL);
            }
            /* Отдает вилки */
            sval.sival_int = -((int) no);
            (void) sigqueue (pid_wt, SIG_FORK, sval);
        }
    
        { /* Обед */
            printf ("Философ %d беседует\n", (int) no);
            if (sleep (trnd) != 0) {
                return (NULL);
            }
    
            /* Пытается взять вилки */
            sval.sigval_int = (int) no;
            (void) sigqueue (pid_wt, SIG_FORK, sval);
    
            /* Пока вилки заняты, приходится беседовать... */
            printf ("Философ %d беседует в ожидании вилок\n", 
                        (int) no);
            sleep (FO);
        } /* Конец обеда */
    
        return (NULL);
    }
    
    /* * * * * * * * * * */
    /* Организация обеда */
    /* * * * * * * * * * */
    int main (void) {
        int no;                     /* Номер философа     */
        struct sigaction sact;    /* Структура для обработки     */
                                        /* обычных сигналов     */
        struct sigevent dend;     /* Структура для генерации     */
                                        /* сигнала таймером     */
        timer_t dend_tmrid;     /* Идентификатор таймера,     */
                                        /* контролирующего длительность     */
                                        /* обеда     */
        struct itimerspec dend_tmrsp  = {{0, 0}, {FO, 0}}; 
                                /* Структура для взведения таймера,     */
                                /* контролирующего длительность обеда    */
        pthread_t pt_wt; /* Идентификатор потока, управляющего     */
                                /* вилками     */
    
        /* Блокируем сигнал SIG_FORK */
        if ((sigemptyset (sact.sa_mask) == 0) 
                (sigaddset (sact.sa_mask, SIG_FORK) == 0)) {
            (void) pthread_sigmask (SIG_BLOCK, sact.sa_mask, 
                (sigset_t *) NULL);
        }
    
        /* Установим для сигнала SIG_FORK флаг SA_SIGINFO */
        sact.sa_flags = SA_SIGINFO;
        sact.sa_sigaction = (void (*) (int, siginfo_t *, 
            void *)) SIG_DFL;
        (void) sigaction (SIG_FORK, sact, 
            (struct sigaction *) NULL);
    
        /* Установим реакцию на сигнал SIG_PHIL */
        sact.sa_handler = phil_eat;
        sigemptyset (sact.sa_mask);
        sact.sa_flags = 0;
        (void) sigaction (SIG_PHIL, sact, 
            (struct sigaction *) NULL);
    
        /* Создадим поток, захватывающий и освобождающий вилки */
        if ((errno = pthread_create (pt_wt, NULL, 
                start_waiter, (void *) SIG_FORK)) != 0) {
            perror ("PTHREAD_CREATE-1");
            return (errno);
        }
    
        /* Создадим ключ индивидуальных данных */
        if ((errno = pthread_key_create (phil_key, 
                phil_destructor)) != 0) {
            perror ("PTHREAD_KEY_CREATE");
            return (errno);
        }
    /* Создадим и взведем таймер,     */
        /* контролирующий длительность обеда */
        dend.sigev_notify = SIGEV_THREAD;
        dend.sigev_signo = SIG_DEND;
        dend.sigev_notify_function = end_ph_dinner;
        dend.sigev_notify_attributes = NULL;
        if (timer_create (CLOCK_REALTIME, dend, 
                dend_tmrid) != 0) {
            perror ("TIMER_CREATE");
            return (-1);
        }
        if (timer_settime (dend_tmrid, 0, dend_tmrsp, 
                NULL) != 0) {
            perror ("TIMER_SETTIME");
            return (-1);
        }
    
        /* Все – к столу */
        for (no = 1;  no <= QPH; no++) {
            if ((errno = pthread_create (pt_id [no – 1], NULL, 
                    start_phil, (void *) no)) != 0) {
                perror ("PTHREAD_CREATE");
                return (no);
            }
        }
    
        /* Ожидание завершения обеда */
        for (no = 1; no <= QPH; no++) {
            (void) pthread_join (pt_id [no – 1], NULL);
        }
    
        (void) pthread_key_delete (phil_key);
        (void) timer_delete (dend_tmrid);
    
        /* Поток, контролирующий вилки,     */
        /* можно не терминировать и не ждать */
    
        return 0;
    }

    Обратим внимание на применение вызова функции end_ph_dinner() в качестве способа уведомления о срабатывании таймера. Существенно, что эта функция выполняется в контексте отдельного потока; в противном случае с терминированием потоков-философов могли возникнуть проблемы.

    Отметим также использование деструктора индивидуальных данных, ассоциированных с ключом phil_key. По сути они играют роль обработчиков завершения потоков-философов; возможно, так их и следовало оформить, но тогда могло возникнуть нежелательное взаимодействие с нелокальными переходами. Кроме того, их можно было сделать более содержательными, позаботившись об освобождении ресурсов (вилок), захваченных философом, но в данном конкретном случае это не требуется.

    Читателю предлагается самостоятельно объяснить, почему важно, что философы нумеруются с единицы, а не с нуля.

    Довольно тонким вопросом является терминирование извне потоков управления. В разных операционных системах оно реализовано по-разному. В принципе, согласно стандарту POSIX-2001, если запрос на терминирование поступает, когда поток приостановлен в точке терминирования (каковой является, например, функция sleep() ), его "будят" и он приступает к обработке запроса. Если, однако, между пробуждением и планируемым началом обработки происходит событие, которого поток ожидал, стандарт не специфицирует, будет ли обработка все же начата, или возобновится нормальное выполнение, а запрос останется ждущим (до достижения потоком следующей точки терминирования). Такого рода оговорки до некоторой степени оправдывают реализацию, при которой поступление запроса прерывает ожидание, но вместо немедленного терминирования происходит возврат из функции с результатом, свидетельствующим об ошибке, и значением errno, равным EINTR. Чтобы сделать приложение мобильным по отношению к подобным вариациям, в программе проверяется статус завершения функций sleep() и sigwaitinfo() с соответствующей реакцией на ошибки.

    Возможные результаты выполнения приведенной программы показаны на листинге 3.26. Если сравнить их с результатами варианта без таймера, можно увидеть ряд любопытных различий на завершающей стадии обеда.

    Философ 1 беседует
    Философ 2 беседует
    Философ 3 беседует
    Философ 4 беседует
    Философ 5 беседует
    Философ 4 беседует в ожидании вилок
    Философ 4 ест
    Философ 2 беседует в ожидании вилок
    Философ 2 ест
    Философ 3 беседует в ожидании вилок
    Философ 4 беседует
    Философ 1 беседует в ожидании вилок
    Философ 5 беседует в ожидании вилок
    Философ 5 ест
    Философ 2 беседует
    Философ 3 ест
    Философ 5 беседует
    Философ 1 ест
    Философ 2 беседует в ожидании вилок
    Философ 4 беседует в ожидании вилок
    Философ 3 беседует
    Философ 4 ест
    Философ 5 беседует в ожидании вилок
    Философ 1 беседует
    Философ 2 ест
    Философ 2 беседует
    Философ 1 беседует в ожидании вилок
    Философ 4 беседует
    Философ 1 ест
    Философ 2 беседует в ожидании вилок
    Философ 3 беседует в ожидании вилок
    Философ 3 ест
    Философ 1 беседует
    Философ 5 ест
    Философ 4 беседует в ожидании вилок
    Философ 5 беседует
    Философ 1 закончил обед
    Философ 2 закончил обед
    Философ 3 беседует
    Философ 4 закончил обед
    Философ 5 закончил обед
    Философ 3 беседует в ожидании вилок
    Философ 3 закончил обед
    Страницы:

    Основные идеи, понятия и объекты

    В рамках решаемой стандартом 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() (см. листинг 3.1).

    #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]);

    Аргументами, отличающими 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() ликвидирует подобный объект (см. листинг 3.2).

    #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);

    Функция 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. Для формирования и опроса подобных объектов служат функции, показанные на листингах 3.3, 3.4 и 3.5.

    #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);
    #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);
    #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);

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

    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 и 3.7).

    typedef struct {
        short posix_attr_flags;
        pid_t posix_attr_pgroup;
        sigset_t posix_attr_sigmask;
        sigset_t posix_attr_sigdefault;
        int posix_attr_schedpolicy;
        struct sched_param posix_attr_schedparam;
    } posix_spawnattr_t;
    
    typedef char *posix_spawn_file_actions_t;
    
    int posix_spawn (pid_t *pid, const char *path,
        const posix_spawn_file_actions_t *file_actions,
        const posix_spawnattr_t *attrp, char *const argv [],
        char *const envp []);
    int posix_spawn (pid_t *pid,
        const char *path,
        const posix_spawn_file_actions_t *file_actions,
        const posix_spawnattr_t *attrp,
        char *const argv [],
        char *const envp []) {
    
        /* Создадим новый процесс */
            if ((*pid = fork()) == (pid_t) 0) {
            /* Порожденный процесс */
            /* Позаботимся о группе процессов */
                if (attrp->posix_attr_flags  POSIX_SPAWN_SETPGROUP) {
                /* Изменим унаследованную группу */
                    if (setpgid (0, attrp->posix_attr_pgroup) != 0) {
                /* Неудача */
                exit (127);
                }
            }
    
            /* Позаботимся о действующих идентификаторах     */
            /* пользователя и группы     */
                if (attrp->posix_attr_flags  POSIX_SPAWN_RESETIDS) {
                /* В данном случае неудачи быть не может */
                setuid (getuid ());
                setgid (getgid ());
            }
    
            /* Позаботимся о подразумеваемом способе     */
            /* обработки сигналов     */
                if (attrp->posix_attr_flags  POSIX_SPAWN_SETSIGDEF) {
                struct sigaction deflt;
                sigset_t all_signals;
                int s;
    
                deflt.sa_handler = SIG_DFL;
                deflt.sa_flags = 0;
    
                sigfillset (all_signals);
    
                /* Цикл по всем сигналам */
                for (s = 0; sigismember (all_signals, s); s++) {
                    if (sigismember (attrp->posix_attr_sigdefault, 
                            s)) {
                        if (sigaction (s, deflt, NULL) == -1) {
                            exit (127);
                        }
                    }
                }
            }
    
            /* Проконтролируем остальные атрибуты     */
            /* . . .                         */
    
            /* Подменим образ процесса */
            execve (path, argv, envp);
            exit (127);
        } else {
            /* Родительский (вызывающий) процесс */
            if (*pid == (pid_t) (-1))
                return errno;
            return 0;
        }
    }

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

    /* Запуск процесса с произвольным идентификатором     */
    /* пользователя                     */
    
    uid_t old_uid;
    uid_t new_uid = ...;
    
    old_uid = getuid ();
    setuid (new_uid);
    
    posix_spawn (...);
    
    setuid (old_uid);

    На листинге 3.9 показан пример перенаправления стандартных ввода и вывода порождаемого процесса с помощью формирования и использования объекта типа 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);

    Любопытно сопоставить реальные накладные расходы на одношаговое и двухшаговое порождение процессов. Программа, порождающая с помощью функции posix_spawn() практически пустые процессы, показана на листинге 3.10. На листинге 3.11 приведены данные о времени ее работы, полученные с помощью команды 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 < 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;
    }
    real 34.37
    user 12.01
    sys 22.07

    Сигналы реального времени

    Приложениям реального времени требуются средства надежного, детерминированного, асинхронного извещения (уведомления) о событиях. Теоретически для этого можно было бы разработать совершенно новый механизм, допускающий исключительно эффективную реализацию, однако авторы стандарта 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  sigval_int; 
        /* Значение сигнала – целое число */
    void *sigval_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() (см. листинг 3.12).

    #include <signal.h>
    int sigqueue (pid_t pid, int signo, 
        const union sigval value);

    Функция sigqueue() посылает сигнал с номером signo и значением value процессу, идентификатор которого задан аргументом pid. Права на посылку сигнала определяются так же, как и для функции kill() ; аналогично kill(), при нулевом значении signo сигнал не посылается, а проверяется существование процесса с идентификатором pid.

    Функция sigqueue() завершается немедленно, без какого-либо ожидания. Если для сигнала signo установлен флаг SA_SIGINFO и в наличии достаточно ресурсов, сигнал ставится в очередь к процессу pid. Если флаг SA_SIGINFO не установлен, сигнал посылается процессу-получателю по крайней мере один раз, но, возможно, без ассоциированного с ним значения.

    Если процесс посылает сигнал сам себе, он ( сигнал ) будет доставлен вызывающему потоку управления до выхода из функции sigqueue().

    Дождаться доставки сигнала реального времени можно с помощью функций sigwaitinfo() и sigtimedwait() (см. листинг 3.13), являющихся аналогами рассмотренной в курсе [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);

    Данные функции возвращают в качестве нормального результата номер полученного сигнала, входящего в набор set. Кроме того, если значение аргумента info отлично от NULL, заполняются поля si_signo (номер сигнала ), si_code (источник сигнала ) и, возможно, si_value (значение, если оно ассоциировано с сигналом ) указуемого объекта структурного типа siginfo_t. Разумеется, полученный сигнал изымается из очереди ждущих, а соответствующие ресурсы освобождаются.

    Функция sigtimedwait() отличается тем, что ограничивает время ожидания сигнала заданным интервалом. Если аргумент timeout является пустым указателем, поведение функции не специфицировано. Если реализация поддерживает монотонные часы ( CLOCK_MONOTONIC ), они и будут использоваться для контроля времени ожидания.

    Если несколько потоков управления ждут один сигнал, он достанется кому-то одному из них.

    Для приложений реального времени, функционирующих на аппаратных конфигурациях с ограниченными ресурсами, может оказаться полезной возможность (распространяющаяся на все сигналы ) выполнять функции обработки сигналов не на основном, а на альтернативном стеке. Специфицировать использование альтернативного стека можно с помощью флага SA_ONSTACK, установленного в поле sa_flags структуры типа sigaction при обращении к функции sigaction(). Функция sigaltstack() (см. листинг 3.14) позволяет установить и/или опросить характеристики альтернативного стека.

    #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.

    Естественно, все заботы по контролю за переполнением и исчерпанием альтернативного стека возлагаются на приложение.

    На листинге 3.15 показана типичная схема определения альтернативного стека.

    #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() (см. листинг 3.16). Они аналогичны функциям setjmp() и longjmp() с единственным содержательным отличием: если значение аргумента savemask функции sigsetjmp() отлично от нуля, маска сигналов вызывающего потока управления сохраняется как часть его окружения и восстанавливается после выполнения siglongjmp().

    #include <setjmp.h>
    
    int sigsetjmp (sigjmp_buf env, 
        int savemask);
    
    void siglongjmp (sigjmp_buf env, int val);

    В качестве примера использования сигналов реального времени приведем еще один вариант реализации обеда философов (см. листинг 3.17).

    /* * * * * * * * * * * * * * * * * * * * * * * */
    /* Многопотоковый вариант обеда философов     */
    /* с использованием сигналов реального времени     */
    /* * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    #include <setjmp.h>
    #include <pthread.h>
    #include <time.h>
    #include <errno.h>
    
    /* Число обедающих философов */
    #define QPH             5
    
    /* Время (в секундах) на обед */
    #define FO             15
    
    /* Длительность еды */
    #define ernd             (rand () % 3 + 1)
    
    /* Длительность разговора */
    #define trnd             (rand () % 5 + 1)
    
    /* Номер сигнала, используемого для захвата и освобождения вилок */
    #define SIG_FORK     SIGRTMIN
    
    /* Номер сигнала, используемого для информирования философа */
    #define SIG_PHIL     SIGINT
    
    static pthread_t pt_id [QPH];     /* Массив идентификаторов    */
                                    /* потоков - философов     */
    static int fork_busy [QPH] = {0, };    /* Состояние вилок     */
    static int phil_req [QPH] = {0, };    /* Невыполненные     */
                                                /* заявки на вилки     */
    static sigjmp_buf phil_env [QPH];     /* Массив буферов для     */
                                                /* нелокальных переходов    */
    
    static pid_t pid_wt;                 /* Идентификатор процесса,*/
                                                /* контролирующего вилки    */
    
    static pthread_key_t phil_key;     /* Ключ индивидуальных     */
                                                /* данных потоков-философов    */
    
    /* * * * * * * * * * * * * * * * * * * */
    /* Функция обработки сигнала SIG_PHIL     */
    /* * * * * * * * * * * * * * * * * * * */
    static void phil_eat (int signo) {
        int no; /* Номер философа, которому достался сигнал */
    
        no = (int) pthread_getspecific (phil_key);
        if ((no > 0)  (no <= QPH)) {
            siglongjmp (phil_env [no – 1], signo);
        }
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * */
    /* Попытка выполнить заявку на захват вилок     */
    /* от философа номер no, если она есть     */
    /* * * * * * * * * * * * * * * * * * * * * * */
    static void fork_lock (int no) {
        if (phil_req [no – 1] != 0) {
            /* Заявка есть.     */
            /* Вилки свободны?    */
            if ((fork_busy [no – 1] == 0)  (fork_busy 
                    [no % QPH] == 0)) {
                /* Выполним заявку */
                fork_busy [no – 1] = fork_busy [no % QPH] = 1;
                phil_req [no – 1] = 0;
                (void) pthread_kill (pt_id [no – 1], SIG_PHIL);
            }
        }
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Стартовая функция потока, обслуживающего заявки на     */
    /* захват и освобождение вилок.     */
    /* Заявка передается в виде значения, ассоциированного     */
    /* с сигналом signo.             */
    /* Значение no > 0 запрашивает захват вилок для философа     */ 
    /* с номером no, no < 0 – освобождение вилок философа -no    */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    void *start_waiter (void *signo) {
        siginfo_t sinfo;    /* Структура для получения данных     */
                                    /* о сигнале     */
        int no;                 /* Номер философа, приславшего заявку    */
        sigset_t s_sgno;    /* Маска ожидаемых сигналов     */
    
        pid_wt = getpid ();
    
        /* Сформируем маску ожидаемых сигналов */
        if ((sigemptyset (s_sgno) != 0) || (sigaddset (s_sgno, 
                (int) signo) != 0)) {
            perror ("SIGEMPTYSET/SIGADDSET");
            return (NULL);
        }
    
        while (1) {
            if (sigwaitinfo (s_sgno, sinfo) != (int) signo) {
                return (NULL);
            } else {
                /* Поступила заявка.     */
                /* Посмотрим, что от нас хотят    */
                if ((no = sinfo.si_value.sival_int) > 0) {
                /* Заявка на захват вилок.   */
                /* Запомним ее ...           */
                phil_req [no – 1] = 1;
                /* ... и попробуем выполнить */
                fork_lock (no);
                } else {
                /* Освобождение вилок */
                no = -no;
                fork_busy [no – 1] = fork_busy [no % QPH] = 0;
                /* Попробуем выполнить заявки от соседей */
                fork_lock (no % QPH + 1);
                fork_lock (no == 1 ? QPH : (no – 1));
                }
            } /* Другие сигналы нас не интересуют */
        } /* while (1) */
    }
    
    /* * * * * * * * * * * * * * * * * * */
    /* Стартовая функция потока-философа.     */
    /* Аргумент – номер философа     */
    /* * * * * * * * * * * * * * * * * * */
    void *start_phil (void *no) {
        int fo;         /* Время до конца обеда     */
        int t;         /* Время очередного отрезка еды или беседы     */
        time_t tbe;    /* Время, когда философу понадобились вилки     */
        union sigval sval;     /* Значение посылаемого сигнала:     */
                                    /* (int) no – заказ вилок     */
                                    /* -(int) no – освобождение вилок     */
    
        /* Запомним значение аргумента в качестве     */
        /* индивидуальных данных потока     */
        (void) pthread_setspecific (phil_key, no);
    
        /* Подготовка к обеду */
        fo = FO;
    
        if (sigsetjmp (phil_env [(int) no – 1], 1) != 0) {
            /* Сюда придем после нелокального перехода     */
            /* из обработчика сигнала SIG_PHIL.     */
            /* Философ просил вилки и получил их     */
            printf ("Философ %d ест\n", (int) no);
            t = ernd; sleep (t);
            /* Нужно вычесть времена еды и ожидания вилок */
            fo -= (int) (time ((time_t *) NULL) – tbe) + t;
            /* Отдает вилки */
            sval.sigval_int = -((int) no);
            (void) sigqueue (pid_wt, SIG_FORK, sval);
        }
    
        while (fo > 0) {
            printf ("Философ %d беседует\n", (int) no);
            t = trnd; sleep (t); fo -= t;
    
                    /* Пытается взять вилки */
            tbe = time ((time_t *) NULL);
            sval.sival_int = (int) no;
            (void) sigqueue (pid_wt, SIG_FORK, sval);
    
                    /* Пока вилки заняты, приходится беседовать... */
            printf ("Философ %d беседует в ожидании вилок\n", 
                        (int) no);
            sleep (fo);
            fo = 0;
        } /* while */
        printf ("Философ %d закончил обед\n", (int) no);
    
        return (NULL);
    }
    
    /* * * * * * * * * * */
    /* Организация обеда */
    /* * * * * * * * * * */
    int main (void) {
        int no;                         /* Номер философа     */
        struct sigaction sact;     /* Структура для обработки     */
                                        /* обычных сигналов     */
        pthread_t pt_wt;             /* Идентификатор потока,     */
                                            /* управляющего вилками     */
    
        /* Блокируем сигнал SIG_FORK */
        if ((sigemptyset (sact.sa_mask) == 0) 
                (sigaddset (sact.sa_mask, SIG_FORK) == 0)) {
            (void) pthread_sigmask (SIG_BLOCK, sact.sa_mask, 
                (sigset_t *) NULL);
        }
    
        /* Установим для сигнала SIG_FORK флаг SA_SIGINFO */
        sact.sa_flags = SA_SIGINFO;
        sact.sa_sigaction = (void (*) (int, siginfo_t *, 
                                        void *)) SIG_DFL;
        (void) sigaction (SIG_FORK, sact, 
            (struct sigaction *) NULL);
    
        /* Установим реакцию на сигнал SIG_PHIL */
        sact.sa_handler = phil_eat;
        sigemptyset (sact.sa_mask);
        sact.sa_flags = 0;
        (void) sigaction (SIG_PHIL, sact, 
            (struct sigaction *) NULL);
    
        /* Создадим поток, захватывающий и освобождающий вилки */
        if ((errno = pthread_create (pt_wt, NULL, 
                start_waiter, (void *) SIG_FORK)) != 0) {
            perror ("PTHREAD_CREATE-1");
            return (errno);
        }
    
        /* Создадим ключ индивидуальных данных */
        if ((errno = pthread_key_create (phil_key, 
                (void (*) (void *)) NULL)) != 0) {
            perror ("PTHREAD_KEY_CREATE");
            return (errno);
        }
    
        /* Все – к столу */
        for (no = 1;  no <= QPH; no++) {
            if ((errno = pthread_create (pt_id [no – 1], NULL, 
                    start_phil, (void *) no)) != 0) {
                perror ("PTHREAD_CREATE");
                return (no);
            }
        }
    
        /* Ожидание завершения обеда */
        for (no = 1; no <= QPH; no++) {
            (void) pthread_join (pt_id [no – 1], NULL);
        }
    
        (void) pthread_key_delete (phil_key);
    
        /* Завершим поток, контролирующий вилки */
        (void) pthread_cancel (pt_wt);
        (void) pthread_join (pt_wt, NULL);
    
        return 0;
    }

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

    Для информирования философов о том, что нужные вилки захвачены и можно приступать к еде, применяются обычные сигналы, обрабатываемые асинхронно.

    В качестве небольшой тонкости обратим внимание на значение второго аргумента (оно отлично от нуля) в вызове функции sigsetjmp(). Таким образом обеспечивается восстановление маски сигналов после нелокального перехода из функции обработки сигнала SIG_PHIL. Если этого не сделать, сигнал SIG_PHIL останется блокированным и второй раз философ вилок уже не получит (точнее, он не узнает о том, что вилки для него захвачены).

    Возможные результаты выполнения приведенной программы показаны на листинге 3.18.

    Философ 1 беседует
    Философ 2 беседует
    Философ 3 беседует
    Философ 4 беседует
    Философ 5 беседует
    Философ 4 беседует в ожидании вилок
    Философ 4 ест
    Философ 2 беседует в ожидании вилок
    Философ 2 ест
    Философ 3 беседует в ожидании вилок
    Философ 4 беседует
    Философ 1 беседует в ожидании вилок
    Философ 5 беседует в ожидании вилок
    Философ 5 ест
    Философ 2 беседует
    Философ 3 ест
    Философ 5 беседует
    Философ 1 ест
    Философ 2 беседует в ожидании вилок
    Философ 4 беседует в ожидании вилок
    Философ 3 беседует
    Философ 4 ест
    Философ 5 беседует в ожидании вилок
    Философ 1 беседует
    Философ 2 ест
    Философ 2 беседует
    Философ 4 закончил обед
    Философ 1 беседует в ожидании вилок
    Философ 5 ест
    Философ 2 беседует в ожидании вилок
    Философ 2 ест
    Философ 3 беседует в ожидании вилок
    Философ 3 закончил обед
    Философ 1 закончил обед
    Философ 5 закончил обед
    Философ 2 закончил обед

    Часы и таймеры

    Стандартом 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() (см. листинг 3.19).

    #include <time.h>
    int clock_nanosleep (clockid_t clock_id, 
        int flags, const struct timespec *rqtp,
        struct timespec *rmtp);

    Аргумент 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() часов процессорного времени вызывающего потока управления считается ошибкой.

    Следующая программа (см. листинг 3.20) иллюстрирует типичную схему применения функции clock_nanosleep() для организации периодических процессов.

    /* * * * * * * * * * * * * * * * * * * */
    /* Организация периодических процессов     */
    /* с помощью функции clock_nanosleep()     */
    /* * * * * * * * * * * * * * * * * * * */
    
    #define _XOPEN_SOURCE 600
    
    #include <time.h>
    #include <stdio.h>
    #include <unistd.h>
    
    /* * * * * * * * * * * * * * * * * * * * */
    /* Сложение двух структур типа timespec     */
    /* * * * * * * * * * * * * * * * * * * * */
    static void tmspc_add (struct timespec *a1, 
            struct timespec *a2, struct timespec *res) {
        res->tv_sec =     a1->tv_sec  + a2->tv_sec +
            (a1->tv_nsec + a2->tv_nsec) / 1000000000;
        res->tv_nsec = (a1->tv_nsec + a2->tv_nsec) % 1000000000;
    }
    
    /* * * * * * * * * * * * * * * * * * * */
    /* Организация периодического процесса     */
    /* * * * * * * * * * * * * * * * * * * */
    int main (void) {
        struct timespec t_bp;        /* Время начала очередного     */
                                            / *периода выполнения     */
        struct timespec prd = {1, 250000000}; /* Период     */
                                                     /* выполнения: 1.25 сек     */
        clockid_t clk_id = CLOCK_REALTIME; /* Идентификатор     */
                                                /* используемых часов     */
        struct timespec t_tmp;
        int i;
    /* Запомним время начала выполнения */
        (void) clock_gettime (clk_id, t_bp);
        printf ("Начало выполнения: %ld сек %ld нсек\n", 
                        t_bp.tv_sec, t_bp.tv_nsec);
    
        for (i = 0; i < 8; i++) {
            /* Содержательные действия.     */
            /* Предполагается, что они укладываются в период     */
            sleep (1);
    
            /* Доспим до конца периода */
            tmspc_add (t_bp, prd, t_bp);
            (void) clock_nanosleep (clk_id, TIMER_ABSTIME, 
                t_bp, NULL);
    
            (void) clock_gettime (clk_id, t_tmp);
            printf ("Конец периода: %ld сек %ld нсек\n", 
                        t_tmp.tv_sec, t_tmp.tv_nsec);
        }
    
        return 0;
    }

    На листинге 3.21 показаны возможные результаты выполнения приведенной программы.

    Начало выполнения: 1079080828 сек 194254000 нсек 
    Конец периода: 1079080829 сек 460021000 нсек 
    Конец периода: 1079080830 сек 710023000 нсек 
    Конец периода: 1079080831 сек 960020000 нсек 
    Конец периода: 1079080833 сек 210021000 нсек 
    Конец периода: 1079080834 сек 460023000 нсек 
    Конец периода: 1079080835 сек 710021000 нсек 
    Конец периода: 1079080836 сек 960094000 нсек 
    Конец периода: 1079080838 сек 210022000 нсек

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

    Для организации периодических процессов можно воспользоваться не только часами, но и таймерами. Чтобы создать для процесса таймер, генерирующий уведомления о наступлении заданного момента в виде сигнала реального времени, следует обратиться к функции timer_create() (см. листинг 3.22).

    #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() (см. листинг 3.23). Взведенный таймер перед удалением автоматически разряжается.

    #include <time.h>
    int timer_delete (timer_t timerid);

    Для выполнения содержательных действий с таймерами служат функции timer_gettime(), timer_settime() и timer_getoverrun() (см. листинг 3.24).

    #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);

    Согласно стандарту 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).

    /* * * * * * * * * * * * * * * * * * * * * * * */
    /* Многопотоковый вариант обеда философов     */
    /* с использованием сигналов реального времени     */
    /* и таймера для контроля длительности обеда     */
    /* * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    #include <setjmp.h>
    #include <pthread.h>
    #include <time.h>
    #include <errno.h>
    
    /* Число обедающих философов */
    #define QPH                 5
    
    /* Время (в секундах) на обед */
    #define FO                 15
    
    /* Длительность еды */
    #define ernd                 (rand () % 3 + 1)
    
    /* Длительность разговора */
    #define trnd                 (rand () % 5 + 1)
    
    /* Номер сигнала, используемого для захвата     */
    /* и освобождения вилок         */
    #define SIG_FORK         SIGRTMIN
    
    /* Номер сигнала, используемого таймером     */
    /* для окончания обеда             */
    #define SIG_DEND         (SIGRTMIN + 1)
    /* Номер сигнала, используемого     */
    /* для информирования философа     */
    #define SIG_PHIL         SIGINT
    
    static pthread_t pt_id [QPH];    /* Массив идентификаторов    */
                                                /* потоков-философов     */
    static int fork_busy [QPH] = {0, }; /* Состояние вилок    */
    static int phil_req [QPH] = {0, }; /* Невыполненные     */
                                                /* заявки на вилки     */
    static sigjmp_buf phil_env [QPH];     /* Массив буферов для    */
                                                /* нелокальных переходов    */
    
    static pid_t pid_wt;     /* Идентификатор процесса,     */
                                    /* контролирующего вилки     */
    
    static pthread_key_t phil_key;     /* Ключ индивидуальных     */
                                            /* данных потоков-философов     */
    
    /* * * * * * * * * * * * * * * * * * */
    /* Функция обработки сигнала SIG_PHIL     */
    /* * * * * * * * * * * * * * * * * * */
    static void phil_eat (int signo) {
        int no; /* Номер философа, которому достался сигнал */
    
        no = (int) pthread_getspecific (phil_key);
        if ((no > 0)  (no <= QPH)) {
            siglongjmp (phil_env [no – 1], signo);
        }
    }
    
    /* * * * * * * * * * * * * * * * * * */
    /* Деструктор индивидуальных данных     */
    /* потоков-философов     */
    /* * * * * * * * * * * * * * * * * * */
    static void phil_destructor (void *no) {
        printf ("Философ %d закончил обед\n", (int) no);
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * * */
    /* Функция, вызываемая при срабатывании таймера, */
    /* контролирующего длительность обеда     */
    /* * * * * * * * * * * * * * * * * * * * * * ** */
    static void end_ph_dinner (union sigval dummy) {
        int i;
    
        for (i = 0; i < QPH; i++) {
            (void) pthread_cancel (pt_id [i]);
        }
    }
    
    /* * * * * * * * * * * * * * * * * * * * * */
    /* Попытка выполнить заявку на захват вилок     */
    /* от философа номер no, если она есть     */
    /* * * * * * * * * * * * * * * * * * * * * */
    static void fork_lock (int no) {
        if (phil_req [no – 1] != 0) {
            /* Заявка есть.    */
            /* Вилки свободны? */
            if ((fork_busy [no – 1] == 0)  (fork_busy 
                    [no % QPH] == 0)) {
                /* Выполним заявку */
                fork_busy [no – 1] = fork_busy [no % QPH] = 1;
                phil_req [no – 1] = 0;
                (void) pthread_kill (pt_id [no – 1], SIG_PHIL);
            }
        }
    }
    
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Стартовая функция потока, обслуживающего заявки на     */
    /* захват и освобождение вилок.     */
    /* Заявка передается в виде значения, ассоциированного    */
    /* с сигналом signo.             */
    /* Значение no > 0 запрашивает захват вилок для философа */
    /* с номером no, no < 0 – освобождение вилок философа -no     */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    void *start_waiter (void *signo) {
        siginfo_t sinfo;     /* Структура для получения данных    */
                                    /* о сигнале     */
        int no;                 /* Номер философа, приславшего     */
                                    /* заявку     */
        sigset_t s_sgno;     /* Маска ожидаемых сигналов     */
    
        pid_wt = getpid ();
    /* Сформируем маску ожидаемых сигналов */
        if ((sigemptyset (s_sgno) != 0) || 
                (sigaddset (s_sgno, (int) signo) != 0)) {
            perror ("SIGEMPTYSET/SIGADDSET");
            return (NULL);
        }
    
        while (1) {
            if (sigwaitinfo (s_sgno, sinfo) != (int) signo) {
                return (NULL);
            } else {
                /* Поступила заявка.     */
                /* Посмотрим, что от нас хотят */
                if ((no = sinfo.si_value.sigval_int) > 0) {
        /* Заявка на захват вилок.   */
        /* Запомним ее ...           */
        phil_req [no – 1] = 1;
        /* ... и попробуем выполнить */
        fork_lock (no);
                } else {
                /* Освобождение вилок */
                no = -no;
                fork_busy [no – 1] = fork_busy [no % QPH] = 0;
                /* Попробуем выполнить заявки от соседей */
                fork_lock (no % QPH + 1);
                fork_lock (no == 1 ? QPH : (no – 1));
                }
            } /* Другие сигналы нас не интересуют */
        } /* while (1) */
    }
    
    /* * * * * * * * * * * * * * * * * * */
    /* Стартовая функция потока-философа.     */
    /* Аргумент – номер философа     */
    /* * * * * * * * * * * * * * * * * * */
    void *start_phil (void *no) {
        union sigval sval; /* Значение посылаемого сигнала:     */
                                    /* (int) no – заказ вилок     */
                                    /* -(int) no – освобождение вилок    */
    
        /* Запомним значение аргумента в качестве     */
        /* индивидуальных данных потока     */
        (void) pthread_setspecific (phil_key, no);
    if (sigsetjmp (phil_env [(int) no – 1], 1) != 0) {
            /* Сюда придем после нелокального перехода    */
            /* из обработчика сигнала SIG_PHIL.     */
            /* Философ просил вилки и получил их      */
            printf ("Философ %d ест\n", (int) no);
            if (sleep (ernd) != 0) {
                return (NULL);
            }
            /* Отдает вилки */
            sval.sival_int = -((int) no);
            (void) sigqueue (pid_wt, SIG_FORK, sval);
        }
    
        { /* Обед */
            printf ("Философ %d беседует\n", (int) no);
            if (sleep (trnd) != 0) {
                return (NULL);
            }
    
            /* Пытается взять вилки */
            sval.sigval_int = (int) no;
            (void) sigqueue (pid_wt, SIG_FORK, sval);
    
            /* Пока вилки заняты, приходится беседовать... */
            printf ("Философ %d беседует в ожидании вилок\n", 
                        (int) no);
            sleep (FO);
        } /* Конец обеда */
    
        return (NULL);
    }
    
    /* * * * * * * * * * */
    /* Организация обеда */
    /* * * * * * * * * * */
    int main (void) {
        int no;                     /* Номер философа     */
        struct sigaction sact;    /* Структура для обработки     */
                                        /* обычных сигналов     */
        struct sigevent dend;     /* Структура для генерации     */
                                        /* сигнала таймером     */
        timer_t dend_tmrid;     /* Идентификатор таймера,     */
                                        /* контролирующего длительность     */
                                        /* обеда     */
        struct itimerspec dend_tmrsp  = {{0, 0}, {FO, 0}}; 
                                /* Структура для взведения таймера,     */
                                /* контролирующего длительность обеда    */
        pthread_t pt_wt; /* Идентификатор потока, управляющего     */
                                /* вилками     */
    
        /* Блокируем сигнал SIG_FORK */
        if ((sigemptyset (sact.sa_mask) == 0) 
                (sigaddset (sact.sa_mask, SIG_FORK) == 0)) {
            (void) pthread_sigmask (SIG_BLOCK, sact.sa_mask, 
                (sigset_t *) NULL);
        }
    
        /* Установим для сигнала SIG_FORK флаг SA_SIGINFO */
        sact.sa_flags = SA_SIGINFO;
        sact.sa_sigaction = (void (*) (int, siginfo_t *, 
            void *)) SIG_DFL;
        (void) sigaction (SIG_FORK, sact, 
            (struct sigaction *) NULL);
    
        /* Установим реакцию на сигнал SIG_PHIL */
        sact.sa_handler = phil_eat;
        sigemptyset (sact.sa_mask);
        sact.sa_flags = 0;
        (void) sigaction (SIG_PHIL, sact, 
            (struct sigaction *) NULL);
    
        /* Создадим поток, захватывающий и освобождающий вилки */
        if ((errno = pthread_create (pt_wt, NULL, 
                start_waiter, (void *) SIG_FORK)) != 0) {
            perror ("PTHREAD_CREATE-1");
            return (errno);
        }
    
        /* Создадим ключ индивидуальных данных */
        if ((errno = pthread_key_create (phil_key, 
                phil_destructor)) != 0) {
            perror ("PTHREAD_KEY_CREATE");
            return (errno);
        }
    /* Создадим и взведем таймер,     */
        /* контролирующий длительность обеда */
        dend.sigev_notify = SIGEV_THREAD;
        dend.sigev_signo = SIG_DEND;
        dend.sigev_notify_function = end_ph_dinner;
        dend.sigev_notify_attributes = NULL;
        if (timer_create (CLOCK_REALTIME, dend, 
                dend_tmrid) != 0) {
            perror ("TIMER_CREATE");
            return (-1);
        }
        if (timer_settime (dend_tmrid, 0, dend_tmrsp, 
                NULL) != 0) {
            perror ("TIMER_SETTIME");
            return (-1);
        }
    
        /* Все – к столу */
        for (no = 1;  no <= QPH; no++) {
            if ((errno = pthread_create (pt_id [no – 1], NULL, 
                    start_phil, (void *) no)) != 0) {
                perror ("PTHREAD_CREATE");
                return (no);
            }
        }
    
        /* Ожидание завершения обеда */
        for (no = 1; no <= QPH; no++) {
            (void) pthread_join (pt_id [no – 1], NULL);
        }
    
        (void) pthread_key_delete (phil_key);
        (void) timer_delete (dend_tmrid);
    
        /* Поток, контролирующий вилки,     */
        /* можно не терминировать и не ждать */
    
        return 0;
    }

    Обратим внимание на применение вызова функции end_ph_dinner() в качестве способа уведомления о срабатывании таймера. Существенно, что эта функция выполняется в контексте отдельного потока; в противном случае с терминированием потоков-философов могли возникнуть проблемы.

    Отметим также использование деструктора индивидуальных данных, ассоциированных с ключом phil_key. По сути они играют роль обработчиков завершения потоков-философов; возможно, так их и следовало оформить, но тогда могло возникнуть нежелательное взаимодействие с нелокальными переходами. Кроме того, их можно было сделать более содержательными, позаботившись об освобождении ресурсов (вилок), захваченных философом, но в данном конкретном случае это не требуется.

    Читателю предлагается самостоятельно объяснить, почему важно, что философы нумеруются с единицы, а не с нуля.

    Довольно тонким вопросом является терминирование извне потоков управления. В разных операционных системах оно реализовано по-разному. В принципе, согласно стандарту POSIX-2001, если запрос на терминирование поступает, когда поток приостановлен в точке терминирования (каковой является, например, функция sleep() ), его "будят" и он приступает к обработке запроса. Если, однако, между пробуждением и планируемым началом обработки происходит событие, которого поток ожидал, стандарт не специфицирует, будет ли обработка все же начата, или возобновится нормальное выполнение, а запрос останется ждущим (до достижения потоком следующей точки терминирования). Такого рода оговорки до некоторой степени оправдывают реализацию, при которой поступление запроса прерывает ожидание, но вместо немедленного терминирования происходит возврат из функции с результатом, свидетельствующим об ошибке, и значением errno, равным EINTR. Чтобы сделать приложение мобильным по отношению к подобным вариациям, в программе проверяется статус завершения функций sleep() и sigwaitinfo() с соответствующей реакцией на ошибки.

    Возможные результаты выполнения приведенной программы показаны на листинге 3.26. Если сравнить их с результатами варианта без таймера, можно увидеть ряд любопытных различий на завершающей стадии обеда.

    Философ 1 беседует
    Философ 2 беседует
    Философ 3 беседует
    Философ 4 беседует
    Философ 5 беседует
    Философ 4 беседует в ожидании вилок
    Философ 4 ест
    Философ 2 беседует в ожидании вилок
    Философ 2 ест
    Философ 3 беседует в ожидании вилок
    Философ 4 беседует
    Философ 1 беседует в ожидании вилок
    Философ 5 беседует в ожидании вилок
    Философ 5 ест
    Философ 2 беседует
    Философ 3 ест
    Философ 5 беседует
    Философ 1 ест
    Философ 2 беседует в ожидании вилок
    Философ 4 беседует в ожидании вилок
    Философ 3 беседует
    Философ 4 ест
    Философ 5 беседует в ожидании вилок
    Философ 1 беседует
    Философ 2 ест
    Философ 2 беседует
    Философ 1 беседует в ожидании вилок
    Философ 4 беседует
    Философ 1 ест
    Философ 2 беседует в ожидании вилок
    Философ 3 беседует в ожидании вилок
    Философ 3 ест
    Философ 1 беседует
    Философ 5 ест
    Философ 4 беседует в ожидании вилок
    Философ 5 беседует
    Философ 1 закончил обед
    Философ 2 закончил обед
    Философ 3 беседует
    Философ 4 закончил обед
    Философ 5 закончил обед
    Философ 3 беседует в ожидании вилок
    Философ 3 закончил обед
    Вернуться к учебному плану