apue伪终端描述中,为何忽略其与进程间通信的关系?
摘要:在看 apue 第 19 章伪终端第 6 节使用 pty 程序时,发现“检查长时间运行程序的输出”这一部分内容的实际运行结果,与书上所说有出入。 于是展开一番研究,最终发现是书上讲的有问题,现在摘出来让大家评评理。 先上代码 pty.c p
在看 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 子进程卡死,这两个进程都没有机会退出。
