如果未在父级中关闭,则从属pty的最终输出将丢失。为什么?

时间:2022-07-16 21:03:40

I wrote and maintain a program rlwrap that uses a pseudo-terminal to communicate with a child process. Pseudo-terminals (ptys) are found in all Unix(-like) systems, but they behave slightly differently on different platforms.

我编写并维护了一个程序rlwrap,它使用伪终端与子进程通信。在所有Unix(类似)系统中都可以找到伪终端(ptys),但它们在不同平台上的表现略有不同。

Case in point: In rlwrap, the parent process keeps the slave pty open to keep tabs on the child's terminal settings (on Linux and FreeBSD one can use the master for that, but not in Solaris, for example)

例证:在rlwrap中,父进程保持slave pty打开以密切关注子进程的终端设置(在Linux和FreeBSD上,可以使用master,例如在Solaris中)

On FreeBSD (8.2) (but not Linux) this leads to the loss of the child's final output. For example:

在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;
}

The parent process will echo the child's output, as expected, but when I omit the close(slave) (keeping it open like in rlwrap):

父进程将按预期回显子进程的输出,但是当我省略close(slave)时(保持它在rlwrap中打开):

  • on FreeBSD, the parent doesn't see the final output line, it reads an EOF instead. (If anything, I would have expected the opposite - that keeping the slave end open would prevent the output from being lost)
  • 在FreeBSD上,父节点没有看到最终输出行,而是读取了EOF。 (如果有的话,我本来期望相反的 - 保持从端打开会阻止输出丢失)
  • On Linux, on the other hand, an EOF is never seen, not even after the child has died (whether we close the slave or not)
  • 另一方面,在Linux上,EOF从未见过,即使在孩子死亡之后也是如此(无论我们是否关闭了奴隶)

Is this behaviour documented somewhere? Is there a rationale for it? Can I circumvent it without closing the slave in the parent process?

这种行为是否记录在某处?有没有理由呢?我可以在不关闭父进程中的奴隶的情况下绕过它吗?

I found out that not making the slave a controlling terminal - replacing the login_tty call with a few simple dup() calls - will cure the problem. This is no solution for rlwrap however: quite a few commands need a controlling terminal (/dev/tty) to talk to, so rlwrap has to provide one for them.

我发现不让奴隶成为控制终端 - 用一些简单的dup()调用替换login_tty调用 - 将解决问题。然而,这不是rlwrap的解决方案:相当多的命令需要一个控制终端(/ dev / tty)来与之通信,因此rlwrap必须为它们提供一个。

5 个解决方案

#1


1  

I think there is unique separate behaviour for the Pty.

我认为Pty有独特的独立行为。

  1. The system terminates if the last data is written
  2. 如果写入最后一个数据,系统将终止
  3. The system terminates if the child exits (broken pipe?)
  4. 如果孩子退出(断管?),系统会终止

The code relies on the pipe existing long enough to send the data through, but the child exiting may cause the virtual channel to be deleted before the data is received.

代码依赖于存在的管道足够长以发送数据,但子项退出可能导致在接收数据之前删除虚拟通道。

This would be unique to Pty, and not exist for real terminal.

这对于Pty来说是独一无二的,对于真正的终端来说并不存在。

#2


0  

On FreeBSD 10-STABLE I do get both output lines.

在FreeBSD 10-STABLE上,我确实得到了两条输出线。

(You can replace openpty and fork with forkpty which basically takes care of login_tty as well.)

(您可以使用forkpty替换openpty和fork,它基本上也会处理login_tty。)

In FreeBSD 8.0, the old pty(4) driver was replaced by pts(4). The new pty(4) behaves differently from the old one. From the manual;

在FreeBSD 8.0中,旧的pty(4)驱动程序被pts(4)取代。新的pty(4)与旧的pty(4)表现不同。从手册;

Unlike previous implementations, the master and slave device nodes are destroyed when the PTY becomes unused. A call to stat(2) on a nonexistent master device will already cause a new master device node to be created. The master device can only be destroyed by opening and closing it.

与先前的实现不同,当PTY未使用时,主设备节点和从设备节点被销毁。在不存在的主设备上调用stat(2)将导致创建新的主设备节点。主设备只能通过打开和关闭来销毁。

There might well have been significant changes between 8.0-RELEASE and 10.0-RELEASE

8.0-RELEASE和10.0-RELEASE之间可能发生了重大变化

You also might want to look at the patch that is applied in the FreeBSD ports tree.

您还可以查看FreeBSD ports树中应用的补丁。

#3


0  

printf does buffered output depending of the class of output device. Just try to put fflush(stdout); after the last printf to see if it's a problem with buffered output.

printf根据输出设备的类别进行缓冲输出。试着把fflush(stdout);在最后一次printf之后看看它是否是缓冲输出的问题。

#4


0  

here is what I found on ubuntu linux Note: always check for errors

这是我在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

when the close() statement is commented out then the parent never exits due to the read() statement blocking

当close()语句被注释掉时,父节点永远不会因read()语句阻塞而退出

when the close() statement is part of the source then the parent exits with a read error from trying to read from a terminal that is 'missing' when the child exits

当close()语句是源的一部分时,父进程出现读取错误,尝试从子进程中“丢失”的终端读取

here is the output when close() commentedpout

这是close()commentedpout时的输出

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

then the parent hangs on the read() statement

here is the output when close() is not commented out

这是没有注释掉close()时的输出

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

#5


0  

I'm not sure if I get this right: independent from pty or not, as long as one process has the channel open, the OS should not pass an EOF to the reader (because there is still a writer). (after the fork there are two open channels) only if you close the parent, a close on the slave should forward the EOF.

我不确定我是否做到了这一点:独立于pty与否,只要一个进程打开了通道,操作系统就不应该将EOF传递给读者(因为还有一个编写者)。 (在叉子之后有两个打开的通道)只有当你关闭父级时,奴隶的关闭才能转发EOF。

On a PTY, are you sure that NL's are handled correctly, since normally a CR should trigger the new-line.

在PTY上,你确定NL的处理是正确的,因为CR通常会触发新行。

(just a thought: if it is a controling tty, things might change since the OS handles singal deliveries differently and closing the chanel would normaly terminate all children processes of the child. Could this be an issue if the parent still has the handle open? )

(只是一个想法:如果它是一个控制tty,事情可能会改变,因为操作系统以不同方式处理单一交付,关闭chanel将正常终止孩子的所有子进程。如果父母仍然处理打开,这可能是一个问题吗? )

#1


1  

I think there is unique separate behaviour for the Pty.

我认为Pty有独特的独立行为。

  1. The system terminates if the last data is written
  2. 如果写入最后一个数据,系统将终止
  3. The system terminates if the child exits (broken pipe?)
  4. 如果孩子退出(断管?),系统会终止

The code relies on the pipe existing long enough to send the data through, but the child exiting may cause the virtual channel to be deleted before the data is received.

代码依赖于存在的管道足够长以发送数据,但子项退出可能导致在接收数据之前删除虚拟通道。

This would be unique to Pty, and not exist for real terminal.

这对于Pty来说是独一无二的,对于真正的终端来说并不存在。

#2


0  

On FreeBSD 10-STABLE I do get both output lines.

在FreeBSD 10-STABLE上,我确实得到了两条输出线。

(You can replace openpty and fork with forkpty which basically takes care of login_tty as well.)

(您可以使用forkpty替换openpty和fork,它基本上也会处理login_tty。)

In FreeBSD 8.0, the old pty(4) driver was replaced by pts(4). The new pty(4) behaves differently from the old one. From the manual;

在FreeBSD 8.0中,旧的pty(4)驱动程序被pts(4)取代。新的pty(4)与旧的pty(4)表现不同。从手册;

Unlike previous implementations, the master and slave device nodes are destroyed when the PTY becomes unused. A call to stat(2) on a nonexistent master device will already cause a new master device node to be created. The master device can only be destroyed by opening and closing it.

与先前的实现不同,当PTY未使用时,主设备节点和从设备节点被销毁。在不存在的主设备上调用stat(2)将导致创建新的主设备节点。主设备只能通过打开和关闭来销毁。

There might well have been significant changes between 8.0-RELEASE and 10.0-RELEASE

8.0-RELEASE和10.0-RELEASE之间可能发生了重大变化

You also might want to look at the patch that is applied in the FreeBSD ports tree.

您还可以查看FreeBSD ports树中应用的补丁。

#3


0  

printf does buffered output depending of the class of output device. Just try to put fflush(stdout); after the last printf to see if it's a problem with buffered output.

printf根据输出设备的类别进行缓冲输出。试着把fflush(stdout);在最后一次printf之后看看它是否是缓冲输出的问题。

#4


0  

here is what I found on ubuntu linux Note: always check for errors

这是我在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

when the close() statement is commented out then the parent never exits due to the read() statement blocking

当close()语句被注释掉时,父节点永远不会因read()语句阻塞而退出

when the close() statement is part of the source then the parent exits with a read error from trying to read from a terminal that is 'missing' when the child exits

当close()语句是源的一部分时,父进程出现读取错误,尝试从子进程中“丢失”的终端读取

here is the output when close() commentedpout

这是close()commentedpout时的输出

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

then the parent hangs on the read() statement

here is the output when close() is not commented out

这是没有注释掉close()时的输出

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

#5


0  

I'm not sure if I get this right: independent from pty or not, as long as one process has the channel open, the OS should not pass an EOF to the reader (because there is still a writer). (after the fork there are two open channels) only if you close the parent, a close on the slave should forward the EOF.

我不确定我是否做到了这一点:独立于pty与否,只要一个进程打开了通道,操作系统就不应该将EOF传递给读者(因为还有一个编写者)。 (在叉子之后有两个打开的通道)只有当你关闭父级时,奴隶的关闭才能转发EOF。

On a PTY, are you sure that NL's are handled correctly, since normally a CR should trigger the new-line.

在PTY上,你确定NL的处理是正确的,因为CR通常会触发新行。

(just a thought: if it is a controling tty, things might change since the OS handles singal deliveries differently and closing the chanel would normaly terminate all children processes of the child. Could this be an issue if the parent still has the handle open? )

(只是一个想法:如果它是一个控制tty,事情可能会改变,因为操作系统以不同方式处理单一交付,关闭chanel将正常终止孩子的所有子进程。如果父母仍然处理打开,这可能是一个问题吗? )