LINUX.ORG.RU

IPC с дочерним процессом по pipes. Как читать больше PIPE_BUF за один запрос чтения?

 , ,


1

3

Допустим перед нами задача по ipc взимодействовать с одним единственным дочерним процессом. Взаимодействие в стиле запрос-родителя-ответ-ребенка, не более.

Но ребенок может вообще ничего не ответить или ответить слишком много, намного превышающим лимит PIPE_BUF (man 7 pipe), поэтому выходом будет процесс чтения защитить таймаутом. Не блокирующее или асинхронное чтение для решения данной проблемы не нужно - достаточно select-а.

Самый простой вариант видится таким:

// k_systemDependetLimitation == PIPE_BUF
struct timeval tv;
tv.tv_sec = k_ipcWaitDataDelay;
tv.tv_usec = 0;

std::string ret;
int returnCode;

while (true) {
    if ((returnCode = select(m_istance(client).m_readFromChildProccess[0] + 1,
        &m_istance(client).m_readFromChildWait, nullptr, nullptr, &tv)) > 0) {

        char rawBuffer[k_bufferLength] = {0};
        ssize_t readLegth;
        if ((readLegth = read(m_istance(client).m_readFromChildProccess[0], rawBuffer, k_systemDependetLimitation)) > 0) {
            ret.append(rawBuffer, rawBuffer + readLegth);
            if (readLegth == k_systemDependetLimitation) {
                continue;
            }
            break;
        } else {
            releaseIpcAndswitchToErrorState(client);
            break;
        }
    } else if (!returnCode) {
        FD_ZERO(&m_istance(client).m_readFromChildWait); // reinit for select (see man select)
        FD_SET(m_istance(client).m_readFromChildProccess[0], &m_istance(client).m_readFromChildWait);
        break;
    } else {
        releaseIpcAndswitchToErrorState(client);
        break;
    }
}

По скольку чтение блокирующее мы не можем быть уверены что после readLegth == k_systemDependetLimitation что-то есть или чего-то нет

поэтому тут неизбежно нужно запрашивать select, если что-то есть select сразу же вернёт управление и read опять начнёт читать, если ничего нет - повисит за зря (а что делать?) и вернет управления по таймайту else if (!returnCode).

Если прочли меньше, значит конец чтения.

Но я хотел бы читать больше чем 4096 за раз. Мотивация: не хочу лишние переключения контекста. И вообще хотел бы например использовать select только изначально, и дальше с помощью каких-то техник определять стоит ли запускать read еще или нет, но похоже это не возможно?

Дело в том что читать >4096 сложно. Даже при условии что пишет в канал только один процесс и даже с гарантией что он пишет ровно тогда когда буффер пайпа полностью свободен (родитель все ранее записанное, от прошлого запроса, прочитал).

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

Это можно попытаться отловить:

Например:

//k_bufferLength == 9000
...
        if ((readLegth = read(m_istance(client).m_readFromChildProccess[0], rawBuffer, k_bufferLength)) > 0) {
            ret.append(rawBuffer, rawBuffer + readLegth);
            if (readLegth == k_bufferLength) {
                // здесь мы по прежнему ничего не можем сказать о том что больше нечего читать
                continue;
            }
            // если чтение прервано не на границе PIPE_BUF то оно конечно
            if (readLegth % k_systemDependetLimitation) { 
                break;
            }
        } else {
            releaseIpcAndswitchToErrorState(client);
            break;
        }
    }
...

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

что может быть прочтено только:

или ==9000 (т.е. 2 полных атомарных цикла записи в пайп и один не атомарный)

или <9000, но не кратный границе 4096 тогда типа явно всё прочитано, потому что типа ядро не прервёт такое, потому что граница атомарности (судя по размеру прочитанного уже пройдена - а значит то что не лежит на такой границе - ядро не прерывает)

или кратный_атомарному - значит тут есть возможность того что ядро первало.

Но такие выводы, оказались не верны, крайне редко но бывает:

что сначала прочитали 9000, потом вместо например 4132 (что бывает в большинстве случаев и тогда ок)

мы прочитали скажем 3800 - и все - тогда код прерывает чтение, а по факту еще осталось читать (4132-3800 байт).

И так есть ли какой-то красивый способ без дополнтельного вызова select пусть и с минимальными миллисекунндыми таймаутами - определять что ядро все передало что пишет дочерний.

Или таких способов нет, и едиственным способом (чтобы передавать много с минимальным дроблением на итерации (ну т.е. читать не по 4096 а по много)) был бы в начале передчи передавать размер передаваемого данного (чтобы размер укладывался в первые байты передачи) или что еще лучше - просто ввести маркер начала передачи и конца, и пока в принятой последовательности нет маркера конца - читать ещё.

но вот фишка в том что то что отправлят дочерний я менять не могу и там таких маркеров нет :)

★★★★★

  1. Ты путаешься в терминологии

  2. скорее всего вместо пайпов тебе нужен больцевой буфер в шаренной памяти, или UNIX-сокеты. Пайпы вне шелла не имеют смысла.

cvv ★★★★★
()

просто ввести маркер начала передачи и конца, и пока в принятой последовательности нет маркера конца - читать ещё

Это ответ на твой вопрос. Точнее, хватить одного маркера конца сообщения.

А пытаться делить поток данных на «сообщения» путем управления потоком данных (open, close, read, wrire, select, poll, …) с последующим разбором кодов возврата - это гадание на кофейной гуще, не надо так делать, ничего полезного ты не узнаешь о «сообщении».

anonymous
()

И так есть ли какой-то красивый способ без дополнтельного вызова select пусть и с минимальными миллисекунндыми таймаутами - определять что ядро все передало что пишет дочерний.

без дополнтельного вызова select

Почему тебя вообще это волнует?

Для IPC на пайпах слишком много текста.

Не блокирующее или асинхронное чтение для решения данной проблемы не нужно - достаточно select-а

што

vasily_pupkin ★★★★★
()
Ответ на: комментарий от cvv

Т.е. можно как-то после fork+execve(другой бинарник) заменить у дочернего его стандартные вход и выход не на пайп а на «кольцевой буффер»?

Про такое я не слышал. Для таких целей я знаю что есть каналы и механизмы подменой этими каналами стандартных потоков ввода и вывода, что в Linux что в Windows.

Ну и да дочерний процесс это такое cli интерфейса приложение которое читает stdin и выплёвывает в stdout.

bonta ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Ну вот я к сожалению не могу никак влиять на дочерний процесс он какой он есть :)

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

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

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

bonta ★★★★★
() автор топика
Последнее исправление: bonta (всего исправлений: 1)
Ответ на: комментарий от vasily_pupkin

што

я о том что чтобы не зависнуть на read нужно или предварительно защищаться select-ом, или настроить читаемый конец канала на неблокирующий или асинхронный режим - но оба эти методики не подходят, остаётся только select.

Для IPC на пайпах слишком много текста.

Если брать экзамплы то там еще больше текста, если есть что-то малотекстовое и способное читать >4096 за раз, покажите пожалуйста.

Почему тебя вообще это волнует?

Сисколы и переключение контекста же, хочется чтобы работало быстро даже если эта быстрота чисто теоритическая, к тому же если она чисто теоритическая - все это можно проверить на практике, и вот на 1000 запусках на мегабайтовых файлах, где на каждое слово из файла может прийти от 80 до намного больше чем 4096 по пайпу - просто уверен что лишний не заход в select сыграл бы очень хорошо на быстроте.

bonta ★★★★★
() автор топика
Последнее исправление: bonta (всего исправлений: 1)
Ответ на: комментарий от bonta

хочется чтобы работало быстро

просто уверен что лишний не заход в select сыграл бы очень хорошо на быстроте.

https://pics.me.me/premature-optimization-come-on-do-it-do-it-now-it-42084265.png

Eсли брать экзамплы то там еще больше текста, если есть что-то малотекстовое и способное читать >4096 за раз, покажите пожалуйста.

Ты акцентируешь внимание на вещах, которые не имеют никакого значения.

Если хочешь жить без селекта, и с блокирующим выводом, но с таймаутами, можешь использовать alarm.

vasily_pupkin ★★★★★
()
Ответ на: комментарий от bonta

Почему тебя вообще это волнует?

Сисколы и переключение контекста же, хочется чтобы работало быстро даже если эта быстрота чисто теоритическая, к тому же если она чисто теоритическая - все это можно проверить на практике, и вот на 1000 запусках на мегабайтовых файлах, где на каждое слово из файла может прийти от 80 до намного больше чем 4096 по пайпу - просто уверен что лишний не заход в select сыграл бы очень хорошо на быстроте.

Уход от селекта при использовании пайпов это как экономия на спичках

cvv ★★★★★
()
Последнее исправление: cvv (всего исправлений: 1)
Ответ на: комментарий от cvv

скорее всего вместо пайпов тебе нужен больцевой буфер в шаренной памяти, или UNIX-сокеты. Пайпы вне шелла не имеют смысла.

+1

Уход от селекта при использовании пайпов это как экономия на спичках

+1

Deleted
()
Ответ на: комментарий от bonta

чтобы не зависнуть на read нужно или предварительно защищаться select-ом

Вообще говоря, это не 100% гарантия.

или настроить читаемый конец канала на неблокирующий … режим

Это уже ближе к делу. Чтобы гарантированно не зависнуть на read, необходимо использовать неблокирующий ввод/вывод на дескрипторе.

но оба эти методики не подходят

Вообще кроме этого ничего нового особо и не придумано, и всех устраивает.

остаётся только select

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

Sorcerer ★★★★★
()
Ответ на: комментарий от deep-purple

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

bonta ★★★★★
() автор топика
Ответ на: комментарий от Sorcerer

Да сокеты я что-то не знаю как привязать к stdin/stdout дочернего процесса.

А не блокирующее не нравится тем что грузит процессор.

В общем еще раз покурил маны, чтож поделать, придется читать по <=4096, ибо тогда никаких проблем. Вот такое вот ограничение на Линуксе (и всех ЮниксПодобных вроде) в 2019м году.

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

Потом читать асинхронно. Но вместо селекта там вэйтфорсинглобджект для асинхронок и цпу не грузится.

В общем вопрос можно считать решенным.

bonta ★★★★★
() автор топика
Последнее исправление: bonta (всего исправлений: 1)
Ответ на: комментарий от bonta

В Винде вроде бы нет таких ограничений,

Есть там всё.

но зато там и нет select-а для пайпов

И это тоже есть.

LamerOk ★★★★★
()
Ответ на: комментарий от bonta

В общем еще раз покурил маны

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

В Винде вроде бы нет таких ограничений

Винда это сплошные ограничения, иначе о Юниксах бы очень давно забыли.

cvv ★★★★★
()
Ответ на: комментарий от LamerOk

select там только для сокетов

bonta ★★★★★
() автор топика
Ответ на: комментарий от cvv

путаешься в терминологии.

а где именно? что-то понять не могу.

пайп=канал, атомарная передача это до <= PIPE_BUF.

Ну и 3-и режима чтения существует: блокирующий, не блокирующий и асинхронный.

Что я там выше напутал не могу понять :)

bonta ★★★★★
() автор топика

Как выше советовали, можешь посмотреть на сокеты. В 99% дочернему процессу все равно, что за дескрипторы для него открывает родительский процесс: пайпы, файлы или сокеты.

Создаешь свою пару сокетов и дексриптор одного из концов передаешь дочернему процессу вместо пайпа.

Так, например, сейчас делается в Node.js (даже не смотря на то, что в документации в api написано pipe), а уж эти ребята знают толк в оптимальной ассинхронной работе. Можешь покурить их исходники и вообще посмотреть на либу libuv.

anonymous
()
Ответ на: комментарий от anonymous

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

anonymous
()
Ответ на: комментарий от anonymous

Пока что ни у кого не получалось выжать из пайпов больше чем из юникс-сокетов.

cvv ★★★★★
()
Ответ на: комментарий от anonymous

В 99% дочернему процессу все равно, что за дескрипторы для него открывает родительский процесс: пайпы, файлы или сокеты.

Программировать сокеты сильно проще, чем пайпы.

cvv ★★★★★
()
Ответ на: комментарий от cvv

ну может быть, но фишка в том что пока есть возможность велосипедить и наслаждаться сложностями в своё удовольствие, почему бы и не делать это )

Всяко интереснее чем какую-нить готовую либу заюзать как где-то выше порекомендовали :)

bonta ★★★★★
() автор топика
Ответ на: комментарий от bonta

Ты итак используешь готовую либу по-сути. В чём проблема? Хочешь руками - реализовывай на базе шмемы.

К тому же, даже если ты не хочешь реализовывать синхронизацию на базе шмема - просто используй её для передачи данных.

Схема такая. Используешь плебейское ipc для синхронизации, отправляешь только управляющие конструкции. Т.е. перед тем как отправить читателю «читай» ты пишешь в шмем. Далее отправляешь читателя читать. Он читает. Далее отправляет тебе «прочитал».

Ты получаешь атомарность+зерокопи для какого угодно объёма данных. Так же, если у тебя возникнут проблемы с трупутом данной схемы - ты можешь заменить его на что-то вроде кольцевого буфера/очереди.

Т.е. посылаешь читателю два офсета(начало + конец). Таким образом ты там можешь аллоцировать сколько угодно памяти. Т.е. это позволит тебе отправлять новые данные без подтверждений от читателя.

Потом(когда нибудь) к тебе придут подтверждения чтения и ты сможешь снести хвост и писать уже в него.

К тому же, ты можешь сколько угодно долго улучшать эту схему. Ты можешь и отключать память в тех зонах, которые уже прочитаны. Много чего можно.

anonymous
()
Ответ на: комментарий от anonymous

Звучит интресно, когда-нибудь попробую, а пока оставлю как есть, тк вроде заработало, а эксперименты потом попробую :)

bonta ★★★★★
() автор топика

И зачем тебе select? select разумно использовать, если у тебя таймауты порядка наносекунд, или нужна портируемость - select есть практически везде.

В остальных случаях разумно использовать poll. В качестве примера:

ssize_t
read_to (int fd, void *buf, size_t size, int ms_to) {
    struct pollfd pfd = { .fd = fd, .events = POLLIN };
    size_t bytesread = 0;

    while (poll (&pfd, 1, ms_to) == POLLIN) {
        ssize_t chunksize = read (fd, buf + bytesread, size);
        if (chunksize == -1)
            return -1;

        bytesread += chunksize;
        size -= chunksize;

        if (size == 0)
            return bytesread;

        pfd.revents = 0;
    }

    return -1; // timeout
}
id_thx1138
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.