我試著以這樣的方式處理SIGINT
( CtrlC),如果使用者不小心按下 ctrl-c,系統會提示他一則訊息:「你想退出嗎?(y/n)」。如果他輸入 yes,則退出腳本。如果不是,則從中斷發生的地方繼續。基本上,我需要CtrlC以類似於SIGTSTP
(CtrlZ)的方式工作,但方式略有不同。我嘗試了各種方法來實現這一目標,但沒有得到預期的結果。以下是我嘗試過的幾個場景。
情況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 的進程組。
對於有問題的腳本,
play.sh
可以透過以下方式完成:在
stop()
函數中替換exit 1
為kill -TERM -$$ # note the dash, negative PID, kills the process group
作為後台程序啟動
loop.sh
(此處可以啟動多個後台進程並由 管理play.sh
)loop.sh &
wait
在腳本末尾添加以等待所有子項目。wait
當您的腳本啟動一個進程時,該子進程將成為進程組的成員,其 PGID 等於$$
父 shell 中父進程的 PID。
例如,腳本在背景trap.sh
啟動了三個進程,現在正在對它們進行 ing,請注意進程組 ID 列 (PGID) 與父進程的 PID 相同:sleep
wait
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]$