2016-01-05 3 views
9

Я делаю небольшое приложение ncurses в Rust, которое должно связываться с дочерним процессом. У меня уже есть прототип, написанный в Common Lisp; gif here, надеюсь, покажет, что я хочу сделать. Я пытаюсь переписать его, потому что CL использует огромный объем памяти для такого маленького инструмента.Как читать выходные данные дочернего процесса без блокировки в Rust?

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

То, что я сейчас делаю примерно так:

  1. Создать процесс:

    let mut program = match Command::new(command) 
        .args(arguments) 
        .stdin(Stdio::piped()) 
        .stdout(Stdio::piped()) 
        .stderr(Stdio::piped()) 
        .spawn() { 
         Ok(child) => child, 
         Err(_) => { 
          println!("Cannot run program '{}'.", command); 
          return; 
         }, 
        }; 
    
  2. передать его бесконечным (до выходов пользователя) цикла, который считывает и обрабатывает входные и ожидает выхода, как это (и записывает его на экран):

    fn listen_for_output(program: &mut Child, 
            output_viewer: &TextViewer) { 
        match program.stdout { 
         Some(ref mut out) => { 
          let mut buf_string = String::new(); 
          match out.read_to_string(&mut buf_string) { 
           Ok(_) => output_viewer.append_string(buf_string), 
           Err(_) => return, 
          }; 
         }, 
         None => return, 
        }; 
    } 
    

Однако вызов read_to_string блокирует программу, пока процесс не завершится. Из того, что я вижу, read_to_end и read также, похоже, блокируются. Если я попытаюсь запустить что-то вроде ls, которое сразу же выходит, оно работает, но с чем-то, что не выходит, как python или sbcl, оно продолжается только после того, как я убью подпроцесс вручную.

Edit:

на основе this answer, я изменил код, чтобы использовать BufReader:

fn listen_for_output(program: &mut Child, 
        output_viewer: &TextViewer) { 
    match program.stdout.as_mut() { 
     Some(out) => { 
      let buf_reader = BufReader::new(out); 
      for line in buf_reader.lines() { 
       match line { 
        Ok(l) => { 
         output_viewer.append_string(l); 
        }, 
        Err(_) => return, 
       }; 
      } 
     }, 
     None => return, 
    } 
} 

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

ответ

11

Потоки блокировка по умолчанию. Потоки TCP/IP, потоки файловой системы, потоки потоков, все они блокируются. Когда вы укажете потоку, чтобы дать вам кусок байтов, он остановится и дождитесь, пока он не получит заданное количество байтов, или пока что-то еще не произойдет (, конец потока, ошибка).

Операционные системы стремятся вернуть данные в процесс чтения, поэтому, если вы хотите только подождать следующей строки и обработать ее, как только она появится, то метод, предложенный Shepmaster в Unable to pipe to or from spawned child process more than once, работает. (Теоретически это не обязательно, потому что операционной системе разрешено делать BufReader больше данных в read, но на практике операционные системы предпочитают ранние «короткие чтения» для ожидания).

Этот простой подход BufReader прекращает работу, когда вам нужно обрабатывать несколько потоков (например, stdout и stderr дочернего процесса) или несколько процессов. Например, подход, основанный на BufReader, может зайти в тупик, когда дочерний процесс ждет вас, чтобы вытереть его трубку stderr, пока ваш процесс заблокирован, ожидая его пустого stdout.

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

Вы не можете использовать подход, основанный на использовании BufReader, если ваша операционная система не хочет возвращать данные в процесс (предпочитает «читать полный текст» до «коротких чтений»), поскольку в этом случае выводится несколько последних строк дочерний процесс может оказаться в серой зоне: операционная система получила их, но они недостаточно велики, чтобы заполнить буфер BufReader.

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

Возможно, вам интересно, почему чтение данных в кусках так важно здесь, почему не может BufReader просто прочитать байт данных байтом. Проблема в том, что для чтения данных из потока нам нужна помощь операционной системы. С другой стороны, мы не операционная система, мы работаем изолированно от нее, чтобы не путаться с ней, если что-то пошло не так с нашим процессом. Поэтому для вызова операционной системы должен быть переход в «режим ядра», который также может иметь «контекстный переключатель». Вот почему вызов операционной системы для чтения каждого байта стоит дорого. Нам нужно как можно меньше OS-вызовов, и поэтому мы получаем потоковые данные в пакетах.

Чтобы дождаться потока без блокировки, вам понадобится неблокирующий поток. MIO promises to have the required non-blocking stream support for pipes, скорее всего, с PipeReader, но я пока не проверил его.

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

В отсутствии неблокирующего потока вам придется прибегать к нерестовым потокам, чтобы блокирующие чтения выполнялись в отдельном потоке и, таким образом, не блокировали бы ваш основной поток. Вы также можете прочитать байты потока по байтам, чтобы немедленно реагировать на разделитель строк, если операционная система не предпочитает «короткие чтения». Вот рабочий пример: https://gist.github.com/ArtemGr/db40ae04b431a95f2b78.

+0

Спасибо за полезное объяснение. Я посмотрю MIO, и если это не сработает, я буду использовать отдельные потоки. – jkiiski

Смежные вопросы