9 if ((childpid = Fork()) == 0) { /* child */
10 sleep(3):
11 if ((n = Read(fd[0], &c, 1)) != 1)
12 err_quit("child: read returned %d", n);
13 printf("child read %c\n", c):
14 Write(fd[0], "c", 1);
15 exit(0);
16 }
17 /* родитель */
18 Write(fd[1], "p", 1);
19 if ((n = Read(fd[1], &c, 1)) != 1)
20 err_quit("parent: read returned %d", n):
21 printf("parent read %c\n", c);
22 exit(0);
23 }
В этой программе сначала создается двусторонний канал, затем делается системный вызов fork. Породивший процесс записывает символ р в канал, а затем считывает из канала данные. Дочерний процесс ждет три секунды, считывает символ из канала, а потом записывает туда символ с. Задержка чтения для дочернего процесса позволяет породившему процессу вызвать read первым — таким образом мы можем узнать, не будет ли им считан обратно только что записанный символ.
При запуске этой программы в Solaris 2.6, в которой организована поддержка двусторонних каналов, мы получим ожидаемый результат:
solaris % fduplex
child read p
parent read с
Символ р передается по одному из двух односторонних каналов, изображенных на рис. 4.10, а именно по верхнему каналу. Символ с передается по нижнему одностороннему каналу. Родительский процесс не считывает обратно записанный им в канал символ р (что и требуется).
При запуске этой программы в Digital Unix 4.0B, в которой по умолчанию создаются односторонние каналы (двусторонние каналы — как в SVR4 — будут создаваться в том случае, если при компиляции указать специальные параметры), мы увидим результат, ожидаемый для одностороннего канала:
alpha % fduplex
read error: Bad file number
alpha % child read p
write error: Bad file number
Родительский процесс записывает символ р, который успешно считывается дочерним процессом, однако при попытке считывания из канала (дескриптор fd[l]) родительский процесс прерывается с ошибкой, как и дочерний процесс, при попытке записи в канал (дескриптор fd[0]). Вспомните рис. 4.8. Функция read возвращает код ошибки EBADF, означающий, что дескриптор не открыт для чтения. Аналогично write возвращает тот же код ошибки, если дескриптор не был открыт на запись.
4.5. Функции popen и pclose
Другим примером использования каналов является имеющаяся в стандартной библиотеке ввода-вывода функция popen, которая создает канал и запускает другой процесс, записывающий данные в этот канал или считывающий их из него:
#include <stdio.h>
FILE *popen(const char *
/* Возвращает указатель FILE * в случае успешного выполнения, NULL – в случае ошибки */
int pclose(FILE *s
/* Возвращает код завершения команды интерпретатора или –1 – в случае ошибки */
Аргумент
■ если
■ если
Функция pclose закрывает стандартный поток ввода-вывода stream, созданный командой popen, ждет завершения работы программы и возвращает код завершения, принимаемый от интерпретатора.
ПРИМЕЧАНИЕ
Информацию о реализациях popen и pclose можно найти в разделе 14.3 [21].
Пример
В листинге 4.5 изображено еще одно решение задачи с клиентом и сервером, использующее функцию popen и программу (утилиту Unix) cat.
//pipe/mainpopen.c
1 #include "unpipc.h"
2 int
3 main(int argc, char **argv)
4 {
5 size_t n;
6 char buff[MAXLINE], command[MAXLINE];
7 FILE *fp;
8 /* считывание полного имени */
9 Fgets(buff, MAXLINE, stdin);
10 n = strlen(buff); /* fgets() гарантирует завершающий ноль */
11 if (buff[n-1] == '\n')
12 n--; /* удаление перевода строки из возврата fgets() */
13 snprintf(command, sizeof(command), "cat %s", buff);
14 fp = Popen(command, "r");
15 /* копирование из канала в стандартный вывод */
16 while(Fgets(buff, MAXLINE, fp) != NULL)
17 Fputs(buff, stdout);
18 Pclose(fp);
19 exit(0);
20 }
8-17 Полное имя файла считывается из стандартного потока ввода, как и в программе в листинге 4.2. Формируется командная строка, которая передается popen. Вывод интерпретатора команд или команды cat копируется в стандартный поток вывода.
Одним из отличий этой реализации от приведенной в листинге 4.1 является отсутствие возможности формировать собственные сообщения об ошибках. Теперь мы целиком зависим от программы cat, а выводимые ею сообщения не всегда адекватны. Например, в системе Solaris 2.6 при попытке считать данные из файла, доступ на чтение к которому для нас запрещен, будет выведена следующая ошибка:
solaris % cat/etc/shadow
cat: cannot open /etc/shadow
А в BSD/OS 3.1 мы получим более информативное сообщение в аналогичной ситуации:
bsdi % cat /etc/master.passwd
cat: /etc/master.passwd: cannot open [Permission denied]
Обратите также внимание на тот факт, что вызов popen в данном случае оказывается успешным, однако при первом же вызове fgets будет возвращен символ конца файла (EOF). Программа cat записывает сообщение об ошибке в стандартный поток сообщений об ошибках (stderr), а popen с этим потоком не связывается — к создаваемому каналу подключается только стандартный поток вывода.
4.6. Именованные каналы (FIFO)
Программные каналы не имеют имен, и их главным недостатком является невозможность передачи информации между неродственными процессами. Два неродственных процесса не могут создать канал для связи между собой (если не передавать дескриптор).
Аббревиатура FIFO расшифровывается как «first in, first out» — «первым вошел, первым вышел», то есть эти каналы работают как очереди. Именованные каналы в Unix функционируют подобно неименованным — они позволяют передавать данные только в одну сторону. Однако в отличие от программных каналов каждому каналу FIFO сопоставляется полное имя в файловой системе, что позволяет двум неродственным процессам обратиться к одному и тому же FIFO.
FIFO создается функцией mkfifо:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *
/* Возвращает 0 при успешном выполнении, –1 – при возникновении ошибок */
Здесь
Аргумент mode указывает битовую маску разрешений доступа к файлу, аналогично второму аргументу команды open. В табл. 2.3 приведены шесть констант, определенных в заголовке <sys/stat.h>. Эти константы могут использоваться для задания разрешений доступа и к FIFO.
Функция mkfifo действует как open, вызванная с аргументом O_CREAT | O_EXCL. Это означает, что создается новый канал FIFO или возвращается ошибка EEXIST, в случае если канал с заданным полным именем уже существует. Если не требуется создавать новый канал, вызывайте open вместо mkfifo. Для открытия существующего канала или создания нового в том случае, если его еще не существует, вызовите mkfifo, проверьте, не возвращена ли ошибка EEXIST, и если такое случится, вызовите функцию open.
Команда mkfifо также создает канал FIFO. Ею можно пользоваться в сценариях интерпретатора или из командной строки.
После создания канал FIFO должен быть открыт на чтение или запись с помощью либо функции open, либо одной из стандартных функций открытия файлов из библиотеки ввода-вывода (например, fopen). FIFO может быть открыт либо только на чтение, либо только на запись. Нельзя открывать канал на чтение и запись, поскольку именованные каналы могут быть только односторонними.
При записи в программный канал или канал FIFO вызовом write данные всегда добавляются к уже имеющимся, а вызов read считывает данные, помещенные в программный канал или FIFO первыми. При вызове функции lseek для программного канала или FIFO будет возвращена ошибка ESPIPE.
Пример
Переделаем программу, приведенную в листинге 4.1, таким образом, чтобы использовать два канала FIFO вместо двух программных каналов. Функции client и server останутся прежними; отличия появятся только в функции main, новый текст которой приведен в листинге 4.6.
//pipe/mainfifo.c
1 #include "unpipc.h"
2 #define FIFO1 "/tmp/fifo.1"
3 #define FIFO2 "/tmp/fifo.2"
4 void client(int, int), server(int. int);
5 int
6 main(int argc, char **argv)
7 {
8 int readfd, writefd;
9 pid_t childpid;
10 /* создание двух FIFO, если существуют – OK */
11 if ((mkfifo(FIF01, FILE_MODE) < 0) && (errno != EEXIST))