如何在 shell 腳本中使用使用者提示字元處理 SIGINT 陷阱?

如何在 shell 腳本中使用使用者提示字元處理 SIGINT 陷阱?

我試著以這樣的方式處理SIGINT( CtrlC),如果使用者不小心按下 ctrl-c,系統會提示他一則訊息:「你想退出嗎?(y/n)」。如果他輸入 yes,則退出腳本。如果不是,則從中斷發生的地方繼續。基本上,我需要CtrlC以類似於SIGTSTPCtrlZ)的方式工作,但方式略有不同。我嘗試了各種方法來實現這一目標,但沒有得到預期的結果。以下是我嘗試過的幾個場景。

情況1

腳本:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
} 
trap 'stop' SIGINT 
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

當我運行上面的腳本時,我得到了預期的結果。

輸出:

$ play.sh 
going to sleep
1
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)n
3
4
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!! 
$  

案例:2

我將for循環移至新腳本loop.sh,從而play.sh成為父進程和loop.sh子進程。

腳本:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
}
trap 'stop' SIGINT 
loop.sh

腳本:loop.sh

#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

這種情況下的輸出不符合預期。

輸出:

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
3
4
^C
Do you wish to stop playing?(y/n)n
$

我知道當一個進程收到SIGINT訊號時,它會將訊號傳播到所有子進程,因此我的第二種情況失敗了。有什麼方法可以避免SIGINT傳播到子進程,使loop.sh工作與第一種情況完全相同?

筆記: 這只是我實際應用的一個例子。我正在開發的應用程式在play.sh和中有幾個子腳本loop.sh。我應該確保接收時的應用程式SIGINT不應該終止,但它應該提示用戶一條訊息。

答案1

關於管理作業和訊號的經典問題,並提供了很好的例子!我開發了一個精簡的測試腳本來專注於訊號處理的機制。

為此,在後台啟動子進程 (loop.sh) 後,調用wait,並在收到 INT 訊號後kill調用 PGID 等於您的 PID 的進程組。

  1. 對於有問題的腳本,play.sh可以透過以下方式完成:

  2. stop()函數中替換exit 1

    kill -TERM -$$  # note the dash, negative PID, kills the process group
    
  3. 作為後台程序啟動loop.sh(此處可以啟動多個後台進程並由 管理play.sh

    loop.sh &
    
  4. wait在腳本末尾添加以等待所有子項目。

    wait
    

當您的腳本啟動一個進程時,該子進程將成為進程組的成員,其 PGID 等於$$父 shell 中父進程的 PID。

例如,腳本在背景trap.sh啟動了三個進程,現在正在對它們進行 ing,請注意進程組 ID 列 (PGID) 與父進程的 PID 相同:sleepwait

  PID  PGID STAT COMMAND
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600

kill在 Unix 和 Linux 中,您可以透過使用負值呼叫來向該進程組中的每個進程發送訊號PGID。如果您給出kill負數,它將用作-PGID。由於腳本的 PID ( $$) 與 PGID 相同,因此您可以kill使用 shell 中的進程組

kill -TERM -$$    # note the dash before $$

你必須給一個訊號編號或名稱,否則某些kill的實作會告訴你「非法選項」或「無效訊號規範」。

下面的簡單程式碼說明了這一切。它設定一個trap訊號處理程序,產生 3 個子進程,然後進入無限等待循環,等待kill訊號處理程序中的進程組指令殺死自身。

$ cat trap.sh
#!/bin/sh

signal_handler() {
        echo
        read -p 'Interrupt: ignore? (y/n) [Y] >' answer
        case $answer in
                [nN]) 
                        kill -TERM -$$  # negative PID, kill process group
                        ;;
        esac
}

trap signal_handler INT 

for i in 1 2 3
do
    sleep 600 &
done

wait  # don't exit until process group is killed or all children die

這是一個範例運行:

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17111 17111 R+   ps -o pid,pgid,stat,args
$ 

好的,沒有額外的進程在運行。啟動測試腳本,中斷它(^C),選擇忽略中斷,然後掛起它(^Z):

$ sh trap.sh 
^C
Interrupt: ignore? (y/n) [Y] >y
^Z
[1]+  Stopped                 sh trap.sh
$

檢查正在執行的進程,記下進程組編號 ( PGID):

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600
17143 17143 R+   ps -o pid,pgid,stat,args
$

將我們的測試腳本帶到前台(fg)並再次中斷(^C),這次選擇不是忽略:

$ fg
sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >n
Terminated
$

檢查正在運作的進程,不再休眠:

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17159 17159 R+   ps -o pid,pgid,stat,args
$ 

請注意您的外殼:

我必須修改您的程式碼才能使其在我的系統上運行。您將其#!/bin/sh作為腳本中的第一行,但腳本使用了 /bin/sh 中不可用的擴充功能(來自 bash 或 zsh)。

答案2

“我知道當一個進程收到 SIGINT 訊號時,它會將訊號傳播到所有子進程”

你從哪裡得到這個錯誤的想法?

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; if (fork()) { say "parent $$"; sleep 3; kill 2, $$ } else { say "child $$"; sleep 99 }'
parent 25831
child 25832
ouch 25831
$

如果正如所聲稱的那樣存在傳播,人們可能會期望子進程發出“哎呀”一聲。

事實上:

“每當我們鍵入終端的中斷鍵(通常是DELETE 或Control-C)或退出鍵(通常是Control-反斜杠)時,都會導致中斷信號或退出信號被發送到前台進程組中的所有進程” -- W理查德·史蒂文斯。 「UNIX® 環境中的高階程式設計」。艾迪生-韋斯利。 1993 年。

這可以透過以下方式觀察到:

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; fork(); sleep 99'
^Couch 25971
ouch 25972
$

由於前台進程組的所有進程都會SIGINT從 中吃掉 a Control+C,因此您需要設計所有進程以適當地處理或忽略該訊號,或者可能讓子進程成為新的前台進程組,以便父進程(例如外殼)看不到訊號,因為它不再位於前台進程組中。

答案3

play.sh來源循環文件中,如下所示:

source ./loop.sh

一旦因陷阱而退出的子 shell 就無法在退出陷阱時返回。

答案4

問題是sleep

當你CTRL+C你殺了一個sleep——不是你的sh (即使你的文法#!/bin/sh有點可疑)。不過,整個進程組都會收到訊號,因為您沒有將處理程序安裝在loop.sh(這是一個子 shell)中,它會立即終止。

你需要一個陷阱loop.sh。除非明確指定,否則每個啟動的子 shell 都會清除陷阱trap ''SIG被家長忽略。

您還需要-m對父級進行工作控制監控,以便它可以追蹤其子級。wait例如,僅適用於作業控制。-m監視器模式是 shell 與終端互動的方式。

sh  -cm '  . /dev/fd/3' 3<<""                     # `-m`onitor is important
     sh -c ' kill -INT "$$"'&                     # get the sig#
     wait %%;INT=$?                               # keep it in $INT
     trap  " stty $(stty -g;stty -icanon)" EXIT   # lose canonical input
     loop()( trap    exit INT                     # this is a child
             while   [ "$#" -le 100 ]             # same as yours
             do      echo "$#"                    # writing the iterator
                     set "" "$@"                  # iterating
                     sleep 3                      # sleeping
             done
     )
     int(){  case    $1      in                   # handler fn
             ($INT)  printf  "\33[2K\rDo you want to stop playing? "
                     case    $(dd bs=1 count=1; echo >&3)  in
                     ([Nn])  return
             esac;   esac;   exit "$1"
     }       3>&2    >&2     2>/dev/null
     while   loop ||
             int "$?"
     do :;   done

0
1
2
3
4
Do you want to stop playing? n
0
1
2
3
Do you want to stop playing? N
0
1
Do you want to stop playing? y
[mikeserv@desktop tmp]$

相關內容