Текст книги "Основы программирования в Linux"
Автор книги: Нейл Мэтью
Соавторы: Ричард Стоунс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 48 (всего у книги 67 страниц)
Резюме
В этой главе вы узнали, как создать несколько потоков исполнения внутри процесса, которые совместно используют глобальные переменные. Вы рассмотрели два способа управления – семафоры и мьютексы, применяемые потоками для доступа к важным фрагментам кода и данным. Далее вы увидели, как управлять атрибутами потоков и, в особенности, как можно отсоединить потоки от основного, не заставляя его ждать завершения созданных им потоков. После краткого обзора способов формирования в одном потоке запросов на отмену других потоков и вариантов управления такими запросами в потоке, получившем их, мы представили программу с множественными одновременно выполняющимися потоками.
Объем книги не позволяет обсудить все до единой функции и тонкости, связанные с потоками, но теперь у вас достаточно знаний для того, чтобы начать писать собственные программы, применяющие потоки, и изучать глубоко скрытые свойства потоков, читая страницы интерактивного справочного руководства.
Глава 13
Связь между процессами: каналы
В главе 11 вы видели очень простой способ пересылки сообщений между процессами с помощью сигналов. Вы формировали уведомляющие события, которые могли бы применяться для вызова ответа, но передаваемая информация была ограничена номером сигнала.
В этой главе вы познакомитесь с каналами, которые позволяют процессам обмениваться более полезной информацией. В конце этой главы вы примените свои вновь приобретенные знания для новой реализации программы, управляющей базой данных компакт-дисков, в виде клиент-серверного приложения.
В данной главе мы обсудим следующие темы:
□ определение канала;
□ каналы процессов;
□ вызовы каналов;
□ родительские и дочерние процессы;
□ именованные каналы – FIFO;
□ замечания, касающиеся клиент-серверных приложений.
Что такое канал?
Мы применяем термин «канал» для обозначения соединения потока данных одного процесса с другим. Обычно вы присоединяете или связываете каналом вывод одного процесса с вводом другого.
Большинство пользователей Linux уже знакомы с идеей конвейера, связывающего вместе команды оболочки так, что вывод одного процесса поставляет данные прямо во ввод другого. В случае команд оболочки это делается с помощью символа конвейера или канала, соединяющего команды следующим образом:
cmd1 | cmd2
Командная оболочка организует стандартный ввод и вывод двух команд так, что:
□ стандартный ввод cmd1
поступает с клавиатуры терминала;
□ стандартный вывод cmd1
поставляется cmd2
как ее стандартный ввод;
□ стандартный вывод cmd2
подсоединен к экрану терминала.
На самом деле командная оболочка заново соединила потоки стандартных ввода и вывода так, что потоки данных проходят с клавиатурного ввода через две команды и выводятся на экран. На рис. 13.1 приведено визуальное представление этого процесса.
Рис. 13.1
В этой главе вы увидите, как достичь этого эффекта в программе и как можно использовать каналы для связи многих процессов, что позволит создать простую клиент-серверную систему.
Каналы процессов
Возможно, простейший способ передачи данных между программами – применение функций popen
и pclose
. У них следующие прототипы:
#include
FILE *popen(const char *command, const char *open_mode);
int pclose(FILE *stream_to_close);
Функция popen позволяет программе запустить другую программу как новый процесс и либо передать ей данные, либо получить их из нее. Строка command
– это имя программы для выполнения вместе с любыми параметрами, параметр open_mode
должен быть "r"
или "w"
.
Если open_mode
– "r"
, вывод вызванной программы становится доступен вызывающей программе и может быть считан из возвращаемого функцией popen
файлового потока FILE*
с помощью обычных функций библиотеки stdio, предназначенных для чтения (например, fread
). Но если open_mode
– "w"
, программа может отправить данные вызванной команде с помощью вызова функции fwrite
. Далее вызванная программа сможет читать данные из своего стандартного ввода. Обычно вызванная программа не знает, что она считывает данные из другого процесса; она просто читает свой поток стандартного ввода и воздействует на него.
Вызов функции popen
должен задавать "r"
или "w"
; никакого другого значения стандартной реализацией popen не поддерживается. Это означает, что вы не можете вызвать другую программу и одновременно читать из нее и писать в нее. В случае сбоя popen
возвращает пустой указатель. Если вы хотите создать двунаправленную связь с помощью каналов, стандартное решение – применить два канала: по одному для потока данных каждого направления.
Когда процесс, стартовавший с помощью popen
, закончится, вы можете закрыть файловый поток, связанный с ним, с помощью функции pclose
. Вызов pclose
вернет управление, только когда процесс, запущенный с помощью popen
, завершится. Если он все еще выполняется во время вызова pclose
, вызов pclose
будет ждать окончания процесса.
Функция pclose
обычно возвращает код завершения процесса, чей файловый поток она закрывает. Если вызывающий процесс уже выполнил оператор wait
перед вызовом pclose
, статус завершения будет потерян, поскольку вызванный процесс закончен, и функция pclose
вернет -1 с переменной errno
, получившей значение ECHILD
.
Выполните упражнение 13.1.
Упражнение 13.1. Чтение вывода внешней программы
Давайте опробуем простой пример popen1.c с функциями popen
и pclose
. Вы будете применять в программе popen
для доступа к информации из uname
. uname
– это команда, выводящая системную информацию, включая тип компьютера, имя ОС, версию и выпуск, а также сетевое имя машины.
Запустив программу, вы откроете канал к uname
; сделаете его читаемым и зададите read_fp
, как указатель на вывод. В конце канал, на который указывает read_fp
, закрывается.
#include
#include
#include
#include
int main() {
FILE *read_fp;
char buffer[BUFSIZ +1];
int chars_read;
memset(buffer, ' ', sizeof(buffer));
read_fp = popen(«uname -a», "r");
if (read_fp ! = NULL) {
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
if (chars_read > 0) {
printf(«Output was:-n%sn», buffer);
}
pclose(read_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
Когда вы выполните программу, то должны получить вывод, похожий на следующий (полученный на одной из машин авторов):
$ ./popen1
Output was:-
Linux suse103 2.6.20.2-2-default #1 SMP Fri Mar 9 21:54:10 UTC 2001 i686 i686 i386 GNU/Linux
Как это работает
Программа применяет функцию popen
для вызова команды uname
с параметром -а
. Затем она использует возвращенный файловый поток для чтения данных, до BUFSIZ
символов (как задано в директиве #define
из файла stdio.h), и затем выводит их на экран. Поскольку вы перехватываете вывод команды uname внутри программы, его можно обрабатывать.
Отправка вывода в popen
Теперь, когда вы рассмотрели пример захвата вывода из внешней программы, давайте познакомимся с отправкой вывода во внешнюю программу. В упражнении 13.2 показана программа popen2.c, передающая по каналу данные другой программе. В этом примере будет использована команда od (от англ. octal dump – восьмеричный дамп).
Упражнение 13.2. Пересылка вывода в другую программу
Взглянув на следующий программный код, вы увидите, что он очень похож на предыдущий пример, за исключением того, что вы пишете данные в канал вместо чтения данных из него. Далее приведена программа popen2.c.
#include
#include
#include
#include
int main() {
FILE *write_fp;
char buffer[BUFSIZ + 1];
sprintf(buffer, «Once upon a time, there was...n»);
write_fp = popen(«od -c», "w");
if (write_fp != NULL) {
fwrite(buffer, sizeof(char), strlen(buffer), write_fp);
pclose(write_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
После выполнения этой программы вы должны получить следующий вывод:
$ ./popen2
0000000 O n c e u p o n a t i m e
0000020 , t h e r e w a s . . . n
0000037
Как это работает
Программа применяет popen
с параметром "w" для запуска команды od -с
таким образом, что может отправить данные этой команде. Затем она отправляет строку, которую команда od -с
получает и обрабатывает; далее команда od -с
выводит результат обработки в своем стандартном выводе.
Такой же вывод можно получить из командной строки с помощью следующей команды:
$ echo «Once upon a time, there was...» | od -c
Механизм, применявшийся до сих пор, просто отправляет и получает все данные в одном вызове fread
или fwrite
. Порой вам может понадобиться отправлять данные меньшими порциями или вы не будете знать размера вывода. Для того чтобы не объявлять слишком большой буфер, можно просто применить множественные вызовы fread
или fwrite
и обрабатывать данные порциями.
В упражнении 13.3 приведена программа popen3.c, читающая все данные из канала.
Упражнение 13.3. Чтение из канала данных большого объема
В этой программе вы читаете данные из вызванного процесса ps ах
. У вас нет возможности узнать заранее, какой величины будет вывод, поэтому вы должны разрешить множественные операции чтения из канала.
#include
#include
#include
#include
int main() {
FILE * read_fp;
char buffer[BUFSIZ + 1];
int chars_read;
memset(buffer, ' ' , sizeof(buffer));
read_fp = popen(«ps ax», "r");
if(read_fp != NULL) {
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
while (chars_read > 0) {
buffer[chars_read – 1] = ' ';
printf(«Reading %d:-n %sn», BUFSIZ, buffer);
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
}
pclose(read_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
Вывод, отредактированный для краткости, подобен приведенному далее:
$ ./popen3
Reading 1024:-
PID TTY STAT TIME COMMAND
1 ? Ss 0:03 init [5]
2 ? SW 0:00 [kflushd]
3 ? SW 0:00 [kpiod]
4 ? SW 0:00 [kswapd]
5 ? SW< 0:00 [mdrecoveryd]
...
240 tty2 S 0:02 emacs draft1.txt
Reading 1024:-
368 tty1 S 0:00 ./popen 3
369 tty1 R 0:00 ps -ax
370 ...
Как это работает
Программа применяет функцию popen
с параметром "r"
аналогично программе popen1.c. В этот раз она продолжает чтение из файлового потока до тех пор, пока в нем есть данные. Учтите, что, хотя программе ps
нужно некоторое время для выполнения, Linux так организует планирование процессов, что обе программы выполняются, когда могут. Если у читающего процесса popen3 нет входных данных, он приостанавливается до появления доступных данных. Если записывающий процесс ps
формирует вывод, больший по объему, чем может вместить буфер, он приостанавливается до тех пор, пока считывающий процесс не обработает какой-то объем данных.
В этом примере строка Reading:-
может не появиться второй раз. Это означает, что BUFSIZ
больше объема вывода команды ps
. В некоторых (самых современных) системах Linux установлен размер буфера BUFSIZ
, равный 8192 байт или даже больше. Для того чтобы проверить корректность работы программы при считывании нескольких порций вывода, попробуйте считывать за один раз меньше символов, чем BUFSIZ
, может быть BUFSIZ/10
.
Вызов popen
выполняет программу, которую вы запросили, прежде всего, вызывая командную оболочку sh
и передавая ей командную строку как аргумент. У этого процесса две стороны: приятная и не очень.
В ОС Linux (как и во всех UNIX-подобных системах) подстановка всех параметров выполняется командной оболочкой, поэтому вызов оболочки для синтаксического анализа командной строки перед вызовом программы дает возможность командной оболочке выполнить любую подстановку, например, определить реальные файлы, на которые ссылается строка *.с до того, как программа начнет выполняться. Часто это очень полезно и позволяет запускать с помощью popen
сложные команды оболочки. Другие функции создания процесса, например execl
, гораздо сложнее применять для вызова, поскольку вызывающий процесс должен самостоятельно выполнять подстановки параметров командной оболочки.
Нежелательный эффект применения командной оболочки состоит в том, что для каждого вызова popen
вместе с требуемой программой вызывается командная оболочка. Далее каждый вызов popen
порождает запуск двух дополнительных процессов, что делает функцию popen
немного расточительной с точки зрения расходования системных ресурсов и вызов нужной команды выполняется медленнее, чем было бы в противном случае.
В упражнении 13.4 приведена программа popen4.c, которую можно использовать для демонстрации поведения popen
. Вы можете сосчитать количество строк во всех файлах с исходным текстом примеров семейства popen
, применив команду cat
к файлам и затем пересылая по каналу вывод в команду wc -l
, которая считает количество строк. В командной строке эквивалентная команда выглядит следующим образом:
$ cat popen*.c | wc -l
Примечание
На самом деле
wc -l popen*.c
легче и гораздо эффективнее ввести с клавиатуры, но пример иллюстрирует основные принципы использования каналов.
Упражнение 13.4. Вызов popen
запускает командную оболочку
Эта программа применяет в точности предыдущую команду, но с помощью popen
, так что она может читать результат.
#include
#include
#include
#include
int main() {
FILE *read_fp;
char buffer[BUFSIZ +1];
int chars_read;
memset(buffer, ' ', sizeof(buffer));
read_fp = popen(«cat popen*.с | wc -l», "r");
if (read_fp != NULL) {
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
while (chars_read > 0) {
buffer[chars_read – 1] = ' ';
printf(«Reading:-n %sn», buffer);
chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
}
pclose(read_fp);
exit(EXIT_SUCCESS);
}
exit(EXIT_FAILURE);
}
Выполнив эту программу, вы получите следующий вывод:
$ ./popen4
Reading:-
94
Как это работает
Программа показывает, что вызывается командная оболочка для того, чтобы развернуть popen*.с
в список всех файлов, начинающихся с popen
и заканчивающихся .с
, а также для обработки символа канала (|
) и отправки вывода команды cat
в команду wс
. Вы вызываете командную оболочку, программы cat
и
wc
и задаете перенаправление – все в одном вызове popen
. Программа, вызвавшая команду, видит только заключительный вывод.
Вызов pipe
Вы познакомились с высокоуровневой функцией popen
, а теперь пойдем дальше и рассмотрим низкоуровневую функцию pipe
. Она предоставляет средства передачи данных между двумя программами без накладных расходов на вызов командной оболочки для интерпретации запрашиваемой команды. Эта функция также позволит вам лучше управлять чтением и записью данных.
У функции pipe
следующее объявление:
#include
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
#include
#include
#include
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
#include
#include
#include
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