Самодел 1 (1114716), страница 29
Текст из файла (страница 29)
В стандартной ситуации (при отсутствии переполнения) система гарантирует атомарность операции записи, т. е. при одновременной записи нескольких процессов в канал их данные не перемешиваются.
Пример. Использование канала.
Процесс посылает данные самому себе. Описан массив из двух целых чисел, который передается в функцию pipe (в системный вызов pipe), pipe его заполнил. Далее, используя нулевой дескриптор, осуществляем чтение из канала, используя первый – запись. Понятно, здесь просто строчка записывается, потом считывается. Затем закрываются оба дескриптора из pipes. Прочитанная строка записывается на стандартный вывод и после этого программа завершается. Пример условный, потому что в рамках одного процесса каналы никто не использует.
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
char *s = ”chanel”;
char buf[80];
int pipes[2];
pipe(pipes);
write(pipes[1], s, strlen(s) + 1);
read(pipes[0], buf, strlen(s) + 1);
close(pipes[0]);
close(pipes[1]);
printf(“%s\n”, buf);
return 0;
}
Как правило каналы используются для взаимодействия между двумя процессами. И тут-то как раз и становится существенным, что это есть средство для родственных процессов. Поскольку канал настолько характеризуется файловыми дескрипторами, то для того, чтобы сделать его доступным для другого процесса, существует один единственный способ – унаследовать дескрипторы при порождении сыновнего процесса. Т.е. должна быть следующая последовательность действий: сначала порождается канал, после чего появляется открытый файловый дескриптор, и затем порождается вся необходимая иерархия на процессы, при этом открытые дескрипторы, естественно, наследуются всеми процессами потомками. Тем самым все процессы потомки, порожденные после того, как этот канал будет создан, и их потомки, если они породят в свою очередь каких-то своих потомков, они имеют доступ к этому каналу, потому что у них есть открытый дескриптор к этому каналу. И больше никакие процессы к нему доступа не имеют и не могут его никак получить, потому что этот дескриптор никак не может быть передан, даже если передать это целое число какому-то другому процессу, то оно для него ничего не будет означать, поскольку у него в таблице файловых дескрипторов отсутствует специальная запись, которая ассоциирована с этим контролем. Именно в этом и заключается смысл фразы, когда говорится о том, что канал – это средство взаимодействия для родственных процессов.
Пример. Схема взаимодействия процессов с использованием канала.
Сначала идет вызов pipe, затем fork(), благодаря которому образуется несколько процессов, и соответственно внутри if (fork()) процесс-отец, он закрывает дескриптор чтения и пользуется только записывающей стороной. В сыне происходит, соответственно, происходит все наоборот, он закрывает дескриптор записи и осуществляет чтение из канала. Как правило, канал используется как однонаправленное средство, т.е. данные будут передвигаться только в одном направлении, в данном случае от отца к сыну: отец записывает данные – сын их читает в том же порядке, в котором их записал отец. Обратите внимание на закрывание дескрипторов. Понятно, что это, в принципе, не нужно, поскольку если программа будет завершена, то все дескрипторы и так закроются. Но в данном случае эта строка имеет очень важный смысл: ранее уже говорилось о том, что при попытке чтения большего числа байт из канала, чем в нем находится, чтение будет заблокировано в том случае, если в канале не находится символ EOF, который туда попадает, когда закрывается последний записывающий дескриптор. Последний записывающий дескриптор будет закрыт тогда, когда его закроет процесс-отец, т.е. он запишет все необходимые данные, после чего закроет дескриптор, и собственно это будет означать, что больше данных не будет. В случае, если сын не закроет свой унаследованный дескриптор записи, то после того, как отец закроет свой дескриптор записи останется еще один записывающий дескриптор к тому же каналу в процессе сыне. Хоть он его и не использует (но системе-то это неизвестно), в последнем чтении процесс-сын зациклится, т.к. символ EOF в канал не попадает (поскольку в момент закрытия записывающей стороны это не последний записывающий дескриптор). Поэтому ненужные дескрипторы записи в канал важно обязательно закрывать, потому что иначе последнее чтение из канала будет заблокировано навечно.
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd[2];
pipe(fd);
if (fork())
{/*процесс-родитель*/
close(fd[0]); /* закрываем ненужный дескриптор */
write (fd[1], …);
…
close(fd[1]);
…
}
else
{/*процесс-потомок*/
close(fd[1]); /* закрываем ненужный дескриптор */
while(read (fd[0], …))
{
…
}
…
}
}
Пример. Реализация конвейера.
. Конвейер – это две программы (два процесса), которые исполняются параллельно и при этом стандартный вывод первой программы посылается на стандартный ввод второй программы, т.е. по мере того, как 1-й процесс генерирует свой вывод, он сразу же выдается на ввод второму процессу. Реализуется это очень легко с помощью каналов. Первым делом порождается канал, затем происходит порождение процесса потомка. В процессе потомке с помощью системного вызова dup2(), записывающая сторона канала дублируется на стандартный вывод, после чего системный вызов dup2() открывает второй дескриптор и теперь дескриптор с номером 1, описывающий стандартный вывод, будет смотреть в канал (весь вывод будет помешен в канал) после чего закрываются ненужные дескрипторы, записывающий в канал и читающий, и вызывается замена тела процесса на ту программу, которая собственно является первой программой. В процессе отце происходит всё наоборот, здесь в читающей стороне канала открывается второй дескриптор с номером 0 (стандартный ввод), это означает, что в дальнейшем весь стандартный ввод будет браться из канала, происходит тоже самое, закрываются ненужные дескрипторы, записывающий в канал и читающий, и происходит замена тела программы на программу wc.
Пример реализации конвейера print|wc – вывод программы print будет подаваться на вход программы wc. Программа print печатает некоторый текст. Программа wc считает количество прочитанных строк, слов и символов.
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
int fd[2];
pipe(fd); /*организован канал*/
if (fork())
{
/*процесс-родитель*/
dup2(fd[1], 1); /* отождествили стандартный вывод с файловым дескриптором канала, предназначенным для записи */
close(fd[1]); /* закрыли файловый дескриптор канала, предназначенный для записи */
close(fd[0]); /* закрыли файловый дескриптор канала, предназначенный для чтения */
exelp(“print”, ”print”, 0); /* запустили программу print */
}
/*процесс-потомок*/
dup2(fd[0], 0); /* отождествили стандартный ввод с файловым дескриптором канала, предназначенным для чтения*/
close(fd[0]); /* закрыли файловый дескриптор канала, предназначенный для чтения */
close(fd[1]); /* закрыли файловый дескриптор канала, предназначенный для записи */
execl(“/usr/bin/wc”, ”wc”, 0); /* запустили программу wc */
}
Пример. Совместное использование сигналов и каналов – «пинг-понг».
Следующий пример - совместное использования сигналов и каналов.
Каналы, как правило, используются как однонаправленное средство. Но их можно использовать и как двунаправленное средство, т.е. для реализации передачи данных в обоих направлениях, но в этом случае понадобятся дополнительные средства синхронизации. Средством синхронизации являются сигналы. Здесь процессы посылают друг другу число, всякий раз увеличивая его на 1, и когда число достигнет определенного максимума, оба процесса завершаются, т.е. фактически они передают друг другу это число определенное количество раз. Средой передачи данных служит канал, средством синхронизации для того, чтобы осуществить двустороннюю передачу данных служит сигнал. Здесь определяется константа максимального значения числа, после которого нужно выйти.
Первым делом смотрим на обработчик сигнала. Обработчик сигнала, в данном случае будет использоватьcя сигнал SIGUSER1 для синхронизации, т.е. посылать его процессу всякий раз когда пришла его очередь читать из канала, т.к. взаимные скорости выполнения отца и сына неизвестны. SIGUSER1 - это сигнал, который не соответствует какому-либо определенному событию в ОС, а его семантика определяется пользователями, т.е. это как раз тот случай, когда семантика сигнала отдается на усмотрение программисту.
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define MAX_CNT 100
int target_pid, cnt;
int fd[2];
int status;
void SigHndlr(int s)
{
/* в обработчике сигнала происходит и чтение, и запись */
signal(SIGUSR1, SigHndlr);
if (cnt < MAX_CNT)
{
read(fd[0], &cnt, sizeof(int));
printf("%d \n", cnt);
cnt++;
write(fd[1], &cnt, sizeof(int));
/* посылаем сигнал второму: пора читать из канала */
kill(target_pid, SIGUSR1);
}
else
if (target_pid == getppid())
{
/* условие окончания игры проверяется потомком */
printf("Child is going to be terminated\n");
close(fd[1]); close(fd[0]);
/* завершается потомок */
exit(0);
} else
kill(target_pid, SIGUSR1);
}
int main(int argc, char **argv)
{
pipe(fd); /* организован канал */
signal (SIGUSR1, SigHndlr);
/* установлен обработчик сигнала для обоих процессов */
cnt = 0;
if (target_pid = fork())
{
/* Предку остается только ждать завершения потомка */
while(wait(&status) == -1);
printf("Parent is going to be terminated\n");
close(fd[1]); close(fd[0]);
return 0;
}
else
{
/* процесс-потомок узнает PID родителя */
target_pid = getppid();
/* потомок начинает пинг-понг */
write(fd[1], &cnt, sizeof(int));
kill(target_pid, SIGUSR1);
for(;;); /* бесконечный цикл */
}
}
Процесс – сын начинает игру. Во-первых, устанавливается target_pid, который в процессе-отце был установлен в fork(), в процессе-сыне он устанавливается с помощью вызова getppid(), который возвращает pid по требованию. И он записывает текущее значение в канал и посылает сигнал SIGUSR1 своему предку. Далее происходит бесконечный цикл. Т.е. вся обработка «пинг-понг», которая заключается в чтении данных из канала, увеличения на 1, проверки значения (не достигло ли оно своего максимума) и записи нового увеличенного значения в канал – все это происходит в обработчике канала, т.е. здесь функция main() в процессе-сыне - бесконечный цикл, а в процессе – отце wait() – ожидание завершения потомка. Процесс-сын завершается когда нашел максимум, а процесс-отец в этом случае выходит из wait(), закрывает канал и тоже выходит.
Неименованные каналы являются достаточно мощным средством передачи данных, однако, у них имеется важный недостаток, о котором уже говорилось, а именно то, что они доступны только родственным процессам. Т.е. поскольку к ним не возможен доступ по имени, а возможен только с помощью файловых дескрипторов, то нужно сначала породить канал, затем породить потомков и только таким образом они могут унаследовать дескрипторы записи и чтения в канал. Это важное ограничение, поскольку два не связанных между собой процесса, которые были порождены ранее, чем средства межпроцессного взаимодействия канал не могут использовать. Эту проблему решает средство - именованные каналы
Именованные каналы.
Особенность именованных каналов в ОС Unix.
Именованные каналы имеют имя, как и файлы. Каждому именованному каналу соответствует один элемент некоторого каталога ОС UNIX, поэтому возможна ссылка к нему по имени файла, которое хранится в поле имени соответствующего элемента каталога.
Системный доступ реализован последовательно. У именованных каналов имеются имя владельца, права доступа, размер не ограничен.
Рассмотренные ранее программные каналы имеют важное ограничение: т.к. доступ к ним возможен только посредством дескрипторов, возвращаемых при порождении канала, необходимым условием взаимодействия процессов через канал является передача этих дескрипторов по наследству при порождении процесса. Именованные каналы (FIFO-файлы) расширяют свою область применения за счет того, что подключиться к ним может любой процесс в любое время, в том числе и после создания канала. Это возможно благодаря наличию у них имен.
FIFO-файл представляет собой отдельный тип файла в файловой системе UNIX, который обладает всеми атрибутами файла, такими как имя владельца, права доступа и размер. Для его создания в UNIX System V.3 и ранее используется системный вызов mknod(), а в BSD UNIX и System V.4 – вызов mkfifo() (этот вызов поддерживается и стандартом POSIX):
int mknod (char *pathname, mode_t mode, dev)
int mkfifo (char *pathname, mode_t mode)
В обоих вызовах первый аргумент представляет собой имя создаваемого канала, во втором указываются права доступа к нему для владельца, группы и прочих пользователей, и кроме того, устанавливается флаг, указывающий на то, что создаваемый объект является именно FIFO-файлом (в разных версиях ОС он может иметь разное символьное обозначение – S_IFIFO или I_FIFO). Третий аргумент вызова mknod() игнорируется.
После создания именованного канала любой процесс может установит с ним связь посредством системного вызова open(). При этом действуют следующие правила:
- если процесс открывает FIFO-файл для чтения, он блокируется до тех пор, пока какой-либо процесс не откроет тот же канал на запись