exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
Если вы выполните программу, то получите следующий вывод:
$ <b>./pipe1</b>
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);
}
После выполнения этой программы вы получите вывод, аналогичный предыдущему:
$ <b>./pipe2</b>
Wrote 3 bytes
Read 3 bytes: 123
Вы можете столкнуться с повторным выводом строки приглашения для ввода команды перед завершающим фрагментом вывода, поскольку родительский процесс завершится раньше дочернего, поэтому мы подчистили вывод, чтобы его легче было читать.
Как это работает
Сначала программа создает канал с помощью вызова pipe. Далее она применяет вызов fork для создания нового процесса. Если fork завершился успешно, родительский процесс пишет данные в канал, в то время как дочерний считывает данные из канала. Оба процесса, и родительский, и дочерний, завершаются после одного вызова write и read. Если родительский процесс завершается раньше дочернего, вы можете увидеть между двумя выводами строку приглашения командной оболочки.
Несмотря на то, что программа внешне похожа на первый пример pipe, мы сделали большой шаг вперед, получив возможность использовать разные процессы для чтения и записи (рис. 13.2).
Рис. 13.2
Родительский и дочерний процессы
Следующий логический шаг в нашем изучении вызова pipe — разрешить дочернему процессу быть другой программой, отличной от своего родителя, а не просто другим процессом, выполняющим ту же самую программу. Сделать это можно с помощью вызова exec. Единственная сложность заключается в том, что новому процессу, созданному exec, нужно знать, какой файловый дескриптор применять для доступа. В предыдущем примере этой проблемы не возникло, потому что дочерний процесс обращался к своей копии данных file_pipes. После вызова exec возникает другая ситуация, поскольку старый процесс заменен новым дочерним процессом. Эту проблему можно обойти, если передать файловый дескриптор (который, в конце концов, просто число) как параметр программе, вновь созданной с помощью вызова exec.
Для того чтобы посмотреть, как это работает, вам понадобятся две программы (упражнение 13.7). Первая — поставщик данных. Она создает канал и затем вызывает дочерний процесс, потребитель данных.
Упражнение 13.7. Каналы и exec
1. Для получения первой программы исправьте pipe2.c, превратив ее в pipe3.c. Измененные строки затенены.