116
Системный вызов waitpid() эквивалентен вызову wait(...), когда аргумент pid равен
(pid_t) (-1), а аргумент options имеет нулевое значение.
Вобщем случае, аргумент pid может принимать несколько значений:
<-1 - означает, что нужно ждать любого дочернего процесса, идентификатор
группы процессов которого равен абсолютному значению pid;
-1 - означает ожидание любого дочернего процесса; функция wait(...) ведет себя точно так же;
0 - означает ожидание любого дочернего процесса, идентификатор группы процессов которого равен идентификатору текущего процесса;
> 0 - означает ожидание дочернего процесса, чей идентификатор равен pid.
Значение options создается путем логического сложения нескольких следую-щих констант, определенных в заголовочном файле <sys/wait.h>:
•WNOHANG - означает немедленное возвращение управления, если ни один дочерний процесс не завершил выполнение;
•WUNTRACED - означает возврат управления и для остановленных (но не
отслеживаемых) дочерних процессов, о статусе которых еще не было сообщено. Статус для отслеживаемых остановленных подпроцессов также обеспечивается без этой опции.
Замечание
При анализе переменной status, кроме рассмотренных выше двух макросов, можно воспользоваться макросами:
•WIFSIGNALED(status) - возвращает истинное значение, если дочерний процесс завершился из-за необработанного сигнала.
•WTERMSIG(status) - возвращает номер сигнала, который привел к заверше-
нию дочернего процесса; этот макрос можно использовать, только если WIFSIGNALED вернул ненулевое значение;
•WIFSTOPPED(status) - возвращает истинное значение, если дочерний
процесс, из-за которого функция вернула управление, в настоящий момент остановлен; это возможно, только если использовался флаг WUNTRACED или когда подпроцесс отслеживается;
•WSTOPSIG(status) - возвращает номер сигнала, из-за которого дочерний
процесс был остановлен; этот макрос можно использовать, только если WIFSTOPPED вернул ненулевое значение.
Теперь рассмотрим системные вызовы, которые непосредственно связаны с самим завершением процесса. Синтаксис этих вызовов имеет вид:
#include <stdlib.h> void exit (int status); void _Exit (int status);
117
#include <unistd.h> void _exit (int status);
Все эти вызовы немедленно завершают процесс и посылают родительскому процессу значение аргумента status, в качестве которого могут служить константы 0, EXIT_SUCCESS, EXIT_FAILURE или любое другое значение, но ожидающему родительскому процессу будет доступено только значение (status & 0377). Также родительскому процессу посылается сигнал SIGCHLD.
Замечание
Сами эти вызовы переводят процесс в состояние «зомби», пока родительский процесс выполнит функции wait(...) или waitpid(...).
Если процесс является лидером сеанса и его управляющий терминал является управляющим терминалом сеанса, то каждому процессу в группе процессов этого управляющего терминала посылается сигнал SIGHUP, и терминал отсоединяется от этого сеанса, что позволяет захватить его новому управляющему процессу.
Если завершение процесса приводит группу процессов к потере родителя, и если любой член такой группы приостанавливается, то каждому процессу группы посылается сигнал SIGHUP, за которым следует сигнал SIGCONT.
Завершающийся процесс может использовать функцию atexit(...) для регистрации других функций, которые будут вызываться, если процесс завершается, обращаясь к exit(...) или возвращаясь из main(...).
Синтаксис вызова:
#include <stdlib.h>
int atexit (void (*func) (void));
Функция atexit(...) возвращает значение 0, если действие выполнено, иначе возвращается ненулевое значение, а значение переменной errno не меняется.
Замечание
Реализация ОС должна поддерживать регистрацию по крайней мере тридцати двух функций и вызывать их в обратном порядке.
Чтобы продемонстрировать прикладные аспекты взаимодействия процессов, рассмотрим задачу «Взаимодействие процессов через неименованные каналы»:
•обычно, многие приложения читают входные данные с устройства 0 и выводят информацию на устройво 1;
•если общий алгоритм приложения организован так, что каждый процесс вы-
полняет только часть обработки данных и передает результат другому процессу, то можно организовать взаимодействие процессов через неименован-
118
ные полудуплексные каналы UNIX; при этом, на N взаимодействующих процессов потребуется N-1 канал.
Такое взаимодействие называется конвейером процессов и может рассматриваться как типовое решение метода последовательной обработки данных.
Вкачестве примера, рассмотрим программу, в которой:
•родительский процесс создает N-1 каналов, а затем запускает N дочерних
процессов и ожидает их завершения; после завершения работы всех процессов, все открытые каналы закрываются и работа родителького процесса — завершается;
•первый дочерний процесс читает строку данных с клавиатуры и по первому каналу передает ее второму процессу;
•последний дочерний процесс (N) читает данные из канала N-1 и печатает результат на терминал;
•все остальные дочерние процессы (i): читают из i-канала и пишут в (i+1)-
канал. Дополнительно:
•с целю унификации, все дочерние процессы: читают из устройства 0 и пишут на устройство 1;
•последний дочерний процесс регистрирует функцию обработки завершения.
Замечание
Полудуплексные каналы UNIX были уже изучены в теме 7 и рассматривались как средство подсистемы управления файлами. Вспомним, что:
•каналы создаются с помощью сисемного вызова pipe(...), который возвраща-ет целочисленный массив дескрипторов fd[2];
•дескриптор fd[0] — предназначен для чтения из канала;
•дескриптор fd[1] — предназначен для записи в канал.
На листинге 3.4 представлена программа, реализующая этот алгоритм.
Листинг 3.4 — Пример конвейера процессов
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h>
// Функция, обрабатывающая вызов завершения процесса void exit_process(void){
printf("Обработчик exit(): Процесс pid = %i - завершается...\n", getpid());
}
// Основная программа int main(void) {
puts("Проект lab9.4 ...");
char bb; |
// |
Буфер ввода |
#define MAX_PROC 10 |
// |
Максимальное число возможных дочерних процессов |
|
|
119 |
int |
fd[MAX_PROC - 1][2];// Максимальное число каналов |
|
int |
N = 5; |
// Реальное число процессов: 2 <= N <= MAX_PROC |
pid_t pid, cpid; int i, status;
printf("Создание %i дуплексных каналов...\n", N - 1); for (i = 0; i < (N - 1); i++) {
if (pipe(fd[i]) == -1) { perror("pipe"); exit(EXIT_FAILURE);
}
}
printf("Запуск %i дочерних процессов:\n", N); for (i=0; i < N; i++) {
if ((pid = fork()) < 0) { perror("fork():"); exit(EXIT_FAILURE);
} |
|
|
|
if (pid == 0) { |
// Дочерний процесс |
||
if (i == 0) { |
// Первый - 0 |
|
|
printf(" |
ДП%i - Введи строку сообщения и нажми Enter:\n", i); |
||
dup2 (fd[i][1], 1); |
|
||
close(fd[i][0]); |
|
||
close(fd[i][1]); |
|
||
}else { |
|
|
|
if(i == (N-1)) { // Последний - N-1 |
|||
atexit(exit_process); |
// Регистрирую функцию обработки |
||
printf(" |
ДП%i - Вывожу принятое сообщение:\n", i); |
||
dup2 (fd[i |
- 1][0], 0); |
|
|
close(fd[i |
- 1][0]); |
|
|
close(fd[i |
- 1][1]); |
|
|
}else{ |
|
|
// Другие - i |
dup2 (fd[i |
- 1][0], 0); |
|
|
dup2 (fd[i |
][1], 1); |
|
|
close(fd[i |
- 1][0]); |
|
|
close(fd[i |
- 1][1]); |
|
|
close(fd[i |
][0]); |
|
|
close(fd[i |
][1]); |
|
|
} |
|
|
|
} |
|
|
|
while (read (0, &bb, 1) > 0) { |
// Чтение из канала в канал |
||
write (1, &bb, 1); |
|
||
if (bb == '\n') break; |
|
||
}
exit(i);
}
//puts("Ожидание завершения дочерних процессов:"); while((cpid = wait(&status)) > 0) {
if (WIFEXITED(status)) {
printf(" Процесс pid = %i завершился со статусом = %i\n", cpid, WEXITSTATUS(status));
}
}
}
while(wait(NULL) > 0){ // Ожидание, если остались незавершенные процессы puts(".");
}
puts("Завершение проекта lab9.4 ..."); return EXIT_SUCCESS;
}
120
Замечание
Поскольку родительский и дочерний процессы запускаются независимо друг от друга (асинхронно), то родителький процесс должен дать поработать дочернему. Делается это с помощью системных вызовов wait(...) или sleep(...).
На рисунке 3.4 приведен пример передачи сообщений через последовательность каналов.
Обратите внимание, что для передачи сообщений не обязательно выделять буфер в памяти ОС, поскольку таким буфером является сам канал.
Рисунок 3.4 — Пример передачи сообщений через четыре канала
Любая программа, написанная на языке С, должна иметь особую точку входа для запуска ее в среде ОС.
Такую точку входа обеспечивает функция main(...), общий классический вид которой:
int main (int argc, char *argv []);
где argc - количество аргументов, указанных при вызове программы;
argv – это массив указателей на аргументы, которые определяются исходя из командной строки, запускающей программу на языке С.
Замечание
В соответствии с принятыми соглашениями, значение argc не меньше единицы, а первый элемент массива argv указывает на цепочку символов, содержащую имя