6.2.2. Создание каналов на Си
Создание каналов на языке программирования Си может оказаться чуть более сложным, чем наш простенький shell-пример. Чтобы создать простой канал на Си, мы прибегаем к использованию системного вызова pipe(). Для него требуется единственный аргумент, который является массивом из двух целых (integer), и, в случае успеха, массив будет содержать два новых файловых дескриптора, которые будут использованы для канала. После создания канала процесс обычно порождает новый процесс (вспомним, что процесс-потомок наследует открытые файловые дескрипторы)
. SYSTEM CALL: pipe();
PROTOTYPE: int pipe( int fd[2] );
RETURNS: 0 в случае успеха
-1 в случае ошибки:
errno = EMFILE (нет свободных дескрипторов)
EMFILE (системная файловая таблица переполнена)
EFAULT (массив fd некорректен)
NOTES: fd[0] устанавливается для чтения, fd[1] - для записи.
Первое целое в массиве (элемент 0) установлено и открыто для чтения, в то время как второе целое (элемент 1) установлено и открыто для записи. Наглядно говоря, вывод fd1 становится вводом для fd0. Еще раз отметим, что все данные, проходящие через канал, перемещаются через ядро.
#include
#include
main()
{
int fd[2];
pipe(fd);
.
.
}
Вспомните, что имя массива decays в указатель на его первый член. fd - это эквивалент &fd[0]. Раз мы установили канал, то ответвим нашего нового потомка
#include
#include
#include
main()
{
int fd[2];
pid_t childpid;
pipe(fd);
if((childpid = fork()) == -1)
{
perror("fork");
exit(1);
}
.
.
}
Если родитель хочет получить данные от потомка, то он должен закрыть fd1, а потомок должен закрыть fd0. Если родитель хочет послать данные потомку, то он должен закрыть fd0, а потомок - fd1. С тех пор как родитель и потомок делят между собой дескрипторы, мы должны всегда быть уверены, что не используемый нами в данный момент конец канала закрыт; EOF никогда не будет возвращен, если ненужные концы канала не закрыты.
#include
main()
{
int fd[2];
pid_t childpid;
if((childpid = fork()) == -1)
{
perror("fork");
exit(1);
}
if(childpid == 0)
{
/* Потомок закрывает вход */
close(fd[0]);
} else
{
/* Родитель закрывает выход */
close(fd[1]); }
.
.
}
Как было упомянуто ранее, раз канал был установлен, то файловые дескрипторы могут обрабатываться подобно дескрипторам нормальных файлов.
Excerpt from "Linux Programmer's Guide - Chapter 6" (C)opyright 1994-1995, Scott Burkett
MODULE: pipe.c
#include
#include
#include int main(void)
{
int fd[2], nbytes;
pid_t childpid;
char string[] = "Hello, world!\n";
char readbuffer[80];
pipe(fd);
if((childpid = fork()) == -1)
{
perror("fork");
exit(1);
}
if(childpid == 0)
{
/* Потомок закрывает вход */
close(fd[0]);
/* Посылаем "string" через выход канала */
write(fd[1], string, strlen(string));
exit(0);
}
else
{
/* Родитель закрывает выход */
close(fd[1]);
/* Чтение строки из канала */
nbytes = read(fd[0], readbuffer, sizeof(readbuffer));
printf("Received string: %s", readbuffer);
} return(0);
}
Часто дескрипторы потомка раздваиваются на стандартный ввод или вывод. Потомок может затем exec() другую программу, которая наследует стандартные потоки. Давайте посмотрим на системный вызов dup():
SYSTEM CALL: dup();
PROTOTYPE: int dup( int oldfd );
RETURNS: new descriptor on success
-1 on error: errno = EBADF (oldfd некорректен)
EBADF ($newfd is out of range$)
EMFILE (слишком много дескрипторов для процесса)
NOTES: старый дескриптор не закрыт! Оба работают совместно!
Несмотря на то, что старый и новосозданный дескрипторы взаимозаменяемы, мы будем сначала закрывать один из стандартных потоков. Системный вызов dup() использует наименьший по номеру неиспользуемый дескриптор для нового.
Рассмотрим:
.
.
childpid = fork();
if(childpid == 0)
{
/* Закрываем стандартный ввод потомка */
close(0);
/* Дублируем вход канала на stdin */
dup(fd[0]);
execlp("sort", "sort", NULL);
.
}
Поскольку файловый дескриптор 0 (stdin) был закрыт, вызов dup() дублировал дескриптор ввода канала (fd0) на его стандартный ввод. Затем мы сделали вызов execlp(), чтобы покрыть код потомка кодом программы sort. Поскольку стандартные потоки exec()-нутой программы наследуются от родителей, это означает, что вход канала ста для потомка стандартным вводом! Теперь все, что первоначальный процесс-родитель посылает в канал, идет в sort.
Существует другой системный вызов, dup2(), который также может использоваться. Этот особенный вызов произошел с Version 7 of UNIX и был поддержан BSD, и теперь требуется по стандарту POSIX.
SYSTEM CALL: dup2();
PROTOTYPE: int dup2( int oldfd, int newfd );
RETURNS: новый дескриптор в случае успеха
-1 в случае ошибки: errno = EBADF (oldfd некорректен)
EBADF ($newfd is out of range$)
EMFILE (слишком много дескрипторов для процесса)
NOTES: старый дескриптор закрыл dup2()! Благодаря этому особенному вызову мы имеем закрытую операцию и действующую копию за один системный вызов. Вдобавок, он гарантированно неделим, что означает, что он никогда не будет прерван поступающим сигналом. С первым системным вызовом dup() программисты были вынуждены предварительно выполнять операцию close(). Это приводилок наличию двух системных вызовов с малой степенью защищенности в краткий промежуток времени между ними. Если бы сигнал поступил в течение этого интервала времени, копия дескриптора не состоялась бы. dup2() разрешает для нас эту проблему.
Рассмотрим:
.
.
childpid = fork();
if(childpid == 0)
{
/* Закрываем стандартный ввод, дублируем вход канала на стандартный ввод */
dup2(0, fd[0]);
execlp("sort", "sort", NULL);
.
.
}