C Контроль буферизации дочернего процесса - PullRequest
0 голосов
/ 14 апреля 2020

Моя цель - контролировать буферизацию дочернего процесса при выполнении с execvp.

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

print("test sdout recup")
print("test stderr recup", file=sys.stderr)

stdout буферизуется строкой, поэтому первая печать мгновенно сбрасывается (print добавляет по умолчанию новую строку в python), затем stderr небуферизован и, таким образом, напрямую запрашивается, что приводит к:

 test sdout recup
 test stderr recup

Когда я выполняю тот же скрипт с моим кодом C (см. код в конце), я получаю все время:

test stderr recup
test sdout recup

Поскольку stdout не является терминалом (это канал), stdout становится полностью буферизованным, в то время как stderr все еще не буферизован, что приводит к этому порядку.

Мне нужен способ управления этими режимами ( с C, а не с помощью другого процесса), чтобы сохранить тот же вывод терминала, а также позже, чтобы отменить буфер stdout (для другой цели), но я действительно не знаю, как это сделать. Я видел некоторый код, который работает с указателем файлов вместо файловых дескрипторов (FD), но я не могу найти те же функции для поля.

Кроме того, я даже не уверен, что это управляемо из родительского процесса. Вот и я.

Вот основной код: output.h:

#include <stddef.h>//size_t
typedef struct Output
{
  char* out;
  int status;
  double times;
} Output;


Output* Output_new();
/*Return an correctly initialized Ouput in regard to the buffer size*/
size_t read_append_into_Output( int fd, Output* out, size_t* current_size );
/*Append the result in the output buffer and manage size properly(actualize constructor with new default size, prevent overflow...*/

executor. c:

#include "executor.h"
#include "argv.h"//buildarg

#include <unistd.h>//fork
#include <stdio.h>//pipe
#include <stdlib.h>//EXIT_SUCCESS
#include <sys/wait.h>
#include <string.h> //strlen
#include <errno.h>//perror


#define READ 0
#define WRITE 1

void child_life(char** argv){
    /*Do child stuff*/
    // char* expected[] = {"test.py", "test", NULL};
    execvp(*argv, argv);
    perror("Process creation failed");


}

//TODO better control over when to send in pipe
void parent_life(int read_fd, int write_fd, char** prompt, size_t prompt_number, Output* output){
    //inject prompt
    for (int i=0; i<prompt_number; i++){
        write(write_fd, prompt[i], strlen(prompt[i]));//TODO dont call strlen and control ourself the size?
    }
    size_t readed=0;
    size_t max_read=0;
    while (max_read==readed){//we stop when we read less what we should or error
        max_read= read_append_into_Output(read_fd, output,&readed);
    }
    output->out[readed]=0;
}

Output* executor_get_output(char* command, char** prompt, size_t prompt_number, double timout)
{

    Output* output=Output_new();
    int pipe_father[2];
    int pipe_son[2];

    pipe(pipe_father);
    pipe(pipe_son);

    pid_t cpid;

    int argc;
    char** argv= buildargv(command,&argc); // We do it here because code betwen fork and exec is dangerous (must not contain malloc for exemple)

    cpid = fork();



    if (cpid == 0) {    /* Child reads from pipe */
        /*Listening on father pipe*/
        close(pipe_father[WRITE]);          /* Close unused write end */
        dup2(pipe_father[READ], STDIN_FILENO); /*Replace STDIN by our pipe*/


        /*Redirecting stdout and stder to the write pipe*/
        close(pipe_son[READ]);          
        dup2(pipe_son[WRITE], STDOUT_FILENO); /*Replace STDOUT by our pipe*/
        dup2(pipe_son[WRITE], STDERR_FILENO);

        child_life( argv);

        //EXIT (executed only if exevp failed)
        close(pipe_father[READ]);
        close(pipe_son[WRITE]);
        _exit(EXIT_FAILURE);

    } 
    //Parent code
    close(pipe_father[READ]);          /* Close unused read end */
    close(pipe_son[WRITE]);
    parent_life( pipe_son[READ], pipe_father[WRITE], prompt, prompt_number, output);

    //EXIT
    close(pipe_father[WRITE]);          /* Reader will see EOF */
    waitpid(cpid, NULL,0);                /* Wait for child terminaison*/

    close (pipe_son[READ]);
    return output;


}

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

git clone -b dev https://github.com/crazyhouse33/authbreak.git
cd authbreak/build
cmake ..
make executor

Эти команды создают двоичный файл в каталоге bin / tests.

связанный исходный код теста находится в tests / execute / executor. c

В ходе теста выполняется выполнение такого показанного python сценария и сравнивается полученный результат с моими уже представленными ожиданиями. Почему-то тестовый segfault запускался из make test (ctest), но теперь, когда вы запускаете его вручную.

Спасибо.

Ответы [ 3 ]

2 голосов
/ 15 апреля 2020

Моя цель состоит в том, чтобы контролировать буферизацию дочернего процесса при выполнении с помощью execvp

Таким образом, в основном вы хотите скопировать задание stdbuf.

Быстрый просмотр при coreutils stdbuf source показывает, что stdbuf просто устанавливает переменные окружения _STDBUF_I=$MODE _STDBUF_O=$MODE и _STDBUF_E=$MODE, а затем ld preloads libstdbuf.so библиотека . libstdbuf затем выполняется внутри дочернего процесса непосредственно перед его запуском - он считывает переменные окружения _STDBUF_I _STDBUF_O и _STDBUF_E и просто применяет правильный режим буферизации, полученный из переменных окружения внутри дочерний процесс.

Поскольку вы, похоже, хотите заново изобрести stdbuf, просто сделайте то же, что и он. (Или, поскольку ваш вопрос действительно кажется мне проблемой XY, просто используйте stdbuf. В оболочке обычно используется stdbuf -oL в каналах ...).

0 голосов
/ 25 апреля 2020

Если кто-то сталкивается с той же проблемой. Кажется, невозможно сделать из родительского кода все, что вы делаете. Я попытался открыть канал как поток (fdopen) и вызвать setvbuf на всех концах канала, на дочернем и родительском объектах. Это молча терпит неудачу.

Поэтому я предполагаю, что, что бы вы ни делали до выполнения execve, загрузчик снова устанавливает буферизацию stdin и stdout и стирает ваши настройки. Поэтому я создал модуль C, предназначенный для работы с envp и argv, чтобы использовать приемы переменных среды, такие как решение stdbuf, предложенное KamilCuk из C. Вот как вы можете использовать его подмножество для ответа на этот вопрос:

#include "argv.h"
#include <unistd.h>


void execvp_buffer_control( char* command, char** argv, char** buffering_mode){
//Like excevp but offer control over child buffering
char **stdbuf_trick_envp = build_stdbuf_exec_envp(buffering_mode);
execve(command, argv,get_envp_appended(stdbuf_trick_envp));// This return an envp vector equal to current envp concatenated with the good stuff to use stdbuff trick 
}
//Where buffering_mode is something as below:
char *mode[] = {DEFAULT_BUFFERING, LINE_BUFFERED, "65536"};# 65536 is the size of the fully buffered stderr stream buffer 

Теперь, если вы скомпилируете этот код и определите переменную LIBSTDBUF_PATH во время сборки к пути stdbuflib, он будет работать как и ожидалось. Если вы не установите его, он выдаст предупреждение во время компиляции, и ваш execvp_buffer_control будет действовать как базовый c execvp.

Я создал этот модуль с учетом гибкости. Он содержит функцию для объединения трюков envp, таких как stdbuf, с другими, которые также используют LD_PRELOAD, чтобы смешивать функции, даже если некоторые переменные envp сталкиваются. Я sh вижу, как люди запрашивают у меня свою собственную функцию, создавая соответствующий вектор envp, такой как build_stdbuf_exec_envp, чтобы этот модуль был действительно полезен для настройки поведения дочернего процесса комбинируемыми способами.

Все проверено , Я, вероятно, сделаю репозиторий для одного модуля, но сейчас он находится в первом опубликованном репозитории, источники находятся в src / common-stuff. Argv.h использует unistd.h для получения текущего envp. Но если вы замените его кроссплатформенной функцией для выполнения этой работы, он станет компилируемым на всех платформах.

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

0 голосов
/ 15 апреля 2020

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

Поскольку ваш ребенок выглядит как сценарий python Вы, вероятно, можете заставить его работать, установив переменную окружения PYTHONUNBUFFERED, но она задает c в python:

putenv("PYTHONUNBUFFERED=1");
...