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

来源:https://www.cnblogs.com/goodcitizen/archive/2020/02/14/12307343.html
-Advertisement-
Play Games

在看 apue 第 19 章偽終端第 6 節使用 pty 程式時,發現“檢查長時間運行程式的輸出”這一部分內容的實際運行結果,與書上所說有出入。 於是展開一番研究,最終發現是書上講的有問題,現在摘出來讓大家評評理。 先上代碼 pty.c pty_fun.c 這是書上標準的 pty 程式,簡單說起來就 ...


在看 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 子進程卡死,這兩個進程都沒有機會退出。

 

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 對於服務端,達到高性能、高擴展離不開非同步。對於客戶端,函數執行時間是1毫秒還是100毫秒差別不大,沒必要為這一點點時間煞費苦心。對於非同步,好多人還有誤解,如: 非同步就是多線程;非同步就是如何利用好線程池。非同步不是這麼簡單,否則微軟沒必要在非同步上花費這麼多心思。本文就介紹非同步最新的實現方式:Tas ...
  • 在工作中,會遇到需要多線程處理相應的業務需求,最典型的包括Socket的通信。 多線程處理里,就會考慮到,哪個線程先運行,哪個線程後運行的情況。 這裡我介紹一下,使用ManualResetEvent類來對線程進行阻塞和繼續操作。 它有三個重要的方法:Reset、Set和WaitOne。 1、首先介紹 ...
  • RedHat7安裝NetCore環境併發布網站 1.註冊Microsoft簽名密鑰並添加Microsoft產品提要,每台機器只需註冊一次 執行下麵的命令即可 rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsof ...
  • 學習劉鐵猛老師《C#語言入門詳解》視頻,針對其中重點知識點進行總結。 1、什麼是類型? 類型又稱為數據類型(Data Type),數據類型在數據結構中的定義是一個值的集合以及定義在這個值集上的一組操作。 可以簡單理解為數據在記憶體中存儲的“型號”;小記憶體容納大尺寸數據會丟失精準度,發生錯誤;而大記憶體容 ...
  • 在WPF用戶界面中,繪製2D圖形內容的最簡單方法是使用形狀(shape)——專門用於表示簡單的直線、橢圓、矩形以及多變形的一些類。從技術角度看,形狀就是所謂的繪圖圖元(primitive)。可組合這些基本元素來創建更複雜的圖形。 關於WPF中形狀的重要細節是,它們都繼承自FrameworkEleme ...
  • Blend 修改TreeViewItem樣式 1、用Blend for Visual Studio 2019 新建Wpf項目,拖動一個TreeView控制項到Grid上 2、在繪圖視窗選中TreeViewItem,右鍵編輯模版 編輯副本 3、繪製水平、垂直虛線( "參考博文" ) 在TreeViewI ...
  • ASPNetCore 發佈到IIS 準備工作 1.1. 安裝IIS。(具體操作不再說明) 安裝成功後再瀏覽器輸入localhost得到的頁面如下 1.2. 安裝dotnet-hosting-2.2.2-win.exe安裝成功後在IIS 中可以看到如下兩個程式 這兩個程式對應得NetCore的版本不一 ...
  • 1.Ctrl+s:快速保存代碼 一定要記得隨時隨地用 Ctrl+s 來保存我們的代碼哦!!!不然等到電腦關機或者是使用的Eclipse突然閃退就欲哭無淚了。此時腦海裡就突然出現了嗶嗶嗶的畫面~ 2.Alt+/:自動補全代碼或者提示代碼後半部分 牆裂推薦大家使用啊,真的是超級好用了。 給大家舉一個例子 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...