[apue] 书中关于伪终端的一个纰漏
在看 apue 第 19 章伪终端第 6 节使用 pty 程序时,发现“检查长时间运行程序的输出”这一部分内容的实际运行结果,与书上所说有出入。
于是展开一番研究,最终发现是书上讲的有问题,现在摘出来让大家评评理。
先上代码
这是书上标准的 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 子进程卡死,这两个进程都没有机会退出。
#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 脚本来实现,正如我一开始做的那样。
#! /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] 书中关于伪终端的一个纰漏的更多相关文章
- apue 第19章 伪终端
伪终端是指对于一个应用程序而言,他看上去像一个终端,但事实上它并不是一个真正的终端. 进程打开伪终端设备,然后fork.子进程建立一个新的会话,打开一个相应的伪终端从设备.复制输入.输出和标准错误文件 ...
- APUE 书中 toll 函数
今天看unix环境高级编程时,随着书上的源码打了一遍,编译时提示 toll函数未定义, 找了半天(恕我对上下文不了解).看了英文版和源代码文件才知道, 中文版打印错了: toll => atol ...
- Linux 的伪终端的基本原理 及其在远程登录(SSH,telnet等)中的应用
本文介绍了linux中伪终端的创建,介绍了终端的回显.行缓存.控制字符等特性,并在此基础上解释和模拟了telnet.SSH开启远程会话的过程. 一.轻量级远程登录 之前制作的一块嵌入式板子,安装了嵌入 ...
- Unix环境高级编程(二十)伪终端
1.综述 伪终端对于一个应用程序而言,看上去像一个终端,但事实上伪终端并不是一个真正的终端.从内核角度看,伪终端看起来像一个双向管道,而事实上Solaris的伪终端就是用STREAMS构建的.伪终端总 ...
- 如何自己编译apue.3e中代码 & 学习写makefile
本来是搜pthread的相关资料,看blog发现很多linux程序员都看的一本神书<APUE>,里面有系统的两章内容专门讲pthread(不过是用c语言做的代码示例,这个不碍事,还是归到原 ...
- linux tty终端个 pts伪终端 telnetd伪终端
转:http://blog.sina.com.cn/s/blog_735da7ae0102v2p7.html 终端tty.虚拟控制台.FrameBuffer的切换过程详解 Framebuffer Dr ...
- 关于apue.3e中apue.h的使用
关于apue.3e中apue.h的使用 近来要学一遍APUE第三版,并于此开博做为记录. 先下载源文件: # url: http://http//www.apuebook.com/code3e.htm ...
- Egret入门学习日记 --- 第十篇(书中 2.9~2.13节 内容)
第十篇(书中 2.9~2.13节 内容) 好的 2.9节 开始! 总结一下重点: 1.之前通过 ImageLoader 类加载图片的方式,改成了 RES.getResByUrl 的方式. 跟着做: 重 ...
- K&R《C语言》书中的一个Bug
最近在重温K&R的C语言圣经,第二章中的练习题2-2引起了我的注意. 原题是: Write a loop equivalent to the for loop above without us ...
随机推荐
- JVM系列(三):java的垃圾回收机制
java垃圾回收机制介绍 上一篇讲述了JVM的内存模型,了解了到了绝大部分的对象是分配在堆上面的,我们在编码的时候并没有显示的指明哪些对象需要回收,但是程序在运行的过程中是会一直创建对象的,之所 ...
- jsp 知识点总结
JSP 入门案例(计算器的实现) 对于jsp 学习的一些总结 1.使用Dreamweaver 搭建网页 计算器 第一个数字 符号 + - * / 第二个数字 <form id="for ...
- 03_input type="number" 输入允许小数点后两位
<input type="number" min="0" max="100" step="0.01"/> & ...
- 从N个元素中抽取K个不重复元素(抽奖问题)
核心就是 把N数组抽中的元素给K数组 把N数组最后一位给N数组被抽走的那一位(这时候N数组最后一位元素和被抽走的那位元素值相等) 把N数组长度减一,去除最后一位
- JVM之GC(二)
昨天总结了GC之前要做的事情,今天介绍一下主流的GC算法. 先介绍一下几个名词: Stop The World(STW):JVM进行GC的时候总不能一边清理垃圾一边制造垃圾把,那么垃圾鉴定的准确性根本 ...
- 致Java初学者
致Java初学者 精心整理资料点击获取 前言 能看到这篇文章的朋友,应该都或多或少的了解Java,也许你现在是个菜鸟还在成长的路上.再此期间你一定遇到了很多困惑疑虑,对未来的学习方向感到很迷惑.作 ...
- mongo repository
using MongoDB.Driver; namespace Dben.Invoice.Repository { /// <summary> /// 仓储基类 /// </summ ...
- Java配置文件读取中文乱码问题
背景 这是我之前在做的用友服务对接开发,昨天领导拿给财务测试时告诉我有乱码,当时我第一想法是用友那边的编码格式有问题,因为还在做其他任务,我说等问一下用友他们用的什么编码格式我们这边改一下,然后今天早 ...
- Logback源码分析
在日常开发中经常通过打印日志记录程序执行的步骤或者排查问题,如下代码类似很多,但是,它是如何执行的呢? package chapters; import org.slf4j.Logger; impor ...
- python认识及环境变量
什么是python? python是一种脚本语言,是高级语言.计算机只能识别机器语言,在机器语言上是汇编语言,再往上是高级语言.高级语言的基础是C语言. python语言较为简单,易入门. pytho ...