zoukankan      html  css  js  c++  java
  • [apue] 书中关于伪终端的一个纰漏

    在看 apue 第 19 章伪终端第 6 节使用 pty 程序时,发现“检查长时间运行程序的输出”这一部分内容的实际运行结果,与书上所说有出入。

    于是展开一番研究,最终发现是书上讲的有问题,现在摘出来让大家评评理。

    先上代码

    pty.c

    pty_fun.c

    这是书上标准的 pty 程序,简单说起来就是提供一个伪终端给被调用程序使用,例如

    pty prog arg1 arg2
    

    相当于在新的伪终端上执行

    prog arg1 arg2
    

    从而可以避免一些直接执行 prog 带来的问题。

    19.6 节重点介绍使用 pty 程序的 6 种场景,其中第 3 种是检查长时间运行程序的输出,

    假设我们有一个程序 slowout,它要执行很长时间,而输出又稀稀拉拉,通过

    slowout > out.log & 
    

    执行,同时

    tail -f out.log 
    

    查看的话,因为输出到文件会被缓存,导致不能及时看到 slowout 的输出,甚至只有等 slowout 退出后,才能看到一点儿输出。

    为了解决这个问题,引入 pty 程序

    pty slowout > out.log &
    

    此时通过 tail 命令查看日志文件就会比较及时,这是因为 pty 提供的伪终端是行缓存的,slowout 输出一行就会被写入文件。

    事情这样就完美了?非也,作者提出了一个场景,当 slowout 有可能读取 stdin 的时候,因为它本身在后台执行,

    一旦妄图读取终端上的输入,就会被系统自动挂起(SIGHUP),从而停止运行,这是作者不想看到的,于是他提出了一种解决方案,

    即将标准输入重定向到 /dev/null,同时开启 pty 的 -i 选项:

    pty -i slowout < /dev/null > out.log &
    

    认为这样可以一劳永逸的解决问题。

    先来看一下 pty 程序的运行态结构,再来看 -i 选项的作用,最后我们分析一下为什么这样做行不通。

    运行时的 pty 首先通过 fork+exec 产生 slowout 子进程,其中标准输入、输出分别重定向到中间的伪终端从设备(pty slave device),

    然后它自身又通过 fork 一分为二,pty 父进程负责读取标准输入,将内容导入到伪终端主设备(pty main device),也就是 slowout 的输入;

    pty 子进程负责从伪终端主设备(pty main device) 读取数据,也就是 slowout 的输出,并将内容导出到标准输出。

    那么 pty 父子进程怎么退出呢? 当 slowout 结束时,子进程读伪终端主设备时返回 0,它知道工作进程结束后,也即将结束自己的工作,

    但是父进程一直卡在读终端输入上,并不知道工作进程已经退出,于是 pty 子进程向父进程发送一个 SIGTERM 信号,由父进程捕获该信号后安全退出。

    同理,当 pty 父进程检查到 stdin 上无更多输入后,会向 pty 子进程发送 SIGTERM 信号(前提是子进程未发送相同信号),从而终结子进程的等待 。

    作者认为问题出现在 pty 父进程向 pty 子进程发送的这个 SIGTERM 信号上,因为重定向到 /dev/null 后,pty 父进程会从 stdin 读到 EOF,

    从而向 pty 子进程发送 SIGTERM,导致子进程没有继续读 slowout 的输出就结束了。所以他为 pty 程序加了一个 -i 选项,如果该选项生效,

    就在父进程读 stdin 失败后,不再向子进程发送 SIGTERM 信号,从而允许 pty 子进程读 slowout 的输出直到 slowout 结束。

    这个想法很丰满,但是现实很骨感。

    我测试的结果是,如果  slowout 不从标准输入读取的话,则一切正常;

    而一旦有任何读取动作,都会导致  slowout 卡死,进而 pty 子进程卡死,这两个进程都没有机会退出。

    slowout.c

     1 #include <stdio.h>
     2 #include <unistd.h>
     3 
     4 int main (void)
     5 {
     6     int i = 0; 
     7     while (i++ < 10)
     8     {
     9         printf ("turn %d
    ", i); 
    10         sleep (1); 
    11         printf ("type any char to continue
    "); 
    12 #ifdef HAS_READ
    13         getchar (); 
    14 #endif
    15     }
    16     return 0; 
    17 }

    未打开 HAS_READ 开关时,输出正常:

    >./pty -i ./slowout < /dev/null > out.log & 
    [1] 7616
    >cat out.log
    turn 1
    type any char to continue
    turn 2
    type any char to continue
    turn 3
    type any char to continue
    turn 4
    type any char to continue
    turn 5
    type any char to continue
    turn 6
    type any char to continue
    turn 7
    type any char to continue
    turn 8
    type any char to continue
    turn 9
    type any char to continue
    turn 10
    type any char to continue
    [1]+  Done                    ./pty -i ./slowout < /dev/null > out.log
    >
    

    打开 HAS_READ 开关后,发现进程卡死:

      PID  PPID  PGID   SID TPGID  SUID  EUID USER     STAT TT       COMMAND
     7650     1  7648 10887  7651   500   500 yunhai   S    pts/1    ./pty -i ./slowout
     7649     1  7649  7649  7649   500   500 yunhai   Ss+  pts/3    ./slowout
    

    可以通过 ps 命令观察到卡死的进程,7650 为 pty 子进程,7649 为 slowout 子进程,7648 为 pty 父进程已退出。

    通过 pstack 命令可以观察到 slowout 进程堵塞在 getchar 上:

    >pstack 7649
    #0  0x009c6424 in __kernel_vsyscall ()
    #1  0x00751c53 in __read_nocancel () from /lib/libc.so.6
    #2  0x006eb41b in _IO_new_file_underflow () from /lib/libc.so.6
    #3  0x006ed13b in _IO_default_uflow_internal () from /lib/libc.so.6
    #4  0x006ee74a in __uflow () from /lib/libc.so.6
    #5  0x006e7d7c in getchar () from /lib/libc.so.6
    #6  0x080485a1 in main ()
    

    查看输出,果然卡死在第一次 getchar 上:

    >cat out.log
    turn 1
    type any char to continue
    

    为什么会这样呢? 我们首先要清楚,重定向到 /dev/null 指的是 pty 父进程,并不是 slowout,因为 slowout 重定向到伪终端是固定的,不随外面的重定向操作而改变;同理,输出重定向到 out.log 指的是 pty 子进程,也不是 slowout。其实所有的重定向操作在 pty 程序运行起来时就已经完成了,根本无法传递到 slowout 的参数上(即使传递到了也不生效,因为没有 shell 做解析)。

    我们可以通过在 slowout 中加入以下代码来验证上面的说法:

    1     int tty = isatty (STDIN_FILENO); 
    2     printf ("stdin isatty ? %s
    ", tty ? "true" : "false"); 
    3     tty = isatty (STDOUT_FILENO); 
    4     printf ("stdout isatty ? %s
    ", tty ? "true" : "false"); 

    重新编译后输出如下:

    stdin isatty ? true
    stdout isatty ? true
    

     如果是重定向到 /dev/null 或文件后,isatty 绝对不可能返回 true,所以可以确定之前的说法是没问题的。

    这样一来,当 slowout 尝试读取时,将从伪终端从设备读取,而这个并不会返回 eof,而是期待 pty 父进程将终端输入导向这里。但是 pty 父进程早就因为读取 /dev/null 得到 EOF 而退出了,只不过临退出前因为指定了 -i 参数,没有将 pty 子进程一并结束罢了。

    所以这样就形成了堵塞的局面,而且这个应该是无解的。

    其实 slowout 也可以通过 shell 脚本来实现,正如我一开始做的那样。

    slowout.sh

    1 #! /bin/sh
    2 for ((i=0; i<10; i=i+1)) {
    3     echo "turn $i"
    4     ping www.glodon.com -c 4
    5     #sleep 4
    6     resp=$(read -p "type any char to continue")
    7 }

    如果使用 slowout.sh 作为工作进程,启动命令也需要改变一下:

    >./pty -i bash -c ./slowout.sh > out.log < /dev/null &
    

    结果是一样的 (我一开始还以为是 bash 从中进行了影响)。

    最终的结论就是:pty 程序并不适用于 slowout 有读取的情况。

  • 相关阅读:
    线性回归学习历程
    CART决策树的学习历程
    markdown测试
    开张大吉+代码测试
    使用tomcat启动dubbo项目
    ThreadLocal 工作原理、部分源码分析
    Dubbo项目demo搭建
    redis 操作 list 的测试
    redis 操作 hash 的测试
    redis 操作string 的测试
  • 原文地址:https://www.cnblogs.com/goodcitizen/p/a_mistake_in_apue_about_terminal.html
Copyright © 2011-2022 走看看