Итак, процессы. Для операционной системы процесс - это наименьшая единица, потребляющая системные ресурсы (процессорное время, оперативная память). Каждый ПРОЦЕСС исполняет ПРОГРАММУ. Или, как уже говорилось, процесс и есть, собственно, исполнение какой-то программы. Разные процессы могут НЕЗАВИСИМО исполнять одну и ту же программу. В Unix процессы связаны друг с другом отношениями "родства". Про процесс, который породил другой процесс, говорят как о РОДИТЕЛЬСКОМ ПРОЦЕССе (parent process), а о порожденном - как о ДОЧЕРНЕМ ПРОЦЕССе (child process) данного процесса. Корень иерархии всех процессов в системе - процесс с именем init. Каждый процесс имеет уникальный внутри системы номер, который называется PID (аббревиатура от process identifier).
Какие средства имеет программист для работы с процессами, иными словами, какие элементарные операции для управления процессами предоставляет ОС Unix программисту? Их не так много. Процесс может СОЗДАТЬ другой ПРОЦЕСС (самый первый процесс, init, создается особым образом при запуске системы), процесс может ЗАВЕРШИТЬ свое исполнение, процесс может ЗАМЕНИТЬ программу, которую он исполняет (ЗАПУСТИТЬ другую программу), процесс может ДОЖДАТЬСЯ завершения дочернего процесса, процесс может ПОЛУЧИТЬ значение PID текущего процесса, то есть свой собственный, а также значение PID своего родительского процесса.
Рассмотрим, с помощью каких системных вызовов и как выполняются эти операции. Начнем с создания процесса. ВСЕ процессы в системе порождаются с помощью системного вызова fork(), исключение составляет, как уже было замечено, самый первый процесс, init, который не может быть создан таким образом по очевидной причине отсутствия процесса, который мог бы это сделать. Этот вызов довольно интересный. Выполняет он простое по сути действие, именно, просто копирует процесс, сделавший этот вызов (fork - развилка, было один, стало два). Порожденный процесс ничем, кроме значений PID и PPID (parent PID), от родительского процесса не отличается. Интересность в том, как и куда он (вызов) возвращается. Если операция завершилась успешно, то он возвращается сразу в два процесса, родительский и порожденный, при этом первому возвращается PID порожденного только что процесса, а второму - 0. Использование кода возврата fork() - единственный способ для программы различить, кто родитель, а кто порожденный. Если же процесс создать не удалось, например по причиние нехватки памяти или потому, что число процессов в системе превысило бы максимально возможное, то этот вызов возвращается, естественно, только к несостоявшемуся родителю и возвращает константу -1.
Завершение процесса. Можно догадаться, что системный вызов называется exit(), и эта догадка будет НЕправильной :-). На самом деле exit() - это библиотечная функция из стандартной библиотеки libc, а системный вызов называется _exit(). Он выполняет следующие действия: все открытые процессом файлы закрываются, все прямые потомки данного процесса "удочеряются" процессом init, родительскому процессу данного процесса посылается сигнал SIGCHLD, и, наконец, процесс завершается, освобождая занимаемую им память (однако, не ВСЮ, смотри ниже про wait()). Функция exit() перед тем, как обратиться к системному вызову _exit(), вызывает все функции, зарегистрированные с помощью функций on_exit(), atexit(), а также сбрасывает (flush) все буфера ввода-вывода. Функция exit() и системный вызов _exit() также интересны тем, что они никогда не возращаются (по понятной причине).
У функции exit() имеется один параметр (целое число), посредством которого завершающийся процесс может передать некоторую информацию родительскому процессу. Заметим, что, несмотря на то, что этот параметр имеет тип int, код возврата дочернего процесса не может превышать значения 255.
Рассмотрим теперь вызов wait() и его "родственников", к которым относятся waitpid(), wait3(), wait4(). Что делает вызов wait()? Он приостанавливает выполнение текущего процесса до тех пор, пока не завершится дочерний процесс или не придет сигнал, который этим процессом не игнорируется (подробнее о сигналах будет дальше). Нас пока интересует первый случай, завершение работы процесса-потомка (если к моменту вызова wait() процесс-потомок уже завершился, вызывающий процесс, естественно, не приостанавливается, возврат происходит сразу). Возникает справедливый вопрос, зачем нужно ждать завершения потомка? Дело в том, что при завершении процесса (когда он сделал exit()) он продолжает использовать небольшое количество оперативной памяти; в частности, где-то же нужно хранить код возврата, например. Эта память освобождается только тогда, когда родительский процесс делает wait(). О процессах, которые завершились, но их родительский процесс еще не ждал их, иногда говорят как о процессах-"зомби". Представьте себе, что некий процесс порождает много дочерних процессов, но не ждет их. Тогда вскоре оперативная память будет захламлена этими вот зомби, а хорошего в большом количестве этой нечисти немного :-). Хорошо, если система устроена таким образом, что при завершении такого плодовитого процесса его потомки становятся потомками процесса init, он их подождет. А вот если нет, то после его завершения потомков ждать будет просто некому и они так и будут бесполезно занимать память до перезагрузки машины.
Продолжаем с wait(). Вызов возвращает PID завершившегося процесса-потомка. Имеет один параметр, указатель на целое число, в которое по возвращении из вызова записывается статус завершившегося потомка. Заметим, что этот статус отнюдь не равен значению числа, которое завершившийся потомок передал в качестве параметра функции exit(), он содержит дополнительную информацию. Что касается непосредственно кода возврата, то оно содержится в младшем байте статуса. Подробности относительно того, какую еще информацию несет статус и как ее оттуда извлекать, смотрите в описании вызова wait(), там же описан и вызов waitpid(), позволяющий более гибко организовать деятельность по ожиданию завершения потомков, в частности, можно дожидаться КОНКРЕТНОГО потомка, можно делать неблокирующее ожидание и еще пару не очень вредных вещей.
Вызовы wait3() и wait4() отличаются от wait() и waitpid() иным синтаксисом, а по поведению вполне аналогичны. Обе они родом из BSD-Unix, в других вариантах Unix имеются для совместимости.
Простой пример, демонстрирующий создание, получение значений PID и PPID, завершение процессов и ожидание завершения дочернего процесса:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> pid_t pid; int main(void) { printf("\nfork(), exit(), getpid(), getppid(), wait() example\n\n"); printf("\\/ step 1 (potential PARENT): forking ... "); fflush(stdout); /* Without flushing CHILD also outputs the string above!!! Of course, flushing can be done by outputting "\n". */ pid = fork(); if ( pid == -1 ) { printf("fork()'ing FAILED\n"); exit(0); }; if ( pid == 0 ) { // we are CHILD process pid_t my_pid = getpid(); pid_t pa_pid = getppid(); printf("\\/ step 1 (CHILD): Hello, world! \ I am child process.\n"); printf("\\/ step 2 (CHILD): I am %d and my \ parent is %d.\n", my_pid, pa_pid); printf("\\/ step 3 (CHILD): exiting ...\n"); sleep(5); // let the parent wait a little bit exit(123); } else { // we are PARENT process pid_t my_pid = getpid(); pid_t pa_pid = getppid(); pid_t fork_pid = pid; int child_status; printf("(PARENT) fork()'ing OK\n"); printf("\\/ step 2 (PARENT): Hello, world! I \ am parent process.\n"); printf("\\/ step 3 (PARENT): I am %d, my parent is %d and \ my child is %d.\n", my_pid, pa_pid, fork_pid); printf("\\/ step 4 (PARENT): waiting for \ child complition ...\n"); pid = wait(&child_status); if ( pid == -1 ) { printf("(PARENT): ooopssss ... \ wait()'ing FAILED.\n"); } else { printf("(PARENT): wait()'ing OK, \ child %d exited, \ exit code is %d.\n", pid, WEXITSTATUS(child_status)); }; printf("\\/ step 5 (PARENT): exiting ...\n"); exit(0); }; }
Что ж, fork() - это хорошо, только вот функциональность процесса от раздвоения не меняется. Если ВСЕ процессы порождаются этим вызовом, то каким образом програмный интерпретатор (оболочка), например, исполняет обязанности почтовой программы или web-сервера ? Ведь неразумно в одной программе иметь код для всех нужд. К тому же, какие у меня завтра появятся нужды, не знает никто, включая меня самого. Итак, от того, что процесс, исполняющий программу-оболочку, раздвоился, он не приобрел способности, присущие процессам, исполняющим программу чтения почты, например. А ведь хочется! Все программы мы запускаем из оболочки. Значит, помимо раздвоения посредством fork(), нужнО средство изменить функциональность процесса или, выражаясь иначе, запустить на исполнение в ЭТОМ же ПРОЦЕССЕ ДРУГУЮ ПРОГРАММУ. И такое средство имеется. Разрешите представить, execve(). У этого вызова 3 параметра, первый - указатель на строку, содержащую имя файла, из которого следует взять программу (это может быть и сценарий, в своей первой строке содержащий #!interpreter [arg], где interpreter - это имя файла, содержащего программу, которая будет интерпретировать сценарий); второй - массив указателей на строки, содержащие аргументы для запускаемой программы и, наконец, третий - массив указателей на строки, содержащие переменные окружения. Вызов меняет содержимое сегментов стека, кода и данных со старого (то, что было у вызывающего процесса) на новое (то, что есть у загружаемой программы) и, таким образом, изменяет программу, которую процесс выполняет. Если требуемую программу запустить не удалось (а причин тому может быть масса, как Вы сами прекрасно понимаете), то вызов возвращает -1. Если же все прошло успешно, то вызов ...:-) правильно, не возвращается вообще.
Необходимо отметить, что к описанному системному вызову в libc есть семейство оберток, а именно, execl(), execlp(), execle(), execv() и execvp(). Отличаются от системного вызова формой передачи параметров и несколько отличным поведением (например, могут искать выполнимый файл, если путь к нему указан не полностью, аналогично тому, как это делает shell); подробности смотрите в описаниях этих функций.
Пример использования вызова execve(). Программа двоит процесс, дочерний процесс запускает программу ps с параметрами aux, родительский просто ожидает завершения потомка. Обратите внимание, что первый (у которого номер - 0) параметр для программы ps - это имя ее самой. В Unix имеется такая договоренность, первым параметром передавать имя программы, однако, отвечает за соблюдение этой договоренности не операционная система, а процессы. Если первым параметром сделать aux, то должного результата не получится, поскольку программа ps, как и все программы в Unix, полагает, что параметры для нее начинаются с argv[1].
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> pid_t pid; int main(void) { printf("\nfork()-exec() example\n\n"); pid = fork(); if ( pid == -1 ) { perror("--PARENT-- fork()"); exit(0); }; if ( pid == 0 ) { // we are CHILD process char * program = "/bin/ps"; char * argv[] = {"/bin/ps","aux", NULL}; char * envp[] = {NULL}; int ret; ret = execve(program, argv, envp); if ( ret == -1 ) { perror("--CHILD-- execve()"); exit(1); }; } else { // we are PARENT process int child_status; pid = wait(&child_status); if ( pid == -1 ) { perror("--PARENT-- wait()"); }; exit(0); } return 0; // for gcc }
В заключение обсуждения примитивов для работы с процессами рассмотрим такую жуть, как демоны. Имеются ввиду процессы-демоны. Вообще, в Unix полно всякой чертовщины :-). Зомби, демоны, детоубийство, число 666 на каждом шагу. Если Вы боитесь, то, мой Вам добрый совет, поставьте себе Форточки и молитесь на иконки г-на $Гейтса :-E. Гы-гы-гы.
Простите, отвлекся. Итак, демоны. Демон - процесс, не имеющий управляющего терминала (а посему не могущий читать со стандартного устройства ввода и выводить на стандартное устройство вывода). Работают в фоновом режиме. Все сервера - демоны (daemons): httpd (hyper text transfer protocol DAEMON), ftpd (file transfer protocol DAEMON) ... знайте, что когда Вы загружаете страничку из и-нета, то с вероятностью 80% Вы имеете дело с де-мо-ном ... :-). Опять отвлекся, простите еще раз. Нас будет интересовать, какие действия должен совершить процесс, чтобы стать представителем нечистой силы. На самом деле это вполне несложная задача, нужно закрыть все стандартные каналы ввода-вывода и создать так называемую СЕССИЮ. Последнее действие совершается с помощью системного вызова setsid().
Впрочем, лучше посмотреть на вот этот коротенький пример:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> pid_t pid; int main(void) { printf(" how to become a daemon example\n"); // forking first pid = fork(); if ( pid == -1 ) { perror("can't fork()"); exit(1); }; if ( pid > 0 ) { // we are parent // do whatever needed, say, make sure that child succeded // with daemonizing itself somehow exit(0); // parent exits } else { // pid == 0, we are child, become a daemon close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); setsid(); // that's all, we r a daemon now while(1) { // do some job here sleep(60); }; // or u can exec() some prog here } }
нет пока
Переходим непосредственно к вводу-выводу и способах организации работы с более чем одним файловым дескриптором. Итак, select(). Этот системный вызов, наряду с несколькими вспомогательными макроопределениями (именно, FD_CLR, FD_ISSET, FD_SET, FD_ZERO) используется для (синхронного) мультиплексирования ввода-вывода. Рассмотрим его подробнее. Он имеет пять параметров; второй, третий и четвертый - это наборы дескрипторов (точнее, указатели на них), за которыми select() будет наблюдать (дескрипторы из первого набора отслеживаются на предмет наличия данных для чтения, из второго - на предмет возможности немедленной записи и из третьего - на предмет возникновения исключительных ситуаций). Первый параметр - целое число, которое должно быть на единицу больше максимального (по значению) файлового дескриптора в этих трех наборах, а третий - это указатель на структуру struct timeval, содержащую максимальное время ожидания изменения состояния дескрипторов в наборах (т.наз. таймаут). Если это время равно 0, то select() возвращается немедленно, если же сам указатель равен NULL, то таймаут равен бесконечности (иными словами, нет таймаута).
Для операций с наборами дескрипторов имеются 4 упомянутые выше макроопределения. Первые три (FD_CLR, FD_ISSET, FD_SET) имеют два параметра, дескриптор и указатель на набор дескрипторов и производят следующие действия: первый исключает данный дескриптор из данного набора, второй проверяет, входит ли данный дескриптор в данный набор и, наконец, третий включает данный дескриптор в данный набор. Последнее макроопределение (FD_ZERO) имеет один параметр, указатель на набор дескрипторов; оно очищает данный набор, то есть удаляет из него все дескрипторы.
Как пользоваться этим вызовом? Перед тем, как его сделать, нужно подготовить нужные нам наборы. Если мы, например, не хотим проверять возможность немедленной записи, то в качестве указателя на соответствующий набор можно передать константу NULL. Подготовить означает включить (используйте FD_SET) в тот или иной набор все дескрипторы, состояние которых мы желаем отслеживать. После возврата из вызова в наборе останутся ТОЛЬКО ТЕ ДЕСКРИПТОРЫ, состояние которых изменилось с момента начала работы вызова. Для выявления того, какие дескрипторы остались в наборе (то есть ГОТОВЫ к той или иной операции), используйте FD_ISSET. Не забывайте отслеживать максимальный по величине дескриптор (полагаться на то, что самый "большой" дескриптор - это тот, который был возращен самым последним вызовом open() или, скажем, socket(), не следует, потому как какие-то из созданных ранее дескрипторов уже могут быть закрыты и их номера будут повторно использованы для вновь создаваемых)
Примера с использованием select() не будет, поскольку он есть в страничке руководства программиста, посвященной этому системному вызову.
Мы рассмотрим лишь, как ведет себя этот вызов по отношению к разным типам дескрипторов. Первый набор - дескрипторы, проверяемые на наличие данных для чтения. Для обычного файла/терминала/сокета наличие дескриптора в наборе после возврата select(), означает, собственно, что имеются данные для чтения (вызов read()) не заблокируется. А вот, например, для сокета, находящемся в режиме прослушивания, готовность дескриптора к чтению означает, что была попытка установления соединения и accept() не заблокируется. Второй набор - дескрипторы, проверяемые на возможность записи данных без блокировки. Тут можно отметить, что для сокетов это можно использовать для определения того, что завершился connect(). Правда, предварительно нужно перевести сокет в неблокирующий режим.
По назначению этот вызов вполне аналогичен предыдущему, отличается только тем, как передается информация о том, за какими дескрипторами надо следить и как возвращается информация о том, какие из них готовы к записи-чтению. Именно, вызов (тут надо отметить, что poll() может быть и системным вызовом и просто оберткой к системному вызову select(), в зависимости от того, предоставляет ли его самого то или иное ядро ОС) имеет три параметра, первый представляет собой массив некоторых структур (точнее, указатель на структуру, но, как всем известно, массив в языке С, как правило, передается как указатель на его первый элемент), второй - длина массива, то есть количество этих структур (как беззнаковое целое) и третий - таймаут в миллисекундах (знаковое целое). Знаковое потому, что любое отрицательное значение используется для указания бесконечного таймаута. При успешном завершении вызов возвращает количество дескрипторов, готовых к операциям ввода-вывода или имеющих какую-то исключительную ситуацию (ошибку), 0 - если произошел таймаут, а при ошибке, как обычно, константу -1 с установлением соотвествующего значения переменной errno.
Элементами массива (первого параметра вызова) являются структуры следующего вида:
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
Первый элемент структуры - собственно, сам файловый дескриптор, второй - БИТОВАЯ МАСКА событий, которые подлежат отслеживанию для этого дескриптора, третий - какие события реально произошли (тоже битовая маска). Второй элемент является для вызова poll() входным параметром, а третий - выходным. В качестве событий для отслеживания можно указывать POLLIN (есть данные для чтения), POLLPRI (есть срочные данные для чтения), POLLOUT (запись не будет заблокирована); в качестве выходных событий могут встретиться три упомянутые (из числа тех, которые были запрошены, естественно) плюс POLLERR (ошибка), POLLHUP (?), POLLINVAL (неправильный запрос, открытого файла с таким дескриптором нет). Запрос на отслеживание нескольких (по типу) событий формируется из упомянутых констант посредством операции побитового ИЛИ (например POLLIN|POLLPRI- хотим знать, когда появятся обыкновенные данные или срочные данные).
Пример использования вызова poll(). Программа работает с одним дескриптором (0, стандартный ввод). Просит ввести чего-нибудь, если в течение заданного таймаута ничего не вводилось, завершается. Таймаут все время уменьшается. Для терминала наличие данных для чтения означает, что была нажата клавиша Enter. Программа не проверяет, какое именно событие произошло (не анализирует stdinfd[0].revents).
#include <stdio.h> #include <unistd.h> #include <sys/poll.h> struct pollfd stdinfd = {0,POLLIN,0}; int poll_ret; char buf[1024]; int timeout = 7000; /* 7s */ int delta = 1000; int main(void) { while (1) { printf("U have %d ms, type \ sumthin (end with /enter/) ... ", timeout); fflush(stdout); poll_ret = poll(&stdinfd, 1, timeout); if ( poll_ret == 0 ) { printf("TIMED OUT, nuthin entered, bye :-P\n"); break; }; if ( poll_ret == -1 ) { perror("poll"); break; }; if ( poll_ret > 0) { scanf("%s", buf); printf("u entered <%s>, cooool, \ lets do it again!\n", buf); }; timeout -= delta; if ( timeout == delta ) delta = delta/10; }; return 0; }
"Сигнал - программное прерывание, доставляемое процессу" (цитата из info libc; кстати, всем программистам, пишущим в Unix и, в частности, в Linux, настоятельно рекомендуется прочитать и заучить наизусть сей замечательный текст, например, ЗДЕСЬ). Как уже отмечалось, "доставка прерывания" означает, что при возникновении некоторого события операционная система вызывает ЗАДАННЫЙ ПРОЦЕССОМ обработчик этого события. Событие может быть вызвано как деятельностью самого процесса, например, обращение к ячейке памяти, к которой процесс не имеет доступа, так и внешними по отношению к процессу причинами, например, изменение размеров окна. В частности, при обращении по "неправильному" адресу вызывается обработчик сигнала SIGSEGV (SEGmentation Violation) или SIGBUS, в зависимости от того, каким именно образом "неправилен" адрес. Всем, кто написал хоть пару программ для Unix, известно любимое
[zed@ip23]$ ./a.out Segmentation fault (core dumped) [zed@ip23]$
Это сообщение выводит на экран стандартный обработчик сигнала SIGSEGV, входящий в состав библиотеки libc (в Window$ этому отчасти соответствует не менее любимое "Программа выполнила недопустимую операцию и будет закрыта").
Процесс может отреагировать на сигнал 3-мя "способами": именно, он может отреагировать СТАНДАРТНО (вызывается стандартный обработчик), может ПРОИГНОРИРОВАТЬ сигнал (никакой обработчик не вызывается) и наконец, он может РЕАГИРОВАТЬ ПРОИЗВОЛЬНЫМ ОБРАЗОМ (вызывается НЕСТАНДАРТНЫЙ обработчик, или обработчик, определенный программистом, разрабатывающим программу). Однако, не для всех сигналов у процессов имеется такая свобода выбора. Для некоторых сигналов реакция фиксирована; такие сигналы нельзя перехватывать (задавать собственный обработчик для них) и их также нельзя игнорировать (SIGKILL, SIGSTOP). Кроме того, есть сигналы, игнорирование которых возможно, но сильно не рекомендуется, ибо приводит к неопределенному поведению (например, SIGSEGV, SIGFPE, SIGILL).
Сигнал может быть послан процессу не непосредственно операционной системой, но также и другим процессом, что можно использовать, например, для синхронизации процессов. Посылка сигнала осуществляется системным вызовом kill(). Имеется также одноименная программа kill. Название и системного вызова, и программы не совсем корректно отражает их функциональность; это связано с тем, что часто оба средства используются именно для посылки сигнала SIGKILL, короче, так сложилось исторически. Вообще, когда нужно оправдать как-то ту или иную нелепость, несуразность, нелогичность, фраза "так сложилось исторически" находит широкое (и успешное) применение :-).
Рассмотрим подробно примитивы работы с сигналами. Можно ОПРЕДЕЛИТЬ РЕАКЦИЮ на сигнал, можно ПОСЛАТЬ сигнал какому-либо процессу, в частности, текущему, то есть самому себе и еще можно ДОЖДАТЬСЯ сигнала ( смотри, однако, ниже про БЛОКИРОВКУ и ДЕБЛОКИРОВКУ сигнала).
Первое действие осуществляется посредством вызова signal(). Имеет 2 параметра, первый - номер сигнала, на который требуется установить обработчик, второй - указатель на функцию-обработчик. Последняя должна принимать один параметр типа int и не должна возвращать никакого значения. В качестве второго параметра можно указать SIG_IGN или SIG_DFL, что означает игнорировать данный сигнал и восстановить реакцию на данный сигнал по умолчанию, соотвественно. Вызов возвращает указатель на старый обработчик или константу SIG_ERR в случае ошибки.
Второе и третье действие совершаются с помощью системных вызовов kill() и pause(), соответственно. Первый из них имеет два параметра, второй - собственно, номер сигнала, который нужно послать, а первый задает получателя сигнала (несколько хитрым образом, смотри описание этого вызова). При успешном завершении возвращается 0, при ошибке -1. Второй вызов переводит вызывающий процесс в состояние сна до тех пор, пока не придет какой-нибудь сигнал. Параметров не имеет, возвращает всегда -1.
Пример использования сигналов для определения готовности файла/устройства к чтению:
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> void io_handler(int sig); char in_buf[256] = ""; char out_buf[256] = ""; int main(void) { printf("\nsignal() (interrupt i/o) exersise\n"); /* если есть чего-нибудь на стандартном входе, посылать SIGIO */ if ( fcntl(STDIN_FILENO, F_SETFL, O_ASYNC) == -1 ) { perror("fcntl/F_SETFL"); exit(1); }; /* SIGIO посылать НАМ */ if ( fcntl(STDIN_FILENO, F_SETOWN, getpid() ) ) { perror("fcntl/F_SETOWN"); exit(1); }; /* установить обработчик на SIGIO */ if ( signal(SIGIO, io_handler) == SIG_ERR ) { perror("signal/SIGIO"); exit(1); }; /* работаем */ while (1) { pause(); // ждем сигнала printf("%s", out_buf); memset(in_buf, 0, 256); memset(out_buf, 0, 256); }; } /* обработчик сигнала SIGIO */ void io_handler(int sig) { int nbytes, r; // how many bytes are ready? r = ioctl(STDIN_FILENO, FIONREAD, &nbytes); if ( r == -1 ) { perror("ioctl()"); }; r = read(STDIN_FILENO, in_buf, nbytes); if ( r == -1 ) { perror("read()"); }; sprintf(out_buf, "got signal %d\n you entered %s", sig, in_buf); }
Вначале программа производит подготовительные действия. С помощью двух вызовов fcntl() устанавливается асинхронный режим ввода-вывода для стандартного устройства ввода (если вы переопределите стандартный ввод, сделав его каким-либо файлом на диске, то программа работать не будет, асинхронный режим с файлами на диске не работает); также устанавливается получатель сигнала SIGIO. Затем устанавливается обработчик этого сигнала. Далее программа переходит в бесконечный цикл, в котором она выполняет следующие действия: ждет прихода сигнала, когда он приходит, вызывается (не программой, естественно, а операционной системой) обработчик этого сигнала, который читает в буфер введенную пользователем с клавиатуры строку и помещает ее, наряду с дополнительной информацией, в другой буфер. После этого вызов pause() возвращается, основная программа выводит на экран подготовленную строку и потом чистит оба буфера. После этого цикл повторяется. Прервать программу можно, нажав на клавиатуре комбинацию ^c. Можно выделить 4 операции с данными: непосредственно чтение, обработка (формирование строки для вывода), отображение и подготовка к следующему циклу. В данной программе первые две операции выполняет обработчик сигнала, а последние две - основная программа. В принципе, все операции можно было бы выполнить непосредственно в обработчике, а можно, наоборот, в основной программе после вызова pause(). В данной программе работа с данными разделена между обработчиком и основной программой (без особой цели, просто для демонстрации). Заметьте, что программа полагается на то, что обработчик исполнится прежде, чем вернется вызов pause(). Если это условие не будет выполняться, программа не будет работать так, как ожидается. Кроме того, если сигнал придет во время того, когда в основном цикле выводится строчка и очищаются буферы, то программа так же будет работать неправильно (для этого нужно, чтобы пользователь успел набрать что-нибудь на клавиатуре, хотя бы просто нажать Enter, во время выполнения этих операций).
Что касается разделения обязанностей между обработчиком сигнала и "главной" программой, то наиболее безопасным с точки зрения возможности написать программу, содержащую очень трудно обнаруживаемые ошибки, является способ "ВСЕ делать в обработчике, а в основной программе просто вызывать pause() или sleep()". Всегда, когда часть программы вызывается АСИНХРОННО, будь то обработчик прерывания от аппаратуры или обработчик программного прерывания (сигнала) и он работает с ресурсом (любой природы), с которым работает и "главная" программа, возникает необходимость синхронизации доступа к этому ресурсу и возможность наделать ошибок. Ошибки эти трудно выловить, поскольку они возникают не каждый раз, а иногда, в зависимости от того, какой участок кода начал работать раньше/позже, от того, в какой именно момент начал работать обработчик прерывания сигнала/прерывания/события. Это называется "но вчера-то работало ведь!!!!" :-). В англоязычной литературе для обозначения ошибок, связанных с такими вещами, употребляется термин "timing errors".
Очень аккуратно, например, нужно использовать всевозможного рода переменные-флаги, которые устанавливаются обработчиком сигнала, а проверяются в главной программе. Операция проверки флага и условного перехода может быть не атомарной (может прерваться обработчиком сигнала), что может привести к нежелательным последствиям в виде, например, зависания программы.
Наряду с системными вызовами kill(), pause(), alarm() стандарт POSIX содержит более сложные и развитые средства для работы с сигналами (вызов signal() в этот стандарт не входит, он входит в стандарт ANSI C). Этот стандарт определяет несколько системных вызовов, именно sigaction(), sigprocmask(), sigpending(), sigsuspend(), а также несколько вспомогательных библиотечных функций для манипуляций наборами сигналов.
Рассмотрим для начала эти функции. Всего их 5, называются они sigemptyset(), sigfillset(), sigaddset(), sigdelset(), sigismember(). Первые два имеют 1 параметр, указатель на переменную типа sigset_t, остальные - 2, первый - такой же указатель, второй - номер сигнала. Названия у этих функций, надо сказать, просто замечательные и говорят сами за себя: sigemptyset() очищает заданный набор сигналов, то есть удаляет из него все сигналы, sigfillset() включает в заданный набор все возможные сигналы, sigaddset() добавляет заданный сигнал в заданный набор, sigdelset() удаляет заданный сигнал из заданного набора sigismember() проверяет, входит ли заданный набор заданный сигнал. Все функции, кроме последней, возвращают -1 при ошибке и 0 при успешном завершении. Последняя возвращает 1, если сигнал имеется в наборе, 0 - если не имеется и -1 при ошибке. Три из системных вызова для работы с сигналами (исключая sigaction()), имеют хотя бы один параметр, представляющий собой указатель на набор сигналов; для подготовки набора сигналов перед передачей их этим вызовам, а также для интерпретации содержимого набора после возврата из них как раз и предназначены описанные только что функции.
Перейдем к рассмотрению непосредственно системных вызовов для организации работы программы с использованием сигналов. Но сначала обратимся к опущенному ранее (в разделе "Работа с сигналами") примитиву работы с сигналами.
Именно, сигналы можно БЛОКИРОВАТЬ и РАЗБЛОКИРОВАТЬ. Когда сигнал ЗАБЛОКИРОВАН, его обработчик не вызывается, даже если имеется сигнал, требующий обработки. Сигнал как бы ожидает обработки (на английском языке такой сигнал называется pending signal - "сигнал, ожидающий обработки", "висячий сигнал"). Обработчик сигнала будет вызван сразу, как только процесс разблокирует этот сигнал (что будет, если во время того, пока сигнал заблокирован, придет еще один ТАКОЙ ЖЕ сигнал - это отдельная долгая грустная история). Каждый сигнал в каждый момент времени у каждого процесса либо ЗАБЛОКИРОВАН, либо РАЗБЛОКИРОВАН. БЛОКИРОВКА сигнала по сути означает ВРЕМЕННОЕ ИГНОРИРОВАНИЕ сигнала; разница в том, что если процесс НИКОГДА не хочет получать данный сигнал, он его игнорирует, а ОС при возникновении данного сигнала для данного процесса его просто аннулирует; если же для процесса нежелательно получать тот или иной сигнал лишь в течение некоторых небесконечных промежутков времени, то он его блокирует, а ОС сохраняет информацию о наличии сигнала для данного процесса и доставляет его (сигнал) процессу, когда последний разблокирует этот сигнал.
СОВОКУПНОСТЬ всех сигналов, которые в данный момент времени ЗАБЛОКИРОВАНЫ, называется МАСКОЙ СИГНАЛОВ (signal mask). У каждого процесса, естественно, своя маска сигналов; при создании процесса она наследуется от родительского процесса. Для манипуляций с МАСКОЙ СИГНАЛОВ (не путать с описанными манипуляциями над НАБОРАМИ СИГНАЛОВ!!! - последние ничего не блокируют и не деблокируют, это просто операции над переменной определенного типа) предназначен системный вызов sigprocmask().
Для чего нужна блокировка сигналов? Как уже говорилось, при наличии участков кода программы, которые получают управление АСИНХРОННО, вне связи с тем, что делает процесс в данный момент, могут возникать несколько специфичные проблемы. Такие проблемы возникают практически всегда, когда два или более участка кода программы, из которых по крайней мере один получает управление асинхронно, используют при работе ОБЩИЙ РЕСУРС. Простейшим примером такого общего ресурса может быть ГЛОБАЛЬНАЯ ПЕРЕМЕННАЯ (не всякая, правда; если любой доступ к этой переменной осуществляется за ОДНУ машинную инструкцию, то описываемых далее проблем не может возникнуть В ПРИНЦИПЕ, поскольку процессор по физическим причинам не может приостановить выполнение инструкции, он всегда ее выполняет до конца; во время выполнения инструкции процессор даже не проверяет, не возникло ли прерывания от какого-либо внешнего устройства; следовательно, ОС, код которой получает управление именно по прерываниям (системные вызовы не в счет, это вещь синхронная, инициируются они как раз таки самим процессом), не может ни переключить контексты процессов, ни вызвать обработчик сигнала). Как известно, участки кода, которые имеют дело с общим ресурсом произвольной природы, называются КРИТИЧЕСКИМИ СЕКЦИЯМИ. Если основная программа и обработчики сигналов имеют общие ресурсы, то на время, пока основная программа с ними работает (читает, пишет, не важно), соответствующие сигналы в большинстве случаев ДОЛЖНЫ БЫТЬ ЗАБЛОКИРОВАНЫ. Кроме того, сигнал может придти во время того, когда исполняется обработчик этого самого сигнала. Поэтому на время работы обработчика сигнала этот сигнал также желательно блокировать; в противном случае может получится вложенный вызов обработчика, что может привести к различным неприятным последствиям.
Еще пример. Допустим, Вам нужно в программе выполнить некое действие только в том случае, если сигнал НЕ пришел. Обработчик сигнала устанавливает флаг. В основной программе Вы проверяете этот флаг и, если он не установлен, выполняете требуемые действия. Такой алгоритм ненадежен! Дело в том, что сигнал может придти в момент времени сразу после проверки флага, но до начала выполнения нужных действий. Вы можете возразить, что так не может быть, что между проверкой флага и началом действий нет никакого промежутка. На самом деле это не так. Эти две операции, КАК ПРАВИЛО, осуществляются за несколько машинных инструкций. После любой из них управление может получить обработчик. Может случится так, что управление он получит тогда, когда основная программа только что проверила флаг, но не начала последующих действий. В результате сигнал пришел, а Вы выполните действия, которые намеревались выполнить только в том случае, если сигнал не придет. Самое неприятное, что так будет получаться не всегда, а время от времени, причем, скорее всего очень редко (возможно, никогда), то есть возникающую ошибку будет очень трудно (читай - невозможно) воспроизвести по собственному желанию. Правильный способ проверки флага состоит в том, чтобы на время проверки блокировать сигнал, обслуживаемый обработчиком, устанавливающим этот флаг.
Итак, имеем следующие элементарные действия в отношении сигналов:
определение реакции на сигнал (установка обработчика)
ожидание сигнала
блокировка сигнала (а также проверка того, какие сигналы в данный момент времени заблокированы)
деблокировка сигнала
посылка сигнала
Что касается последнего действа, то никаких дополнительных средств, кроме kill(), стандарт POSIX не определяет. Переходим, наконец, к рассмотрению остальных примитивов. Начнем с блокировки и деблокировки (раз уж мы посвятили столько буковок этому вопросу, то постараемся сделать изложение как можно более плавным). Осуществляется сие с помощью вызова sigprocmask(). Вызов имеет три параметра, первый (целое число) задает, что надо делать с маской сигналов, вторые два - указатели на наборы сигналов, первый из которых задает требуемую маску (входной параметр), а второй используется для возвращения старой маски сигналов, то есть той, которую процесс имел в момент обращения к sigprocmask(). Первый параметр может принимать значения SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK. В первом случае к маске сигналов (помним, что маска сигналов процесса - это совокупность сигналов, которые заблокированы в данный момент времени) добавляется набор сигналов, задаваемый вторым параметром, во втором случае из маски сигналов удаляется сигналы, входящие в набор, задаваемый вторым параметром (то есть эти сигналы разблокируются), наконец, в третьем - маска сигналов заменяется на этот набор. Если неинтересно, какая была маска до вызова, в качестве третьего параметра передаем NULL. Если не желаем ничего менять, просто нужно знать, какая сейчас маска, передаем в качестве второго параметра NULL. Вызов возвращает 0, если все нормально, -1 при ошибке.
Если вызов приводит к разблокировке сигналов, которые уже ожидали обработки, то он возвращается только после того, как будет вызван ПО КРАЙНЕЙ МЕРЕ ОДИН из обработчиков этих сигналов. Порядок вызова обработчиков никак не определяется; если это важно, разблокируйте по одному сигналу за один вызов.
Давно не было примеров. Ниже приведен исходный текст простой бесполезной программы, демонстрирующей блокировку и деблокировку сигналов, а также использование примитивов для работы с наборами сигналов.
#include <stdio.h> #include <signal.h> extern const char * const sys_siglist[]; void print_blocked(void) { int nblocked = 0; int sig; sigset_t blocked; int ret; // get signal mask ret = sigprocmask(SIG_BLOCK, NULL, &blocked); if ( ret == -1 ) { perror("sigprocmask()"); exit(1); }; for ( sig = 0; sig < NSIG; sig++ ) { if ( sys_siglist[sig] ) { if ( sigismember(&blocked, sig) ) { printf("#%d (%s)\n", sig, sys_siglist[sig]); nblocked++; }; }; }; if ( nblocked ) printf(" total %d signal(s) blocked } \n\n", nblocked); else printf(" none } \n\n"); } sigset_t blocked_orig; sigset_t blocked_new; int ret; int main(void) { printf("\nsigprocmask() example\n\ ~~~~~~~~~~~~~~~~~~~~~\n\n"); printf(" { originally blocked signals:\n"); print_blocked(); // form a set, containing SIGINT, SIGTERM ret = sigemptyset(&blocked_new); ret = sigaddset(&blocked_new, SIGINT); ret = sigaddset(&blocked_new, SIGTERM); ret = sigaddset(&blocked_new, SIGSEGV); // block SIGINT, SIGTERM, SIGSEGV, save original mask ret = sigprocmask(SIG_BLOCK, &blocked_new, &blocked_orig); if ( ret == -1 ) { perror("sigprocmask()"); exit(1); }; printf(" { currently blocked signals:\n"); print_blocked(); ret = sigdelset(&blocked_new, SIGINT); ret = sigdelset(&blocked_new, SIGTERM); // SIGSEGV still in the set // unblock SIGSEGV, save original mask ret = sigprocmask(SIG_UNBLOCK, &blocked_new, NULL); if ( ret == -1 ) { perror("sigprocmask()"); exit(1); }; printf(" { currently blocked signals:\n"); print_blocked(); // restore original signal mask ret = sigprocmask(SIG_SETMASK, &blocked_orig, NULL); if ( ret == -1 ) { perror("sigprocmask()"); exit(1); }; return 0; }
В программе определена одна функция (помимо main()), которая распечатывает на стандартное устройство вывода список заблокированных сигналов. Главная функция распечатывает с ее помощью этот список, затем блокирует сигналы SIGINT, SIGTERM, SIGSEGV (одновременно запоминая текущую маску сигналов), снова печатает список, наконец, деблокирует SIGSEGV и опять же распечатывает список. Перед завершением программа восстанавливает первоначальную маску сигналов.
Переходим к операции ожидания сигнала. Выше был приведен пример программы, которая для этой цели использовала вызов pause(). Повторим, что использование его наиболее надежно в случае, если все манипуляции с данными выполняются в обработчике сигнала, а главная программа ничего, кроме вызова pause(), не делает. Если же имеется необходимость разделить обработку данных между обработчиком сигнала и главной программой, то просто использование вызова pause() весьма ненадежно (смотри раздел, где говорится о том, зачем нужна блокировка).
Стандарт POSIX, однако, предлагает другое средство для ожидания сигнала, именно, системный вызов sigsuspend(). Рассмотрим его подробно. Вызов имеет один параметр, указатель на набор сигналов. Вызов переводит процесс в состояние сна до тех пор, пока не придет один из сигналов, НЕ входящий в заданный набор, иными словами, сигналы, входящие в набор, на время исполнения вызова блокированы. После возврата маска сигналов восстанавливается. Фактически данный вызов позволяет ожидать определенные сигналы, в то время как другие сигналы будут обрабатываться соответствующими обработчиками.
Приведем отрывок исходного кода (взято из info libc), демонстрирующий, как правильно ожидать какой-то сигнал с помощью sigsuspend():
(1) sigset_t mask, oldmask; ... /* Set up the mask of signals to temporarily block. */ (2) sigemptyset (&mask); (3) sigaddset (&mask, SIGUSR1); ... /* Wait for a signal to arrive. */ (4) sigprocmask (SIG_BLOCK, &mask, &oldmask); (5) while (!usr_interrupt) (6) sigsuspend (&oldmask); (7) sigprocmask (SIG_UNBLOCK, &mask, NULL);
Пример не очень тривиальный, а посему требует пояснений. В строке (1) просто описаны 2 набора сигналов. После строк (2) и (3) набор сигналов mask состоит из одного сигнала, именно, SIGUSR1. В строке (4) в маску сигналов процесса добавляется этот SIGUSR1, таким образом, этот сигнал оказывается заблокированным, при этом в oldmask записывается старая маска сигналов, в которой SIGUSR1 НЕТ. (предполагается, видимо, что изначально SIGUSR1 НЕ заблокирован). Флаг usr_interrupt устанавливается обработчиком сигнала SIGUSR1, изначально имеет, видимо, нулевое значение (не установлен). Далее. В строке (5) организован цикл while(), который прекращается тогда, когда обработчик SIGUSR1 установит флаг usr_interrupt. Строчка (6) - тело цикла, состоящее из вызова sigsuspend(). В качестве параметра этому вызову передается СТАРАЯ МАСКА. Это значит, что на время исполнения вызова те из сигналов, которые изначально не были блокированы, будут заблокированы, а те, которые были блокированы, будут разблокированы. Незаблокированным будет и ожидаемый нами SIGUSR1 (в старой маске, его, по предположению, нет). Отметим два не очень очевидных момента.
Первый. Вызов sigsuspend() возвращается НЕ ТОЛЬКО после того как возвратился обработчик сигнала SIGUSR1, но также и после прихода и обработки ЛЮБОГО ДРУГОГО незаблокированного сигнала. Обработчики этих других сигналов нужный нам флаг, естественно, не устанавливают, поэтому цикл здесь необходим. Если произойдет так, что sigsuspend() вернется из-за того, что процесс получил какой-то другой сигнал, не SIGUSR1 (и флаг не установится), то он опять приостановится и так будет продолжаться до тех пор, пока не придет SIGUSR1 и его обработчик не установит флаг.
Второй. После возврата sigsuspend() устанавливается маска сигналов, которая имела место быть до вызова. А в ней сигнал SIGUSR1 был заблокирован. Поэтому в строке (7) этот сигнал разблокируется явным образом.
У нас остался нерассмотренным один вызов, sigaction(). Как и вызов signal(), он предназначен для определения реакции процесса на сигнал, но по сравнению с последним предоставляет программисту более богатые возможности. За все, однако, нужно платить, в данном случае ценой за богатство возможностей является бОльшая сложность в использовании.
Итак, системный вызов sigaction(). Имеет 3 параметра: первый задает номер сигнала, на который устанавливается реакция, второй задает, собственно, эту реакцию в виде указателя на структуру struct sigaction, третий параметр (такой же указатель) используется для возвращения информации о старом обработчике. Сигнал, на который определяется реакция, может быть любым допустимым в данной системе сигналом, кроме SIGKILL и SIGSTOP. Структура struct sigaction содержит 5 полей:
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }
Последний элемент считается устаревшим, использовать его не следует (в заголовочных файлах, входящих в состав современных (2006 год) дистрибутивов Linux это поле уже отсутствует). Первый элемент - указатель на функцию-обработчик сигнала, может также иметь значения SIG_DFL (установить на данный сигнал реакцию по умолчанию) и SIG_IGN (игнорировать данный сигнал). Третий элемент (sa_mask) задает набор сигналов, которые будут блокированы во время работы обработчика. Блокирован будет (по умолчанию) также сигнал, который обрабатывается. Четвертый элемент (sa_flags) - набор флагов, модифицирующих поведение подсистем ОС и libc, ответственных за управление сигналами. Он формируется посредством операции побитового ИЛИ из следующих возможных значений:
Структура siginfo_t, указатель на которую передается обработчику сигнала (в случае, если используется элемент sa_sigaction()), содержит массу информации, в частности, номер сигнала, значение errno, PID процесса-посылателя сигнала, номер файлового дескриптора (для случая сигнала SIGIO). Полное описание элементов этой структуры смотрите в описании sigaction().
Пример, аналогичный примеру использования вызова signal():
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #include <string.h> void io_handler(int sig, siginfo_t* sinfo, void * add_info); struct sigaction io_action; struct sigaction io_action_old; sigset_t blocked; int main(void) { printf("\nsigaction() (interrupt i/o) exersise\n"); if ( fcntl(STDIN_FILENO, F_SETFL, O_ASYNC) == -1 ) { perror("fcntl/F_SETFL"); exit(1); }; if ( fcntl(STDIN_FILENO, F_SETSIG, SIGIO) == -1 ) { perror("fcntl/F_SETSIG"); exit(1); }; if ( fcntl(STDIN_FILENO, F_SETOWN, getpid() ) ) { perror("fcntl/F_SETOWN"); exit(1); }; /* установить обработчик на SIGIO */ io_action.sa_handler = NULL; io_action.sa_sigaction = io_handler; io_action.sa_flags = SA_SIGINFO; io_action.sa_restorer = NULL; if ( sigaction(SIGPOLL, &io_action, &io_action_old) == -1 ) { perror("sigaction()"); exit(1); }; /* работаем */ sigemptyset(&blocked); while (1) sigsuspend(&blocked); } char in_buf[256] = ""; char out_buf[256] = ""; /* обработчик сигналов */ void io_handler(int sig, siginfo_t* sinfo, void * add_info) { int nbytes, r, fd; if ( sig != SIGIO) return; if (sinfo->si_signo != SIGIO ) return; printf("\nSignal info:\n~~~~~~~~~~~~\n"); printf("numb:\t %d (%d)\n", sinfo->si_signo, sig); printf("errn:\t %d\n", sinfo->si_errno); printf("code:\t %d ", sinfo->si_code); switch ( sinfo->si_code ) { case POLL_IN: printf("(POLL_IN - \ data input available)\n"); break; case POLL_OUT: printf("(POLL_OUT - \ output buffers availble)\n"); break; case POLL_PRI: printf("(POLL_PRI - \ urgent input data available)\n"); break; case POLL_MSG: printf("(POLL_MSG - \ input message available)\n"); break; case POLL_ERR: printf("(POLL_ERR - \ i/o error)\n"); break; case POLL_HUP: printf("(POLL_HUP - \ device disconnected)\n"); break; default: printf("???\n"); }; printf("chan:\t %d\n", sinfo->si_fd); if ( sinfo->si_code == POLL_IN) { fd = sinfo->si_fd; r = ioctl(fd, FIONREAD, &nbytes); if ( r == -1 ) { perror("ioctl()"); }; r = read(fd, in_buf, nbytes); if ( r == -1 ) { perror("read()"); }; sprintf(out_buf, "data:\t %s", in_buf); printf("%s", out_buf); memset(in_buf, 0, 256); memset(out_buf, 0, 256); }; }
Программа выполняет следующие действия: с помощью вызова fcntl() устанавливается асинхронный режим для стандартного устройства ввода: кстати, работает и без этого (с тем ОС и ядром, на котором проверялось). с его же помощью производится волшебное действие, без которого в поле sinfo->si_fd никак не хочет записываться номер файлового дескриптора, а именно это наиболее полезно. Замечание: F_SETSIG определено только тогда, когда определено _GNU_SOURCE. еще один вызов fcntl() нужен для определения получателя сигнала SIGIO, а иначе он будет послан бог знает какому процессу, возможно, shell, из которого мы запускаем эту программу. далее заполняется структура sigaction; здесь важно в поле flags установить флаг SA_SIGINFO. после этого с помощью вызова sigaction() устанавливается обработчик сигнала SIGIO. далее программа переходит в бесконечный цикл, в котором с помощью sigsuspend() ожидает прихода сигнала; во время ожидания все сигналы разблокированы.
Обработчик сигнала выводит информацию о сигнале (номер, код события, номер дескриптора) и, если событие состоит в том, что готовы данные, читает их и выводит на экран.
Еще пример (не имеющий, правда, отношения к вводу-выводу):
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <setjmp.h> void segv_handler(int sig, siginfo_t* sinfo, void * add_info); struct sigaction segv_action; struct sigaction segv_action_old; sigset_t blocked; int * bad_ptr; jmp_buf jump_data; int main(void) { printf("\nsigaction()/segv\n"); segv_action.sa_handler = NULL; segv_action.sa_sigaction = segv_handler; segv_action.sa_flags = SA_SIGINFO; segv_action.sa_restorer = NULL; if ( sigaction(SIGSEGV, &segv_action, &segv_action_old) == -1 ) { perror("sigaction()"); exit(1); }; while (1) { bad_ptr = (int*)0x12345678; setjmp(jump_data); sleep(5); *bad_ptr = 0xAAAA5555; }; return 0; } void segv_handler(int sig, siginfo_t* sinfo, void * add_info) { printf("\nSignal info:\n~~~~~~~~~~~~\n"); printf("numb:\t %d (%d)\n", sinfo->si_signo, sig); printf("errn:\t %d\n", sinfo->si_errno); printf("code:\t %d ", sinfo->si_code); switch ( sinfo->si_code ) { case SEGV_MAPERR: printf("(SEGV_MAPERR - \ address not mapped to object)\n"); break; case SEGV_ACCERR: printf("(SEGV_ACCERR - \ invalid permissions for mapped object)\n"); break; default: printf("???\n"); }; printf("addr:\t 0x%.8X\n", (unsigned int)sinfo->si_addr); longjmp(jump_data, 0); }
Программа устанавливает обработчик на сигнал SIGSEGV. Далее в цикле совершает следующие действия. Инициализирует указатель на целое число значением 0x12345678 (обращение по такому адресу, скорее всего, приведет к ошибке). Затем сохраняется контекст с помощью вызова функции setjmp(). После этого программа 5 секунд спит, предвкушая задуманное ею безобразие. По пробуждении задуманное осуществляется - программа обращается по обозначенному выше адресу, пытаясь записать туда некое число. Результатом с почти 100% вероятностью является, естественно, segmentation violation. Однако, никакого
[zed@ip23]$ ./a.out Segmentation fault (core dumped) [zed@ip23]$
не происходит, потому как у нас на SIGSEGV собственный обработчик, который мирно распечатывает информацию о том, что произошло (номер сигнала, код события и адрес памяти, вызвавший исключение процессора). Программа вредная, ей одного раза мало. Поэтому нужно теперь как-то повторить, то есть попасть как-нибудь на место перед sleep(). Это делается с помощью вызова longjmp(). Процесс возвращается в то место, где был сделан последний setjmp().
Несмотря на кажущуюся бесполезность наличия обработчика SIGSEGV, в этом все-таки иногда может быть смысл. Представьте себе, что Вы разрабатываете программную систему, которая позволяет пользователю расширять ее функциональность на основе использования динамических библиотек, загружаемых явно. Пользователь Вашей системы, разрабатывая некое расширение (plugin, как сейчас модно выражаться), по чистой случайности забыл проинициализировать некий указатель. В результате Ваша замечательная программа неминуемо со страшным грохотом падает в файл с известным именем. Самое обидное, что не по Вашей вине. А вот если перед тем, как вызывать функции из написанной невнимательным пользователем библиотеки, Вы установили бы обработчик на нужные сигналы, то в нем можно было вернуться в нужное место и сообщить этому пользователю, что, мол, в такой-то библиотеке в такой-то функции имел место segmentation violation. А так пользователь первым делом будет кричать о том, какую ему глючную программу подсунули, что она с его библиотеками работать не умеет и все такое в этом духе. Это же обидно, правда? :-). А тут Вы ему в нос, так сказать, прискорбный для него факт наличия у него жирного бага гыгыгы :-).
Подытожим кратко все сказанное о сигналах и их обработчиках. Обработчик сигнала - это обычная функция, определеннная где-то в исходном коде программы. Разница между "просто функцией" и функцией-обработчиком сигнала состоит в том, что "просто функцию" Вы вызываете ЯВНО, а обработчики вызываются операционной системой при возникновении того или иного события. Вместо выражения "вызывается обработчик" часто употребляется выражение "посылается сигнал". Можно выделить три основных стратегии использования обработчиков, именно, в них обычно делают следующее: изменяют какую-то глобальную переменную, которая проверяется основной программой для того, чтобы узнать, пришел сигнал или нет. завершают процесс. передают управление в точку, где можно будет исправить ситуацию, вызвавшую появление сигнала.
Самое важное свойство обработчиков сигналов, однако, состоит не сколько в том, что вызов их происходит НЕЯВНО, а в том, что вызываются они АСИНХРОННО. Это означает, что в большинстве случаев Вы не можете предсказать, в каком именно месте программы придет сигнал и будет вызван обработчик. Следствием этого является то, что не всякое действие допустимо внутри обработчика. Поэтому при написании его кода нужно соблюдать ряд предосторожностей, к рассмотрению которых мы и переходим.
Начнем с безобидной на первый взгляд процедуры возврата из обработчика. Но перед этим приведем все стандартные сигналы, распределив их по группам. Макроопределения для сигналов помещены в заголовочный файл signal.h, в частности, там имеется макроопределение NSIG, число сигналов в системе. Итак, имеются следующие группы сигналов:
Продолжаем с возвращением из обработчика сигнала. Здесь возможны три варианта: Нормальное возвращение. Как правило, используется для сигналов будильников и сигналов о возможности ввода-вывода. Возможно, конечно, нормально возвращаться и из обработчика, например, SIGINT, при этом нужно установить флаг, используя который, программа завершится в удобный для нее момент. Обработчики, завершающиеся нормально, должны изменять какие-то глобальные переменные, иначе в них просто не смысла. Нормальное возвращение из обработчика сигнала, уведомляющего о серьезной ошибке - небезопасно, ибо приводит к неопределенному поведению. Завершение процесса. Так обычно реагируют на сигналы, свидетельствующие об ошибке или на сигналы, посылаемые пользователем (интерактивные сигналы, например SIGINT). При этом производится освобождение занятых ресурсов. Наболее честный способ завершить процесс в обработчике сигнала состоит в том, чтобы еще раз явно послать сигнал текущему процессу (то есть себе), предварительно восстановив стандартную реакцию на этот сигнал. Использовать exit() или abort() не рекомендуется, поскольку при этом статус возврата процесса может принять неправильное значение. Нелокальная передача управления из обработчика в какое-то произвольное место программы с использованием функций setjmp() и longjmp(). Тут надо иметь ввиду, что если в момент прихода сигнала программа занималась какой-то обработкой данных, то после возврата из обработчика и из функции setjmp() эта обработка не будет продолжена. Управление передастся на следующую после возврата из setjmp() инструкцию. Избежать этого можно двумя способами: либо блокировать сигнал на время обработки данных, либо доделывать манипуляции с данными в обработчике (или приводить их в какое исходное состояние).
Следующая тонкость связана с возможностью прихода сигнала во время работы какого-либо обработчика. Если для установки обработчика был использован вызов signal(), то в некоторых вариантах Unix (System V Unix) на время работы обработчика реакция автоматически переопределяется на стандартную. Если это нежелательно, то обработчик должен в самом начале переустановить самого себя. Однако, сигнал может придти в момент, когда обработчик уже начал выполняться, но переопределения реакции еще не произошло (вызов библиотечной функции плюс передача ей параметров занимают отнюдь не одну машинную инструкцию). В результате второй сигнал будет обрабатываться стандартным обработчиком, а он может, например завершить процесс, а вы этого не хотели.
Если же для установки реакции на сигнал был использован вызов sigaction(), то на время обработки сигнала ЭТОТ сигнал блокируется и разблокируется только по завершению обработчика. Если очень хочется получать сигнал внутри обработчика ЭТОГО же сигнала, разблокируйте его явно с помощью вызова sigprocmask(). Однако, обработчик сигнала может быть прерван обработчиком сигнала другого типа. Если это нежелательно, то с помощью поля sa_mask cтруктуры struct sigaction, указатель на которую передается системному вызову sigaction(), можно указать, какие сигналы должны быть блокированы в дополнению к тому, который обрабатывается. Отметим еще, что явное использование sigprocmask() в обработчике действует только внутри обработчика; после его завершения маска сигналов восстанавливается, то есть принимает то значение, которое она имела до возникновения сигнала.
Не рекомендуется смешивать использование sigaction() и signal(), на некоторых системах это может привести к странностям; например, адрес обработчика, полученного с помощью sigaction(), может отличаться от адреса, который был указан вызову signal(). Если Вы придерживаетесь стандарта POSIX, используйте sigaction() и все перечисленные POSIX средства для работы с сигналами. Если же важна совместимость с ISO/ANSI C, следует использовать signal().
Ранее где-то уже упоминалась не очень хорошая ситуация, состоящая в том, что в период времени, когда тот или иной сигнал заблокирован, может придти более чем один сигнал этого типа. Что произойдет после того, как сигнал будет разблокирован? Сколько раз будет вызван обработчик? Ответ - обработчик будет вызван ОДИН раз. Иными словами, сигналы не накапливаются, они сливаются. Это значит, в частности, что в обработчике нельзя надежно посчитать число сигналов; единственный достоверный вывод, который можно сделать из того факта, что обработчик получил управление, состоит в том, что за какой то период времени пришел ПО КРАЙНЕЙ МЕРЕ один сигнал. Приведем пример, когда нужно учитывать слияние сигналов. Допустим, некий процесс имеет много потомков, которых, как мы знаем, полагается ждать с помощью вызова wait() после того как они завершатся. Допустим также, что по какой-то надобности наш процесс блокировал SIGCHLD и пока этот сигнал был заблокирован, завершилось несколько потомков. После разблокировки данного сигнала обработчик будет вызван всего один раз. В обработчике нельзя предполагать, что к моменту его вызова завершился только 1 процесс-потомок. Нужно вызывать функции ожидания несколько раз; алгоритм обработчика в значительной мере усложняется.
Теперь о том, что можно делать в обработчиках, не задумываясь о последствиях, и что делать нельзя без необходимых мер предосторожности. Считается, что наилучшим подходом является подход, при котором обработчик не делает ничего, кроме изменения некоторой глобальной переменной, свидетельствующей о том, что пришел сигнал, эта переменная проверяется основной программой, которая и выполняет всю фактическую работу, которую необходимо выполнить при получении того или иного сигнала. Связано это опять же с тем, что вызов обработчика, как правило, непредсказуем в смысле момента времени и может прервать все что угодно, начиная с системных вызовов и заканчивая элементарной операций присваивания:
int a, b; ... a = b;
Последняя на большинстве процессоров выполняется не за одну инструкцию.
Однако, по разным причинам, возможно даже психологического или эстетического характера, не всегда обработчики пишутся в соответствии с упомянутым подходом. Тогда надо поступать следующим образом: Если Ваш обработчик сигнала обращается к какой-либо глобальной переменной, объявляйте ее с модификатором volatile. Таким образом компилятор уведомляется, что значение этой переменной может меняться как бы извне, причем асинхронно и он (компилятор) не будет использовать некоторые способы оптимизации кода; оптимизация может привести к тому, что изменения переменной, которые должен произвести обработчик, будут невозможны. Если вы вызываете в обработчике какую-либо функцию, то нужно убедиться, что эта функция является реентерабельной по отношению к сигналам, в противном случае принять меры к тому, чтобы сигнал не прервал вызываемую функцию.
Под нереентерабельностью функции понимается следующее. Пусть выполнение функции прерывается и в какой-то момент снова начинает выполняться эта же самая функция (а предыдущий ее вызов еще не завершился). Если при этом не может возникнуть никаких нежелательных последствий, то функция реентерабельна (повторно входима), в противном случае - нет, ее нельзя или, по крайней мере, не следует вызывать, пока не завершился ее предыдущий вызов, иными словами, в нее нельзя повторно входить.
Какая функция является нереентерабельной? Таковой МОЖЕТ БЫТЬ любая функция, которая использует, помимо стека, другую память. Рассмотрим несколько примеров.
Если функция использует какую-либо глобальную перменную, статическую или динамическую (но которая для нее заранее подготовлена, а не передается в качестве параметра), то такая функция нереентерабельна. В стандартных библиотеках полно таких функций. В качестве примера можно привести gethostbyname(). Читаем руководство по этой функции: ... хм .... можно не читать, там этот момент не отмечен, догадайтесь, мол, сами. В общем, сия замечательная штука возвращает указатель на некую структуру. Сама структура хранится в некоем буфере. Последовательные вызовы функции используют ОДИН И ТОТ ЖЕ буфер. Теперь представьте, что в обработчике какого-либо сигнала вызываете эту вот gethostbyname(). И еще вы вызываете ее где-то в основной программе. И вот во время того, как работает эта функция, будучи вызванной из основной программы (или даже вызов уже завершился, а программа использует полученную информацию), приходит сигнал, в обработчике которого за какой-то надобностью тоже вызывается gethostbyname(). Поскольку буфер со структурой один на всех, после возврата из обработчика в нем будут уже не те данные, которые намеревалась использовать программа. Может приключиться такая, например, история: пользователь HTTP-клиента набирает "www.white.com", а попадает на "www.black.com" :-). Если функция используется только в обработчике или только в программе, то проблем нет. Если же позарез нужно использовать и там, и там, то извольте в основной программе блокировать сигнал на время работы с подобного рода функцией и полученными с ее помощью данными.
Функция, использующая или модифицирующая некий объект, ей передаваемый, также потенциально нереентерабельная. Примером могут служить функции потокового ввода-вывода, fprintf(), например, ибо они могут модифицировать структуру, описывающую этот поток.
На многих системах нереентерабельны malloc() и free() и, следовательно, все функции, их использующие. Посему память, нужную обработчику, распределяйте заранее и освобождайте ее тоже вне обработчика.
Функции, модифицирующие errno, также нереентерабельны. Если используются POSIX функции, то обработчику передается значение этой переменной; его можно сохранить, а перед выходом восстановить.
Дата последней модификации: 2009-10-16