• Что такое процесс?
  • Структура процесса
  • Таблица процессов
  • Просмотр процессов
  • Системные процессы
  • Планирование процессов
  • Запуск новых процессов
  • Замена образа процесса
  • Дублирование образа процесса
  • Ожидание процесса
  • Процессы-зомби
  • Перенаправление ввода и вывода
  • Потоки 
  • Сигналы
  • Отправка сигналов
  • Множества сигналов
  • Резюме 
  • Глава 11

    Процессы и сигналы

    Процессы и сигналы формируют главную часть операционной среды Linux. Они управляют почти всеми видами деятельности ОС Linux и UNIX-подобных компьютерных систем. Понимание того, как Linux и UNIX управляют процессами, сослужит добрую службу системным и прикладным программистам или системным администраторам.

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

    □ структуре процесса, его типе и планировании;

    □ разных способах запуска новых процессов;

    □ порождающих (родительских), порожденных (дочерних) процессах и процессах-зомби;

    □ сигналах и их применении.

    Что такое процесс?

    Стандарты UNIX, а именно IEEE Std 1003.1, 2004 Edition, определяют процесс как "адресное пространство с одним или несколькими потоками, выполняющимися в нем, и системные ресурсы, необходимые этим потокам. Мы будем рассматривать потоки в главе 12, а пока будем считать процессом просто любую выполняющуюся программу.

    Многозадачные системы, такие как Linux, позволяют многим программам выполняться одновременно. Каждый экземпляр выполняющейся программы создает процесс. Это особенно заметно в оконной системе, например Window System (часто называемой просто X). Как и ОС Windows, X предоставляет графический пользовательский интерфейс, позволяющий многим приложениям выполняться одновременно. Каждое приложение может отображаться в одном или нескольких окнах.

    Будучи многопользовательской системой, Linux разрешает многим пользователям одновременно обращаться к системе. Каждый пользователь в одно и то же время может запускать много программ или даже несколько экземпляров одной и той же программы. Сама система выполняет в это время другие программы, управляющие системными ресурсами и контролирующие доступ пользователей.

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

    Структура процесса

    Давайте посмотрим, как организовано сосуществование двух процессов в операционной системе. Если два пользователя neil и rick запускают в одно и то же время программу grep для поиска разных строк в различных файлах, применяемые для этого процессы могут выглядеть так, как показано на рис. 11.1.

    Рис. 11.1 


    Если вы сможете выполнить команду

    ps
    , как в приведенном далее коде, достаточно быстро и до того, как завершатся поиски строк, вывод будет выглядеть подобно следующим строкам:

    $ ps -ef

    UID  PID PPID С STIME TTY  TIME     CMD

    rick 101 96   0 18:24 tty2 00:00:00 grep troi nextgen.doc

    neil 102 92   0 18:24 tty4 00:00:00 grep kirk trek.txt

    Каждому процессу выделяется уникальный номер, именуемый идентификатором процесса или PID. Обычно это положительное целое в диапазоне от 2 до 32 768. Когда процесс стартует, в последовательности выбирается следующее неиспользованное число. Когда все номера будут исчерпаны, выбор опять начнется с 2. Номер 1 обычно зарезервирован для специального процесса

    init
    , который управляет другими процессами. Мы скоро вернемся к процессу
    init
    . А пока вы видите, что двум процессам, запущенным пользователями neil и rick, выделены идентификаторы 101 и 102.

    Код программы, которая будет выполняться командой

    grep
    , хранится в файле на диске. Обычно процесс Linux не может писать в область памяти, применяемую для хранения кода программы, поэтому программный код загружается в память как доступный только для чтения. На рис. 11.1 видно, что несмотря на то, что в данную область нельзя писать, она может безопасно использоваться совместно.

    Системные библиотеки также можно совместно использовать. Следовательно, в памяти нужна, например, только одна копия функции

    printf
    , даже если многие выполняющиеся программы вызывают ее. Эта схема более сложная, но аналогичная той, которую используют для работы динамически подключаемые библиотеки в ОС Windows.

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

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

    Конечно не все, что нужно программе, может быть совместно использовано. Например, переменные отдельно используются каждым процессом. В данном примере искомая строка, передаваемая команде

    grep
    , — это переменная
    s
    , принадлежащая пространству данных каждого процесса. Эти пространства разделены и, как правило, не могут читаться другим процессом. Файлы, которые применяются в двух командах grep, тоже разные; у каждого процесса есть свой набор файловых дескрипторов, используемых для доступа к файлам.

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

    putenv
    и
    getenv
    , как было показано в главе 4. Процесс должен поддерживать собственный счетчик программы, запись того места, до которого он добрался за время выполнения, или поток исполнения. В следующей главе вы увидите, что процессы могут иметь несколько потоков исполнения.

    Во многих системах Linux и некоторых системах UNIX существует специальный набор "файлов" в каталоге /proc. Это скорее специальные, чем истинные файлы, т.к. позволяют "заглянуть внутрь" процессов во время их выполнения, как если бы они были файлами в каталогах, В главе 3 мы приводили краткий обзор файловой системы /proc.

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

    Таблица процессов

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

    ps
    . Операционная система управляет процессами с помощью их идентификаторов, PID, которые применяются как указатели в таблице процессов. У таблицы ограниченный размер, поэтому число процессов, поддерживаемых системой, ограничено. В первых системах UNIX оно равнялось 256 процессам. Более современные реализации значительно ослабили это ограничение и ограничены только объемом памяти, доступным для формирования элемента таблицы процессов.

    Просмотр процессов

    Команда

    ps
    показывает выполняемые вами процессы, процессы, выполняемые другим пользователем, или все процессы в системе. Далее приведен еще один пример вывода:

    $ ps -ef

    UID  PID PPID  С STIME  TTY      TIME CMD

    root 433  425  0 18:12  tty1 00:00:00 [bash]

    rick 445  426  0 18:12  tty2 00:00:00 -bash

    rick 456  427  0 18:12  tty3 00:00:00 [bash]

    root 467  433  0 18:12  tty1 00:00:00 sh /usr/X11R6/bin/startx

    root 474  467  0 18:12  tty1 00:00:00 xinit /etc/X11/xinit/xinitrc --

    root 478  474  0 18:12  tty1 00:00:00 /usr/bin/gnome-session

    root 487    1  0 18:12  tty1 00:00:00 gnome-smproxy --sm-client-id def

    root 493    1  0 18:12  tty1 00:00:01 [enlightenment]

    root 506    1  0 18:12  tty1 00:00:03 panel --sm-client-id defaults

    root 508    1  0 18:12  tty1 00:00:00 xscreensaver -no-splash -timeout

    root 510    1  0 18:12  tty1 00:00:01 gmc --sm-client-id default10

    root 512    1  0 18:12  tty1 00:00:01 gnome-help-browser --sm-client-i

    root 649  445  0 18:24  tty2 00:00:00 su

    root 653  649  0 18:24  tty2 00:00:00 bash

    neil 655  428  0 18:24  tty4 00:00:00 -bash

    root 713    1  2 18:27  tty1 00:00:00 gnome-terminal

    root 715  713  0 18:28  tty1 00:00:00 gnome-pty-helper

    root 717  716 13 18:28 pts/0 00:00:01 emacs

    root 718  653  0 18:28  tty2 00:00:00 ps -ef

    Вывод отображает информацию о многих процессах, включая процессы, запущенные редактором Emacs в графической среде X ОС Linux. Например, столбец

    TTY
    показывает, с какого терминала стартовал процесс, столбец
    TIME
    показывает время ЦПУ, затраченное к данному моменту, а столбец
    CMD
    — команду, примененную для запуска процесса. Давайте познакомимся поближе с некоторыми из этих процессов.

    neil 655  428  0 18:24  tty4 00:00:00 -bash

    Начальная регистрация была произведена на консоли номер 4. Это просто консоль на данном компьютере. Выполняемая программа командной оболочки — это стандартная оболочка Linux,

    bash
    .

    root 467  433  0 18:12  tty1 00:00:00 sh /usr/X11R6/bin/startx

    X Window System была запущена командой

    startx
    . Это сценарий командной оболочки, который запускает сервер X и выполняет некоторые начальные программы системы X.

    root 717  716 13 18:28 pts/0 00:00:01 emacs

    Этот процесс представляет окно в системе X, выполняющее программу Emacs. Он был запущен оконным диспетчером в ответ на запрос нового окна. Командной оболочке был назначен новый псевдотерминал pts/0 для считывания и записи.

    root 512    1  0 18:12  tty1 00:00:01 gnome-help-browser --sm-client-i

    Это обозреватель системы помощи среды GNOME, запущенный оконным диспетчером.

    По умолчанию программа

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

    Примечание

    Точная синтаксическая запись команды

    ps
    и формат вывода могут немного отличаться в разных системах. Версия GNU команды
    ps
    , применяемая в Linux, поддерживает опции, взятые из нескольких предшествующих реализаций
    ps
    , включая варианты из UNIX-систем BSD и AT&T, и добавляет множество своих опций. См. интерактивное справочное руководство для получения подробных сведений о доступных опциях и форматах вывода команды
    ps
    .

    Системные процессы

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

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


    Таблица 11.1

    Код STAT Описание
    S
    Спящий. Обычно ждет появления события, такого как сигнал или активизация ввода
    R
    Выполняющийся. Строго говоря "работоспособный", т.е. в очереди на выполнение, либо выполняющийся, либо готовый к выполнению
    D
    Непрерывно спящий (ожидающий). Обычно ждущий завершения ввода или вывода
    T
    Остановленный. Обычно остановленный системой управления заданиями командной оболочки или находящийся под контролем отладчика
    Z
    Умерший или процесс-зомби
    N
    Задача с низким приоритетом, "nice"
    W
    Разбитый на страницы (не используется в Linux с ядром версии 2.6 и последующих версий)
    S
    Ведущий процесс сеанса
    +
    Процесс в группе фоновых процессов
    l
    Многопотоковый процесс
    <
    Задача с высоким приоритетом

    $ ps ах

    PID   TTY   STAT TIME COMMAND

    1     ?     Ss   0:03 init [5]

    2     ?     S    0:00 [migration/0]

    3     ?     SN   0:00 [ksoftirqd/0]

    4     ?     S<   0:05 [events/0]

    5     ?     S<   0:00 [khelper]

    6     ?     S<   0:00 [kthread]

    840   ?     S<   2:52 [kjournald]

    888   ?     S<s  0:03 /sbin/udevd --daemon

    3069  ?     Ss   0:00 /sbin/acpid

    3098  ?     Ss   0:11 /usr/sbin/hald --daemon=yes

    3099  ?     S    0:00 hald-runner

    8357  ?     Ss   0:03 /sbin/syslog-ng

    8677  ?     Ss   0:00 /opt/kde3/bin/kdm

    9119  ?     S    0:11 konsole [kdeinit]

    9120  pts/2 Ss   0:00 /bin/bash

    9151  ?     Ss   0:00 /usr/sbin/cupsd

    9457  ?     Ss   0:00 /usr/sbin/cron

    9479  ?     Ss   0:00 /usr/sbin/sshd -o PidFile=/var/run/sshd.init.pid

    9618  tty1  Ss+  0:00 /sbin/mingetty --noclear tty1

    9619  tty2  Ss+  0:00 /sbin/mingetty tty2

    9621  tty3  Ss+  0:00 /sbin/mingetty tty3

    9622  tty4  Ss+  0:00 /sbin/mingetty tty4

    9623  tty5  Ss+  0:00 /sbin/mingetty tty5

    9638  tty6  Ss+  0:00 /sbin/mingetty tty6

    10359 tty1  Ss+ 10:05 /usr/bin/Xorg -br -nolisten tcp :0 vt7 -auth

    10360 ?     S    0:00 -:0

    10381 ?     Ss   0:00 /bin/sh /usr/bin/kde

    10438 ?     Ss   0:00 /usr/bin/ssh-agent /bin/bash /etc/X11/xinit/xinitrc

    10478 ?     S    0:00 start_kdeinit --new-startup +kcminit_startup

    10479 ?     Ss   0:00 kdeinit Running...

    10500 ?     S    0:53 kdesktop [kdeinit]

    10502 ?     S    1:54 kicker [kdeinit]

    10524 ?     Sl   0:47 beagled /usr/lib/beagle/BeagleDaemon.exe --bg

    10530 ?     S    0:02 opensuseupdater

    10539 ?     S    0:02 kpowersave [kdeinit]

    10541 ?     S    0:03 klipper [kdeinit]

    10555 ?     S    0:01 kio_uiserver [kdeinit]

    10688 ?     S    0:53 konsole [kdeinit]

    10689 pts/1 Ss+  0:07 /bin/bash

    10784 ?     S    0:00 /opt/kde3/bin/kdesud

    11052 ?     S    0:01 [pdflush]

    19996 ?     SN1  0:20 beagled-helper /usr/lib/beagle/IndexHelper.exe

    20254 ?     S    0:00 qmgr -1 -t fifo -u

    21192 ?     Ss   0:00 /usr/sbin/ntpd -p /var/run/ntp/ntpd.pid -u ntp -i /v

    21198 ?     S    0:00 pickup -1 -t fifo -u

    21475 pts/2 R+   0:00 ps ax

    Здесь вы видите на самом деле очень важный процесс

    1     ?     Ss   0:03 init [5]

    В основном каждый процесс запускается другим процессом, называемым родительским или порождающим процессом. Подобным образом запущенный процесс называют дочерним или порожденным. Когда стартует ОС Linux, она выполняет единственную программу, первого предка и процесс с номером 1,

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

    Один из таких примеров — процедура регистрации. Процесс

    init
    запускает программу
    getty
    для каждого последовательного терминала или модема коммутируемой линии передачи, которые можно применять для регистрации. Эти процессы отображены в следующем выводе команды
    ps
    :

    9619  tty2  Ss+  0:00 /sbin/mingetty tty2

    Процессы

    getty
    ждут работы на терминале, приглашая пользователя зарегистрироваться хорошо всем знакомой строкой, и затем передают управление программе регистрации, которая устанавливает окружение пользователя и в конце запускает сеанс командной оболочки. Когда пользовательский сеанс командной оболочки завершается, процесс
    init
    запускает новый процесс
    getty
    .

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

    fork
    ,
    exec
    и
    wait
    .

    Планирование процессов

    В следующем примере вывода команды

    ps
    приведен элемент списка для самой команды
    ps
    .

    21475 pts/2 R+   0:00 ps ax

    Эта строка означает, что процесс

    21475
    находится в состоянии выполнения (
    R
    ) и выполняет он команду
    ps ах
    . Таким образом, процесс описан в своем собственном выводе! Индикатор состояния показывает только то, что программа готова к выполнению, а не то, что она обязательно выполняется в данный момент. На однопроцессорном компьютере в каждый момент времени может выполняться только один процесс, в то время как другие процессы ждут своего рабочего периода. Эти периоды, называемые квантами времени, очень короткие и создают впечатление одновременного выполнения программ. Опция
    R+
    просто показывает, что данная программа — фоновая задача, не ждущая завершения других процессов или окончания ввода или вывода данных. Именно поэтому можно увидеть два таких процесса, приведенные в списке вывода
    команды
    ps. (Другой, часто встречающийся процесс, помечаемый как выполняющийся, — дисплейный сервер системы X.)

    Ядро Linux применяет планировщик процессов для того, чтобы решить, какой процесс получит следующий квант времени. Решение принимается исходя из приоритета процесса (мы обсуждали приоритеты процессов в главе 4). Процессы с высоким приоритетом выполняются чаще, а другие, такие как низкоприоритетные фоновые задачи, — реже. В ОС Linux процессы не могут превысить выделенный им квант времени. Они преимущественно относятся к разным задачам, поэтому приостанавливаются и возобновляются без взаимодействия друг с другом. В более старых системах, например Windows 3.х, как правило, для возобновления других процессов требовалось явное согласие процесса.

    В многозадачных системах, таких как Linux, несколько программ могут претендовать на один и тот же ресурс, поэтому программы с короткими рабочими циклами, прерывающиеся для ввода, считаются лучше ведущими себя, чем программы, прибирающие к рукам процессор для продолжительного вычисления какого-либо значения или непрерывных запросов к системе, касающихся готовности ввода данных. Хорошо ведущие себя программы называют nice-программами (привлекательными программами) и в известном смысле эту "привлекательность" можно измерить. Операционная система определяет приоритет процесса на основе значения "nice", по умолчанию равного 0, и поведения программы. Программы, выполняющиеся без пауз в течение долгих периодов, как правило, получают более низкие приоритеты. Программы, делающие паузы время от времени, например в ожидании ввода, получают награду. Это помогает сохранить отзывчивость программы, взаимодействующей с пользователем; пока она ждет какого-либо ввода от пользователя, система увеличивает ее приоритет, чтобы, когда программа будет готова возобновить выполнение, у нее был высокий приоритет. Задать значение

    nice
    для процесса можно с помощью команды
    nice
    , а изменить его — с помощью команды
    renice
    . Команда
    nice
    увеличивает на 10 значение
    nice
    процесса, присваивая ему более низкий приоритет. Просмотреть значения
    nice
    активных процессов можно с помощью опций
    -l
    или
    -f
    (для полного вывода) команды
    ps
    . Интересующие вас значения представлены в столбце
    NI
    (nice).

    $ ps -l

      F S UID  PID PPID С PRI NI ADDR SZ WCHAN  TTY   TIME     CMD

    000 S 500 1259 1254 0  75  0 -   710 wait4  pts/2 00:00:00 bash

    000 S 500 1262 1251 0  75  0 -   714 wait4  pts/1 00:00:00 bash

    000 S 500 1313 1262 0  75  0 -  2762 schedu pts/1 00:00:00 emacs

    000 S 500 1362 1262 2  80  0 -   789 schedu pts/1 00:00:00 oclook

    000 R 500 1363 1262 0  81  0 -   782 -      pts/1 00:00:00 ps

    Как видно из списка, программа

    oclock
    выполняется (как процесс 1362) со значением
    nice
    по умолчанию. Если бы она была запущена командой

    $ nice oclock &

    то получила бы значение

    nice
    +10. Если вы откорректируете это значение командой

    $ renice 10 1362

    1362: old priority 0, new priority 10

    программа

    oclock
    будет выполняться реже. Увидеть измененное значение nice можно снова с помощью команды
    ps
    :

    $ ps -l

    F   S UID  PID PPID С PRI NI ADDR SZ WCHAN  TTY   TIME     CMD

    000 S 500 1259 1254 0  75  0 -   710 wait4  pts/2 00:00:00 bash

    000 S 500 1262 1251 0  75  0 -   714 wait4  pts/1 00:00:00 bash

    000 S 500 1313 1262 0  75  0 -  2762 schedu pts/1 00:00:00 emacs

    000 S 500 1362 1262 0  90 10 -   789 schedu pts/1 00:00:00 oclock

    000 R 500 1365 1262 0  81  0 -   782 -      pts/1 00:00:00 ps

    Столбец состояния теперь также содержит

    N
    , указывая на то, что значение
    nice
    было изменено по сравнению с принятым по умолчанию:

    ps х

    PID  TTY   STAT TIME COMMAND

    1362 pts/1 SN   0:00 oclock

    Поле

    PPID
    в выводе команды
    ps
    содержит ID родительского процесса (PID), либо процесса, запустившего данный процесс, либо, если этот процесс уже не выполняется, процесса
    init
    (PID, равный 1).

    Планировщик процессов ОС Linux решает, какому процессу разрешить выполнение, на основе приоритета. Конкретные реализации конечно отличаются, но высокоприоритетные процессы выполняются чаще. В некоторых случаях низкоприоритетные процессы не выполняются совсем, если высокоприоритетные процессы готовы к выполнению.

    Запуск новых процессов

    Применив библиотечную функцию

    system
    , вы можете заставить программу выполняться из другой программы и тем самым создать новый процесс:

    #include <stdlib.h>

    int system(const char *string);

    Функция

    system
    выполняет команду, переданную ей как строку, и ждет ее завершения. Команда выполняется, как если бы командной оболочке была передана следующая команда:

    $ sh -с string

    Функция

    system
    возвращает код 127, если командная оболочка не может быть запущена для выполнения команды, и -1 в случае другой ошибки. Иначе
    system
    вернет код завершения команды.

    Выполните упражнение 11.1.

    Упражнение 11.1. Функция
    system

    Вы можете использовать

    system
    для написания программы, выполняющей команду
    ps
    . Хотя нельзя сказать, что она необычайно полезна, вы увидите, как применять этот метод в последующих примерах. (Для простоты примера мы не проверяем, работает ли на самом деле системный вызов.)

    #include <stdlib.h>

    #include <stdio.h>


    int main() {

     printf("Running ps with system\n");

     system("ps ax");

     printf("Done \n");

     exit(0);

    }

    Когда вы откомпилируете и выполните программу system1.с, то получите вывод, похожий на приведенный далее:

    $ ./system1

    Running ps with system

     PID TTY   STAT TIME COMMAND

       1 ?     Ss   0:03 init [5]

    ...

    1262 pts/1 Ss   0:00 /bin/bash

    1273 pts/2 S    0:00 su -

    1274 pts/2 S+   0:00 -bash

    1463 pts/2 SN   0:00 oclock

    1465 pts/1 S    0:01 emacs Makefile

    1480 pts/1 S+   0:00 ./system1

    1481 pts/1 R+    0:00 ps ax

    Done.

    Поскольку функция

    system
    применяет командную оболочку для запуска нужной программы, вы можете перевести ее в фоновый режим, заменив вызов функции в файле system1.с на следующий:

    system("ps ах &");

    Когда вы откомпилируете и выполните эту версию программы, то получите следующий вывод:

    $ ./system2

    Running ps with system

     PID TTY  STAT TIME COMMAND

       1 ?    S    0:03 init [5]

     ...

    Done.

    $ 1274 pts/2 3+ 0:00 -bash

    1463 pts/2 SN  0:00 oclock

    1465 pts/1 S   0:01 emacs Makefile

    1484 pts/1 R   0:00 ps ax

    Как это работает

    В первом примере программа вызывает функцию

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

    Во втором примере вызов функции

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

    $ ps ах &

    Далее программа system2 выводит

    Done.
    и завершается до того, как у команды
    ps
    появится возможность отобразить до конца весь свой вывод. Вывод
    ps
    продолжает формироваться после завершения system2 и в этом случае не включает в список элемент, описывающий процесс
    system2
    . Такое поведение процесса может сильно сбить с толку пользователей. Для того чтобы умело применять процессы, вы должны лучше управлять их действиями. Давайте рассмотрим низкоуровневый интерфейс для создания процесса,
    exec
    .

    Примечание

    Вообще применение функции

    system
    — далеко не идеальный способ создания процессов, потому что запускаемая программа использует командную оболочку. Он неэффективен вдвойне: и потому что перед запуском программы запускается оболочка, и потому что сильно зависим от варианта установки командной оболочки и применяемого окружения. В следующем разделе вы увидите гораздо более удачный способ запуска программ, который почти всегда предпочтительней применения вызова
    system
    .

    Замена образа процесса

    Существует целое семейство родственных функций, сгруппированных под заголовком

    exec
    . Они отличаются способом запуска процессов и представлением аргументов программы. Функция
    exec
    замещает текущий процесс новым, заданным в аргументе
    path
    или
    file
    . Функции
    exec
    можно применять для передачи выполнения вашей программы другой программе. Например, перед запуском другого приложения с политикой ограниченного применения вы можете проверить имя пользователя и пароль. Функции
    exec
    более эффективны по сравнению с
    system
    , т.к. исходная программа больше не будет выполняться после запуска новой программы.

    #include <unistd.h>

    char **environ;

    int execl(const char *path, const char *arg0, ..., (char *)0);

    int execlp(const char *file, const char *arg0, ..., (char *)0);

    int execle(const char *path, const char *arg0, ..., (char *)0,

     char *const envp[]);

    int execv(const char *path, char *const argv[]);

    int execvp(const char *file, char *const argv[]);

    int execve(const char *path, char *const argv[], char *const envp[]);

    Эти функции делятся на два вида.

    execl
    ,
    execlp
    и
    execle
    принимают переменное число аргументов, заканчивающихся указателем
    null
    . У
    execv
    и
    execvp
    второй аргумент — массив строк. В обоих случаях новая программа стартует с заданными аргументами, представленными в массиве
    argv
    , передаваемом функции
    main
    .

    Эти функции реализованы, как правило, с использованием

    execve
    , хотя нет обязательных требований на этот счет.

    Функции, имена которых содержат суффикс

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

    Передать значение окружению программы может глобальная переменная

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

    Если вы хотите применить функцию

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

    #include <unistd.h>


    /* Пример списка аргументов */

    /* Учтите, что для argv[0] необходимо имя программы */

    char *const ps_argv[] = {"ps", "ax", 0};

    /* He слишком полезный пример окружения */

    char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", 0};


    /* Возможные вызовы функций exec */

    execl("/bin/ps", "ps", "ax", 0);

    /* предполагается, что ps в /bin */

    execlp("ps", "ps", "ax", 0);

    /* предполагается, что /bin в PATH */

    execle("/bin/ps", "ps", "ax", 0, ps_envp);

    /* передается свое окружение */

    execv("/bin/ps", ps_argv);

    execvp("ps", ps_argv);

    execve("/bin/ps", ps_argv, ps_envp);

    А теперь выполните упражнение 11.2.

    Упражнение 11.2. Функция
    execlp

    Давайте изменим пример и используем вызов

    execlp
    :

    #include <unistd.h>

    #include <stdio.h>

    #include <stdlib.h>


    int main() {

     printf("Running ps with execlp\n");

     execlp("ps", "ps", "ax", 0);

     printf("Done.\n");

     exit(0);

    }

    Когда вы выполните эту программу, рехес.с, то получите обычный вывод команды

    ps
    , но без сообщения
    Done
    . Кроме того, обратите внимание на то, что в выводе нет процесса с именем
    рехес
    :

    $ ./рехес

    Running ps with execlp

     PID TTY   STAT TIME COMMAND

    1    ?     S    0:03 init [5]

    ...

    1262 pts/1 Ss   0:00 /bin/bash

    1273 pts/2 S    0:00 su -

    1274 pts/2 S+   0:00 -bash

    1463 pts/1 SN   0:00 oclock

    1465 pts/1 S    0:01 emacs Makefile

    1514 pts/1 R+   0:00 ps ax

    Как это работает

    Программа выводит первое сообщение и затем вызывает функцию

    execlp
    , которая ищет каталоги, заданные в переменной окружения
    PATH
    для обнаружения программы
    ps
    . Далее она выполняет команду вместо программы
    рехес
    , запустив ее так, как будто вы ввели команду командной оболочки

    $ ps ax

    Когда

    ps
    завершается, вы получаете новую строку приглашения командной оболочки. Возврата в программу
    рехес
    не происходит, поэтому второе сообщение не выводится. PID нового процесса тот же, что и у исходного, то же самое можно сказать о PID родительского процесса и значении
    nice
    . В сущности, происходит следующее: выполняющаяся программа запустила на выполнение новый код и новый исполняемый файл, заданный в вызове функции
    exec
    .

    Существует ограничение для общего размера списка аргументов и окружения процесса, запускаемого функциями

    exec
    . Оно задается в переменной
    ARG_MAX
    и в системах Linux равно 128 Кбайт. В других системах может задаваться меньший предельный размер, что способно порождать проблемы. Стандарт POSIX гласит, что
    ARG_MAX
    должна быть не менее 4096 байтов.

    Функции

    exec
    , как правило, не возвращаются в программу до тех пор, пока не возникла ошибка, в этом случае задается переменная
    errno
    и функция
    exec
    возвращает -1.

    Новые процессы, запущенные exec, наследуют многие свойства исходного процесса. В частности, открытые файловые дескрипторы остаются открытыми в новом процессе, пока не установлен их флаг

    FD_CLOEXEC
    (close on exec) (подробную информацию см. в описании системного вызова
    fcntl
    в главе 3). Любые открытые в исходном процессе потоки каталогов закрываются.

    Дублирование образа процесса

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

    init
    , вместо замещения текущего потока исполнения, как в случае применения функции
    exec
    .

    Создать новый процесс можно с помощью вызова

    fork
    . Системный вызов дублирует текущий процесс, создавая новый элемент в таблице процессов с множеством атрибутов, таких же как у текущего процесса. Новый процесс почти идентичен исходному, выполняет тот же программный код, но в своем пространстве данных, окружении и со своими файловыми дескрипторами. В комбинации с функциями
    exec
    вызов
    fork
    — все, что вам нужно для создания новых процессов.

    #include <sys/types.h>

    #include <unistd.h>

    pid_t fork(void);

    Как видно из рис. 11.2, вызов

    fork
    возвращает в родительский процесс PID нового дочернего процесса. Новый процесс продолжает выполнение так же, как и исходный, за исключением того, что в дочерний процесс вызов
    fork
    возвращает 0. Это позволяет родительскому и дочернему процессам определить, "кто есть кто".

    Рис. 11.2 


    Если вызов

    fork
    завершается аварийно, он возвращает -1. Обычно это происходит из-за ограничения числа дочерних процессов, которые может иметь родительский процесс (
    CHILD_MAX
    ), в этом случае переменной
    errno
    будет присвоено значение
    EAGAIN
    . Если для элемента таблицы процессов недостаточно места или не хватает виртуальной памяти, переменная
    errno
    получит значение
    ENOMEM
    .

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

    fork
    :

    pid_t new_pid;

    new_pid = fork();

    switch(new_pid) {

    case -1:

     /* Ошибка */

     break;

    case 0:

     /* Мы — дочерний процесс */

     break;

    default:

     /* Мы — родительский процесс */

     break;

    }

    Выполните упражнение 11.3.

    Упражнение 11.3. Системный вызов
    fork

    Давайте рассмотрим простой пример fork1.с:

    #include <sys/types.h>

    #include <unistd.h>

    #include <stdio.h>

    #include <stdlib.h>


    int main() {

     pid_t pid;

     char* message;

     int n;

     printf("fork program starting\n");

     pid = fork();

     switch(pid) {

     case -1:

      perror("fork failed");

      exit(1);

     case 0:

      message = "This is the child";

      n = 5;

      break;

     default:

      message = "This is the parent";

      n = 3;

      break;

     }

     for (; n > 0; n--) {

      puts(message);

      sleep(1);

     }

     exit(0);

    }

    Эта программа выполняет два процесса. Дочерний процесс создается и выводит пять раз сообщение. Исходный процесс (родитель) выводит сообщение только три раза. Родительский процесс завершается до того, как дочерний процесс выведет все свои сообщения, поэтому в вывод попадает очередное приглашение командной оболочки.

    $ ./fork1

    fork program starting

    This is the child

    This is the parent

    This is the parent

    This is the child

    This is the parent

    This is the child

    $ This is the child

    This is the child

    Как это работает

    Когда вызывается

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

    Ожидание процесса

    Когда вы запускаете дочерний процесс с помощью вызова

    fork
    , он начинает жить собственной жизнью и выполняется независимо. Иногда вам нужно знать, когда закончился дочерний процесс. Например, в предыдущей программе родительский процесс завершается раньше дочернего, и вы получаете слегка беспорядочный вывод, потому что дочерний процесс продолжает выполняться. Вы можете с помощью системного вызова
    wait
    заставить родительский процесс дождаться завершения дочернего процесса перед своим продолжением.

    #include <sys/types.h>

    #include <sys/wait.h>

    pid_t wait(int *stat_loc);

    Системный вызов

    wait
    заставляет родительский процесс сделать паузу до тех пор, пока один из его дочерних процессов не остановится. Вызов возвращает PID дочернего процесса. Обычно это дочерний процесс, который завершился. Сведения о состоянии позволяют родительскому процессу определить статус завершения дочернего процесса, т.е. значение, возвращенное из функции
    main
    или переданное функции
    exit
    . Если
    stat_loc
    не равен пустому указателю, информация о состоянии будет записана в то место, на которое указывает этот параметр.

    Интерпретировать информацию о состоянии процесса можно с помощью макросов, описанных в файле sys/wait.h и приведенных в табл. 11.2.


    Таблица 11.2

    Макрос Описание
    WIFEXITED(stat_val)
    Ненулевой, если дочерний процесс завершен нормально
    WEXITSTATUS(stat_val)
    Если
    WIFEXITED
    ненулевой, возвращает код завершения дочернего процесса
    WIFSIGNALED(stat_val)
    Ненулевой, если дочерний процесс завершается неперехватываемым сигналом
    WTERMSIG(stat_val)
    Если
    WIFSIGNALED
    ненулевой, возвращает номер сигнала
    WIFSTOPPED(stat_val)
    Ненулевой, если дочерний процесс остановился
    WSTOPSIG(stat_val)
    Если
    WIFSTOPPED
    ненулевой, возвращает номер сигнала

    Выполните упражнение 11.4.

    Упражнение 11.4. Системный вызов
    wait

    В этом упражнении вы слегка измените программу, чтобы можно было подождать и проверить код состояния дочернего процесса. Назовите новую программу wait.c.

    #include <sys/types.h>

    #include <sys/wait.h>

    #include <unistd.h>

    #include <stdio.h>

    #include <stdlib.h>


    int main() {

     pid_t pid;

     char* message;

     int n;

     int exit_code;

     printf("fork program starting\n");

     pid = fork();

     switch(pid) {

     case -1:

      perror("fork failed");

      exit(1);

     case 0:

      message = "This is the child";

      n = 5;

      exit_code = 37;

      break;

     default:

      message = "This is the parent";

      n = 3;

      exit_code = 0;

      break;

     }

     for (; n > 0; n--) {

      puts(message);

      sleep(1);

     }

    Следующий фрагмент программы ждет окончания дочернего процесса:

     if (pid != 0) {

      int stat_val;

      pid_t child_pid;

      child_pid = wait(&stat_val);

      printf("Child has finished: PID = %d\n", child_pid);

      if (WIFEXITED(stat_val))

       printf("Child exited with code %d\n", WEXITSTATUS(stat_val));

      else printf("Child terminated abnormally\n");

     }

     exit(exit_code);

    }

    Когда вы выполните эту программу, то увидите, что родительский процесс ждет дочерний:

    $ ./wait

    fork program starting

    This is the child

    This is the parent

    This is the parent

    This is the child

    This is the parent

    This is the child

    This is the child

    This is the child

    Child has finished: PID = 1582

    Child exited with code 37

    $

    Как это работает

    Родительский процесс, получивший ненулевое значение, возвращенное из вызова

    fork
    , применяет системный вызов
    wait
    для приостановки своего выполнения до тех пор, пока информация о состоянии дочернего процесса не станет доступной. Это произойдет, когда дочерний процесс вызовет функцию
    exit
    ; мы присвоили ему код завершения 37. Далее родительский процесс продолжается, определяет, протестировав значение, возвращенное вызовом
    wait
    , что дочерний процесс завершился нормально, и извлекает код завершения из информации о состоянии процесса.

    Процессы-зомби

    Применение вызова

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

    Вы сможете увидеть создание процесса-зомби, если измените количество сообщений в программе из примера с вызовом

    fork
    . Если дочерний процесс выводит меньше сообщений, чем родительский, он закончится первым и будет существовать как зомби, пока не завершится родительский процесс.

    Упражнение 11.5. Зомби

    Программа fork2.c такая же, как программа fork1.с, за исключением того, что количества сообщений, выводимых родительским и дочерним процессами, поменяли местами. Далее приведены соответствующие строки кода:

    switch (pid) {

    case -1:

     perror("fork failed");

     exit(1);

    case 0:

     message = "This is the child";

     n = 3;

     break;

    default:

     message = "This is the parent";

     n = 5;

     break;

    }

    Как это работает

    Если вы выполните только что приведенную программу с помощью команды

    ./fork2 &
    и затем вызовите программу
    ps
    после завершения дочернего процесса, но до окончания родительского, то увидите строку, подобную следующей. (Некоторые системы могут сказать
    <zombie>
    вместо
    <defunct>
    .)

    $ ps -аl

      F S UID  PID PPID С PRI NI ADDR SZ WCHAN  TTY   TIME     CMD

    004 S   0 1273 1259 0  75  0 -   589 wait4  pts/2 00:00:00 su

    000 S   0 1274 1273 0  75  0 -   731 schedu pts/2 00:00:00 bash

    000 S 500 1463 1262 0  75  0 -   788 schedu pts/1 00:00:00 oclock

    000 S 500 1465 1262 0  75  0 -  2569 schedu pts/1 00:00:01 emacs

    000 S 500 1603 1262 0  75  0 -   313 schedu pts/1 00:00:00 fork2

    003 Z 500 1604 1603 0  75  0 -     0 do_exi pts/1 00:00:00 fork2 <defunct>

    000 R 500 1605 1262 0  81  0 -   781 -      pts/1 00:00:00 ps

    Если родительский процесс завершится необычно, дочерний процесс автоматически получит в качестве родителя процесс с PID, равным 1 (init). Теперь дочерний процесс — зомби, который уже не выполняется, но унаследован процессом

    init
    из-за необычного окончания родительского процесса. Зомби останется в таблице процессов, пока не пойман процессом
    init
    . Чем больше таблица, тем медленнее эта процедура. Следует избегать процессов-зомби, поскольку они потребляют ресурсы до тех пор, пока процесс init не вычистит их.

    Есть еще один системный вызов, который можно применять для ожидания дочернего процесса. Он называется

    waitpid
    и применяется для ожидания завершения определенного процесса.

    #include <sys/types.h>

    #include <sys/wait.h>

    pid_t waitpid(pid_t pid, int *stat_loc, int options);

    Аргумент

    pid
    — конкретный дочерний процесс, окончания которого нужно ждать. Если он равен –1,
    waitpid
    вернет информацию о любом дочернем процессе. Как и вызов
    wait
    , он записывает информацию о состоянии процесса в место, указанное аргументом
    stat_loc
    , если последний не равен пустому указателю. Аргумент
    options
    позволяет изменить поведение
    waitpid
    . Наиболее полезная опция
    WNOHANG
    мешает вызову
    waitpid
    приостанавливать выполнение вызвавшего его процесса. Ее можно применять для выяснения, завершился ли какой-либо из дочерних процессов, и если нет, то продолжать выполнение. Остальные опции такие же, как в вызове
    wait
    .

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

    waitpid(child_pid, (int *)0, WNOHANG);

    Он вернет ноль, если дочерний процесс не завершился и не остановлен, или

    child_pid
    , если это произошло. Вызов waitpid вернет -1 в случае ошибки и установит переменную
    errno
    . Это может произойти, если нет дочерних процессов (
    errno
    равна
    ECHILD
    ), если вызов прерван сигналом (
    EINTR
    ) или аргумент
    options
    неверный (
    EINVAL
    ).

    Перенаправление ввода и вывода

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

    fork
    и
    exec
    . Следующий пример из упражнения 11.6 содержит программу-фильтр, которая читает из стандартного ввода и пишет в свой стандартный вывод, выполняя при этом некоторое полезное преобразование.

    Далее приведена программа очень простой фильтрации upper.c, которая читает ввод и преобразует строчные буквы в прописные:

    #include <stdio.h>

    #include <ctype.h>

    #include <stdlib.h>


    int main() {

     int ch;

     while ((ch = getchar()) != EOF) {

      putchar(toupper(ch));

     }

     exit(0);

    }

    Когда вы выполните программу, она сделает то, что и ожидалось:

    $ ./upper

    hello THERE

    HELLO THERE

    ^D

    $

    Вы, конечно, можете применить ее для преобразования символов файла, используя перенаправление, применяемое командной оболочкой:

    $ cat file.txt

    this is the file, file.txt, it is all lower case.

    $ ./upper < file.txt

    THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.

    Что если вы хотите применить этот фильтр из другой программы? Программа useupper.c принимает имя файла как аргумент и откликается сообщением об ошибке при некорректном вызове:

    #include <unistd.h>

    #include <stdio.h>

    #include <stdlib.h>


    int main(int argc, char *argv[]) {

     char *filename;

     if (argc != 2) {

      fprintf (stderr, "usage: useupper file\n");

      exit(1);

     }

     filename = argv[1];

    Вы повторно открываете стандартный ввод, снова при этом проверяете наличие любых ошибок, а затем применяете функцию

    execl
    для вызова программы upper:

     if (!freopen(filename, "r", stdin)) {

      fprintf(stderr, "could not redirect stdin from file %s\n", filename);

      exit(2);

     }

     execl("./upper", "upper", 0);

    He забудьте, что

    execl
    заменяет текущий процесс, если ошибок нет, оставшиеся строки не выполняются.

     perror("could not exec ./upper");

     exit(3);

    }

    Как это работает

    Когда вы выполняете эту программу, ей можно передать файл для преобразования в прописные буквы. Работа делается программой upper, которая не обрабатывает аргументы с именами файлов. Обратите внимание на то, что вам не нужен исходный код программы upper; таким способом можно запустить любую исполняемую программу.

    $ ./useupper file.txt

    THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.

    Программа useupper применяет

    freopen
    для закрытия стандартного ввода и связывания потока файла с файлом, заданным как аргумент программы. Затем она вызывает
    execl
    , чтобы заменить код выполняемого процесса кодом программы upper. Поскольку файловые дескрипторы сохраняются, пройдя сквозь вызов
    execl
    , программа upper выполняется так же, как при вводе ее в строке командной оболочки

    $ ./upper < file.txt

    Потоки 

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

    Существует класс процесса, именуемый потоком (thread), который доступен во многих системах UNIX и Linux. Несмотря на то, что потоки трудно, программировать, они могут быть очень важны для некоторых приложений, таких как многопоточные серверы баз данных. Программирование потоков в Linux (и вообще в UNIX) не так распространено, как применение множественных процессов, поскольку процессы Linux очень легко применять и программирование множественных взаимодействующих процессов гораздо легче программирования потоков. Потоки обсуждаются в главе 12.

    Сигналы

    Сигнал — это событие, генерируемое системами UNIX и Linux в ответ на некоторую ситуацию, получив сообщение о котором процесс, в свою очередь, может предпринять какое-то действие. Мы применяем термин "возбуждать" (raise) для обозначения генерации сигнала и термин "захватывать" (catch) для обозначения получения или приема сигнала. Сигналы возбуждаются некоторыми ошибочными ситуациями, например нарушениями сегментации памяти, ошибками процессора при выполнении операций с плавающей точкой или некорректными командами. Они генерируются командной оболочкой и обработчиками терминалов для вызова прерываний и могут явно пересылаться от одного процесса к другому как способ передачи информации или коррекции поведения. Во всех этих случаях программный интерфейс один и тот же. Сигналы могут возбуждаться, улавливаться и соответственно обрабатываться или (по крайней мере, некоторые) игнорироваться.

    Имена сигналов задаются с помощью включенного заголовочного файла signal.h. Они начинаются с префикса

    SIG
    и включают приведенные в табл. 11.3 сигналы.


    Таблица 11.3

    Имя сигнала Описание
    SIGABORT
    *Процесс аварийно завершается
    SIGALRM
    Сигнал тревоги
    SIGFPE
    *Исключение операции с плавающей точкой
    SIGHUP
    Неожиданный останов или разъединение
    SIGILL
    *Некорректная команда
    SIGINT
    Прерывание терминала
    SIGKILL
    Уничтожение (не может быть перехвачен или игнорирован)
    SIGPIPE
    Запись в канал без считывателя
    SIGQUIT
    Завершение работы терминала
    SIGSEGV
    *Некорректный доступ к сегменту памяти
    SIGTERM
    Завершение, выход
    SIGUSR1
    Сигнал 1, определенный пользователем
    SIGUSR2
    Сигнал 2, определенный пользователем

    *Могут быть также предприняты действия, зависящие от конкретной реализации.


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

    К дополнительным относятся сигналы, приведенные в табл. 11.4.


    Таблица 11.4

    Имя сигнала Описание
    SIGCHLD
    Дочерний процесс остановлен или завершился
    SIGCONT
    Продолжить выполнение, если процесс был приостановлен
    SIGSTOP
    Остановить выполнение (не может захватываться или игнорироваться)
    SIGTSTP
    Сигнал останова, посылаемый с терминала
    SIGTTIN
    Фоновый процесс пытается читать
    SIGTTOU
    Фоновый процесс пытается писать

    Сигнал

    SIGCHLD
    может быть полезен для управления дочерними процессами. По умолчанию он игнорируется. Остальные сигналы заставляют процессы, получившие их, остановиться, за исключением сигнала
    SIGCONT
    , который вызывает возобновление процесса. Они применяются программами командной оболочки для контроля работы и редко используются в пользовательских программах.

    Чуть позже мы рассмотрим более подробно первую группу сигналов. Пока же достаточно знать, что если командная оболочка и драйвер терминала нормально настроены, ввод символа прерывания (обычно от нажатия комбинации клавиш <Ctrl>+<C>) с клавиатуры приведет к отправке сигнала

    SIGINT
    приоритетному процессу, т.е. программе, выполняющейся в данный момент. Это вызовет завершение программы, если в ней не предусмотрен перехват сигнала,

    Если вы хотите отправить сигнал не текущей приоритетной задаче, а другому процессу, используйте команду

    kill
    . Она принимает для отправки процессу в качестве необязательного параметра имя сигнала или его номер и PID (который, как правило, можно определить с помощью команды ps). Например, для отправки сигнала "останов или разъединение" командной оболочке, выполняющейся на другом терминале с PID 512, вы должны применить следующую команду:

    $ kill -HUP 512

    Удобный вариант команды

    kill
    — команда
    killall
    , которая позволяет отправить сигнал всем процессам, выполняющим конкретную команду. Не все системы UNIX поддерживают ее, но ОС Linux, как правило, поддерживает. Этот вариант полезен, когда вы не знаете PID процесса или хотите отправить сигнал нескольким разным процессам, выполняющим одну и ту же команду. Обычное применение — заставить программу
    inetd
    перечитать параметры настройки. Для этого можно воспользоваться следующей командой:

    $ killall -HUP inetd

    Программы могут обрабатывать сигналы с помощью библиотечной функции

    signal
    .

    #include <signal.h>

    void (*signal(int sig, void (*func)(int)))(int);

    Это довольно сложное объявление говорит о том, что

    signal
    — это функция, принимающая два параметра,
    sig
    и
    func
    . Сигнал, который нужно перехватить или игнорировать, задается аргументом
    sig
    . Функция, которую следует вызвать при получении заданного сигнала, содержится в аргументе
    func
    . Эта функция должна принимать единственный аргумент типа
    int
    (принятый сигнал) и иметь тип
    void
    . Функция сигнала возвращает функцию того же типа, которая является предыдущим значением функции, заданной для обработки сигнала, или одно из двух специальных значений:

    SIG_IGN
    — игнорировать сигнал;

    SIG_DFL
    — восстановить поведение по умолчанию.

    Пример сделает все понятным. В упражнении 11.7 вы напишете программу ctrlc.c, которая реагирует на нажатие комбинации клавиш <Ctrl>+<C> вместо обычного завершения выводом соответствующего сообщения. Повторное нажатие <Ctrl>+<C> завершает программу.

    Упражнение 11.7. Обработка сигнала

    Функция

    ouch
    реагирует на сигнал, передаваемый в параметре
    sig
    . Эта функция будет вызываться, когда возникнет сигнал. Она выводит сообщение и затем восстанавливает обработку сигнала по умолчанию для сигнала SIGINT (генерируется при нажатии комбинации клавиш <Ctrl>+<C>).

    #include <signal.h>

    #include <stdio.h>

    #include <unistd.h>


    void ouch(int sig) {

     printf("OUCH! - I got signal %d\n", sig);

     (void)signal(SIGINT, SIG_DFL);

    }

    Функция

    main
    должна взаимодействовать с сигналом
    SIGINT
    , генерируемым при нажатии комбинации клавиш <Ctrl>+<C>. В остальное время она находится в бесконечном цикле, выводя один раз в секунду сообщение.

    int main() {

     (void)signal(SIGINT, ouch);

     while(1) {

      printf("Hello World!\n");

      sleep(1);

     }

    }

    Ввод комбинации клавиш <Ctrl>+<C> (отображается как

    ^C
    в следующем далее выводе) в первый раз заставляет программу отреагировать и продолжиться. Когда вы нажимаете <Ctrl>+<C> снова, программа завершается, т.к. сигнал
    SIGINT
    вернул программе стандартное поведение, заставляющее ее завершиться.

    $ ./ctrlcl

    Hello World!

    Hello World!

    Hello World!

    Hello World!

    ^C

    OUCH! - I got signal 2

    Hello World!

    Hello World!

    Hello World!

    Hello World!

    ^C

    $

    Как видно из данного примера, функция обработки сигнала принимает один целочисленный параметр — номер сигнала, приводящий к вызову функции. Это удобно, если одна и та же функция применяется для обработки нескольких сигналов. В данном случае вы выводите значение

    SIGINT
    , которое в этой системе оказывается равным 2. Не стоит полагаться на стандартные числовые значения сигналов, в новых программах всегда пользуйтесь именами сигналов.

    Примечание

    Вызывать из обработчика сигнала все функции, например,

    printf
    , небезопасно. Удобный метод — использовать флаг, устанавливаемый в обработчике сигнала, и затем проверять этот флаг в функции
    main
    и выводить сообщение, если нужно. В конце этой главы вы найдете список вызовов, которые можно безопасно применять в теле обработчиков сигналов.

    Как это работает

    Программа устроена так, что, когда вы задаете сигнал

    SIGINT
    , нажимая комбинацию клавиш <Ctrl>+<C>, вызывает функцию
    ouch
    . После того как функция прерывания
    ouch
    завершится, программа продолжает выполняться, но восстанавливает реакцию на сигнал, принятую по умолчанию. (У разных версий UNIX, в особенности у потомков системы Berkeley UNIX, в течение многих лет сложилось разное поведение при получении сигналов. Если вы хотите восстановить поведение по умолчанию после возникновения сигнала, лучше всего запрограммировать его на конкретные действия.) Когда программа получает второй сигнал
    SIGINT
    , она выполняет стандартное действие, приводящее к завершению программы.

    Если вы хотите сохранить обработчик сигнала и продолжать реагировать на комбинацию клавиш <Ctrl>+<C>, вам придется восстановить его, вызвав функцию

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

    Примечание

    Мы не рекомендуем вам пользоваться функцией

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

    Функция

    signal
    возвращает предыдущее значение обработчика для заданного типа сигнала, если таковой есть, или в противном случае
    SIG_ERR
    с установкой положительного значения в переменной
    errno
    . Если задан неверный сигнал или делается попытка обработать сигнал, который не может быть перехвачен или игнорироваться, например
    SIGKILL
    , переменной
    errno
    присваивается значение
    EINVAL
    .

    Отправка сигналов

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

    kill
    . Вызов завершится аварийно, если у программы нет полномочий на отправку сигнала, часто потому что процесс-получатель принадлежит другому пользователю. Эта функция эквивалентна команде оболочки с тем же именем.

    #include <sys/types.h>

    #include <signal.h>

    int kill(pid_t pid, int sig);

    Функция

    kill
    посылает заданный сигнал
    sig
    процессу с идентификатором, заданным в аргументе
    pid
    . В случае успеха она возвращает 0. Для отправки сигнала посылающий процесс должен иметь право на выполнение этого действия. Обычно это означает, что у обоих процессов должен быть один и тот же идентификатор пользователя ID (т.е. вы можете отправить сигнал только одному из собственных процессов, хотя суперпользователь может отправлять сигналы любому процессу).

    Функция

    kill
    завершится аварийно, вернет -1 и установит значение переменной
    errno
    , если задан неверный сигнал, (
    errno
    равна
    EINVAL
    ), у процесса нет полномочий (
    EPERM
    ) или заданный процесс не существует (
    ESRCH
    ).

    Сигналы предоставляют полезное средство, именуемое будильником или сигналом тревоги. Вызов функции

    alarm
    может применяться для формирования сигнала
    SIGALRM
    в определенное время в будущем.

    #include <unistd.h>

    unsigned int alarm(unsigned int seconds);

    Вызов

    alarm
    намечает доставку сигнала
    SIGALRM
    через
    seconds
    секунд. В действительности сигнал будильника будет доставлен чуть позже из-за обработки задержек и учета неопределенностей. Значение 0 отменяет любой невыполненный запрос на сигнал будильника. Вызов функции
    alarm
    до получения сигнала может вызвать сброс графика доставки. У каждого процесса может быть только один невыполненный сигнал будильника. Функция
    alarm
    возвращает количество секунд, оставшихся до отправки любого невыполненного вызова,
    alarm
    , или -1 в случае аварийного завершения.

    Для того чтобы увидеть как работает функция

    alarm
    , можно сымитировать ее действие, используя вызовы
    fork
    ,
    sleep
    и
    signal
    (упражнение 11.8). Программа сможет запустить новый процесс с единственной целью — отправить сигнал спустя какое- то время.

    Упражнение 11.8 Будильник

    В программе alarm.c первая функция,

    ding
    , имитирует будильник.

    #include <sys/types.h>

    #include <signal.h>

    #include <stdio.h>

    #include <unistd.h>

    #include <stdlib.h>


    static int alarm_fired = 0;

    void ding(int sig) {

     alarm_fired = 1;

    }

    В функции

    main
    вы заставляете дочерний процесс ждать пять секунд перед отправкой сигнала
    SIGALRM
    в свой родительский процесс:

    int main() {

     pid_t pid;

     printf("alarm application starting\n");

     pid = fork();

     switch(pid) {

     case -1:

      /* Аварийное завершение */

      perror("fork failed");

      exit(1);

     case 0:

      /* Дочерний процесс */

      sleep(5);

      kill(getppid(), SIGALRM);

      exit(0);

     }

    Родительский процесс устроен так, что перехватывает сигнал

    SIGALRM
    с помощью вызова
    signal
    и затем ждет неизбежности:

     /* Если мы оказались здесь, то мы — родительский процесс */

     printf("waiting for alarm to go off\n");

     (void)signal(SIGALRM, ding);

     pause();

     if (alarm_fired) printf("Ding!\n");

     printf("done\n");

     exit(0);

    }

    Когда вы выполните программу, то увидите, что она делает паузу на пять секунд, в течение которых ждет имитации будильника:

    $ ./alarm

    alarm application starting

    waiting for alarm to go off

    <5 second pause>

    Ding!

    done $

    В этой программе вводится новая функция

    pause
    , которая просто приостанавливает выполнение программы до появления сигнала. Когда она получит сигнал, выполняется любой установленный обработчик, и выполнение продолжается как обычно. Она объявляется следующим образом:

    #include <unistd.h>

    int pause(void);

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

    errno
    , равной
    EINTR
    , в случае прерывания сигналом. Лучше для ожидания сигналов применять функцию
    sigsuspend
    , которую мы обсудим чуть позже в этой главе.

    Как это работает

    Программа имитации будильника запускает новый процесс вызовом

    fork
    . Этот дочерний процесс ожидает пять секунд и затем посылает сигнал
    SIGALRM
    своему родителю. Родитель подготавливается к получению сигнала
    SIGALRM
    и затем делает паузу до тех пор, пока не будет получен сигнал. Функция
    printf
    не вызывается непосредственно в обработчике, вместо этого вы устанавливаете флаг, который проверяете позже.

    Применение сигналов и приостановка выполнения — важные составляющие программирования в ОС Linux. Это означает, что программа необязательно должна выполняться все время. Вместо того чтобы долго работать в цикле, проверяя, не произошло ли событие, она может ждать его наступления. Это особенно важно в многопользовательской среде, где процессы совместно используют один процессор, и такой вид деятельного ожидания оказывает большое влияние на производительность системы. Особая проблема, связанная с сигналами, заключается в том, что вы никогда не знаете наверняка, что произойдет, если сигнал появится в середине системного вызова? (Ответ весьма неудовлетворительный: все зависит от ситуации.) Вообще следует беспокоиться только о "медленных" системных вызовах, таких как считывание с терминала, когда системный вызов может вернуться с ошибкой, если сигнал появится во время его пребывания в режиме ожидания. Если вы начнете применять сигналы в своих программах, нужно учитывать, что некоторые системные вызовы могут закончиться аварийно, если сигнал создаст ошибочную ситуацию, которую вы могли не принимать во внимание до того, как добавили обработку сигналов.

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

    Надежный интерфейс сигналов

    Мы рассмотрели подробно возбуждение и перехват сигналов с помощью

    signal
    и родственных функций, поскольку они очень часто применяются в старых UNIX-программах. Тем не менее, стандарты X/Open и спецификации UNIX рекомендуют более современный программный интерфейс для сигналов
    sigaction
    , который более надежен.

    #include <signal.h>

    int sigaction<int sig, const struct sigaction *act, struct sigaction *oact);

    Структура

    sigaction
    , применяемая для определения действий, предпринимаемых при получении сигнала, заданного в аргументе
    sig
    , определена в файле signal.h и как минимум включает следующие элементы:

    void (*)(int)sa_handler /* функция, SIG_DFL или SIG_IGN */

    sigset_t sa_mask        /* сигналы, заблокированные для sa_handler */

    int sa_flags            /* модификаторы действий сигнала */

    Функция

    sigaction
    задает действие, связанное с сигналом
    sig
    . Если
    oact
    не
    null
    ,
    sigaction
    записывает предыдущее действие для сигнала в указанное
    oact
    место. Если
    act
    равен
    null
    , это все, что делает функция
    sigaction
    . Если указатель
    act
    не
    null
    , задается действие для указанного сигнала.

    Как и функция

    signal
    ,
    sigaction
    возвращает 0 в случае успешного выполнения и -1 в случае ошибки. Переменная
    errno
    получит значение
    EINVAL
    , если заданный сигнал некорректен или была предпринята попытка захватить или проигнорировать сигнал, который нельзя захватывать или игнорировать.

    В структуре

    sigaction
    , на которую указывает аргумент
    act
    ,
    sa_handler
    — это указатель на функцию, вызываемую при получении сигнала
    sig
    . Она очень похожа на функцию
    func
    , которая, как вы видели раньше, передавалась функции
    signal
    . Вы можете применять специальные значения
    SIG_IGN
    и
    SIG_DFL
    в поле
    sa_handler
    для обозначения того, что сигнал должен игнорироваться или должно быть восстановлено действие по умолчанию, соответственно.

    Поле

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

    Однако сигналы, захватываемые обработчиками, заданными в структуре

    sigaction
    , по умолчанию не восстанавливаются, и нужно задать в поле
    sa_flags
    значение
    SA_RESETHAND
    , если хотите добиться поведения, виденного вами раньше при обсуждении функции
    signal
    . Прежде чем обсуждать подробнее
    sigaction
    , давайте перепишем программу ctrlc.c, применяя
    sigaction
    вместо функции
    signal
    (упражнение 11.9).

    Упражнение 11.9. Функция
    sigaction

    Внесите приведенные далее изменения, так чтобы сигнал

    SIGINT
    перехватывался
    sigaction
    . Назовите новую программу ctrlc2.c.

    #include <signal.h>

    #include <stdio.h>

    #include <unistd.h>


    void ouch(int sig) {

     printf("OUCH! - I got signal %d\n", sig);

    }


    int main() {

     struct sigaction act;

     act.sa_handler = ouch;

     sigemptyset(&act.sa_mask);

     act.sa_flags = 0;

     sigaction(SIGINT, &act, 0);

     while (1) {

      printf("Hello World!\n");

      sleep(1);

     }

    }

    Когда вы выполните эту версию программы, то всегда будете получать сообщение при нажатии комбинации клавиш <Ctrl>+<C>, поскольку

    SIGINT
    обрабатывается неоднократно функцией
    sigaction
    . Для завершения программы следует нажать комбинацию клавиш <Ctrl>+<\>, которая генерирует по умолчанию сигнал
    SIIGQUIT
    .

    $ ./ctrlc2

    Hello World!

    Hello World!

    Hello World!

    ^C

    OUCH! - I got signal 2

    Hello World!

    Hello World!

    ^C

    OUCH! - I got signal 2

    Hello World!

    Hello World!

    ^\

    Quit

    $

    Как это работает

    Программа вместо функции

    signal
    вызывает
    sigaction
    для задания функции
    ouch
    как обработчика сигнала, возникающего при нажатии комбинации клавиш <Ctrl>+<C> (
    SIGINT
    ). Прежде всего, она должна определить структуру
    sigaction
    , содержащую обработчик, маску сигналов и флаги, В данном случае вам не нужны никакие флаги, и создается пустая маска сигналов с помощью новой функции
    sigemptyset
    .

    Примечание

    После выполнения программы вы можете обнаружить дамп ядра (в файле core). Его можно безбоязненно удалить.

    Множества сигналов

    В заголовочном файле signal.h определены тип

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

    #include <signal.h>

    int sigaddset(sigset_t *set, int signo);

    int sigemptyset(sigset_t *set);

    int sigfillset(sigset_t *set);

    int sigdelset(sigset_t *set, int signo);

    Приведенные функции выполняют операции, соответствующие их названиям,

    sigemptyset
    инициализирует пустое множество сигналов. Функция
    sigfillset
    инициализирует множество сигналов, заполняя его всеми заданными сигналами,
    sigaddset
    и
    sigdelset
    добавляют заданный сигнал (
    signo
    ) в множество сигналов и удаляют его из множества. Они все возвращают 0 в случае успешного завершения и -1 в случае ошибки, заданной в переменной
    errno
    . Единственная определенная ошибка
    EINVAL
    описывает сигнал как некорректный.

    Функция

    sigismember
    определяет, включен ли заданный сигнал в множество сигналов. Она возвращает 1, если сигнал является элементом множества, 0, если нет и -1 с
    errno
    , равной
    EINVAL
    , если сигнал неверный.

    #include <signal.h>

    int sigismember(sigset_t *set, int signo);

    Маска сигналов процесса задается и просматривается с помощью функции

    sigprocmask
    . Маска сигналов — это множество сигналов, которые заблокированы в данный момент и не будут приниматься текущим процессом.

    #include <signal.h>

    int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

    Функция

    sigprocmask
    может изменять маску сигналов процесса разными способами в соответствии с аргументом
    how
    . Новые значения маски сигналов передаются в аргументе
    set
    , если он не равен
    null
    , а предыдущая маска сигналов будет записана в множество сигналов
    oset
    .

    Аргумент

    how
    может принимать одно из следующих значений:

    SIG_BLOCK
    — сигналы аргумента
    set
    добавляются к маске сигналов;

    SIG_SETMASK
    —маска сигналов задается аргументом
    set
    ;

    SIG_UNBLOCK
    — сигналы в аргументе
    set
    удаляются из маски сигналов.

    Если аргумент

    set
    равен
    null
    , значение
    how
    не используется и единственная цель вызова — перенести значение текущей маски сигналов в аргумент
    oset
    .

    Если функция

    sigprocmask
    завершается успешно, она возвращает 0. Функция вернет -1, если параметр
    how
    неверен, в этом случае переменная
    errno
    будет равна
    EINVAL
    .

    Если сигнал заблокирован процессом, он не будет доставлен, но останется ждать обработки. Программа может определить с помощью функции

    sigpending
    , какие из заблокированных ею сигналов ждут обработки.

    #include <signal.h>

    int sigpending(sigset_t *set);

    Она записывает множество сигналов, заблокированных от доставки и ждущих обработки, в множество сигналов, на которое указывает аргумент

    set
    . Функция возвращает 0 при успешном завершении и -1 в противном случае с переменной
    errno
    , содержащей ошибку. Данная функция может пригодиться, когда программе потребуется обрабатывать сигналы и управлять моментом вызова функции обработки.

    С помощью функции

    sigsuspend
    процесс может приостановить выполнение, пока не будет доставлен один сигнал из множества сигналов. Это более общая форма функции
    pause
    , с которой вы уже встречались.

    #include <signal.h>

    int sigsuspend(const sigset_t *sigmask);

    Функция

    sigsuspend
    замещает маску сигналов процесса множеством сигналов, заданным в аргументе
    sigmask
    , и затем приостанавливает выполнение. Оно будет возобновлено после выполнения функции обработки сигнала. Если полученный сигнал завершает программу,
    sigsuspend
    никогда не вернет ей управление. Если полученный сигнал не завершает программу,
    sigsuspend
    вернет с переменной
    errno
    , равной
    EINTR
    .

    Флаги sigaction

    Поле

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


    Таблица 11.5

    Имя сигнала Описание
    SA_NOCLDSTOP
    Не генерируется
    SIGCHLD
    , когда дочерние процессы остановлены
    SA_RESETHAND
    Восстанавливает при получении действие, соответствующее значению
    SIG_DFL
    SA_RESTART
    Перезапускает прерванные функции вместо ошибки
    EINTR
    SA_NODEFER
    При перехвате сигнала не добавляет его а маску сигналов

    Флаг

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

    Многие системные вызовы, которые использует программа, прерываемые, т.е. при получении сигнала они вернутся с ошибкой и переменная

    errno
    получит значение
    EINTR
    , чтобы указать, что функция вернула управление в результате получения сигнала. Поведение требует повышенного внимания со стороны приложения, использующего сигналы. Если в поле
    sa_flags
    функции
    sigaction
    установлен флаг
    SA_RESTART
    , функция, которая в противном случае могла быть прервана сигналом, вместо этого будет возобновлена, как только выполнится функция обработки сигнала.

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

    SA_NODEFER
    , маска сигнала не меняется при получении этого сигнала.

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

    Функции, которые безопасно вызываются в обработчике сигнала и в стандарте X/Open гарантированно описанные либо как реентерабельные, либо как самостоятельно не возбуждающие сигналов, перечислены в табл. 11.6.

    Все функции, не включенные в табл. 11.6, следует считать небезопасными в том, что касается сигналов.


    Таблица 11.6

    access
    alarm
    cfgetispeed
    cfgetospeed
    cfsetispeed
    cfsetospeed
    chdir
    chmod
    chown
    close
    creat
    dup2
    dup
    execle
    execve
    exit
    fcntl
    fork
    fstat
    getegid
    geteuid
    getgid
    getgroups
    getpgrp
    getpid
    getppid
    getuid
    kill
    link
    lseek
    mkdir
    mkfifo
    open
    pathconf
    pause
    pipe
    read
    rename
    rmdir
    setgid
    setpgid
    setsid
    setuid
    sigaction
    sigaddset
    sigdelset
    sigemptyset
    sigfillset
    sigismember
    signal
    sigpending
    sigprocmask
    sigsuspend
    sleep
    stat
    sysconf
    tcdrain
    tcflow
    tcflush
    tcgetattr
    tcgetpgrp
    tcsendbreak
    tcsetattr
    tcsetpgrp
    time
    times
    umask
    uname
    unlink
    utime
    wait
    waitpid
    write
         
    Общая сводка сигналов

    В этом разделе мы перечисляем сигналы, в которых нуждаются программы Linux и UNIX для обеспечения стандартных реакций.

    Стандартное действие для сигналов, перечисленных в табл. 11.7, — аварийное завершение процесса со всеми последствиями вызова функции

    _exit
    (которая похожа на
    exit
    , но не выполняет никакой очистки перед возвратом управления ядру). Тем не менее, состояние становится доступным функции
    wait
    , а функция
    waitpid
    указывает на аварийное завершение, вызванное описанным сигналом.


    Таблица 11.7

    Имя сигнала Описание
    SIGALRM
    Генерируется таймером, установленным функцией
    alarm
    SIGHUP
    Посылается управляющему процессу отключающимся терминалом или управляющим процессом во время завершения каждому процессу с высоким приоритетом
    SIGINT
    Обычно возбуждается с терминала при нажатии комбинации клавиш <Ctrl>+<C> или сконфигурированного символа прерывания
    SIGKILL
    Обычно используется из командной оболочки для принудительного завершения процесса с ошибкой, т.к. этот сигнал не может быть перехвачен или проигнорирован
    SIGPIPE
    Генерируется при попытке записи в канал при отсутствии связанного с ним считывателя
    SIGTERM
    Отправляется процессу как требование завершиться. Применяется UNIX при выключении для запроса остановки системных сервисов. Это сигнал, по умолчанию посылаемый командой
    kill
    SIGUSR1
    ,
    SIGUSR2
    Может использоваться процессами для взаимодействия друг с другом, возможно, чтобы заставить их сообщить информацию о состоянии

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


    Таблица 11.8

    Имя сигнала Описание
    SIGFPE
    Генерируется исключительной ситуацией во время операций с плавающей точкой
    SIGILL
    Процессор выполнил недопустимую команду. Обычно возбуждается испорченной программой или некорректным модулем совместно используемой памяти
    SIGQUIT
    Обычно возбуждается с терминала при нажатии комбинации клавиш <Ctrl>+<\> или сконфигурированного символа завершения (quit)
    SIGSEGV
    Нарушение сегментации, обычно возбуждается при чтении из некорректного участка памяти или записи в него, а также выход за границы массива или разыменование неверного указателя. Перезапись локального массива и повреждение стека могут вызвать сигнал
    SIGSEGV
    при возврате функции по неверному адресу

    При получении одного из сигналов, приведенных в табл. 11.9, по умолчанию процесс приостанавливается.


    Таблица 11.9

    Имя сигнала Описание
    SIGSTOP
    Останавливает выполнение (не может быть захвачен или проигнорирован)
    SIGTSTP
    Сигнал останова терминала часто возбуждается нажатием комбинации клавиш <Ctrl>+<Z>
    SIGTTIN
    ,
    SIGTTOU
    Применяются командной оболочкой для обозначения того, что фоновые задания остановлены, т.к. им необходимо прочесть данные с терминала или выполнить вывод

    Сигнал

    SIGCONT
    возобновляет остановленный процесс и игнорируется при получении неостановленным процессом. Сигнал
    SIGCHLD
    по умолчанию игнорируется (табл. 11.10).


    Таблица 11.10

    Имя сигнала Описание
    SIGCONT
    Продолжает выполнение, если процесс остановлен
    SIGCHLD
    Возбуждается, когда останавливается или завершается дочерний процесс

    Резюме 

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

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








    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх