Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ОСиС_2008

.pdf
Скачиваний:
96
Добавлен:
29.05.2015
Размер:
2.65 Mб
Скачать

9. Лабораторный курс

335

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

#include <unistd.h> main()

{

printf(“Запуск программы cat\n”); execl(“/bin/cat”, “cat”, “letter”, (char *) 0);

/* Если execl возвращает значение, значит ошибка */ perror(“Вызов execl не смог запустить программу cat”); exit(1);

}

Следует обратить внимание, что в случае успеха функции execl (и любой другой exec) она никогда не возвращает управление в старую программу. Поэтому единственным возвращаемым значением является признак ошибки (–1).

В ы п о л н и т е запуск из своей программы какой-то известной вам готовой программы, например, ed.

9.6.5. Совместное применение вызовов fork и exec

Полученный в результате применения вызова fork дочерний процесс может продолжать выполнение копии программы про- цесса-отца или потребовать от ядра ОС выполнения своей собственной программы, используя вызов exec. Первый вариант был рассмотрен ранее, а теперь перейдем ко второму.

Пример

Следующая программа command выполняет запуск командного интерпретатора bash:

#include <unistd.h> main(int argc, char *argv[ ])

{

pid_t pid; if (argc < 2)

{

printf(“Отсутствуют параметры в командной

строке\n”);

 

exit(1);

/* Выход по ошибке */

}

 

switch(pid = fork()) {

 

case –1:

 

336 Одиноков В.В., Коцубинский В.П.

perror(“Error fork”); exit(1);

break; case 0:

/* Выполнение в дочернем процессе */ printf(“Запуск shell\n”);

switch(argc) case 2:

/* Только имя команды */

execl(“/bin/bash”, “bash”, “-c”, argv[1], (char *) 0); break;

case 3:

/* У команды один параметр */ execl(“/bin/bash”,“bash”,“-c”, argv[1], argv[2], (char *)0); break;

case 4:

/* У команды два параметра */

execl(“/bin/bash”, “bash”, “-c”, argv[1], argv[2], argv[3], (char *) 0);

break;

/* Если execl возвращает значение, значит ошибка */ perror(“Вызов execl не смог запустить программу

bash”);

exit(1);

default:

/* Ожидание завершения bash */ wait((int *) 0);

exit(0);

}

}

Данный пример иллюстрирует тот факт, что относительно ядра ОС shell является обыкновенной прикладной программой и поэтому может быть запущен из любой другой прикладной программы, например, из рассматриваемой программы command. Сама программа command запускается из командной строки shell, получив из нее свои параметры. Первый из этих параметров есть «command», второй параметр — имя запускаемой программы, например cat или ed. Остальные параметры потребуются для передачи запускаемой программе в качестве ее параметров.

Выполнение программы command начинается с проверки числа переданных ей параметров. Если это число меньше двух, то про-

9. Лабораторный курс

337

грамма завершается с ненулевым кодом завершения. Иначе — с помощью вызова fork порождается дочерний процесс, который, в свою очередь, загружает программу bash. Оператор switch обеспечивает передачу этой программе тех параметров, которые получила ранее command (кроме ее собственного имени, которое заменяется на строку «bash»), а также параметра -c. Этот параметр означает, что bash должен брать для себя команды не из стандартного ввода (с клавиатуры), а из последующей символьной строки. Определив из этой команды имя программы, bash обеспечивает ее выполнение.

Интересно отметить, что bash (как и любой другой shell) использует для запуска требуемой программы точно такие же системные вызовы fork, exec и wait, что и рассматриваемая прикладная программа. При этом вызов wait используется только для запуска дочерних процессов в оперативном режиме. Отсутствие этого вызова приводит к запуску процесса в фоновом режиме.

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

9.6.6. Наследование данных при создании процесса и при загрузке программы

Дочерний процесс, созданный в результате вызова fork, наследует точно такое же адресное пространство, что и процесс-отец. Термин «точно такое же» не означает «это же самое». Это означает, что все данные процесса-отца копируются в адресное пространство процесса-потомка в момент его создания. По мере дальнейшего выполнения процессов их данные могут все более различаться.

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

338

Одиноков В.В., Коцубинский В.П.

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

Рассмотрим более внимательно, что представляют собой переменные окружения. Каждая переменная окружения есть строка вида

имя переменной = ее содержание

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

extern char **environ;

Пример

Следующая программа выполняет вывод на экран своих переменных окружения:

#include <stddef.h> #include <stdio.h> extern char **environ;

main(int argc, char *argv[ ])

{

int i, ch;

for(i=0; i<20; i++)

printf(“Переменная окружения %d: %s\n”, i, environ[i]); ch=getchar();

for(i=20; i<40; i++) {

if(environ[i] == NULL) break;

printf(“Переменная окружения %d: %s\n”, i, environ[i]);

}

9. Лабораторный курс

339

}

Данная программа сначала выводит на экран первые двадцать переменных окружения. Затем она ожидает ввода с клавиатуры любого символа и нажатия <Enter>. После этого на экран выводятся оставшиеся переменные окружения.

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

9.6.7. Задание

Разработайте программу, выполняющую:

1)создание файла и размещение в нем небольшого текста;

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

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

4)осуществление над результирующим файлом одного из действий (по вашему усмотрению):

– вывод на экран количества слов в файле;

– вывод на экран строк с заданным ключевым словом.

9.7.Лабораторная работа № 7.

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

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

9.7.1. Подготовка к выполнению работы

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

1) синхронизация процессов с помощью сигналов (п. 2.4.1) — понятие сигнала; типы сигналов; выдача сигнала процессом;

struct sigaction {
void (*sa_handler)(int); sigset_t sa_masc;
int sa_flags;

340

Одиноков В.В., Коцубинский В.П.

2)терминальное управление процессами (п. 2.4.2) — управляющий терминал; сеанс; группа процессов; получение информации

опроцессах с помощью команды shell – ps с флагом -j; применение команды shell – kill с целью посылки сигнала процессу;

3)обработка сигналов (подразд. 4.3) — варианты обработки сигналов; диспозиция сигналов;

4)применение таймера для управления процессами (подразд. 4.5) — таймер; аларм; таймер интервала.

9.7.2. Изменение диспозиции сигналов

Сразу же после создания процесса с помощью вызова fork диспозиция сигналов нового процесса точно такая же, как и у процесса-отца (наследование диспозиции). Но если далее для загрузки программы выполняется вызов exec, то для всех сигналов устанавливается диспозиция «по умолчанию». Это объясняется тем, что все прежние обработчики сигналов уничтожены (как и все другие прикладные подпрограммы). Если такая начальная диспозиция не устраивает, то для ее изменения процесс выполняет системный вызов sigaction:

#include <signal.h>

int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

Первый параметр (signo) задает сигнал, диспозицию которого требуется изменить. Второй параметр (act) задает новый обработчик сигнала. Третий параметр (oact) указывает на структуру, в которой будет сохранено описание прежнего обработчика сигнала. Если в качестве этого параметра задано значение NULL, то это описание сохранено не будет.

Структура sigaction определена в файле <signal.h>:

/* Функция обработчика */ /* Сигналы, которые блокируются во время обработки сигнала */

/* Флаги, влияющие на поведение сигнала */ /*Указатель на обработчик*/

void (*sa_sigaction) (int, siginfo_t *, void *);

};

9. Лабораторный курс

341

Первое поле (sa_handler) задает обработчик сигнала. Оно может содержать одно из значений:

1)SIG_DFL — восстановить обработку сигнала по умолчанию;

2)SIG_IGN — игнорировать данный сигнал;

3)адрес функции, имеющей аргумент типа int. Эта функция используется в качестве обработчика сигнала signo, а численный идентификатор этого сигнала будет передан ей в качестве входного параметра.

Второе поле (sa_masc) задает набор сигналов, которые должны блокироваться на время обработки сигнала signo. Блокирование сигнала означает не его игнорирование, а лишь задержку в его обработке. Благодаря блокированию появляется уверенность, что прикладной обработчик сигнала, выполняясь в режиме «Задача», может полностью выполнить свою обработку. Тип sigset_t, используемый для задания набора сигналов, будет описан позже.

Третье поле (sa_flags) позволяет менять характер реакции на сигнал. Например, поместив в это поле константу SA_RESETHAND, мы обеспечим после завершения своего обработчика изменение диспозиции сигнала так, что она будет задавать обработку сигнала по умолчанию. Другой пример: значение SA_SIGINFO позволяет обработчику сигнала получать дополнительную информацию. В этом случае обработчик сигнала задается не полем sa_handler, а полем sa_sigaction. Передаваемая на вход обработчика структура siginfo_t содержит номер сигнала, номер пославше-

го сигнал процесса и идентификатор пользователя процесса. Не допускается одновременное использование полей sa_handler и sa_sigaction в программе процесса для одного и того же сигнала.

Пример

Следующая программа задает диспозицию для двух сигналов (SIGINT и SIGUSR1), поступающих в процесс.

#include <signal.h>

/* Функция – обработчик сигнала SIGINT */ void sig_hndlr (int signo)

{

printf(“Получен сигнал SIGINT\n”);

}

main()

{

static struct sigaction act1, act2;

/* Изменение диспозиции сигнала SIGINT */

342

Одиноков В.В., Коцубинский В.П.

 

act1.sa_handler = sig_ hndlr;

/* Запись адреса

 

 

обработчика */

sigaction(SIGINT, &act1, NULL);

/* Изменение диспозиции сигнала SIGUSR1 */ act2.sa_handler = SIG_IGN; /* Игнорировать сигнал */ sigaction(SIGUSR1, &act2, NULL);

/* Бесконечный цикл */ for ( ; ; )

pause ();

}

В данном примере структура act типа sigaction определена как static. Поэтому при инициализации структуры все ее поля (в том числе sa_flags) обнуляются. Используемый в программе системный вызов pause приостанавливает ее выполнение до прихода в процесс любого сигнала.

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

$ ./a.out &

 

284767

pid порожденного процесса

$ kill

-SIGINT 284767

 

Получен сигнал SIGINT

сигнал SIGINT перехвачен

$ kill - SIGUSR1 284767

сигнал SIGUSR1

 

 

игнорируется

$ kill

284767

сигнал SIGTERM вызывает

 

 

завершение процесса

Пр о в е р ь т е работу приведенной выше программы.

9.7.3.Наборы сигналов

Каждый такой набор представляет собой список сигналов, который используется для выполнения системных вызовов. Набор сигналов определяется в программе с помощью типа sigset_t, определенного в файле <signal.h>. Размер этого типа таков, что в нем может поместиться весь набор сигналов, определенных для данной системы.

9. Лабораторный курс

343

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

#include <signal.h>

int sigfillset (sigset_t *set);

int sigdelset (sigset_t *set, int signo);

Первый параметр обеих процедур (set) представляет собой указатель на требуемый набор сигналов. Второй параметр процедуры sigdelset (signo) есть номер сигнала, исключаемого из набора set. Нетрудно заметить, что многократное применение процедуры sigdelset позволяет получить из полного набора сигналов любой требуемый набор.

Пример

В следующем фрагменте программы формируется набор сигналов, отличающийся от полного набора отсутствием сигнала

SIGCHLD:

#include <signal.h> sigset_t mask1;

. . . . . .

sigfillset(&mask1); sigdelset(&mask1, SIGCHLD);

. . . . . .

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

#include <signal.h>

int sigemptyset (sigset_t *set);

int sigaddset (sigset_t *set, int signo);

Второй параметр процедуры sigaddset есть номер сигнала, включаемого в набор set. Многократное применение данной процедуры позволяет получить из пустого набора сигналов любой требуемый набор.

Пример

В следующем фрагменте программы формируется набор из двух сигналов — SIGINT и SIGQUIT:

344

Одиноков В.В., Коцубинский В.П.

#include <signal.h> sigset_t mask2;

. . . . . .

sigemptyset(&mask2); sigaddset(&mask2, SIGINT); sigaddset(&mask2, SIGQUIT);

. . . . . .

Одно из наиболее полезных применений наборов сигналов — блокирование обработки сигналов на время выполнения участка программы. Такое блокирование предназначено для обеспечения целостности данных программы путем временного запрета обработки сигналов, которая может повредить эти данные. (Нетрудно заметить аналогию с запретом прерываний в однопрограммной системе.) Ранее мы рассмотрели блокирование сигналов на время выполнения обработчика сигнала. Теперь рассмотрим более универсальный подход, применимый для защиты любого участка программы (а точнее — обрабатываемых этим участком данных).

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

#include <signal.h>

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

Первый параметр (how) задает тип действия этого вызова. Например, значение SIG_SETMASK приведет к блокированию с момента выполнения вызова всех сигналов, заданных вторым параметром (set). А значение SIG_UNBLOCK, наоборот, отменяет блокирование сигналов, заданных вторым параметром. Третий параметр задает текущую маску блокируемых сигналов. Если эта маска нас не интересует, то в качестве этого параметра помещается

NULL.

Пример

В следующей программе выполнено блокирование всех сигналов за исключением SIGINT и SIGQUIT:

#include

<signal.h>

 

main ()

 

 

{

 

 

sigset_t

set1;

 

sigfillset (&set1);

/* Создать полный набор сигналов */