Bashスクリプトでサスペンス(ctrl-z)を消去するには?

Bashスクリプトでサスペンス(ctrl-z)を消去するには?

次のスクリプトがあります。

suspense_cleanup () {
  echo "Suspense clean up..."
}

int_cleanup () {
  echo "Int clean up..."
  exit 0
}

trap 'suspense_cleanup' SIGTSTP 
trap 'int_cleanup' SIGINT

sleep 600

実行してを押すと表示され、Ctrl-C終了Int clean up...します。

Ctrl-Zただし、を押すと、^Z文字が画面に表示されて停止します。

私がすることができます:

  • そこにクリーンアップコードを実行しCtrl-Z、多分何かをエコーすることもできます。
  • その後も引き続き一時停止しますか?

glibcのドキュメントをランダムに読み込んで、次のことを見つけました。

SUSP文字の一般的な解釈を無効にするアプリケーションは、ユーザーに操作を中止するための別のメカニズムを提供する必要があります。ユーザーがこのメカニズムを呼び出すと、プログラムはプロセス自体だけでなくプロセスのプロセスグループにSIGTSTPシグナルを送信する必要があります。

しかし、これがここに当てはまるかどうかはわかりませんが、とにかく機能しないようです。

コンテキスト:Vim / Neovimでサポートされているすべてのサスペンス関連機能をサポートするインタラクティブシェルスクリプトを作成しようとしています。今すぐ:

  1. プログラムで中断する機能(:suspendユーザーがCtrl-zを押すのではなくCtrl-zを使用)
  2. 一時停止する前に操作を実行する機能(Vimに自動保存)
  3. 再開する前にタスクを実行する機能(NeoVimのVimResume)

編集する:sleep 600に変更してもfor x in {1..100}; do sleep 6; done機能しません。

編集2:sleep 600と交換すると機能しますsleep 600 & wait。私はそれがなぜ、それがどのように機能するのか、またはそのようなものの制限が何であるかを全く確信していません。

答え1

Linuxや他のUNIX系システムでの信号処理は、カーネル端末ドライバ、上位→下位関係、プロセスグループ、端末制御、タスク制御のためのシェル信号処理の有効/無効、各プロセスの信号ハンドラなど、多くのプレイヤーが関わる非常に複雑なテーマです。 、等。

まず、Control- CControl-keyバインディングはZシェルではなくカーネルによって処理されます。次のコマンドを使用してデフォルト定義を表示できますstty -a

$ stty -a
speed 38400 baud; rows 64; columns 212; line = 0;
intr = ^C; quit = ^\; erase = ^H; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc

ここで私たちはintr = ^C科を見ますsusp = ^Z。 sttyはTCGETS ioctlを使用してカーネルからこの情報を取得します。 システムコール:

$ strace stty -a |& grep TCGETS
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0

主キーバインディングはLinuxカーネルコードで定義されています。

#define INIT_C_CC {         \
        [VINTR] = 'C'-0x40, \
        [VQUIT] = '\\'-0x40,        \
        [VERASE] = '\177',  \
        [VKILL] = 'U'-0x40, \
        [VEOF] = 'D'-0x40,  \
        [VSTART] = 'Q'-0x40,        \
        [VSTOP] = 'S'-0x40, \
        [VSUSP] = 'Z'-0x40, \
        [VREPRINT] = 'R'-0x40,      \
        [VDISCARD] = 'O'-0x40,      \
        [VWERASE] = 'W'-0x40,       \
        [VLNEXT] = 'V'-0x40,        \
        INIT_C_CC_VDSUSP_EXTRA      \
        [VMIN] = 1 }

基本操作も定義されます。

static void n_tty_receive_char_special(struct tty_struct *tty, unsigned char c,
                                       bool lookahead_done)
{
        struct n_tty_data *ldata = tty->disc_data;

        if (I_IXON(tty) && n_tty_receive_char_flow_ctrl(tty, c, lookahead_done))
                return;

        if (L_ISIG(tty)) {
                if (c == INTR_CHAR(tty)) {
                        n_tty_receive_signal_char(tty, SIGINT, c);
                        return;
                } else if (c == QUIT_CHAR(tty)) {
                        n_tty_receive_signal_char(tty, SIGQUIT, c);
                        return;
                } else if (c == SUSP_CHAR(tty)) {
                        n_tty_receive_signal_char(tty, SIGTSTP, c);
                        return;
                }

シグナルはついに次の__kill_pgrp_info()に達します。

/*
 * __kill_pgrp_info() sends a signal to a process group: this is what the tty
 * control characters do (^C, ^Z etc)
 * - the caller must hold at least a readlock on tasklist_lock
 */

Controlこれは私たちの物語にとって重要です。C-and-を使用してControl生成された信号が Z前景に送信されます。プロセスグループ新しく実行されるスクリプトは、リーダーである親対話型シェルによって生成されます。スクリプトとその子供たちグループに属しています。

したがって、ユーザーの意見で正しく指摘したように、カミール・マコロフスキー、送信時Control-Zスクリプトを起動した後、スクリプトはSIGTSTP信号を受信します。そして sleep 信号がグループに送信されると、グループ内のすべてのプロセスが信号を受信するためです。コードからトラップを削除すると、次のように表示されるかどうかを簡単に確認できます(ただし、常に https://en.wikipedia.org/wiki/shebang_(Unix)、Shebangがなければどうなるかという定義はありません.))

#!/usr/bin/env bash

# suspense_cleanup () {
#   echo "Suspense clean up..."
# }

# int_cleanup () {
#   echo "Int clean up..."
#   exit 0
# }

# trap 'suspense_cleanup' SIGTSTP
# trap 'int_cleanup' SIGINT

sleep 600

実行し(名前をsigtstp.shとして指定)、停止します。

$ ./sigtstp.sh
^Z
[1]+  Stopped                 ./sigtstp.sh
$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
ja       27062  0.0  0.0   6908  3144 pts/25   T    23:50   0:00 sh ./sigstop.sh
ja       27063  0.0  0.0   2960  1664 pts/25   T    23:50   0:00 sleep 600

ja私のユーザー名は異なり、ユーザー名も異なり、PIDも異なる場合があります。ただし、重要なのは、文字「T」で示されているように、両方のプロセスが停止していることです。からman ps

PROCESS STATE CODES
(...)
T    stopped by job control signal

これは、両方のプロセスがSIGTSTP信号を受信したことを意味します。 2つのプロセス(sigstop.shを含む)がシグナルを受信したときにシグナルハンドラが suspense_cleanup()実行されないのはなぜですか? Bashは終了するまでこれを実行しませんsleep 600。その要求は以下によって行われます。 POSIX:

シェルがユーティリティのフォアグラウンドコマンドの実行が完了するのを待っている間にトラップセット信号が受信されると、その信号に関連するトラップはフォアグラウンドコマンドが完了するまで実行されません。

(オープンソースの世界では、IT一般標準は単なるヒントの集まりに過ぎず、誰にも従うように強制する法的要件はありません。)睡眠が少ない(3秒など)、それも役に立ちません。sleepどうせプロセスが停止して完了しないからです。すぐに呼び出すには、suspense_cleanup()バックグラウンドで実行し、上記のwaitPOSIXリンクの指示に従う必要があります。

#!/usr/bin/env bash

suspense_cleanup () {
  echo "Suspense clean up..."
}

int_cleanup () {
  echo "Int clean up..."
  exit 0
}

trap 'suspense_cleanup' SIGTSTP
trap 'int_cleanup' SIGINT

sleep 600 &
wait

実行して停止します。

$ ./sigstop.sh
^ZSuspense clean up...

sleep 600今消えsigtstp.shた。

$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
$

sigtstp.shが消えた理由は明らかです。waitシグナルによって中断され、スクリプトの最後の行だったので終了しました。さらに驚くべきことは、SIGINTを送信するとsigtstp.shが終了した後もスリープモードが実行され続けることです。

$ ./sigtstp.sh
^CInt clean up...
$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
ja       32354  0.0  0.0   2960  1632 pts/25   S    00:12   0:00 sleep 600

しかし、親が死んだので、initによって採用されます。

$ grep PPid /proc/32354/status
PPid:   1

その理由は、シェルがバックグラウンドで子プロセスを実行すると、デフォルトのSIGINTハンドラを無効にしてその中のプロセスを終了するためです(signal(7))](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html):

シェルが非同期リストを実行しているときにジョブ制御が無効になっている場合(set -mの説明を参照)、リストのコマンドはシェルのSIGINTおよびSIGQUITシグナルの無視(SIG_IGN)シグナル操作を継承します。他のすべての場合、シェルによって実行されるコマンドは、トラップ特殊組み込み関数によってシグナル操作が変更されない限り、親からシェルによって継承されたのと同じシグナル操作を継承する必要があります(トラップを参照)。

いくつかのSOリファレンス: https://stackoverflow.com/questions/46061694/bash-why-cant-i-set-a-trap-for-sigint-in-a-Background-shell/46061734#46061734https://stackoverflow.com/questions/45106725/why-do-shells-ignore-sigint-and-sigquit-in-Backgrounded-processes。 SIGINTを受信した後にすべての子プロセスを終了するには、トラップハンドラでこれを手動で実行する必要があります。ただし、SIGINT はまだすべての子孫に渡されますが、無視されます。 sleepを使用せずに、代わりにコマンド用の独自のSIGINTハンドラをインストールした場合(例:tcpdumpを試してください)!glibc マニュアル 説明する:

与えられた信号が以前に無視されるように設定されている場合、このコードはその設定を変更しません。これは、非タスク制御シェルが子プロセスを開始するときに特定の信号を無視することが多いため、子プロセスがそれを尊重することが重要です。

しかし、私たちが自分でそれを殺さないならば、なぜスリープを殺さずにブロックする必要があるSIGTSTPを送った後にスリープが死ぬのでしょうか?孤児プロセスグループに属するすべての停止プロセスカーネルからSIGHUPをインポートする:

プロセスの終了によってプロセスグループが孤立プロセスグループになり、新しく孤立したプロセスグループのメンバーが停止した場合は、SIGHUP信号とSIGCONT信号を新しく孤立したプロセスグループの各プロセスに送信する必要があります。 。

カスタムハンドラがインストールされていない場合、SIGHUPはプロセスを終了します(signal(7)):

Signal      Standard   Action   Comment
SIGHUP       P1990      Term    Hangup detected on controlling terminal
                                or death of controlling process

(straceでスリープを実行すると、状況がより複雑になります...)。

いいですね。元の質問に戻るのはどうですか?

私がすることができます:

いくつかのクリーンアップコードを実行するには、Ctrl-Zを押して何かをエコーし​​てから一時停止を再開しますか?

私がする方法は次のとおりです。

#!/usr/bin/env bash

suspense_cleanup () {
    echo "Suspense clean up..."
    trap - SIGTSTP
    kill -TSTP $$
    trap 'suspense_cleanup' SIGTSTP
}

int_cleanup () {
    echo "Int clean up..."
    exit 0
}

trap 'suspense_cleanup' SIGTSTP
trap 'int_cleanup' SIGINT
sleep 600 &
while true
do
    if wait
    then
        echo child died, exiting
        exit 0
    fi
done

これでsuspense_cleanup()、プロセスを停止する前に呼び出されます。

$ ./sigtstp.sh
^ZSuspense clean up...

[1]+  Stopped                 ./sigtstp.sh
$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
ja        4129  0.0  0.0   6920  3196 pts/25   T    00:29   0:00 bash ./sigtstp.sh
ja        4130  0.0  0.0   2960  1660 pts/25   T    00:29   0:00 sleep 600
$ fg
./sigtstp.sh
^ZSuspense clean up...

[1]+  Stopped                 ./sigtstp.sh
$  fg
./sigtstp.sh
^CInt clean up...
$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
ja        4130  0.0  0.0   2960  1660 pts/25   S    00:29   0:00 sleep 600
$ grep PPid /proc/4130/status
PPid:   1

10秒ほど眠り、睡眠が完了したらスクリプトが終了することを確認できます。

#!/usr/bin/env bash

suspense_cleanup () {
    echo "Suspense clean up..."
    trap - SIGTSTP
    kill -TSTP $$
    trap 'suspense_cleanup' SIGTSTP
}

int_cleanup () {
    echo "Int clean up..."
    exit 0
}

trap 'suspense_cleanup' SIGTSTP
trap 'int_cleanup' SIGINT
# sleep 600 &
sleep 10 &
while true
do
    if wait
    then
        echo child died, exiting
        exit 0
    fi
done

走る:

$ time ./sigtstp.sh
child died, exiting

real    0m10.007s
user    0m0.003s
sys     0m0.004s

答え2

Bash ジョブ制御が通常の tty 特殊文字を妨げるようです。 SIGTSTPはBashに転送できますが、実行中のプロセスには転送できません。

~からhttps://www.gnu.org/software/bash/manual/bash.html#Job-Control-Basics:

Bashが実行されているオペレーティングシステムがジョブ制御をサポートしている場合、Bashにはそれを使用するツールが含まれています。プロセスの実行中に一時停止文字(通常は「^ Z」、Control-Z)を入力すると、プロセスは停止し、コントロールはBashに返されます。

続行(Ctrl-Q、startから呼び出すstty)はBashジョブ制御では機能しません。停止をキャンセルするにはプロセスがfg必要です。bg

答え3

man 7 signal、またはを読むと、https://www.man7.org/linux/man-pages/man7/signal.7.html次のようになります。

SIGKILLおよびSIGSTOP信号は捕捉、ブロック、または無視できません。

だから、あなたはできません。

関連情報