Окончательный вывод на подчиненном pty теряется, если он не был закрыт в родительском. Зачем?

Я написал и поддерживаю программу rlwrap, которая использует псевдотерминал для связи с дочерним процессом. Псевдо-терминалы (ptys) встречаются во всех Unix-подобных системах, но на разных платформах ведут себя немного по-разному.

Показательный пример: В rlwrapродительский процесс сохраняет ведомый pty открытым, чтобы держать вкладки в настройках дочернего терминала (в Linux и FreeBSD для этого можно использовать мастер, но не в Solaris, например)

В FreeBSD (8.2) (но не в Linux) это приводит к потере конечного результата дочернего процесса. Например:

#include <stdio.h>

/* save as test.c and compile with gcc -o test test.c -lutil */

#define BUFSIZE 255

int main(void) {
  int master, slave;
  char buf[BUFSIZE];
  int nread;

  openpty(&master, &slave, NULL, NULL, NULL);

  if (fork()) {       /* parent:                                                      */
    close(slave);     /* leave this out and lose slave's final words ... WHY?         */
    do {
      nread = read(master, buf, BUFSIZE);
      write(STDOUT_FILENO, buf, nread); /* echo child's output to stdout              */
    } while (nread > 0);     
  } else {             /* child:                                                      */
    login_tty(slave);  /* this makes child a session leader and slave a controlling   */
                       /* terminal for it, then dup()s std{in,out,err} to slave       */ 
    printf("Feeling OK :-)\n");
    sleep(1);
    printf("Feeling unwell ... Arghhh!\n"); /* this line may get lost                 */
  }
  return 0;
}

Родительский процесс будет отображать вывод дочернего процесса, как и ожидалось, но когда я опускаю close(slave) (держать его открытым, как в rlwrap):

  • во FreeBSD родитель не видит последнюю строку вывода, вместо этого он читает EOF. (Во всяком случае, я бы ожидал обратного - если держать подчиненный конец открытым, это предотвратит потерю вывода)
  • В Linux, с другой стороны, EOF никогда не видны, даже после того, как ребенок умер (независимо от того, закрываем ли мы раба или нет)

Это поведение где-то задокументировано? Есть ли для этого обоснование? Могу ли я обойти это, не закрывая ведомое устройство в родительском процессе?

Я обнаружил, что не превращение подчиненного в управляющий терминал - замена login_tty позвоните с несколькими простыми dup() звонки - вылечит проблему. Это не решение для rlwrap однако: довольно многим командам нужен управляющий терминал (/dev/tty) поговорить, так rlwrap должен предоставить один для них.

5 ответов

Я думаю, что есть уникальное отдельное поведение для Pty.

  1. Система завершает работу, если последние данные записаны
  2. Система завершается, если ребенок выходит (сломанная труба?)

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

Это будет уникально для Pty и не будет существовать для реального терминала.

Я не уверен, правильно ли я понял: независимо от pty или нет, пока один процесс имеет открытый канал, ОС не должна передавать EOF читателю (потому что все еще есть писатель). (после разветвления есть два открытых канала), только если вы закрываете родительский канал, закрытие на ведомом должно пересылать EOF.

На PTY вы уверены, что NL обрабатываются правильно, так как обычно CR должен запускать новую строку.

(просто мысль: если это управляющий tty, все может измениться, так как ОС по-разному обрабатывает доставку сингла, и закрытие канала обычно завершает все дочерние процессы дочернего процесса. Может ли это быть проблемой, если у родителя по-прежнему открыт дескриптор?)

Вот что я нашел на Ubuntu Linux Примечание: всегда проверять на ошибки

#include <stdio.h>
#include <stdlib.h>
#include <pty.h>    // openpty(), 
#include <utmp.h>   // login_tty()
#include <unistd.h> // read(), write()

/* save as test.c and compile with gcc -o test test.c -lutil */

#define BUFSIZE (255)

int main(void) 
{
    int master, slave;
    char buf[BUFSIZE];
    int nread;
    pid_t pid;

    if( -1 == openpty(&master, &slave, NULL, NULL, NULL) )
    { // then openpty failed 
        perror( "openpty failed" );
        exit( EXIT_FAILURE );
    }

    // implied else, openpty successful

    pid = fork();
    if( -1 == pid ) 
    { // then fork failed
        perror( "fork failed" );
        exit( EXIT_FAILURE );
    }

    // implied else, fork successful

    if( pid ) 
    {    /* parent:                                                      */
        close(slave);     /* leave this out and lose slave's final words ... WHY?         */
        do 
        {
            if( -1 == (nread = read(master, buf, BUFSIZE) ) )
            {// then, error occurred
                perror( "read failed" );
                exit( EXIT_FAILURE );
            }

            // implied else, read successful

            if ( nread )
            {   
                write(STDOUT_FILENO, buf, nread); /* echo child's output to stdout  */
            }
        } while (nread);    /* nread == 0 indicates EOF */     
    } 

    else // pid == 0
    {    /* child:                                                      */
        if( -1 == login_tty(slave) )  /* this makes child a session leader and slave a controlling   */
                       /* terminal for it, then dup()s std{in,out,err} to slave       */ 
        { // then login_tty failed
            perror( "login_tty failed" );
            exit( EXIT_FAILURE );
        }

        // implied else, login_tty successful

        printf("Feeling OK :-)\n");
        sleep(1);
        printf("Feeling unwell ... Arghhh!\n"); /* this line may get lost */
    } // end if

    return 0;
} // end function: main

когда оператор close () закомментирован, родительский объект никогда не завершается из-за блокировки оператора read ()

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

здесь вывод, когда close () commentedpout

Feeling OK :-)
Feeling unwell ... Arghhh!

then the parent hangs on the read() statement

здесь вывод, когда close () не закомментирован

Feeling OK :-)
Feeling unwell ... Arghhh!
read failed: Input/output error

printf выполняет буферизованный вывод в зависимости от класса устройства вывода. Просто попробуйте поставить fflush(stdout); после последнего printf, чтобы увидеть, если это проблема с буферизованным выводом.

На FreeBSD 10-STABLE я получаю обе строки вывода.

(Вы можете заменить openpty а также fork с forkpty который в основном заботится о login_tty также.)

В FreeBSD 8.0 старый pty(4) водитель был заменен pts(4), Новый pty(4) ведет себя иначе, чем старый. Из руководства;

В отличие от предыдущих реализаций, узлы главного и подчиненного устройства разрушаются, когда PTY становится неиспользованным. Вызов stat(2) на несуществующем главном устройстве уже приведет к созданию нового узла главного устройства. Главное устройство может быть уничтожено только открыванием и закрыванием.

Вполне возможно, что произошли значительные изменения между 8.0-RELEASE и 10.0-RELEASE.

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

Другие вопросы по тегам