Linux

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

  • November 10, 2015

我正在嘗試以這樣一種方式處理 SIGINT/CTRL+C 中斷,如果使用者不小心按了 ctrl-c,則會提示他一條消息,“你想退出嗎?(y/n)”。如果他輸入yes,則退出腳本。如果否,則從發生中斷的地方繼續。基本上,我需要 Ctrl-C 以類似於 Ctrl-Z/SINTSTP 的方式工作,但方式略有不同。我已經嘗試了各種方法來實現這一點,但我沒有得到預期的結果。以下是我嘗試過的幾個場景。

情況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 的應用程序不應該終止,但它應該提示使用者一條消息。

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

為此,在後台啟動子程序 (loop.sh) 後,呼叫wait,並在收到 INT 信號後,呼叫killPGID 等於您的 PID 的程序組。

  1. 對於有問題的腳本play.sh,這可以通過以下方式完成:
  2. stop()函式中替換exit 1
kill -TERM -$$  # note the dash, negative PID, kills the process group
  1. 作為後台程序啟動loop.sh(這裡可以啟動多個後台程序並由 管理play.sh
loop.sh &
  1. 在腳本末尾添加wait以等待所有孩子。
wait

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

例如,腳本在後台trap.sh啟動了三個sleep程序,現在正在wait執行它們,請注意程序組 ID 列(PGID)與父程序的 PID 相同:

 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

在 Unix 和 Linux 中,您可以通過kill呼叫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)。

引用自:https://unix.stackexchange.com/questions/240602