Текст книги "Основы программирования в Linux"
Автор книги: Нейл Мэтью
Соавторы: Ричард Стоунс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 49 (всего у книги 67 страниц)
Родительский и дочерний процессы
Следующий логический шаг в нашем изучении вызова pipe
– разрешить дочернему процессу быть другой программой, отличной от своего родителя, а не просто другим процессом, выполняющим ту же самую программу. Сделать это можно с помощью вызова exec
. Единственная сложность заключается в том, что новому процессу, созданному exec
, нужно знать, какой файловый дескриптор применять для доступа. В предыдущем примере этой проблемы не возникло, потому что дочерний процесс обращался к своей копии данных file_pipes
. После вызова exec
возникает другая ситуация, поскольку старый процесс заменен новым дочерним процессом. Эту проблему можно обойти, если передать файловый дескриптор (который, в конце концов, просто число) как параметр программе, вновь созданной с помощью вызова exec
.
Для того чтобы посмотреть, как это работает, вам понадобятся две программы (упражнение 13.7). Первая — поставщик данных. Она создает канал и затем вызывает дочерний процесс, потребитель данных.
Упражнение 13.7. Каналы и exec
1. Для получения первой программы исправьте pipe2.c, превратив ее в pipe3.c. Измененные строки затенены.
#include
#include
#include
#include
int main() {
int data_processed;
int file_pipes[2];
const char somedata[] = «123»;
char buffer[BUFSIZ + 1];
pid_t fork_result;
memset(buffer, ' ', sizeof(buffer));
if (pipe(file_pipes) == 0) {
fork_result = fork();
if (fork_result == (pid_t)-1) {
fprintf(stderr, «Fork failure»);
exit(EXIT_FAILURE);
}
if (fork_result == 0) {
sprintf(buffer, «%d», file_pipes[0]);
(void)execl(«pipe4», «pipe4», buffer, (char*)0);
exit(EXIT_FAILURE);
} else {
data_processed = write(file_pipes[1], some_data, strlen(some_data));
printf («%d – wrote %d bytesn», getpid(), data_processed);
}
}
exit(EXIT_SUCCESS);
}
2. Программа-потребитель pipe4.c, читающая данные, гораздо проще:
#include
#include
#include
#include
int main(int argc, char *argv[]) {
int data_processed;
char buffer[BUFSIZ + 1];
int file_descriptor;
memset(buffer, ' ', sizeof(buffer));
sscanf(argv[1], «%d», &file_descriptor);
data_processed = read(file_descriptor, buffer, BUFSIZ);
printf(«%d – read %d bytes: %sn», getpid(), data_processed,
buffer);
exit(EXIT_SUCCESS);
}
Выполнив pipe3 и помня о том, что она вызывает программу pipe4, вы получите вывод, аналогичный приведенному далее:
$ ./pipe3
22460 – wrote 3 bytes
22461 – read 3 bytes: 123
Как это работает
Программа pipe3 начинается как предыдущий пример, используя вызов pipe
для создания канала и затем вызов fork
для создания нового процесса. Далее она применяет функцию sprintf
для сохранения в буфере номера файлового дескриптора чтения из канала, который формирует аргумент программы pipe4.
Вызов execl
применен для вызова программы pipe4. В нем использованы следующие аргументы:
□ вызванная программа;
□ argv[0]
, принимающий имя программы;
□ argv[1]
, содержащий номер файлового дескриптора, из которого программа должна читать;
□ (char *)0
, завершающий список параметров.
Программа pipe4 извлекает номер файлового дескриптора из строки аргументов и затем читает из него данные.
Чтение закрытых каналовПрежде чем двигаться дальше, необходимо более внимательно рассмотреть файловые дескрипторы, которые открыты. До этого момента вы разрешали читающему процессу просто читать какие-то данные и завершаться, полагая, что ОС Linux уберет файлы в ходе завершения процесса.
В большинстве программ, читающих данные из стандартного ввода, это делается несколько иначе, чем в виденных вами до сих пор примерах. Обычно программы не знают, сколько данных они должны считать, поэтому они, как правило, выполняют цикл – чтение данных, их обработка и затем снова чтение данных и так до тех пор, пока не останется данных для чтения.
Вызов read
обычно будет задерживать выполнение процесса, т.е. он заставит процесс ждать до тех пор, пока не появятся данные. Если другой конец канала был закрыт, следовательно, нет ни одного процесса, имеющего канал для записи, и вызов read
блокируется. Поскольку это не очень полезно, вызов read
, пытающийся читать из канала, не открытого для записи, возвращает 0 вместо блокирования. Это позволит читающему процессу обнаружить канальный эквивалент метки «конец файла» и действовать соответствующим образом. Учтите, что это не то же самое, что чтение некорректного дескриптора файла, которое вызов read считает ошибкой и обозначает возвратом -1.
Если вы применяете канал с вызовом fork
, есть два файловых дескриптора, которые можно использовать для записи в канал: один в родительском, а другой в дочернем процессах. Вы должны закрыть файловые дескрипторы записи в канал в обоих этих процессах, прежде чем канал будет считаться закрытым и вызов read
для чтения из канала завершится аварийно. Мы рассмотрим пример этого позже, когда вернемся к данной теме, для того чтобы подробно обсудить флаг O_NONBLOCK
и каналы FIFO.
Теперь, когда вы знаете, как заставить вызов read
, примененный к пустому каналу, завершиться аварийно, можно рассмотреть более простой метод соединения каналом двух процессов. Вы устраиваете так, что у одного из файловых дескрипторов канала будет известное значение, обычно стандартный ввод, 0, или стандартный вывод, 1. Его немного сложнее установить в родительском процессе, но при этом значительно упрощается программа дочернего процесса.
Одно неоспоримое достоинство заключается в том, что вы можете вызывать стандартные программы, которым не нужен файловый дескриптор как параметр. Для этого вам следует применить функцию dup
, с которой вы встречались в главе 3. Существуют две тесно связанные версии функции dup
, которые объявляются следующим образом:
#include
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
Назначение вызова dup
– открыть новый дескриптор файла, немного похоже на то, как это делает вызов open
. Разница в том, что файловый дескриптор, созданный dup
, ссылается на тот же файл (или канал), что и существующий файловый дескриптор. В случае вызова dup
новый файловый дескриптор всегда имеет самый маленький доступный номер, а в случае dup2
– первый доступный дескриптор, больший чем значение параметра file_descriptor_two
.
Примечание
Того же эффекта, что и применение вызовов
dup
иdup2
можно добиться, применяя более общий вызовfcntl
с командойF_DUPFD
. Как говорилось, вызовdup
легче использовать, поскольку он разработан специально для создания дубликатов файловых дескрипторов. Он также очень широко применяется, поэтому вы встретите его гораздо чаще в существующих программах, чем вызовfcntl
и командуF_DUPFD
.
Итак, как же dup
помогает в обмене данными между процессами? Хитрость кроется в знании того, что дескриптор стандартного файла ввода всегда 0 и что dup
всегда возвращает новый файловый дескриптор, применяя наименьший доступный номер. Сначала закрыв дескриптор 0, а затем вызвав dup
, вы получите новый файловый дескриптор с номером 0. Поскольку новый файловый дескриптор – это дубликат существующего, стандартный ввод изменится и получит доступ к файлу или каналу, файловый дескриптор которого вы передали в функцию dup
. В результате вы создадите два файловых дескриптора, которые ссылаются на один и тот же файл или канал и один из них будет стандартным вводом.
Управление файловым дескриптором с помощью close и dup
Легче всего понять, что происходит, когда вы закрываете файловый дескриптор 0 и затем вызываете dup
, если рассмотреть состояние первых четырех файловых дескрипторов, изменяющихся последовательно друг за другом (табл. 13.1).
Таблица 13.1
dup | |||
---|---|---|---|
0 | Стандартный ввод | {closed} | Файловый дескриптор канала |
1 | Стандартный вывод | Стандартный вывод | Стандартный вывод |
2 | Стандартный поток ошибок | Стандартный поток ошибок | Стандартный поток ошибок |
3 | Файловый дескриптор канала | Файловый дескриптор канала | Файловый дескриптор канала |
А теперь выполните упражнение 13.8.
Упражнение 13.3. Каналы и dup
Давайте вернемся к предыдущему примеру, но на этот раз вы измените дочернюю программу, заменив в ней файловый дескриптор stdin концом считывания read
созданного вами канала. Вы также выполните некоторую реорганизацию файловых дескрипторов, чтобы дочерняя программа могла правильно определить конец данных в канале. Как обычно, мы пропустили некоторые проверки ошибок для краткости.
Превратите программу pipe3.c в pipe5.c с помощью следующего программного кода:
#include
#include
#include
#include
int main() {
int data_processed;
int file pipes[2];
const char some_data[] = «123»;
pid_t fork_result;
if (pipe(file_pipes) == 0) {
fork_result = fork();
if (fork_result == (pid_t)-1) {
fprintf(stderr, «Fork failure»);
exit(EXIT_FAILURE);
}
if (fork_result == (pid_t)0) {
close(0);
dup(file_pipes[0];
close(file_pipes[0]);
close(file_pipes[1]);
execlp(«od», «od», «-c», (char*)0);
exit(EXIT_FAILURE);
} else {
close(file_pipes[0]);
data_processed = write(file_pipes[1], some_data,
strlen(some_data));
close(file_pipes[1]);
printf(«%d – wrote %d bytesn», (int)getpid(), data_processed);
}
}
exit(EXIT_SUCCESS);
}
У этой программы следующий вывод:
$ ./pipe5
22495 – wrote 3 bytes
0000000 1 2 3
0000003
Как это работает
Как и прежде, программа создает канал, затем выполняет вызов fork
, создавая дочерний процесс. В этот моменту обоих процессов, родительского и дочернего, есть файловые дескрипторы для доступа к каналу, по одному для чтения и записи, т.е. всего четыре открытых файловых дескриптора.
Давайте первым рассмотрим дочерний процесс. Он закрывает свой стандартный ввод с помощью close(0)
и затем вызывает dup(file_pipes[0])
. Этот вызов дублирует файловый дескриптор, связанный с концом read
канала, как файловый дескриптор 0, стандартный ввод. Далее дочерний процесс закрывает исходный файловый дескриптор для чтения из канала, file_pipes[0]
. Поскольку этот процесс никогда не будет писать в канал, он также закрывает файловый дескриптор для записи в канал, file_pipes[1]
. Теперь у дочернего процесса единственный файловый дескриптор, связанный с каналом, файловый дескриптор 0, его стандартный ввод.
Далее дочерний процесс может применить exec
для вызова любой программы, которая читает стандартный ввод. В данном случае мы используем команду od
. Команда od
будет ждать, когда данные станут ей доступны, как если бы она ждала ввода с терминала пользователя. В действительности без специального программного кода, позволяющего непосредственно выяснить разницу, она не будет знать, что ввод приходит из канала, а не с терминала.
Родительский процесс начинает с закрытия конца чтения канала, file_pipes[0]
, потому что он никогда не будет читать из канала. Затем он пишет данные в канал. Когда все данные записаны, родительский процесс закрывает конец записи в канал и завершается. Поскольку теперь нет файловых дескрипторов, открытых для записи в канал, программа od
сможет считать три байта, записанных в канал, но последующие операции чтения далее будут возвращать 0 байтов, указывая на конец файла. Когда read
вернет 0, программа od
завершится. Это аналогично выполнению команды od
, введенной с терминала, и последующему нажатию комбинации клавиш od
.
На рис. 13.3 показан результат вызова pipe
, на рис. 13.4 – результат вызова fork
, а на рис. 13.5 представлена программа, когда она готова к передаче данных.
Рис. 13.3
Рис. 13.4
Рис. 13.5
Именованные каналы: FIFO
До сих пор вы могли передавать данные только между связанными программами, т.е. программами, которые стартовали из общего процесса-предка. Часто это очень неудобно, хотелось бы, чтобы и у несвязанных процессов была возможность обмениваться данными.
Вы можете сделать это с помощью каналов FIFO, часто называемых именованными каналами. Именованный канал – это файл специального типа (помните, что в ОС Linux все, что угодно, – файл!), существующий в виде имени в файловой системе, но ведущий себя как неименованные каналы, которые вы уже встречали.
Вы можете создавать именованные каналы из командной строки и внутри программы. С давних времен программой создания их в командной строке была команда mknod
:
$ mknod имя_файла p
Однако команды mknod
нет в списке команд X/Open, поэтому она включена не во все UNIX-подобные системы. Предпочтительнее применять в командной строке
$ mkfifo имя_файла
Примечание
У некоторых более старых версий UNIX была только команда
mknod
. В стандарте X/Open issue 4 Version 2 есть вызов функцииmknod
, но не программа командной строки. ОС Linux, как всегда настроенная дружелюбно, предлагает оба варианта:mknod
иmkfifo
.
Внутри программы можете применять два разных вызова:
#include
#include
int mkfifo(const char *filename, mode_t mode);
int mknod(const char* filename, mode_t mode | S_IFIFO, (dev_t)0);
Помимо команды mknod
вы можете использовать функцию mknod
для создания файлов специальных типов. Единственный переносимый вариант применения этой функции, создающий именованный канал, – использование значения 0 типа dev_t
и объединений с помощью операции or режима доступа к файлу и S_IFIFO
. В примерах мы будем применять более простую функцию mkfifo
.
Итак, выполните упражнение 13.9.
Упражнение 13.9. Создание именованного канала
Далее приведен исходный текст примера fifo1.c.
#include
#include
#include
#include
#include
int main() {
int res = mkfifo(«/tmp/my_fifo», 0777);
if (res == 0) printf («FIFO createdn»);
exit(EXIT_SUCCESS);
}
Вы можете создать канал и заглянуть в него:
$ ./fifo1
FIFO created
$ ls -lF /tmp/my_fifo
prwxr-xr-x 1 rick users 0 2007-06-16 17:18 /tmp/my_fifo|
Обратите внимание на то, что первый символ вывода – р
, обозначающий канал. Символ |
в конце добавлен опцией -F
команды ls
и тоже обозначает канал.
Как это работает
Программа применяет функцию mkfifo
для создания специального файла. Несмотря на то, что запрашиваете режим 0777
, он заменяется пользовательской маской (umask
), устанавливаемой (в данном случае 022
) точно так же, как при создании обычного файла, поэтому у результирующего файла режим 755
. Если ваша umask
установлена иначе, например, ее значение 0002
, вы увидите другие права доступа у созданного файла.
Удалить FIFO можно как традиционный файл с помощью команды rm
или внутри программы посредством системного вызова unlink
.
У именованных каналов есть одно очень полезное свойство: поскольку они появляются в файловой системе, их можно применять в командах на месте обычного имени файла. Прежде чем вы продолжите программирование с использованием созданного вами файла FIFO, давайте исследуем поведение такого файла с помощью обычных команд для работы с файлом (упражнение 13.10).
Упражнение 13.10. Организации доступа к файлу FIFO
1. Сначала попробуйте прочесть (пустой) файл FIFO:
$ cat < /tmp/my_fifo
2. Теперь попытайтесь записать в FIFO. Вам придется использовать другой терминал, поскольку первая команда в данный момент "зависла" в ожидании появления каких-нибудь данных в FIFO:
$ echo «Hello World» > /tmp/my_fifo
Вы увидите вывод команды cat
. Если не посылать никаких данных в канал FIFO, команда cat
будет ждать до тех пор, пока вы не прервете ее выполнение, традиционно комбинацией клавиш
3. Можно выполнить обе команды одновременно, переведя первую в фоновый режим:
$ cat < /tmp/my_fifo &
[1] 1316
$ echo «Hello World» > /tmp/my_fifo
Hello World
[1]+ Done cat
$
Как это работает
Поскольку в канале FIFO не было данных, обе команды, cat
и echo
, приостанавливают выполнение, ожидая, соответственно, поступления каких-нибудь данных и какого-либо процесса для их чтения.
На третьем шаге процесс cat
с самого начала заблокирован в фоновом режиме. Когда echo
делает доступными некоторые данные, команда cat
читает их и выводит в стандартный вывод. Обратите внимание на то, что она затем завершается, не дожидаясь дополнительных данных. Программа cat
не блокируется, т.к. канал уже закрылся, когда завершилась вторая команда, поместившая данные в FIFO, поэтому вызовы read
в программе cat
вернут 0 байтов, обозначая этим конец файла.
Теперь, когда вы посмотрели, как ведут себя каналы FIFO при обращении к ним с помощью программ командной строки, давайте рассмотрим более подробно программный интерфейс, предоставляющий больше возможностей управления операциями чтения и записи при организации доступа к FIFO.
Примечание
В отличие от канала, созданного вызовом
pipe
, FIFO существует как именованный файл, но не как открытый файловый дескриптор, и должен быть открыт перед тем, как можно будет из него читать данные или в него записывать их. Открывается и закрывается канал FIFO с помощью функцийopen
иclose
, которые вы ранее применяли к файлам, но с дополнительными функциональными возможностями. Вызовуopen
передается полное имя FIFO вместо полного имени обычного файла.
Открытие FIFO с помощью open
Основное ограничение при открытии канала FIFO состоит в том, что программа не может открыть FIFO для чтения и записи с режимом O_RDWR
. Если программа нарушит это ограничение, результат будет непредсказуемым. Это очень разумное ограничение, т.к., обычно канал FIFO применяется для передачи данных в одном направлении, поэтому нет нужды в режиме O_RDWR
. Процесс стал бы считывать обратно свой вывод, если бы канал был открыт для чтения/записи.
Если вы действительно хотите передавать данные между программами в обоих направлениях, гораздо лучше использовать пару FIFO или неименованных каналов, по одному для каждого направления передачи, или (что нетипично) явно изменить направление потока данных, закрыв и снова открыв канал FIFO. Мы вернемся к двунаправленному обмену данными с помощью каналов FIFO чуть позже в этой главе.
Другое различие между открытием канала FIFO и обычного файла заключается в использовании флага open_flag
(второй параметр функции open
) со значением O_NONBLOCK
. Применение этого режима open
изменяет способ обработки не только вызова open
, но и запросов read
и write
для возвращаемого файлового дескриптора.
Существует четыре допустимых комбинации значений O_RDONLY
, O_WRONLY
и O_NONBLOCK
флага. Рассмотрим их все по очереди.
open(const char *path, O_RDONLY);
В этом случае вызов open
блокируется, он не вернет управление программе до тех пор, пока процесс не откроет этот FIFO для записи. Это похоже на первый пример с командой cat
.
open(const char *path, O_RDONLY | O_NONBLOCK);
Теперь вызов open
завершится успешно и вернет управление сразу, даже если канал FIFO не был открыт для записи каким-либо процессом.
open(const char *path, O_WRONLY);
В данном случае вызов open
будет заблокирован до тех пор, пока процесс не откроет тот же канал FIFO для чтения.
open(const char *path, O_WRONLY | O_NONBLOCK);
Этот вариант вызова всегда будет возвращать управление немедленно, но если ни один процесс не открыл этот канал FIFO для чтения, open
вернет ошибку, -1, и FIFO не будет открыт. Если есть процесс, открывший FIFO для чтения, возвращенный файловый дескриптор может использоваться для записи в канал FIFO.
Примечание
Обратите внимание на асимметрию в использовании
O_NONBLOCK
сO_RDONLY
иO_WRONLY
, заключающуюся в том, что неблокирующий вызовopen
для записи завершается аварийно, если ни один процесс не открыл канал для чтения, а неблокирующий вызовopen
для чтения не возвращает ошибку. На поведение вызоваclose
флагO_NONBLOCK
влияния не оказывает.
Выполните упражнение 13.11.
Упражнение 13.11. Открытие файлов FIFO
Теперь рассмотрим, как можно использовать поведение вызова open
с флагом, содержащим O_NONBLOCK
, для синхронизации двух процессов. Вместо применения нескольких программ-примеров вы напишите одну тестовую программу fifo2.c, которая позволит исследовать поведение каналов FIFO при передаче ей разных параметров.
1. Начните с заголовочных файлов, директивы #define
и проверки правильности количества предоставленных аргументов командной строки:
#include
#include
#include
#include
#include
#include
#include
#define FIFO_NAME «/tmp/my_fifo»
int main(int argc, char *argv[]) {
int res;
int open_mode = 0;
int i;
if (argc < 2) {
fprintf(stderr, "Usage: %s
O_RDONLY O_WRONLY O_NONBLOCK>n", *argv);
exit(EXIT_FAILURE);
}
2. Полагая, что программа передает тестовые данные, вы задаете параметр open_mode
из следующих аргументов:
for(i = 1; i
if (strncmp(*++argv, «O_RDONLY», 8) == 0) open_mode |= O_RDONLY;
if (strncmp(*argv, «O_WRONLY», 8) == 0) open_mode |= O_WRONLY;
if (strncmp(*argv, «O_NONBLOCK», 10) == 0) open_mode |= O_NONBLOCK;
}
3. Далее проверьте, существует ли канал FIFO, и при необходимости создайте его. Затем FIFO открывается, и пока программа засыпает на короткое время, выполняется результирующий вывод. В заключение FIFO закрывается.
if (access(FIFO_NAME, F_OK) == -1) {
res = mkfifo(FIFO_NAME, 0777);
if (res != 0) {
fprintf(stderr, «Gould not create fifo %sn», FIFO_NAME);
exit(EXIT_FAILURE);
}
}
printf(«Process %d opening FIF0n», getpid());
res = open(FIFO_NAME, open_mode);
printf(«Process %d result %dn», getpid(), res);
sleep(5);
if (res != -1) (void)close(res);
printf(«Process %d finishedn», getpid());
exit(EXIT_SUCCESS);
}
Как это работает
Эта программа позволяет задать в командной строке комбинации значений O_RDONLY
, O_WRONLY
и O_NONBLOCK
, которые вы хотите применить. Делается это сравнением известных строк с параметрами командной строки и установкой (с помощью |=
) соответствующего флага при совпадении строки. В программе используется функция access
, проверяющая, существует ли уже файл FIFO, и создающая его при необходимости.
Никогда не уничтожайте FIFO, т.к. у вас нет способа узнать, не использует ли FIFO другая программа.
O_RDONLY и O_WRONLY без O_NONBLOCK
Теперь у вас есть тестовая программа, и вы можете проверить комбинации пар. Обратите внимание на то, что первая программа, считыватель, помещена в фоновый режим.
$ ./fifo2 O_RDONLY &
[1] 152
Process 152 opening FIFO
$ ./fifo2 O_WRONLY
Process 153 opening FIFO
Process 152 result 3
Process 153 result 3
Process 152 finished
Process 153 finished
Это, наверное, самое распространенное применение именованных каналов. Оно позволяет читающему процессу стартовать и ждать в вызове open
, а затем разрешает обеим программам продолжить выполнение, когда вторая программа откроет канал FIFO. Обратите внимание на то, что и читающий, и пишущий процессы были синхронизированы вызовом open
.
Примечание
Когда процесс в ОС Linux заблокирован, он не потребляет ресурсы ЦП, поэтому этот метод синхронизации очень эффективен с точки зрения использования ЦП.
O_RDONLY с O_NONBLOCK и O_WRONLY
В следующем примере читающий процесс выполняет вызов open
и немедленно продолжается, даже если нет ни одного пишущего процесса. Пишущий процесс тоже немедленно продолжает выполняться после вызова open
, потому что канал FIFO уже открыт для чтения.
$ ./fifо2 O_RDONLY O_NONBLOCK &
[1] 160
Process 160 opening fifo
$ ./fifo2 O_WRONLY
Process 161 opening FIFO
Process 160 result 3
Process 161 result 3
Process 160 finished
Process 161 finished
[1]+ Done ./fifo2 O_RDONLY O_NONBLOCK
Эти два примера – вероятно, самые распространенные комбинации режимов open
. Не стесняйтесь использовать программу-пример для экспериментов с другими возможными комбинациями.
Чтение из каналов FIFO и запись в них
Применение режима O_NONBLOCK
влияет на поведение вызовов read
и write
в каналах FIFO.
Вызов read
, применяемый для чтения из пустого блокирующего FIFO (открытого без флага O_NONBLOCK
), будет ждать до тех пор, пока не появятся данные, которые можно прочесть. Вызов read
, применяемый в неблокирующем FIFO, напротив, при отсутствии данных вернет 0 байтов.
Вызов write
для записи в полностью блокирующий канал FIFO будет ждать до тех пор, пока данные не смогут быть записаны. Вызов write
, применяемый к FIFO, который не может принять все байты, предназначенные для записи, либо:
□ будет аварийно завершен, если был запрос на запись PIPE_BUF
байтов или меньше и данные не могут быть записаны;
□ запишет часть данных, если был запрос на запись более чем PIPE_BUF
байтов, и вернет количество реально записанных байтов, которое может быть и 0.
Размер FIFO – очень важная характеристика. Существует накладываемый системой предел объема данных, которые могут быть в FIFO в любой момент времени. Он задается директивой #define PIPE_BUF
, обычно находящейся в файле limits.h. В ОС Linux и многих других UNIX-подобных системах он обычно равен 4096 байт, но в некоторых системах может быть и 512 байт. Система гарантирует, что операции записи PIPE_BUF или меньшего количества байтов в канал FIFO, который был открыт O_WRONLY
(т.е. блокирующий), запишут или все байты, или ни одного.
Несмотря на то, что этот предел не слишком важен в простом случае с одним записывающим каналом FIFO и одним читающим FIFO, очень распространено использование одного канала FIFO, позволяющего разным программам отправлять запросы к этому единственному каналу FIFO. Если несколько разных программ попытаются писать в FIFO в одно и то же время, жизненно важно, чтобы блоки данных из разных программ не перемежались друг с другом, т. е. каждая операция write должна быть "атомарной". Как это сделать?
Если вы ручаетесь, что все ваши запросы write
адресованы блокирующему каналу FIFO и их размер меньше PIPE_BUF
байтов, система гарантирует, что данные никогда не будут разделены. Вообще это неплохая идея – ограничить объем данных, передаваемых через FIFO блоком в PIPE_BUF
байтов, если вы не используете единственный пишущий и единственный читающий процессы.
Выполните упражнение 13.12.
Упражнение 13.12. Связь процессов с помощью каналов FIFO
Для того чтобы увидеть, как несвязанные процессы могут общаться с помощью именованных каналов, вам понадобятся две отдельные программы fifo3.c и fifo4.c.
1. Первая программа – поставщик. Она создает канал, если требуется, и затем записывает в него данные как можно быстрее.
Примечание
Поскольку пример иллюстративный, нас не интересуют конкретные данные, и мы не беспокоимся об инициализации буфера, В обоих листингах затененные строки содержат изменения, внесенные в программу fifo2.c помимо удаления кода со всеми аргументами командной строки.
#include
#include
#include
#include
#include
#include
#include
#include
#define FIFO_NAME «/tmp/my_fifo»
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 * 10)
int main() {
int pipe_fd;
int res;
int open_mode = O_WRONLY;
int bytes_sent = 0;
char buffer[BUFFER_SIZE + 1];
if (access(FIFO_NAME, F_OK) == -1) {
res = mkfifo(FIFO_NAME, 0777);
if (res != 0) {
fprintf(stderr, «Could not create fifo %sn», FIFO_NAME);
exit(EXIT_FAILURE);
}
}
printf(«Process %d opening FIFO O_WRONLYn», getpid());
pipe_fd = open(FIFO_NAME, open_name);
printf(«Process %d result %dn», getpid(), pipe_fd);
if (pipe_fd != -1) {
while (bytes_sent < TEN_MEG) {
res = write(pipe_fd, buffer, BUFFER_SIZE);
if (res == -1) {
fprintf(stderr, "Write error on pipen);
exit(EXIT_FAILURE);
}
bytes_sent += res;
}
(void)close(pipe_fd);
} else {
exit(EXIT_FAILURE);
}
printf(«Process %d finishedn», getpid());
exit(EXIT_SUCCESS);
}
2. Вторая программа, потребитель, гораздо проще. Она читает и выбрасывает данные из канала FIFO.
#include
#include
#include
#include
#include
#include
#include
#include
#define FIFO_NAME «/tmp/my_fifo»
#define BUFFER_SIZE PIPE_BUF
int main() {
int pipe_fd;
int res;
int open_mode = O_RDONLY;
char buffer[BUFFER_SIZE – 1];
int bytes_read = 0;
memset(buffer, ' ', sizeof(buffer));
printf(«Process %d opening FIFO O_RDONLYn», getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf(«Prосеss %d result %dn», getpid(), pipe_fd);
if (pipe_fd != -1) {
do {
res = read(pipe_fd, buffer,BUFFER_SIZE);
bytes_read += res;
} while (res > 0);
(void)close(pipe_fd);
} else {
exit(EXIT_FAILURE);
}
printf(«Process %d finished, %d bytes readn», getpid(), bytes_read);
exit(EXIT_SUCCESS);
}
Когда вы выполните эти программы одновременно, с использованием команды time
для хронометража читающего процесса, то получите следующий (с некоторыми пропусками для краткости) вывод:
$ ./fifo3 &
[1] 375
Process 375 opening FIFO O_WRONLY
$ time ./fifo4
Process 377 opening FIFO O_RDONLY
Process 375 result 3
Process 377 result 3
Process 375 finished
Process 377 finished, 10485760 bytes read
real 0m0.053s
user 0m0.020s
sys 0m0.040s
[1]+ Done ./fifo3
Как это работает
Обе программы применяют FIFO в режиме блокировки. Вы запускаете первой программу fifo3 (пишущий процесс/поставщик), которая блокируется, ожидая, когда читающий процесс откроет канал FIFO. Когда программа fifo4 (потребитель) запускается, пишущий процесс разблокируется и начинает записывать данные в канал. В это же время читающий процесс начинает считывать данные из канала.
Примечание
ОС Linux так организует планирование двух процессов, что они оба выполняются, когда могут, и заблокированы в противном случае. Следовательно, пишущий процесс блокируется, когда канал полон, а читающий – когда канал пуст.
Вывод команды time
показывает, что читающему процессу потребовалось гораздо меньше одной десятой секунды для считывания 10 Мбайт данных в процесс. Это свидетельствует о том, что каналы, по крайней мере, их реализация в современных версиях Linux, могут быть эффективным средством обмена данными между программами.