Подготовка к лабораторной работе №2
Введение в межпроцессное взаимодействие
Наличие в Unix-системах простых и эффективных средств взаимодействия между процессами играет очень большую роль в системном программировании. Благодаря межпроцессному взаимодействию программист может разбить решение какой-либо задачи на несколько более простых операций, каждая из которых будет реализована отдельной небольшой программой.
Работать с одной большой программой зачастую сложнее, а ее модернизация иногда бывает и вовсе невозможна. Ведь вполне может возникнуть ситуация, когда изменения, которые необходимо внести в программу потребуют ее полного изменения. Именно поэтому целесообразно разбивать ее на некоторое количество функциональных блоков.
Сразу же, конечно, встанет вопрос о взаимодействии и передаче данных между этими блоками. И эта задача достаточно просто решается в Unix-системах с помощью каналов.
Вы уже знакомы с понятием конвейера. Поэтому принцип межпроцессного взаимодействия будет понять достаточно просто. По аналогии с конвейером, данные со стандартного потока вывода одной программы перенаправляются на стандартный поток ввода другой программы, чей стандартный поток вывода может быть также перенаправлен. Но как быть в том случае, если необходимо использовать канал внутри самой программы?
Один из самых простых способов межпроцессного взаимодействия - внутри-программное использование каналов: программа запускает другую программу и считывает данные, которые запущенная выводит в свой стандартный поток вывода. С помощью этого метода программист может использовать в своей программе функциональность другой программы, не вмешиваясь во внутренние детали ее работы.
Простой пример использования неименованных каналов:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#define BUF_SIZE 128 //размер буфера чтения
int main(int argc, char * argv[])
{
FILE * open_from_proc; //указатель на структуру FILE
FILE * save;
int len;
char buf[BUF_SIZE]; //массив для чтения\записи
if (argc != 2)
{
errno=EINVAL; //установка переменной для вывода ошибки числа агрументов
perror(argv[0]); //уведомление об ошибке
return -1;
}
open_from_proc = popen(argv[1], "r"); //запуск внешней программы(описание ниже)
if (open_from_proc == NULL) //обработка неудачи функции popen(2s)
{
perror("Error:\n");
return -1;
}
save = fopen("log", "w"); //создание файла, если он еще не создан, иначе перезапись
while ((len = fread(buf, 1, BUF_SIZE, open_from_proc)) != 0) //чтение до конца
//считывается одна строка файла (о чем говорит параметр 1) в массив buf
{
write(1, buf, len); //вывод на экран, по дескриптору стандартного потока вывода (это 1)
fwrite(buf, 1, len, save);
//запись в файл, одного элемента содержимого
buf, размером
// len-байт,
// в файл структуры save
}
pclose(open_from_proc); //завершение работы с внешним приложением, закрытие канала
fclose(save); //завершение работы с файлом, разрыв связи с inode файла
return 0; //код возврата
}
Функция popen() запускает внешнюю программу и возвращает вызвавшему ее приложению указатель на структуру FILE, связанный либо со стандартным потоком ввода, либо со стандартным потоком вывода запущенного процесса. Первый параметр функции popen() - строка, содержащая команду, запускающую внешнюю программу. Второй параметр определяет, какой из стандартных потоков (вывода или ввода) будет возвращен. Аргумент ?w? соответствует потоку ввода запускаемой программы, в этом случае приложение, вызвавшее popen(), записывает данные в поток. Аргумент ?r? соответствует потоку вывода.
Особенность функции popen() заключается в том, что эта функция не возвращает NULL, даже если переданная ей команда не является корректной. Самый простой способ обнаружить ошибку в этой ситуации - попытаться прочесть данные из потока вывода. Если в потоке вывода нет данных fread() возвращает значение 0), значит произошла ошибка.
Функция pclose() служит для завершения работы с внешним приложением и закрытием канала. Данная программа выполняет команду оболочки, переданную ей в качестве параметра и записывает данные, выводимые этой командой, одновременно на стандартное устройство вывода и в файл log. Следует иметь в виду, что pclose() вернет управление вызывающему потоку только после того как запущенное с помощью popen() приложение завершит свою работу. Еще одна особенность функции popen() : для выполнения переданной ей команды popen() сперва запускает собственный экземпляр оболочки. Хорошо это потому, что при вызове popen() автоматически выполняются внутренние операции оболочки (такие как обработка шаблонов имен файлов), наследование переменных окружения и т.п. Отрицательная сторона связана с дополнительными расходами ресурсов на запуск процесса оболочки в том случае, когда для выполнения команды собственная оболочка не нужна.
fork()
Для обмена данными с внешним приложением функция popen() использует каналы неявным образом. Однако есть возможность использовать каналы и непосредственно. Наиболее распространенный тип каналов - неименованные однонаправленные каналы создаваемые функцией pipe(2). Для программиста такой канал представляется двумя дескрипторами файлов, один из которых служит для чтения данных, а другой - для записи. Каналы не поддерживают произвольный доступ, т. е. данные могут считываться только в том же порядке, в котором они записывались. Неименованные каналы используются преимущественно вместе с функцией fork(2) и служат для обмена данными между родительским и дочерним процессами. Для организации подобного обмена данными, сначала, с помощью функции pipe(), создается канал. Функции pipe() передается единственный параметр - массив типа int, состоящий из двух элементов. В первом элементе массива функция возвращает дескриптор файла, служащий для чтения данных из канала, а во втором - дескриптор для записи. Затем, с помощью функции fork() процесс ?раздваивается?. Дочерний процесс наследует от родительского процесса оба дескриптора, открытых с помощью pipe(), но, также как и родительский процесс, он должен использовать только один из дескрипторов. Направление передачи данных между родительским и дочерним процессом определяется тем, какой дескриптор будет использоваться родительским процессом, а какой - дочерним.
Пример использования функций fork() и pipe():
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#define BUF_SIZE 1024
int main (int argc, char * argv[])
{
int descriptors[2]; //оба дескриптора канала; 0 ? для чтения, 1 ? для записи
int pid; //переменная для вызова fork()
pipe(descriptors);
//возврат дескрипторов файлов для чтения
и данных из канала
// и записи данных
в канал
pid = fork(); //вызов функции ?раздвоения? процесса на родительский и дочерний
if ( pid > 0 ) //родительский процесс
{
char symb[] = "Hello!\n";
int length=sizeof(symb); // размер массива символов
close(descriptors[0]); //закрытие дескриптора для чтения
write(descriptors[1], symb, length + 1); //запись строки в файл, по дескриптору для записи
close(descriptors[1]); //разрыв связи с дескриптором для записи
}
else //дочерний процесс
{
char buf[BUF_SIZE];
int len;
close(descriptors[1]); //закрытие дескриптора для записи
while ((len =
read(descriptors[0], buf, BUF_SIZE)) != 0)
//чтение файла,
куда была
//была записана строка
write(2, buf, len); //вывод строки на экран
close(descriptors[0]); //разрыв связи с дескриптором чтения
}
return 0; //код возврата
}
Оба дескриптора канала хранятся в массиве descriptors. После вызова fork() процесс раздваивается и родительский процесс (тот, в котором fork() вернула ненулевое значение, равное, кстати, PID дочернего процесса) закрывает дескриптор, открытый для чтения, и записывает данные в канал, используя дескриптор, открытый для записи (descriptors[1]). Дочерний процесс (в котором fork() вернула 0) первым делом закрывает дескриптор, открытый для записи, и затем считывает данные из канала, используя дескриптор, открытый для чтения (descriptors[0]).