Книга: Основы программирования в Linux
Вызов pipe
Разделы на этой странице:
Вызов pipe
Вы познакомились с высокоуровневой функцией popen
, а теперь пойдем дальше и рассмотрим низкоуровневую функцию pipe
. Она предоставляет средства передачи данных между двумя программами без накладных расходов на вызов командной оболочки для интерпретации запрашиваемой команды. Эта функция также позволит вам лучше управлять чтением и записью данных.
У функции pipe
следующее объявление:
#include <unistd.h>
int pipe(int file_descriptor[2]);
Функции pipe
передается указатель на массив из двух целочисленных файловых дескрипторов. Она заполняет массив двумя новыми файловыми дескрипторами и возвращает 0. В случае неудачи она вернет -1 и установит переменную errno
для указания причины сбоя. В интерактивном справочном руководстве Linux на странице, посвященной функций pipe
(в разделе 2 руководства), определены следующие ошибки:
? EMFILE
— процесс использует слишком много файловых дескрипторов;
? ENFILE
— системная таблица файлов полна;
? EFAULT
— некорректный файловый дескриптор.
Два возвращаемых файловых дескриптора подсоединяются специальным образом. Любые данные, записанные в file_descriptor[1]
, могут быть считаны обратно из file_descriptor[0]
. Данные обрабатываются по алгоритму "первым пришел, первым обслужен", обычно обозначаемому как FIFO. Это означает, что если вы записываете байты 1
, 2
, 3
в file_descriptor[1]
, чтение из file_descriptor[0]
выполняется в следующем порядке: 1
, 2
, 3
. Этот способ отличается от стека, который функционирует по алгоритму "последним пришел, первым обслужен", который обычно называют сокращенно LIFO.
Примечание
Важно уяснить, что речь идет о файловых дескрипторах, а не о файловых потоках, поэтому для доступа к данным вы должны применять низкоуровневые системные вызовы read
и write
вместо библиотечных функций потоков fread
и fwrite
.
В упражнении 13.5 приведена программа pipe1.с, которая использует вызов pipe
для создания канала.
Упражнение 13.5 Функция pipe
Следующий пример — программа pipe1.c. Обратите внимание на массив file_pipes
, который передается функции pipe
как параметр.
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main() {
int data_processed;
int filepipes[2];
const char some_data[] = "123";
char buffer[BUFSIZ + 1];
memset(buffer, '', sizeof(buffer));
if (pipe(file_pipes) == 0) {
data_processed = write(file_pipes[1], some_data, strlen(somedata));
printf("Wrote %d bytesn", data_processed);
data_processed = read(file_pipes[0], buffer, BUFSIZ);
printf("Read %d bytes: %sn", data_processed, buffer);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
Если вы выполните программу, то получите следующий вывод:
$ ./pipe1
Wrote 3 bytes
Read 3 bytes: 123
Как это работает
Программа создает канал с помощью двух файловых дескрипторов из массива file_pipes[]
. Далее она записывает данные в канал, используя файловый дескриптор file_pipes[1]
, и считывает их обратно из file_pipes[0]
. Учтите, что у канала есть внутренняя буферизация, позволяющая хранить данные между вызовами функций write
и read
.
Следует знать, что реакция на попытку писать с помощью дескриптора file_descriptor[0]
или читать с помощью дескриптора file_descriptor[1]
не определена, поэтому поведение программы может быть очень странным и меняться без каких-либо предупреждений. В системах авторов такие вызовы заканчивались аварийно и возвращали -1, что, по крайней мере, гарантирует легкость обнаружения такой ошибки.
На первый взгляд этот пример использования канала ничего не предлагает такого, чего мы не могли бы сделать с помощью простого файла. Действительные преимущества каналов проявятся, когда вам нужно будет передавать данные между двумя процессами. Как вы видели в главе 11, когда программа создает новый процесс с помощью вызова fork
, уже открытые к этому моменту файловые дескрипторы так и остаются открытыми. Создав канал в исходном процессе и затем сформировав с помощью fork
новый процесс, вы сможете передать данные из одного процесса в другой через канал (упражнение 13.6).
Упражнение 13.6. Каналы через вызов fork
1. Это пример pipe2.c. Он выполняется также как первый до того момента, пока вы не вызовете функцию fork
.
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main() {
int data_processed;
int file_pipes[2];
const char some_data[] = "123";
char buffer[BUFSIZ + 1];
pid_t fork_result;
memset(buffer, '0', sizeof(buffer));
if (pipe(file_pipes) == 0) {
fork_result = fork();
if (fork_result == -1) {
fprintf(stderr, "Fork failure");
exit(EXIT_FAILURE);
}
2. Вы убедились, что вызов fork
отработал, поэтому, если его результат равен нулю, вы находитесь в дочернем процессе:
if (fork_result == 0) {
data_processed = read(file_pipes[0], buffer, BUFSIZ);
printf("Read %d bytes: %sn", data_processed, buffer);
exit(EXIT_SUCCESS);
}
3. В противном случае вы должны быть в родительском процессе:
else {
data_processed = write(file_pipes[1], some_data,
strlen(some_data));
printf("Wrote %d bytesn", data_processed);
}
}
exit(EXIT_SUCCESS);
}
После выполнения этой программы вы получите вывод, аналогичный предыдущему:
$ ./pipe2
Wrote 3 bytes
Read 3 bytes: 123
Вы можете столкнуться с повторным выводом строки приглашения для ввода команды перед завершающим фрагментом вывода, поскольку родительский процесс завершится раньше дочернего, поэтому мы подчистили вывод, чтобы его легче было читать.
Как это работает
Сначала программа создает канал с помощью вызова pipe
. Далее она применяет вызов fork
для создания нового процесса. Если fork
завершился успешно, родительский процесс пишет данные в канал, в то время как дочерний считывает данные из канала. Оба процесса, и родительский, и дочерний, завершаются после одного вызова write
и read
. Если родительский процесс завершается раньше дочернего, вы можете увидеть между двумя выводами строку приглашения командной оболочки.
Несмотря на то, что программа внешне похожа на первый пример pipe
, мы сделали большой шаг вперед, получив возможность использовать разные процессы для чтения и записи (рис. 13.2).
Рис. 13.2
- Вызов хранимых процедур InterBase с использованием стандартного синтаксиса ODBC
- Системные вызовы и драйверы устройств
- Определение необходимого системного вызова
- Системные вызовы управления процессорной привязкой
- Вызовы функций
- Другие системные вызовы для управления файлами
- Вызов справки из приложений
- 21.4 Вызовы socket
- Вызов окна программного кода
- ЧАСТЬ 5 УДАЛЕННЫЙ ВЫЗОВ ПРОЦЕДУР
- 3.2.3. Системные вызовы: brk() и sbrk()
- 8.2 Pipes