Книга: Linux программирование в примерах

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

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

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

#include <unistd.h> /* 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 <stdio.h>
5  #include <errno.h>
6  #include <sys/types.h>
7  #include <sys/wait.h>
8  #include <unistd.h>
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).

Оглавление книги


Генерация: 1.250. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз