在看 apue 第 19 章偽終端第 6 節使用 pty 程式時,發現“檢查長時間運行程式的輸出”這一部分內容的實際運行結果,與書上所說有出入。 於是展開一番研究,最終發現是書上講的有問題,現在摘出來讓大家評評理。 先上代碼 pty.c pty_fun.c 這是書上標準的 pty 程式,簡單說起來就 ...
在看 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.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 腳本來實現,正如我一開始做的那樣。
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 有讀取的情況。