355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Арнольд Роббинс » Linux программирование в примерах » Текст книги (страница 26)
Linux программирование в примерах
  • Текст добавлен: 6 мая 2017, 11:00

Текст книги "Linux программирование в примерах"


Автор книги: Арнольд Роббинс



сообщить о нарушении

Текущая страница: 26 (всего у книги 55 страниц)

9.2. Группы процессов

Группа процесса является группой связанных процессов, которые в целях управления заданием (job) рассматриваются вместе. Процессы с одним и тем же ID группы процессов являются членами группы процессов, а процесс, PID которого равен ID группы процессов, является лидеров группы процессов. Новые процессы наследуют ID группы процессов своих родительских процессов.

Мы уже видели, что waitpid() позволяет вам ждать любой процесс в данной группе процессов. В разделе 10.6.7 «Отправка сигналов: kill() и killpg()» мы увидим также, что вы можете отправить сигнал всем процессам в определенной группе процессов. (Всегда применяется проверка прав доступа; вы не можете послать сигнал процессу, которым не владеете.)

9.2.1. Обзор управления заданиями

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

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

Сеанс (session) является коллекцией групп процессов, связанных с управляющим терминалом. На одном терминале имеется лишь один сеанс, с несколькими группами процессов в сеансе. Один процесс назначен лидером сеанса; обычно это оболочка, такая, как Bash, pdksh, zsh или ksh93[93]93
  csh и tcsh также могут быть включены в эту категорию, но мы предпочитаем оболочки в стиле оболочки Борна – Примеч. автора.


[Закрыть]
, которая может осуществлять управление заданиями. Мы называем такую оболочку оболочкой, управляющей заданиями.

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

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

Группе процессов, ID которой совпадает с ID управляющего терминала, разрешено записывать в терминал и читать с него. Эта группа называется приоритетной (foreground) группой процессов. (Она получает также генерируемые клавиатурой сигналы.) Любые другие группы процессов в сеансе являются фоновыми (background) группами процессов и не могут читать или записывать в терминал; они получают специальные сигналы, которые их останавливают, если они пытаются это делать.

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

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

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

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

9.2.2. Идентификация группы процессов: getpgrp() и getpgid()

Для совместимости с более старыми системами POSIX предоставляет множество способов получения сведений о группе процессов:

#include

pid_t getpgrp(void);      /* POSIX */

pid_t getpgid(pid_t pid); /* XSI */

Функция getpgrp() возвращает ID группы процессов текущего процесса. getpgid() является расширением XSI. Она возвращает ID группы процессов для данного pid группы процессов. pid, равный 0, означает «группа процессов текущего процесса». Таким образом, 'getpgid(0)' является тем же самым, что и 'getpgrp()'. При обычном программировании следует использовать getpgrp().

В BSD 4.2 и 4.3 также есть функция getpgrp(), но она действует как функция POSIX getpgid(), требуя аргумент pid. Поскольку современные системы поддерживают POSIX, в новом коде следует использовать версию POSIX. (Если вы думаете, что это сбивает с толку, вы правы. Несколько способов для получения одного и того же результата является обычным итогом проектирования комитетом, поскольку комитет считает, что он должен удовлетворить каждого.)

9.2.3. Установка группы процесса: setpgid() и setpgrp()

Две функции устанавливают группу процесса:

#include

int setpgid(pid_t pid, pid_t pgid); /* POSIX */

int setpgrp(void);                  /* XSI */

Функция setpgrp() проста: она устанавливает ID группы процесса равной ID процесса. Это создает новую группу процессов в том же сеансе, а вызывающий функцию процесс становится лидером группы процессов.

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

При использовании setpgid() pgid должна быть группой существующего процесса, которая является частью текущего сеанса, фактически подключая pid к этой группе процессов. В противном случае pgid должна равняться pid, создавая новую группу процессов.

Имеется несколько значений для особых случаев как для pid, так и для pgid:

pid = 0 В данном случае setpgid() изменяет группу процессов вызывающего процесса на pgid. Это эквивалентно 'setpgid(getpid(), pgid)'.

pgid = 0 Это устанавливает ID группы процессов для данного процесса равным его PID. Таким образом, 'setpgid(pid, 0)' является тем же самым, что и 'setpgid(pid, pid)'. Это делает процесс с PID, равным pid, лидером группы процессов.

Во всех случаях лидеры сеанса являются особыми; их PID, ID группы процессов и ID сеанса идентичны, a ID группы процессов лидера не может быть изменена. (ID сеанса устанавливаются посредством setsid(), а получаются посредством getsid(). Это особые вызовы: см. справочные страницы setsid(2) и getsid(2)).

9.3. Базовое межпроцессное взаимодействие: каналы и очереди FIFO

Межпроцессное взаимодействие (Interprocess communication – IPC) соответствует своему названию: это способ взаимодействия для двух отдельных процессов. Самым старым способом IPC на системах Unix является канал (pipe): односторонняя линия связи. Данные, записанные в один конец канала, выходят из другого конца.

9.3.1. Каналы

Каналы проявляют себя как обычные дескрипторы файлов. Без особого разбирательства вы не можете сказать, представляет ли дескриптор файла сам файл или канал. Это особенность; программы, которые читают из стандартного ввода и записывают в стандартный вывод, не должны знать или заботиться о том, что они могут взаимодействовать с другим процессом. Если хотите знать, каноническим способом проверки этого является попытка выполнить с дескриптором 'lseek(fd, 0L, SEEK_CUR)'; этот вызов пытается отсчитать 0 байтов от текущего положения, т е. операция, которая ничего не делает[94]94
  Такая операция часто обозначается no-op – «no operation» (нет операции) – Примеч. автора.


[Закрыть]
. Эта операция завершается неудачей для каналов и не наносит никакого вреда другим файлам.

9.3.1.1. Создание каналов

Системный вызов pipe() создает канал:

#include /* POSIX */

int pipe(int filedes[2]);

Значение аргумента является адресом массива из двух элементов целого типа, pipe() возвращает 0 при успешном возвращении и -1, если была ошибка.

Если вызов был успешным, у процесса теперь есть два дополнительных открытых дескриптора файла. Значение filedes[0] является читаемым концом канала, a filedes [1] – записываемым концом. (Удобным мнемоническим способом запоминания является то, что читаемый конец использует индекс 0, аналогичный дескриптору стандартного ввода 0, а записываемый конец использует индекс 1, аналогичный дескриптору стандартного вывода 1.)

Как упоминалось, данные, записанные в записываемый конец, считываются из читаемого конца. После завершения работы с каналом оба конца закрываются с помощью вызова close(). Следующая простая программа, ch09-pipedemo.c, демонстрирует каналы путем создания канала, записи в него данных, а затем чтения этих данных из него:

1  /* ch09-pipedemo.c – демонстрация ввода/вывода с каналом. */

2

3  #include

4  #include

5  #include

6

7  /* main – создание канала, запись в него и чтение из него. */

8

9  int main(int argc, char **argv)

10 {

11  static const char mesg[] = "Don't Panic!"; /* известное сообщение */

12  char buf[BUFSIZ];

13  ssize_t rcount, wcount;

14  int pipefd[2];

15  size_t l;

16

17  if (pipe(pipefd) < 0) {

18   fprintf(stderr, "%s: pipe failed: %sn", argv[0],

19    strerror(errno));

20   exit(1);

21  }

22

23  printf("Read end = fd %d, write end = fd %dn",

24   pipefd[0], pipefd[1]);

25

26  l = strlen(mesg);

27  if ((wcount = write(pipefd[1], mesg, 1)) != 1) {

28   fprintf(stderr, "%s: write failed: %sn", argv[0],

29    strerror(errno));

30   exit(1);

31  }

32

33  if ((rcount = read(pipefd[0], buf, BUFSIZ)) != wcount) {

34   fprintf(stderr, "%s: read failed: %sn", argv[0],

35    strerror(errno));

36   exit(1);

37  }

38

39  buf[rcount] = '';

40

41  printf("Read <%s> from pipen", buf);

42  (void)close(pipefd[0]);

43  (void)close(pipefd[1]);

44

45  return 0;

46 }

Строки 11–15 объявляют локальные переменные; наибольший интерес представляет mesg, который представляет текст, проходящий по каналу.

Строки 17–21 создают канал с проверкой ошибок; строки 23–24 выводят значения новых дескрипторов файлов (просто для подтверждения, что они не равны 0, 1 или 2)

В строке 26 получают длину сообщения для использования с write(). Строки 27–31 записывают сообщение в канал, снова с проверкой ошибок.

Строки 33–37 считывают содержимое канала, опять с проверкой ошибок. Строка 39 предоставляет завершающий нулевой байт, так что прочитанные данные могут использоваться в качестве обычной строки. Строка 41 выводит данные, а строки 42–43 закрывают оба конца канала. Вот что происходит при запуске программы:

$ ch09-pipedemo

Read end = fd 3, write end = fd 4

Read from pipe

Эта программа не делает ничего полезного, но она демонстрирует основы. Обратите внимание, что нет вызовов open() или creat() и что программа не использует три своих унаследованных дескриптора. Тем не менее, write() и read() завершаются успешно, показывая, что дескрипторы файлов действительны и что данные, поступающие в канал, действительно выходят из него.[95]95
  Мы уверены, что вы не волновались. В конце концов, вы, возможно, используете конвейеры из оболочки десятки раз в день – Примеч. автора.


[Закрыть]
Конечно, будь сообщение слишком большим, наша программа не работала бы. Это происходит из-за того, что размер (памяти) каналов ограничен, факт, который мы обсудим в следующем разделе.

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

9.3.1.2. Буферирование каналов

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

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

Подобным же образом, если канал пустой, потребитель блокируется в read() до тех пор, пока в канале не появятся данные для чтения. (Блокирующее поведение можно отключить; это обсуждается в разделе 9.4.3.4 «Неблокирующий ввод/вывод для каналов и очередей FIFO».)

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

Напротив, если потребитель закрывает читаемый конец, write() на записываемом конце завершается неудачей. В частности, ядро посылает производителю сигнал «нарушенный канал», действием по умолчанию для которого является завершение процесса.

Нашей любимой аналогией для каналов является то, как муж и жена вместе моют и сушат тарелки. Один супруг моет тарелки, помещая чистые, но влажные тарелки в сушилку на раковине. Другой супруг вынимает тарелки из сушилки и вытирает их. Моющий тарелки является производителем, сушилка является каналом, а вытирающий является потребителем.[96]96
  Что они ели на обед, остается не указанным. – Примеч. автора.


[Закрыть]

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

Рис. 9.3. Синхронизация процессов канала

9.3.2. Очереди FIFO

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

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

Для решения этой проблемы System III предложила идею о FIFO. FIFO,[97]97
  FIFO означает «first in, first out» – «первым вошел, первым вышел». Так работают каналы. – Примеч. автора.


[Закрыть]
или именованный канал, является файлом в файловой системе, который действует подобно каналу. Другими словами, один процесс открывает FIFO для записи, тогда как другой открывает его для чтения. Затем данные, записанные; в FIFO, читаются читателем. Данные буферируются ядром, а не хранятся на диске.

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

Функция mkfifo() создает файлы FIFO:

#include /* POSIX */

#include

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

Аргумент pathname является именем создаваемого FIFO, a mode является данными ему правами доступа, аналогичными второму аргументу функции creat() или третьему аргументу функции open() (см. раздел 4.6 «Создание файлов»). Файлы FIFO удаляются, как любые другие, с помощью remove() или unlink() (см. раздел 5.1.5.1 «Удаление открытых файлов»).

Справочная страница GNU/Linux mkfifo(3) указывает, что FIFO должен быть открыт как для чтения, так и для записи в одно и то же время, до того, как может быть осуществлен ввод/вывод: «Открытие FIFO для чтения обычно блокирует до тех пор, пока какой-нибудь другой процесс не откроет тот же FIFO для записи, и наоборот». После открытия файла FIFO он действует подобно обычному каналу; т.е. это просто еще один дескриптор файла.

Команда mkfifo доставляет этот системный вызов на командный уровень. Это упрощает показ файла FIFO в действии:

$ mkfifo afifo /* Создание файла FIFO */

$ ls -l afifo

 /* Показать тип и права доступа, обратите внимание на 'p' впереди */

prw-r–r– 1 arnold devel 0 Oct 23 15:49 afifo

$ cat < afifo & /* Запустить читателя в фоновом режиме */

[1] 22100

$ echo It was a Blustery Day > afifo /* Послать данные в FIFO */

$ It was a Blustery Day /* Приглашение оболочки, cat выводит данные */

 /* Нажмите ENTER, чтобы увидеть статус завершения задания */

[1]+ Done cat

9.4. Управление дескрипторами файлов

На данный момент части загадки почти полностью составлены, fork() и exec() создают процессы и запускают в них программы, pipe() создает канал, который может использоваться для IPC. Чего до сих пор не хватает, так это способа помещения дескрипторов канала на место стандартных ввода и вывода для производителя и потребителя канала.

Системные вызовы dup() и dup2(), совместно с close() дают вам возможность поместить (скопировать) открытый дескриптор файла на другой номер. Системный вызов fcntl() дает вам возможность то же самое и управлять несколькими важными атрибутами открытых файлов.

9.4.1. Дублирование открытых файлов: dup() и dup2()

Два системных вызова создают копию открытого дескриптора файла:

#include /* POSIX */

int dup(int oldfd);

int dup2(int oldfd, int newfd);

Функции следующие:

int dup(int oldfd)

Возвращает наименьшее значение неиспользуемого дескриптора файла; это копия oldfd. dup() возвращает неотрицательное целое в случае успеха и -1 при неудаче.

int dup2(int oldfd, int newfd)

Делает newfd копией oldfd; если newfd открыт, он сначала закрывается, как при использовании close(). dup2() возвращает новый дескриптор или -1, если была проблема. Помните рис. 9.1, в котором два процесса разделяли общие указатели на один и тот же элемент файла в таблице файлов ядра? dup() и dup2() создают ту же ситуацию внутри одного процесса. См. рис. 9.4.

Рис. 9.4. Разделение дескриптора файла как результат 'dup2(1, 3)'

На этом рисунке процесс выполнил 'dup2(1, 3)', чтобы сделать дескриптор файла 3-й копией стандартного вывода, дескриптора файла 1. Точно как описано ранее, эти два дескриптора разделяют общее смещение открытого файла.

В разделе 4.4.2 «Открытие и закрытие файлов» мы упомянули, что open()creat()) всегда возвращают наименьшее целое значение неиспользуемого дескриптора для открываемого файла. Этому правилу следуют почти все системные вызовы, которые возвращают новые дескрипторы файлов, а не только open() и creat(). (dup2() является исключением, поскольку он предусматривает способ получения конкретного нового дескриптора файла, даже если он не является наименьшим неиспользуемым дескриптором.)

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

1. Создать канал с помощью pipe(). Это должно быть сделано сначала, чтобы два порожденных процесса могли унаследовать дескрипторы открытых файлов.

2. Создать то, что мы называем «левым потомком». Это процесс, стандартный вывод которого идет в канал. В данном процессе сделать следующее:

 a. Использовать 'close(pipefd[0])', поскольку читаемый конец канала в левом потомке не нужен.

 b. Использовать 'close(1)', чтобы закрыть первоначальный стандартный вывод.

 c. Использовать 'dup(pipefd[1])' для копирования записываемого конца канала в дескриптор файла 1.

 d. Использовать 'close(pipefd[1])', поскольку нам не нужны две копии открытого дескриптора.

 e. Выполнить exec для запускаемой программы.

3. Создать то, что мы называем «правым потомком». Это процесс, стандартный ввод которого поступает из канала. Шаги для этого потомка являются зеркальным отражением шагов для левого потомка:

 a. Использовать 'close(pipefd[1])', поскольку записываемый конец канала в правом потомке не нужен.

 b. Использовать 'close(0)', чтобы закрыть первоначальный стандартный ввод.

 c. Использовать 'dup(pipefd[0])' для копирования читаемого конца канала в дескриптор файла 0.

 d. Использовать 'close(pipefd[0])', поскольку нам не нужны две копии открытого дескриптора.

 e. Выполнить exec для запускаемой программы.

4. В родителе закрыть оба конца канала – 'close(pipefd[0]); close(pipefd[1])'.

5. Наконец, использовать в родителе wait() для ожидания завершения обоих порожденных процессов.

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

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

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

Следующая программа, ch09-pipeline.c, создает эквивалент следующего конвейера оболочки:

$ echo hi there | sed s/hi/hello/g

hello there

Вот программа:

1  /* ch09-pipeline.c – ответвляет два процесса в их собственный конвейер.

2     Для краткости проверка ошибок сведена к минимуму. */

3

4  #include

5  #include

6  #include

7  #include

8  #include

9

10 int pipefd[2];

11

12 extern void left_child(void), right_child(void);

13

14 /* main – порождение процессов и ожидание их завершения */

15

16 int main(int argc, char **argv)

17 {

18  pid_t left_pid, right_pid;

19  pid_t ret;

20  int status;

21

22  if (pipe(pipefd) < 0) { /* создать канал в самом начале */

23   perror("pipe");

24   exit(1);

25  }

26

27  if ((left_pid = fork()) < 0) { /* порождение левого потомка */

28   perror("fork");

29   exit(1);

30  } else if (left_pid == 0)

31  left_child();

32

33  if ((right_pid = fork()) < 0) { /* порождение правого потомка */

34   perror("fork");

35   exit(1);

36  } else if (right_pid == 0)

37  right_child();

38

39  close(pipefd[0])); /* закрыть родительские копии канала */

40  close(pipefd[1]);

41

42  while ((ret = wait(&status)) > 0) { /* wait for children */

43   if (ret == left_pid)

44    printf("left child terminated, status: %xn", status);

45   else if (ret == right_pid)

46    printf("right child terminated, status: %xn", status);

47   else

48    printf("yow! unknown child %d terminated, status %xn",

49     ret, status);

50  }

51

52  return 0;

53 }

Строки 22–25 создают канал. Это должно быть сделано в самом начале.

Строки 27–31 создают левого потомка, а строки 33–37 создают правого потомка. В обоих случаях родитель продолжает линейное исполнение ветви main() до тех пор, пока порожденный процесс не вызовет соответствующую функцию для манипулирования дескрипторами файла и осуществления exec.

Строки 39–40 закрывают родительскую копию канала.

Строки 42–50 в цикле ожидают потомков, пока wait() не вернет ошибку.

55 /* left_child – осуществляет работу левого потомка */

56

57 void left_child(void)

58 {

59  static char *left_argv[] = { "echo", "hi", "there", NULL };

60

61  close(pipefd[0]);

62  close(1);

63  dup(pipefd[1]);

64  close(pipefd[1]);

65

66  execvp("echo", left_argv);

67  _exit(errno == ENOENT ? 127 : 126);

68 }

69

70 /* right_child – осуществляет работу правого потомка */

71

72 void right_child(void)

73 {

74  static char *right_argv[] = { "sed", "s/hi/hello/g", NULL };

75

76  close(pipefd[1]);

77  close(0);

78  dup(pipefd[0]);

79  close(pipefd[0]));

80

81  execvp("sed", right_argv);

82  _exit(errno == ENOENT ? 127 : 126);

83 }

Строки 57–68 являются кодом для левого потомка. Процедура следует приведенным выше шагам, закрывая ненужный конец канала, закрывая первоначальный стандартный вывод, помещая с помощью dup() записываемый конец канала на номер 1 и закрывая затем первоначальный записываемый конец. В этот момент строка 66 вызывает execvp(), и если она завершается неудачей, строка 67 вызывает _exit(). (Помните, что строка 67 никогда не выполняется, если execvp() завершается удачно.)

Строки 72–83 делают подобные же шаги для правого потомка. Вот что происходит при запуске:

$ ch09-pipeline /* Запуск программы */

left child terminated, status: 0 /* Левый потомок завершается до вывода (!) */

hello there /* Вывод от правого потомка */

right child terminated, status: 0

$ ch09-pipeline /* Повторный запуск программы */

hello there /* Вывод от правого потомка и ... */

right child terminated, status: 0 /* Правый потомок завершается до левого */

left child terminated, status: 0

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

Весь процесс показан на рис. 9.5.



Рис. 9.5. Создание конвейера родителем

На рис. 9.5 (а) изображена ситуация после создания родителем канала (строки 22–25) и двух порожденных процессов (строки 27–37).

На рис. 9.5 (b) показана ситуация после закрытия родителем канала (строки 39–40) и начала ожидания порожденных процессов (строки 42–50). Каждый порожденный процесс поместил канал на место стандартного вывода (левый потомок, строки 61–63) и стандартного ввода (строки 76–78).

Наконец, рис. 9.5 (с) изображает ситуацию после закрытия потомками первоначального канала (строки 64 и 79) и вызова execvp() (строки 66 и 81).


    Ваша оценка произведения:

Популярные книги за неделю