Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
СПО / Semestr 2 / Lectures 2semestr / Lecture 2_01 / fork_threads_mutex_FIFO_sockets.doc
Скачиваний:
27
Добавлен:
11.04.2015
Размер:
301.57 Кб
Скачать

Занимательное пингвиностроение. Чертовы вилы

Прежде чем вникать в глубины пингвиностроения я решил рассказать о функциях (а точнее системных вызовах), которые всему голова в UNIX. Имя им: fork() и exec(). Почему они так уж важны? Да потому, что практически все, что связано с взаимодействием процессов UNIX и организацией ее многозадачности связано именно с ними. А заодно немного поговорим и о системных вызовах вцелом.

Это не черт с вилкой, а daemon с fork'ом

Я долго пытался самостоятельно описать их, но сделать это лучше чем Андрей Робачевский в книге "Операционная система UNIX"у меня вряд-ли получится, поэтому кое-что я буду брать оттуда. Эту книгу я, кстати, настоятельно рекомендую для прочтения. С нее я начинал изучение программирования в UNIX и даже при написании этих статей я иногда туда подглядываю :-). Стоит она недорого -- на Петровке за 21 грн. -- свободно, а если поискать, то, может, и дешевле. Итак, когда стартует UNIX-подобная ОС, запускается процесс init -- прародитель всех остальных процессов. Остальные процессы порождаются как раз вызовом fork() и exec() в этом процессе и т.д. И получается иерархия процессов, которую вы можете наблюдать выполнив команду

$ ps -axfww

Взглянув на результат вы можете спросить, почему init не является корнем дерева, а идет параллельно некоторым остальным. Да потому, что эти процессы являются демонами, а демоны ответвляются от основного ствола, кстати тоже через вызов fork() (вы поймете что и как, когда мы сами будем писать демона :-)).

А вот обратите внимание на вот этот кусочек:

508 ? S 0:00 login -- serge

1008 tty2 S 0:00 \_ -bash

1042 tty2 S 0:00 \_ mc

1044 pts/3 S 0:00 \_ bash -rcfile .bashrc

Здесь процесс login при моем входе в систему выполнил fork() и exec'нул bash. Тот в свою очередь опять от'fork'ался и exec'нул mc ну и т.д. Думаю значимость этих команд понятна, а теперь разберемся что они делают.

Но сперва, что же такое системный вызов? В UNIX-подобных ОС ядро обеспечивает базовые функции ОС, но нужно иметь способ заставить его выполнять их из прикладных программ. Для организации взаимодействия прикладных задач с ядром используется интерфейс системных вызовов. Он представляет собой набор услуг ядра и определяет формат запросов на них. В программировании они определяются как функции C, независимо от их реализации в ядре. Каждый системный вызов имеет одну (а то и больше) соответствующую функцию C. Хранятся эти функции в стандартной библиотеке C (которая в свою очередь является частью пакета glibc). Они не содержат фактического кода реализации операции, а лишь передают соответствующие команды ядру, то есть являются программной оболочкой для системных вызовов.

Итак, любой процесс в UNIX создается вызовом fork(). Процесс вызвавший его называется родительским (родителем), а порожденный fork'ом -- дочерним (потомком). Новый процесс является точной копией родительского (подчеркиваю - точной копией!) за исключением идентификатора процесса PID (по нему в программе и определяют где мы находимся -- в родителе или потомке). Причем потомок наследует все данные родителя, вплоть до того, что выполнение родительского и дочернего процесса начинается с той же инструкции.

Системный вызов exec() не порождает процесс, а полностью замещает код процесса, который его вызвал кодом переданной exec'у программы. Причем большинство параметров окружения процесса сохраняются. Например, сохраняются значения переменных окружения и дескрипторы стандартных входа/выхода. По завершению программы, вызванной exec() процесс "умирает" и мы возвращаемся в родительский процесс.

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

#include <stdio.h>

/* Эти два модуля используются для вызовов fork() и exec() */

#include <unistd.h>

#include <sys/types.h>

/* Обработка строк в программе */

#include <string.h>

int main()

{

char cmd[80]; // Введенная команда

pid_t pid; // Идентификатор процесса после fork()

int stat_lock; // Сюда wait() возвращает значение статуса дочернего процесса

while (1) {

/* Выводим приглашение и считываем командную строку */

printf("cmd# ");

gets(cmd);

/* Условие выхода - команда exit */

if (strcmp("exit", cmd) == 0) {

fprintf(stdout, "exiting...\n");

break;

}

/* Передана команда. Обработаем ее */

else {

/* Пытаемся fork'нуться */

if ((pid=fork()) < 0) {

/* Не вышло */

fprintf(stderr, "fork() failed\n");

}

else if (pid==0) {

/* Если PID=0, то мы в потомке и все нормально. Выполняем команду */

execlp(cmd, cmd, NULL);

/* Если все ОК, то следующие строки не выведутся, поскольку программа,

вызванная exec'ом сама завершает процесс */

fprintf(stderr, "exec() failed\n");

exit(0);

}

else

/* Это обработчик родителя. Ждем завершения работы потомка */

wait(&stat_lock);

}

}

return(0);

}

Здесь, пожалуй, следует прояснить пару моментов. Первый -- я тут все exec() упоминаю, а вызвал execlp(). Это потому, что exec() -- это системный вызов, а мы используем его программную оболочку -- функцию. Основная оболочка exec() -- execve() -- мощная и сравнительно сложная в использовании (для этого примера). Для него есть несколько фронт-ендов, заточенных под те или иные нужды. execlp() -- один из них. Он прост и идеально подходит для этой задачи. Детальнее -- man 3 exec.

Второе -- еще не упоминавшийся мной системный вызов wait() в родителе. Этот вызов приостанавливает работу родителя до завершения работы потомка или поступления сигнала который завершает текущий процесс. В указателе, который передается этой функции хранится информация о статусе дочернего процесса. Детали -- man 2 wait. Ради эксперимента попробуйте "заремить" ее и наблюдать за изменениями в поведении программы (хотя для настоящего командного интерпретатора такое поведение неприемлемо).

Так, вроде с этим все. Возникнут вопросы -- пишите -- разберем вместе. В следующей статье мы поговорим об организации взаимодействия между процессами посредством односторонних (half-duplex) каналах (pipe).

P.S. При компиляции вышеописанного исходного кода gcc выдает предупреждение. Вот вам небольшая тренировка: скажите почему и решите проблему. Ведь именно вылизывание таких мелочей и делает программы в Linux быстрыми и стабильными.

Каналы

Итак, приступаем к изучению способов взаимодействия между процессами в Linux. Таких способов довольно много: каналы (именованные и односторонние), сигналы, очереди сообщений, BSD-сокеты и др. Начнем же мы с первых как с самых простых.

Сначала немного теории. С программистской точки зрения принцип работы с ними ничем не отличается от работы с обычным файлом. Только здесь вы открываете не файл, а поток стандартного входа или выхода вызываемой программы. При вызове функции открытия такого канала система передаст для выполнения оболочке (shell) указанную командную строку и создаст канал связывающий ее вход или выход с вашей программой. Причем, запись в канал передается на стандартный поток ввода вызываемой команды или наоборот, кроме случаев, когда потоки вывода-вывода переопределены самой командой. Открытие канала производится с помощью функции popen:

FILE *popen(const char *command, const char *type);

где command -- соответственно строка с командой для оболочки, а type -- режим открытия. К сожалению, Linux поддерживает только односторонние безымянные каналы. То есть, вы вы можете открыть их либо на запись (указав "w" ), либо на чтение (соответственно "r"), но не на оба сразу. Возвращает она, как видите, обычный дескриптор файла с которым вы можете работать с помощью стандартных функций работы с файлами типа fputs, fprintf, fscanf и.т.д. Только закрывается такой файл соответствующей функцией:

int pclose(FILE *stream);

Разобрались? Хорошо. Нет? Тогда напишем простенькую программку для отправки почтовых сообщений, использующую команду mail для фактической отправки сообщений чтобы расставить все точки над i.

#include <stdio.h>

#include <unistd.h>

int main() {

int rsize;

char to[10]; // Адресат

char subj[100]; // Тема

char s[80]; // буфер для вводимых строк

char cl[100]; // командная строка

printf(" To: ");

scanf("%s", to);

write(1, "Subject: ", sizeof("Subject: "));

subj[read(0, subj, 100)-1]='\0';

sprintf(cl, "mail %s -s\"%s\"", to, subj);

FILE *f = popen(cl, "w");

while ((rsize = read(0, s, 80)) > 0) {

s[rsize]='\0';

fputs(s, f);

}

pclose(f);

printf("Mail to %s sent.\n", to);

return(0);

}

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

$ touch Makefile

и наполняем чем-то вроде (пусть имя исходника -- cmails.c):

CC=gcc

all:

$CC -o cmails cmails.c

clean

clean:

rm -f *.o

Обратите внимание, что там, где написано нужно вставить символ табуляции -- таков формат make-файла. Более сложный пример make-файла рассмотрим как-нибудь позже. Итак выполняем

$ make

и запускаем прогу:

$ ./cmails

По идее должно работать. Вот мы и написали первое приложение с half-duplex каналом. А теперь давайте посмотрим как же это все работает внутри. Благо это не очень сложно. Вообще popen/pclose значительно упрощают работу с такими каналами, но ценой гибкости программирования. На самом деле канал создается системным вызовом pipe():

int pipe(filedes[2]);

где filedes[2] -- массив из двух файловых дескрипторов, причем первый используется для чтения, а второй -- для записи. Возвращаемое значение: 0 в случае успеха и -1-- если ошибка. Немного упростив предыдущую программу я покажу, что выполняется на самом деле. Вы еще помните про системные вызовы fork() и exec()? Хорошо, потому, что они используются и здесь:

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

int main ()

{

int fd[2], rsize;

pid_t childpid;

char s[80];

pipe (fd); //Создаем канал fd

// Порождаем дочерний процесс

if ((childpid = fork ()) == -1)

{ // Ooops, не вышло

perror ("fork");

exit (1);

}

if (childpid == 0)

{ // Обработка дочернего процесса

close (fd[1]);

close(0);

dup (fd[0]);

execlp ("mail", "mail", "serge",

"-s\"Hi to myself.\"", NULL);

exit (0);

}

else

{ // Обработка родителя

close (fd[0]);

while ((rsize = read(0, s, 80)) > 0)

{

s[rsize]='\0';

write(fd[1], s, rsize);

}

}

return (0);

}

Итак, если вспомнить предыдущий урок, то многое должно быть ясно, но на некоторые моменты все же стоит обратить внимание. Первое -- вызов perror() для обработки ошибок. Эта штука очень удобна, когда лень обрабатывать значение переменной errno для вывода сообщения об ошибке. Если дело дойдет до ее вызова, то на стандартный выход ошибок (stderr) выведется строка вида:

fork: <сообщение об ошибке, соответствующее значению errno>

Удобно, а? :-) А теперь к более серьезным вещам. Для корректной работы канала необходимо, чтобы в родителе и ребенке была открыта только одна сторона канала -- та с которой он должен работать, поэтому в родителе закрывается дескриптор чтения через

close(fd[0]);

а в ребенке соответственно -- записи. Посмотрим дальше обработку дочернего процесса:

close(0);

dup(fd[0]);

Что же мы делаем. Помните, упоминая за fork'анье я упоминал, что потомок наследует практически все окружение родителя, в т.ч. и стандартные дескрипторы ввода/вывода. Таким образом нам необходимо сперва закрыть в потомке стандартный ввод (таким образом символы вводимые на терминале больше не будут напрямую передаваться потомку), а потом -- продублировать fd[0] (дескриптор чтения из канала). Системный вызов dup() при дублировании использует наименьший свободный номер дескриптора, а так как мы перед этим закрыли стандартный вход и соответственно освободили номер 0, то после дублирования к дескриптору входа fd[0] созданного канала можно будет обращаться как к стандартному входу. Теперь мы вызываем exec(). А он уже наследует переопределенный нами стандартный вход и таким образом информация приходящая из канала fd будет восприниматься как данные введенные с клавиатуры. Однако, как пишется в "Linux Programmers Guide", последовательное использование этих системных вызовов не очень безопасно, поэтому вместо них используется один:

int dup2(int oldfd, int newfd);

Этот вызов дублирует дескриптор oldfd в newfd, закрывая последний при необходимости. Для нашего случая соответственно можно было бы записать:

dup2(fd[0], 0);

Вот так. Хотя со стороны это может показаться сложно, но на самом деле все ведь интуитивно просто. Я где-то читал, что если вы пришли из мира Windows, то для того чтобы понять как работают UNIX-подобные ОС, вам всего лишь :-) надо поменять образ мышления. И все станет на свои места.

Ну а далее мы поговорим о именованных каналах, которые также называют FIFO. В общем веселого изучения. Главное -- пишите программы и читайте man'ы :-).

FIFO

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

Итак, что же представляют собой эти каналы FIFO? Как и анонимные каналы, они являются односторонними, но имеют иную организацию. Именованными они названы потому, что при создании канала ему присваивается уникальное системное имя, используя которое можно организовать одностороннюю связь между двумя абсолютно разными процессами (для удобства назовем их клиентом и сервером). Имеется в виду, что это могут быть не только две части одной и той же, а и две разные программы. Уже само название канала (First In - First Out) говорит о том, что чтение из канала производится в том же порядке, в котором происходила запись. Еще одной особенностью FIFO-канала является то, что он по сути является файлом специального типа (устройством).

Итак, начинается работа с FIFO с его создания. Сделать это можно как программным путем, так и из оболочки. Для второго случая необходимо выполнить команду:

$ mknod -m a=rw MYFIFO p

или

$ mkfifo a=rw MYFIFO

Первая более универсальна, поскольку с ее помощью можно создавать не только каналы (на что в указывает опция "p" ), а и другие устройства, но это нам пока и не надо. Как вы наверное догадались, строка "a=rw" устанавливает права доступа на канал. В описанном случае это равносильно 0666 в октальном (восьмеричном) виде. mkfifo же специализируется только на создании FIFO.

Для программного создания канала FIFO необходимо воспользоваться системным вызовом mknod:

int mknod(char *pathname, mode_t mode, dev_t dev);

Очевидно, что pathname -- это путь к файлу канала (обычно их помещают в /tmp, но вы можете разместить его где хотите), dev нужно установить в 0, поскольку это не физическое, а псевдо-устройство. А вот с mode нужно немного разобраться. Поскольку файл канала -- это специальный файл, то и в битах доступа этот факт отображается соответствующим образом, а именно через макрос S_IFIFO. Таким образом параметр mode необходимо сформировать так:

S_IFIFO | требуемые_права

где S_IFIFO -- макрос-маска FIFO-канала, а требуемые_права -- соответственно стандартные UNIX'овские права на чтение, запись и выполнение. Таким образом программный вызов:

mknod("MYFIFO", S_IFIFO | 0666, 0);

будет полностью аналогичен вышеупомянутым командам shell. mknod возвращает 0 в случае успеха и -1 с установкой значения переменной errno в случае ошибки.

Как и в случае с shell возможен и более короткий вариант:

int mkfifo(const char *pathname, mode_t mode);

здесь в mode_t надо указать только права на доступ к файлу канала, то есть по аналогии: 0666.

Потом созданный канал необходимо открыть с помощью стандартных операций open или fopen -- по вкусу :) (причем, в зависимости от направления потока данных, в одном из процессов -- только на запись, а в другом, соответственно, -- только на чтение) и производить операции чтения/записи с помощью системных вызовов read и write или fgets, fputs и т.д..

Заканчивается работа с FIFO закрытием на обоих сторонах канала файла-устройства канала (с помощью close или fclose) и удаления этого устройства с помощью:

unlink(char *pathname)

Ну и наконец рассмотрим простенький пример. Простенький, потому что придумать действительно хорошую и в то же время небольшую программку довольно трудно. Так что ограничимся "Hello World'ом" :-) из "Linux Programmers Guide" (в дальнейшем -- просто LPG). Правда я его несколько видоизменил и сделал все в виде одной программки.

#include

#include

#include

#include

#include

#define FIFO_FILE "MYFIFO"

int main(void)

{

FILE *fp_s, *fp_c;

char readbuf[80];

pid_t pid;

/* Создаем FIFO если он не существует */

umask(0);

mknod(FIFO_FILE, S_IFIFO | 0666, 0);

switch (fork())

{

case -1:

perror("fork");

break;

case 0:

fp_c = fopen(FIFO_FILE, "w");

fputs("Hello parent!", fp_c);

fclose(fp_c);

exit(0);

break;

default:

fp_s = fopen(FIFO_FILE, "r");

fgets(readbuf, 80, fp_s);

printf("Received string: %s\n", readbuf);

fclose(fp_s);

break;

}

/* Закомментируйте следующую строку

чтобы по завершению работы программа

не удаляла канал */

unlink(FIFO_FILE);

return(0);

}

Вот и все. ИМХО если вы читали предыдущие статьи, начало этой и хоть немного знаете C, то все должно быть понятно. Разве что кроме системного вызова umask():

mode_t umask(mode_t mask);

Он устанавливает значение umask (уже не системного вызова, а параметра) в значение mask. Этот параметр неявно используется системным вызовом open() для установки прав на вновь созданный файл. Он действует по тому принципу, что права установленные в umask "снимаются" с аргумента mode, переданного open(). То есть окончательные права формируются по формуле:

mode & ~umask

Таким образом если бы umask был установлен в 0022, например, то мы создавая канал получили бы в качестве его прав не 0666, а 0666 & ~0022 = 0644. Установка umask в 0 является гарантом того, что файл создастся именно с теми правами, что были переданы open(). Скорее всего его использование здесь не является обязательным, но поможет избежать некоторых казусов с установкой прав.

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

Как правило, этот способ взаимодействия используется довольно редко (вместо него удобнее использовать механизм локальных BSD-сокетов). С другой стороны, принципы, заложенные в основе работы с ними, используются для организации других типов взаимодействия процессов, в том числе и UNIX System V IPC, о которых речь пойдет в следующих статьях.

Signals

Сегодня я немножко отойду от намеченного плана занятий и мы поговорим о еще одной разновидности взаимодействия процессов в UNIX - сигналах.

Когда мы работаем с UNIX-подобными ОС, мы всегда используем сигналы. Когда вы выключаете компьютер, то среди прочих выводимых сообщений могли заметить "Sending all processes the TERM signal", например, да и подвисшие процессы убираются отправлением этому процессу сигнала TERM или KILL. Они относятся, пожалуй, к наиболее древней разновидности взаимодействия процессов и представляют собой своеобразное уведомление программе (а точнее процессу) о произошедшем событии при получении которого это самый процесс нарушает свое нормальное функционирование. Каждый сигнал имеет свое имя и номер. В табл. 1 показаны некоторые наиболее часто используемые сигналы.

Название

Номер

Действие по умолчанию

Описание

SIGINT

2

Завершить работу

Посылается ядром приложению при нажатии на Ctrl+C.

SIGKILL

9

Завершить работу

Сигнал, при котором завершается выполнение процесса. Этот сигнал не может быть перехвачен или проигнорирован.

SIGTERM

15

Завершить работу

Сигнал-предупреждение, о том, что процесс будет уничтожен для того, чтобы он мог как следует "подготовится к смерти". :)

SIGUSR1

30,10,16

Завершить работу

Предназначен для переопределения пользователем. Позволяет выполнить какие-нибудь простые прикладные задачи.

На самом деле же их намного больше. Только стандартом POSIX.1 определено 19 таких сигналов, а еще есть специфичные для двух ветвей UNIX -- System V и BSD. В большинстве случаев при получении сигнала процесс завершает работу, а для некоторых сигналов действием по умолчанию является создание файла core вместе с завершением процесса. Также некоторые сигналы (например SIGCHLD) по умолчанию просто игнорируются. Они могут генерироваться:

 Ядром при нажатии неких предопределенных клавиш или их комбинаций типа вышеупомянутого Ctrl+C для убиения непокорного процесса :)

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

 Программой с помощью системного вызова kill():

int kill(pid_t pid, int sig);

Как видим процессу передается идентификатор процесса (pid) и сигнал, который следует ему передать (sig). Сигналы можно отправлять не только из программы, а и из консоли. Соответствующая команда называется... kill :-)

kill [-s signal] pid

Указав опцию -s можно определить, какой сигнал мы отошлем процессу. По умолчанию, кстати, отправляется SIGTERM, а не SIGKILL, как можно было бы судить из названия.

"Но отправка отправкой, а к чему это все в моей программе?" -- спросит нетерпеливый читатель. Отвечаю. Как я уже упоминал выше, поступление большинства сигналов можно (а порой и нужно) перехватывать и соответственно обрабатывать. Сегодня мы рассмотрим создание своей обработки пользовательских сигналов SIGUSR1 и SIGUSR2, а в следующей статье (об очередях сообщений) мы рассмотрим пример, который будет переопределять обработку SIGTERM и SIGINT для того, чтоб замести следы выполнения программы и не оставлять свидетелей после смерти :-) .

Итак, приступим... Пытаясь придумать пример, я вспомнил, что когда-то использовал отправку сигнала SIGUSR1 для получения некоторых внутренних данных одной из программ (а именно -- mrouted). Вот мы и сделаем нечто похожее. Предположим, что у нас есть некоторые данные, представленные аж одной переменной "a" :-) , которые используется программой. Текущее значение этой переменной нам необходимо при получении сигнала SIGUSR1 скинуть в файл sigex.out в каталоге /tmp. А при получении сигнала SIGUSR2 мы его просто будем игнорировать. Для перехвата поступивших сигналов используется системный вызов signal():

sighandler_t signal(int signum, sighandler_t handler);

Этот вызов переопределяет обработку сигнала с номером signum (естественно, здесь можно использовать и символьное обозначение сигнала, что намного более удобно). Вторым параметром может быть SIG_IGN (тогда поступивший сигнал игнорируется), SIG_DFL (тогда сигнал обрабатывается стандартным образом) или указатель на вашу функцию, которая выполнится при поступелении соответствующего сигнала. Вызов signal() возвращает указатель либо на старый обработчик (если все ОК), либо на SIG_ERR, если произошла ошибка.

А теперь, как и обещалось, маленький пример:

#include

#include

int a;

/* Обработчик сигнала SIGUSR1 */

static void handle_usr1(int signo){

FILE *f;

f = fopen("/tmp/sigex.out", "w");

fprintf(f, "Signal SIGUSR1 caught!\n");

fprintf(f, "Variable a = %d\n", a);

fclose(f);

}

main() {

a=1;

/* Устанавливаем свой обработчик перехваченного сигнала SIGUSR1 */

signal(SIGUSR1, handle_usr1);

/* и игнорируем SIGUSR2 */

signal(SIGUSR2, SIG_IGN);

while(1) {

sleep(1);

/* Или ваша обработка, где значение "a" будет меняться - так будет

намного интересней :-) */

}

}

Скомпилив эту программу теперь можем запустить ее в фоне

$ ./sigex &

и проверить перехват сигналов SIGUSR1 и SIGUSR2 подавая команды (предположим, полученный идентификатор процесса -- 1234):

$ kill -s SIGUSR1 1234

и заглядываем в /tmp/sigex.out... Теперь делаем:

$ kill -s SIGUSR2 1234

и... ничего. Сигнал игнорируется.

А наигравшись, убиваем его или обычным kill'ом. Хочу также предупредить о двух вещах: первое -- сигналы SIGKILL и SIGSTOP не могут быть перехвачены или игнорированы, и второе -- некоторые сигналы (например SIGINT) после первого своего выполнения сбрасывают свой обработчик в стандартный (SIG_DFL) и поэтому для них вам надо будет в ваших обработчиках заново устанавливать их на вашу функцию. То есть если у вас за его обработку отвечает функция hndl(), то она должна иметь вид (пример для сигнала SIGINT):

static void hndl(int signo){

// ... Ваша обработка ...

/* Восстанавливаем обработчик на себя, поскольку после

перехвата он сбросился в обработчик по умолчанию */

signal(SIGINT, hndl);

}

Кроме того, примите во внимание, что при вызове fork() все обработчики перехвачиваемых сигналов наследуются дочерним процессом, а при exec() -- сбрасываются в действие по умолчанию.

К сожалению, древность такого вида сигнальной обработки повлекла за собой некоторые ограничения и проблемы с безопасностью. Поэтому стандартом POSIX.1 был принят иной интерфейс работы с сигналами, основанный на интерфейсе системы 4.2BSD и лишенный недостатков, присущих вышеупомянутой реализации. Этот механизм был назван "надежными сигналами". Он более сложен в реализации и поэтому мы рассмотрим его немного позже.

IPC

Ну вот мы наконец рассмотрим и такие методы IPC (InterProcess Comunications), как очереди сообщений, семафоры и распределение памяти (все это также известно как UNIX System V IPC).

Итак, начнем издалека. Безымянные каналы хороши для связи родственных процессов, а вот чтобы "подружить" независимые процессы можно использовать именованные каналы, очереди сообщений, семафоры и т. д. Очевидно, что для того, чтобы обратится к конкретной очереди сообщений или другим средствам System V IPC необходимо иметь ее имя и это имя должно быть уникальным и общесистемным. То есть важно, чтобы очередь с номером 10, например, для любой программы была бы одной и той же. Для каналов FIFO таким именем является имя файла-канала, для остальных вышеупомянутых - это некоторый целочисленный идентификатор. Для работы с конкретным IPC используются дескрипторы. (прошу не путать с именами/идентификаторами). Опять же, для FIFO - это файловый дескриптор, возвращаемый при открытии файла-канала (имя которого является именем канала, еще не забыли?). Для чего я все это рассказываю? А для того, чтобы вы поняли, что принцип работы с System V IPC внешне похож на работу с FIFO-каналами, но имеет совсем другую внутреннюю организацию. Имя для UNIX System V IPC формируется с помощью функции:

key_t ftok(char *filename, char proj);

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

Полученный через ftok() ключ можно использовать сразу для нескольких разных средств System V IPC. Это возможно потому, что пространство имен разных IPC является независимыми, то есть может существовать как очередь с идентификатором 12, так и, например, группа семафоров с таким же номером. Разделение на конкретные "средства общения" идет с "открытия" этого идентификатора.

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

Соседние файлы в папке Lecture 2_01