Трубопровод как межпроцессная коммуникация

StackOverflow https://stackoverflow.com/questions/363221

  •  21-08-2019
  •  | 
  •  

Вопрос

Я заинтересован в написании отдельных программных модулей, которые выполняются как независимые потоки, которые я мог бы соединить с каналами.Мотивацией было бы то, что я мог бы писать и тестировать каждый модуль совершенно независимо, возможно, даже писать их на разных языках или запускать разные модули на разных машинах.Здесь существует большое разнообразие возможностей.Я некоторое время пользовался трубопроводом, но я незнаком с нюансами его поведения.

  • Похоже, что принимающая сторона заблокирует ожидание ввода, чего я бы ожидал, но будет ли отправляющая сторона иногда блокировать ожидание, пока кто-то прочитает из потока?
  • Если я запишу eof в поток, могу ли я продолжать запись в этот поток, пока не закрою его?
  • Существуют ли различия в поведении именованных и неназванных каналов?
  • Имеет ли значение, какой конец канала я открываю первым с помощью именованных каналов?
  • Согласовано ли поведение каналов между различными системами Linux?
  • Зависит ли поведение каналов от используемой мной оболочки или от того, как я ее настроил?
  • Есть ли какие-либо другие вопросы, которые я должен задать, или проблемы, о которых я должен знать, если я хочу использовать каналы таким образом?
Это было полезно?

Решение

Ух ты, сколько вопросов.Давайте посмотрим, смогу ли я охватить все...

Похоже, что принимающая сторона будет блокировать ожидание ввода, чего я бы ожидал

Вы правильно ожидаете, что фактический вызов 'read' будет блокироваться до тех пор, пока что-то не появится.Тем не менее, я полагаю, что есть некоторые функции C, которые позволят вам "заглянуть" в то, что (и сколько) ожидает в канале.К сожалению, я не помню, блокируется ли это также.

будет ли иногда блокироваться конец отправки ожидание, пока кто-нибудь прочитает из потока

Нет, отправка никогда не должна блокироваться.Подумайте о последствиях, если бы это был канал через сеть к другому компьютеру.Хотели бы вы подождать (возможно, с большой задержкой), пока другой компьютер ответит, что он его получил?Теперь это другой случай, если дескриптор считывателя назначения был закрыт.В этом случае у вас должна быть некоторая проверка на ошибки, чтобы справиться с этим.

Если я запишу eof в поток, смогу ли я продолжать запись в этот поток пока я его не закрою

Я бы подумал, что это зависит от того, какой язык вы используете, и от его реализации каналов.На языке Си я бы сказал "нет".В оболочке Linux я бы сказал "да".На этот вопрос должен был бы ответить кто-то другой, более опытный.

Существуют ли различия в поведении именованные и неназванные каналы?Насколько я знаю, да.Тем не менее, у меня нет большого опыта работы с named и unnamed.Я считаю, что разница в том, что:

  • Однонаправленная связь против двунаправленной связи
  • Чтение И запись в потоки "in" и "out" потока

Не все ли равно, какой конец трубы я открыть с помощью именованных каналов?

Как правило, нет, но вы можете столкнуться с проблемами при инициализации, пытаясь создать и связать потоки друг с другом.Вам нужно было бы иметь один основной поток, который создает все подпотоки и синхронизирует их соответствующие каналы друг с другом.

Согласовано ли поведение каналов между различными системами Linux?

Опять же, это зависит от того, на каком языке, но в целом да.Вы когда-нибудь слышали о POSIX?Это стандарт (по крайней мере, для linux, Windows делает это по-своему).

Зависит ли поведение каналов от оболочки, которую я использую, или от способа, которым я ее настроил?

Это переходит в еще более серую область.Ответ следует быть no, поскольку оболочка, по сути, должна выполнять системные вызовы.Тем не менее, все, что было сделано до этого момента, можно использовать.

Есть ли еще какие-либо вопросы, которые я должен задать

Вопросы, которые вы задали, показывают, что вы неплохо разбираетесь в системе.Продолжайте исследовать и сосредоточьтесь на том, над каким уровнем вы собираетесь работать (shell, C и так далее).Однако, просто попробовав это, вы узнаете гораздо больше.

Другие советы

Все это основано на UNIX-подобной системе;Я не знаком со специфическим поведением последних версий Windows.

Похоже, что принимающая сторона заблокирует ожидание ввода, чего я бы ожидал, но будет ли отправляющая сторона иногда блокировать ожидание, пока кто-то прочитает из потока?

Да, хотя на современной машине это может происходить не часто.Канал имеет промежуточный буфер, который может потенциально заполняется.Если это произойдет, сторона записи канала действительно заблокируется.Но если вдуматься, то существует не так уж много файлов, достаточно больших, чтобы рисковать подобным образом.

Если я запишу eof в поток, могу ли я продолжать запись в этот поток, пока не закрою его?

Эм, ты имеешь в виду что-то вроде CTRL-D, 0x04?Конечно, до тех пор, пока поток настроен таким образом.А именно.

506 # cat | od -c
abc
^D
efg
0000000    a   b   c  \n 004  \n   e   f   g  \n                        
0000012

Существуют ли различия в поведении именованных и неназванных каналов?

Да, но они тонкие и зависят от реализации.Самый большой из них заключается в том, что вы можете выполнить запись в именованный канал до того, как будет запущен другой конец;при использовании неназванных каналов файловые дескрипторы становятся общими во время процесса fork / exec, поэтому нет способа получить доступ к временному буферу без запуска процессов.

Имеет ли значение, какой конец канала я открываю первым с помощью именованных каналов?

Нет.

Согласовано ли поведение каналов в разных системах Linux?

В разумных пределах, да.Размеры буфера и т.д. Могут варьироваться.

Зависит ли поведение каналов от используемой мной оболочки или от того, как я ее настроил?

Нет.Когда вы создаете канал, под прикрытием происходит то, что ваш родительский процесс (оболочка) создает канал, который имеет пару файловых дескрипторов, затем выполняет fork exec, подобный этому псевдокоду:

Родитель:

create pipe, returning two file descriptors, call them fd[0] and fd[1]
fork write-side process
fork read-side process

Сторона записи:

close fd[0]
connect fd[1] to stdout
exec writer program

Сторона чтения:

close fd[1]
connect fd[0] to stdin
exec reader program

Есть ли какие-либо другие вопросы, которые я должен задать, или проблемы, о которых я должен знать, если я хочу использовать каналы таким образом?

Действительно ли все, что вы хотите сделать, будет выложено в виде такой строки?Если нет, возможно, вы захотите подумать о более общей архитектуре.Но понимание того, что желательно иметь множество отдельных процессов, взаимодействующих через "узкий" интерфейс канала, является хорошим.

[Обновлено:Сначала я поменял индексы файловых дескрипторов местами.Теперь они верны, понимаете man 2 pipe.]

Как отметили Дашогун и Чарли Мартин, это большой вопрос.Некоторые части их ответов неточны, поэтому я тоже собираюсь ответить.

Я заинтересован в написании отдельных программных модулей, которые выполняются как независимые потоки, которые я мог бы соединить с каналами.

Будьте осторожны при попытке использовать каналы в качестве механизма связи между потоками одного процесса.Поскольку у вас были бы открыты оба конца канала для чтения и записи в одном процессе, вы бы никогда не получили указание EOF (нулевые байты).

Если вы действительно имели в виду процессы, то это основа классического подхода Unix к созданию инструментов.Многие стандартные программы Unix представляют собой фильтры, которые считывают данные из стандартного ввода, каким-то образом преобразуют его и записывают результат в стандартный вывод.Например, tr, sort, grep, и cat это все фильтры, и это лишь некоторые из них.Это отличная парадигма, которой следует следовать, когда данные, которыми вы манипулируете, позволяют это.Не все манипуляции с данными способствуют такому подходу, но есть много таких, которые способствуют.

Мотивацией было бы то, что я мог бы писать и тестировать каждый модуль совершенно независимо, возможно, даже писать их на разных языках или запускать разные модули на разных машинах.

Хорошие моменты.Имейте в виду, что на самом деле не существует конвейерного механизма между машинами, хотя вы можете приблизиться к нему с помощью таких программ, как rsh или (лучше) ssh.Однако внутренне такие программы могут считывать локальные данные из каналов и отправлять эти данные на удаленные машины, но они обмениваются данными между машинами через сокеты, не используя каналы.

Здесь существует большое разнообразие возможностей.Я некоторое время пользовался трубопроводом, но я незнаком с нюансами его поведения.

ОК;задавать вопросы - это один (хороший) способ учиться.Другое дело, конечно, экспериментировать.

Похоже, что принимающая сторона заблокирует ожидание ввода, чего я бы ожидал, но будет ли отправляющая сторона иногда блокировать ожидание, пока кто-то прочитает из потока?

ДА.Существует ограничение на размер буфера канала.Классически это было довольно мало - 4096 или 5120 были общими значениями.Вы можете обнаружить, что современный Linux использует большее значение.Вы можете использовать fpathconf() и _PC_PIPE_BUF, чтобы узнать размер буфера канала.POSIX требует, чтобы буфер был равен только 512 (то есть _POSIX_PIPE_BUF равен 512).

Если я запишу eof в поток, могу ли я продолжать запись в этот поток, пока не закрою его?

Технически, нет никакого способа записать EOF в поток;вы закрываете дескриптор канала, чтобы указать EOF.Если вы думаете о control-D или control-Z как о символе EOF, то это просто обычные символы в том, что касается каналов - они имеют эффект, подобный EOF, только при вводе на терминале, который работает в каноническом режиме (приготовленный или обычный).

Существуют ли различия в поведении именованных и неназванных каналов?

И да, и нет.Самые большие различия заключаются в том, что безымянные каналы должны быть настроены одним процессом и могут использоваться только этим процессом и дочерними элементами, которые используют этот процесс как общего предка.Напротив, именованные каналы могут использоваться ранее неассоциированными процессами.Следующее большое различие является следствием первого;используя неназванный канал, вы получаете обратно два файловых дескриптора из одного вызова функции (системной) в pipe(), но вы открываете FIFO или именованный канал , используя обычный open() функция.(Кто-то должен создать FIFO с mkfifo() позвоните, прежде чем сможете его открыть;безымянные каналы не нуждаются в такой предварительной настройке.) Однако, как только у вас открыт файловый дескриптор, разница между именованным каналом и безымянным каналом практически невелика.

Имеет ли значение, какой конец канала я открываю первым с помощью именованных каналов?

Нет.Первый процесс, открывший FIFO, будет (обычно) блокироваться до тех пор, пока не появится процесс с открытым другим концом.Если вы откроете его для чтения и записи (традиционно, но возможно), то вы не будете заблокированы;если вы используете флаг O_NONBLOCK, вы не будете заблокированы.

Согласовано ли поведение каналов между различными системами Linux?

ДА.Я не слышал и не испытывал никаких проблем с каналами ни в одной из систем, где я их использовал.

Зависит ли поведение каналов от используемой мной оболочки или от того, как я ее настроил?

НЕТ:каналы и FIFOS не зависят от используемой вами оболочки.

Есть ли какие-либо другие вопросы, которые я должен задать, или проблемы, о которых я должен знать, если я хочу использовать каналы таким образом?

Просто помните, что вы должны закрыть считывающий конец канала в процессе, который будет выполнять запись, и записывающий конец канала в процессе, который будет выполнять чтение.Если вам нужна двунаправленная связь по каналам, используйте два отдельных канала.Если вы создаете сложные сантехнические устройства, остерегайтесь тупиковых ситуаций - это возможно.Однако линейный конвейер не приводит к взаимоблокировке (хотя, если первый процесс никогда не закрывает свой вывод, последующие процессы могут ждать бесконечно).


Я заметил как выше, так и в комментариях к другим ответам, что буферы каналов классически ограничены довольно небольшими размерами.@Charlie Martin откомментировал, что некоторые версии Unix имеют динамические буферы канала, и они могут быть довольно большими.

Я не уверен, какие из них он имеет в виду.Я использовал приведенную ниже тестовую программу для Solaris, AIX, HP-UX, macOS X, Linux и Cygwin / Windows XP (результаты приведены ниже).:

#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

static const char *arg0;

static void err_syserr(char *str)
{
    int errnum = errno;
    fprintf(stderr, "%s: %s - (%d) %s\n", arg0, str, errnum, strerror(errnum));
    exit(1);
}

int main(int argc, char **argv)
{
    int pd[2];
    pid_t kid;
    size_t i = 0;
    char buffer[2] = "a";
    int flags;

    arg0 = argv[0];

    if (pipe(pd) != 0)
        err_syserr("pipe() failed");
    if ((kid = fork()) < 0)
        err_syserr("fork() failed");
    else if (kid == 0)
    {
        close(pd[1]);
        pause();
    }
    /* else */
    close(pd[0]);
    if (fcntl(pd[1], F_GETFL, &flags) == -1)
        err_syserr("fcntl(F_GETFL) failed");
    flags |= O_NONBLOCK;
    if (fcntl(pd[1], F_SETFL, &flags) == -1)
        err_syserr("fcntl(F_SETFL) failed");
    while (write(pd[1], buffer, sizeof(buffer)-1) == sizeof(buffer)-1)
    {
        putchar('.');
        if (++i % 50 ==  0)
            printf("%u\n", (unsigned)i);
    }
    if (i % 50 !=  0)
        printf("%u\n", (unsigned)i);
    kill(kid, SIGINT);
    return 0;
}

Мне было бы любопытно получить дополнительные результаты с других платформ.Вот размеры, которые я нашел.Должен признаться, все результаты больше, чем я ожидал, но мы с Чарли, возможно, спорим о значении слова "довольно большой", когда речь заходит о размерах буфера.

  •   8196 - HP-UX 11.23 для IA-64 (сбой fcntl(F_SETFL))
  • 16384 - Солярис 10
  • 16384 - macOS X 10.5 (O_NONBLOCK не сработал, хотя fcntl (F_SETFL) не вышел из строя)
  • 32768 - AIX 5.3
  • 65536 - Cygwin / Windows XP (O_NONBLOCK не сработал, хотя fcntl (F_SETFL) не вышел из строя)
  • 65536 - SuSE Linux 10 (и CentOS) (сбой fcntl (F_SETFL))

Один момент, который ясен из этих тестов, заключается в том, что O_NONBLOCK работает с каналами на одних платформах и не работает на других.

Программа создает канал и разветвляется.Дочерний элемент закрывает конец канала записи, а затем переходит в спящий режим до тех пор, пока не получит сигнал - это то, что делает pause().Затем родительский элемент закрывает конец канала чтения и устанавливает флаги для дескриптора записи, чтобы он не блокировался при попытке записи в полный канал.Затем он выполняет цикл, записывая по одному символу за раз и печатая точку для каждого написанного символа, а также количество и перевод строки через каждые 50 символов.Когда он обнаруживает проблему с записью (буфер заполнен, поскольку дочерний элемент ничего не читает), он останавливает цикл, записывает окончательный подсчет и убивает дочерний элемент.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top