[apue] 書中關於僞終端的一個紕漏

在看 apue 第 19 章僞終端第 6 節使用 pty 程序時,發現「檢查長時間運行程序的輸出」這一部份內容的實際運行結果,與書上所說有出入。git

因而展開一番研究,最終發現是書上講的有問題,如今摘出來讓你們評評理。github

 

先上代碼shell

pty.c緩存

pty_fun.c安全

 

這是書上標準的 pty 程序,簡單提及來就是提供一個僞終端給被調用程序使用,例如bash

pty prog arg1 arg2

至關於在新的僞終端上執行測試

prog arg1 arg2

從而能夠避免一些直接執行 prog 帶來的問題。spa

 

19.6 節重點介紹使用 pty 程序的 6 種場景,其中第 3 種是檢查長時間運行程序的輸出,日誌

假設咱們有一個程序 slowout,它要執行很長時間,而輸出又稀稀拉拉,經過code

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\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.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\n", tty ? "true" : "false"); 
3     tty = isatty (STDOUT_FILENO); 
4     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

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 有讀取的狀況。

相關文章
相關標籤/搜索