时间:2020-02-14来源:系统城作者:电脑系统城
在看 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 子进程卡死,这两个进程都没有机会退出。
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\n", i);
10 sleep (1);
11 printf ("type any char to continue\n");
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.logturn 1type any char to continueturn 2type any char to continueturn 3type any char to continueturn 4type any char to continueturn 5type any char to continueturn 6type any char to continueturn 7type any char to continueturn 8type any char to continueturn 9type any char to continueturn 10type 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 COMMAND7650 1 7648 10887 7651 500 500 yunhai S pts/1 ./pty -i ./slowout7649 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.logturn 1type 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\n", tty ? "true" : "false");
3 tty = isatty (STDOUT_FILENO);
4 printf ("stdout isatty ? %s\n", tty ? "true" : "false");
重新编译后输出如下:
stdin isatty ? truestdout isatty ? true |
如果是重定向到 /dev/null 或文件后,isatty 绝对不可能返回 true,所以可以确定之前的说法是没问题的。
这样一来,当 slowout 尝试读取时,将从伪终端从设备读取,而这个并不会返回 eof,而是期待 pty 父进程将终端输入导向这里。但是 pty 父进程早就因为读取 /dev/null 得到 EOF 而退出了,只不过临退出前因为指定了 -i 参数,没有将 pty 子进程一并结束罢了。
所以这样就形成了堵塞的局面,而且这个应该是无解的。
其实 slowout 也可以通过 shell 脚本来实现,正如我一开始做的那样。
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 有读取的情况。
2024-07-18
Centos 7 二进制安装配置 MariaDB数据库2024-07-18
Centos7默认firewalld防火墙使用命令大全2024-07-07
四种执行python系统命令的方法常用权限linux系统内有档案有三种身份 u:拥有者 g:群组 o:其他人这些身份对于文档常用的有下面权限:r:读权限,用户可以读取文档的内容,如用cat,more查看w:写权限,用户可以编辑文档x...
2024-07-07
然而,如果我们遵循通常的 WordPress 最佳实践,这些安全问题可以避免。在本篇中,我们会向你展示如何使用 WPSeku,一个 Linux 中的 WordPress 漏洞扫描器,它可以被用来找出你安装...
2024-07-03