在看 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

 #include <stdio.h>
#include <unistd.h> int main (void)
{
int i = ;
while (i++ < )
{
printf ("turn %d\n", i);
sleep ();
printf ("type any char to continue\n");
#ifdef HAS_READ
getchar ();
#endif
}
return ;
}

未打开 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 中加入以下代码来验证上面的说法:

     int tty = isatty (STDIN_FILENO);
printf ("stdin isatty ? %s\n", tty ? "true" : "false");
tty = isatty (STDOUT_FILENO);
printf ("stdout isatty ? %s\n", 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

 #! /bin/sh
for ((i=; i<; i=i+)) {
echo "turn $i"
ping www.glodon.com -c
#sleep
resp=$(read -p "type any char to continue")
}

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

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

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

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

[apue] 书中关于伪终端的一个纰漏的更多相关文章

  1. apue 第19章 伪终端

    伪终端是指对于一个应用程序而言,他看上去像一个终端,但事实上它并不是一个真正的终端. 进程打开伪终端设备,然后fork.子进程建立一个新的会话,打开一个相应的伪终端从设备.复制输入.输出和标准错误文件 ...

  2. APUE 书中 toll 函数

    今天看unix环境高级编程时,随着书上的源码打了一遍,编译时提示 toll函数未定义, 找了半天(恕我对上下文不了解).看了英文版和源代码文件才知道, 中文版打印错了: toll => atol ...

  3. Linux 的伪终端的基本原理 及其在远程登录(SSH,telnet等)中的应用

    本文介绍了linux中伪终端的创建,介绍了终端的回显.行缓存.控制字符等特性,并在此基础上解释和模拟了telnet.SSH开启远程会话的过程. 一.轻量级远程登录 之前制作的一块嵌入式板子,安装了嵌入 ...

  4. Unix环境高级编程(二十)伪终端

    1.综述 伪终端对于一个应用程序而言,看上去像一个终端,但事实上伪终端并不是一个真正的终端.从内核角度看,伪终端看起来像一个双向管道,而事实上Solaris的伪终端就是用STREAMS构建的.伪终端总 ...

  5. 如何自己编译apue.3e中代码 & 学习写makefile

    本来是搜pthread的相关资料,看blog发现很多linux程序员都看的一本神书<APUE>,里面有系统的两章内容专门讲pthread(不过是用c语言做的代码示例,这个不碍事,还是归到原 ...

  6. linux tty终端个 pts伪终端 telnetd伪终端

    转:http://blog.sina.com.cn/s/blog_735da7ae0102v2p7.html 终端tty.虚拟控制台.FrameBuffer的切换过程详解 Framebuffer Dr ...

  7. 关于apue.3e中apue.h的使用

    关于apue.3e中apue.h的使用 近来要学一遍APUE第三版,并于此开博做为记录. 先下载源文件: # url: http://http//www.apuebook.com/code3e.htm ...

  8. Egret入门学习日记 --- 第十篇(书中 2.9~2.13节 内容)

    第十篇(书中 2.9~2.13节 内容) 好的 2.9节 开始! 总结一下重点: 1.之前通过 ImageLoader 类加载图片的方式,改成了 RES.getResByUrl 的方式. 跟着做: 重 ...

  9. K&R《C语言》书中的一个Bug

    最近在重温K&R的C语言圣经,第二章中的练习题2-2引起了我的注意. 原题是: Write a loop equivalent to the for loop above without us ...

随机推荐

  1. 「UVA1185」Big Number 解题报告

    UVA1185 Big Number In many applications very large integers numbers are required. Some of these appl ...

  2. C#调用Fortran生成的DLL的方法报内存不足

    最近在研究一个程序,公司给的,程序是VB写的,程序里面还有一个计算的模型,用Fortran语言写的. 在调试到这个模型里面的方法时报错,说是内存不足,于是就在网上查找方法,看了两篇博客之后问题解决了. ...

  3. Linux 7.5 SSH服务和SFTP服务分离

    SFTP是SSH的一部分,SFTP没有单独的守护进程,它必须使用SSHD守护进程(端口号默认是22)来完成相应的连接操作,所以从某种意义上来说,SFTP并不像是一个服务器程序,而更像是一个客户端程序. ...

  4. php5.6.39 源码安装

    1 安装依赖库 yum install -y autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel li ...

  5. 双射 - hash去重

    题目描述Two undirected simple graphs and where are isomorphic when there exists a bijection on V satisfy ...

  6. DateTime.Now

    // 2008年4月24日 System.DateTime.Now.ToString( " D " );// 2008-4-24 System.DateTime.Now.ToStr ...

  7. Centos7.6部署k8s v1.16.4高可用集群(主备模式)

    一.部署环境 主机列表: 主机名 Centos版本 ip docker version flannel version Keepalived version 主机配置 备注 master01 7.6. ...

  8. Gitlab应用——系统管理

    ​ 查看linux系统信息 ​ 查看日志 ​ ​ 创建账号 ​ 选择regular,这是一个普通账号,点击“create user”账号创建完成 ​ 点击“User”,然后点击“New user”.使 ...

  9. C#图片采集软件 自动翻页 自动分类(收集美图必备工具)(一)

    网站管理员希望将别人的整站数据下载到自己的网站里或者将别人网站的一些内容保存到自己的服务器上.从内容中抽取相关的字段,发布到自己的网站系统中.有时需要将网页相关的文件也保存到本地,如图片.附件等. 图 ...

  10. 异数OS 织梦师-水桶(三)-- RAM共享存储方案

    . 异数OS 织梦师-水桶(三)– RAM共享存储方案 本文来自异数OS社区 github: https://github.com/yds086/HereticOS 异数OS社区QQ群: 652455 ...