Tidak dapat menghentikan skrip bash dengan Ctrl + C


42

Saya menulis skrip bash sederhana dengan loop untuk mencetak tanggal dan melakukan ping ke mesin jarak jauh:

#!/bin/bash
while true; do
    #     *** DATE: Thu Sep 17 10:17:50 CEST 2015  ***
    echo -e "\n*** DATE:" `date` " ***";
    echo "********************************************"
    ping -c5 $1;
done

Ketika saya menjalankannya dari terminal saya tidak dapat menghentikannya Ctrl+C. Tampaknya mengirimkan ^Cke terminal, tetapi skrip tidak berhenti.

MacAir:~ tomas$ ping-tester.bash www.google.com

*** DATE: Thu Sep 17 23:58:42 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.228): 56 data bytes
64 bytes from 216.58.211.228: icmp_seq=0 ttl=55 time=39.195 ms
64 bytes from 216.58.211.228: icmp_seq=1 ttl=55 time=37.759 ms
^C                                                          <= That is Ctrl+C press
--- www.google.com ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 40.887/59.699/78.510/18.812 ms

*** DATE: Thu Sep 17 23:58:48 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.196): 56 data bytes
64 bytes from 216.58.211.196: icmp_seq=0 ttl=55 time=37.460 ms
64 bytes from 216.58.211.196: icmp_seq=1 ttl=55 time=37.371 ms

Tidak peduli berapa kali saya menekannya atau seberapa cepat saya melakukannya. Saya tidak bisa menghentikannya.
Lakukan tes dan sadari sendiri.

Sebagai solusi sampingan, saya menghentikannya Ctrl+Z, yang menghentikannya dan kemudiankill %1 .

Dengan apa sebenarnya yang terjadi di sini ^C?

Jawaban:


26

Apa yang terjadi adalah bahwa baik bashdan pingmenerima SIGINT ( bashmenjadi tidak interaktif, baik pingdan bashberjalan dalam kelompok proses yang sama yang telah dibuat dan ditetapkan sebagai kelompok proses latar depan terminal oleh shell interaktif Anda berlari bahwa script dari).

Namun, bashmenangani SIGINT itu secara tidak sinkron, hanya setelah perintah yang saat ini berjalan telah keluar. bashhanya keluar setelah menerima SIGINT itu jika perintah yang sedang berjalan mati karena SIGINT (yaitu status keluarnya menunjukkan bahwa ia telah dibunuh oleh SIGINT).

$ bash -c 'sh -c "trap exit\ 0 INT; sleep 10; :"; echo here'
^Chere

Di atas, bash, shdan sleepmenerima SIGINT ketika saya menekan Ctrl-C, tapi shkeluar biasanya dengan kode 0 keluar, sehingga bashmengabaikan SIGINT, yang sebabnya kita melihat "di sini".

ping, setidaknya yang dari iputils, berperilaku seperti itu. Ketika terganggu, ia mencetak statistik dan keluar dengan status keluar 0 atau 1 tergantung pada apakah pingnya dijawab atau tidak. Jadi, ketika Anda menekan Ctrl-C saat pingsedang berjalan, bashperhatikan bahwa Anda telah menekan Ctrl-Cdi penangan SIGINT-nya, tetapi karena pingkeluar secara normal, bashtidak keluar.

Jika Anda menambahkan sleep 1dalam loop itu dan tekan Ctrl-Csaat sleepsedang berjalan, karena sleeptidak memiliki penangan khusus pada SIGINT, itu akan mati dan melaporkan bashbahwa itu mati karena SIGINT, dan dalam kasus itubash akan keluar (itu benar-benar akan bunuh diri dengan SIGINT sehingga untuk melaporkan gangguan ke induknya).

Mengenai mengapa bashberperilaku seperti itu, saya tidak yakin dan saya perhatikan perilaku tidak selalu deterministik. Saya baru saja mengajukan pertanyaan pada bashmilis pengembangan ( Pembaruan : @Jilles sekarang telah menemukan alasannya dalam jawabannya ).

Satu-satunya shell lain yang saya temukan berperilaku serupa adalah ksh93 (Pembaruan, seperti yang disebutkan oleh @Jilles, begitu juga FreeBSDsh ). Di sana, SIGINT tampaknya benar-benar diabaikan. Dan ksh93keluar setiap kali perintah dibunuh oleh SIGINT.

Anda mendapatkan perilaku yang sama seperti di bashatas tetapi juga:

ksh -c 'sh -c "kill -INT \$\$"; echo test'

Tidak menampilkan "tes". Yaitu, ia keluar (dengan membunuh dirinya sendiri dengan SIGINT di sana) jika perintah yang ditungguinya mati oleh SIGINT, bahkan jika itu, ia sendiri tidak menerima SIGINT itu.

Pekerjaan di sekitar adalah menambahkan:

trap 'exit 130' INT

Di bagian atas skrip untuk memaksa bashkeluar setelah menerima SIGINT (perhatikan bahwa dalam hal apa pun, SIGINT tidak akan diproses secara serempak, hanya setelah perintah yang saat ini berjalan telah keluar).

Idealnya, kami ingin melaporkan kepada orang tua kami bahwa kami meninggal karena SIGINT (sehingga jika itu adalah bashskrip lain misalnya, bashskrip itu juga terputus). Melakukan exit 130tidak sama dengan sekarat SIGINT (meskipun beberapa shell akan diatur$? nilai yang sama untuk kedua kasus), namun sering digunakan untuk melaporkan kematian oleh SIGINT (pada sistem di mana SIGINT adalah 2 yang paling banyak).

Namun untuk bash, ksh93atau FreeBSD sh, itu tidak berhasil. 130 status keluar tidak dianggap sebagai kematian oleh SIGINT dan skrip induk tidak akan dibatalkan di sana.

Jadi, alternatif yang mungkin lebih baik adalah bunuh diri dengan SIGINT setelah menerima SIGINT:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT

1
Jawaban jilles menjelaskan "mengapa". Sebagai contoh ilustrasi , pertimbangkan  for f in *.txt; do vi "$f"; cp "$f" newdir; done. Jika pengguna mengetik Ctrl + C saat mengedit salah satu file, vicukup tampilkan pesan. Tampaknya masuk akal bahwa perulangan harus dilanjutkan setelah pengguna selesai mengedit file. (Dan ya, saya tahu Anda bisa mengatakan vi *.txt; cp *.txt newdir; Saya hanya mengirimkan forperulangan sebagai contoh.)
Scott

@Scott, poin bagus. Meskipun vi( vimsetidaknya setidaknya) tidak menonaktifkan tty isigsaat mengedit (itu tidak jelas ketika Anda menjalankan :!cmd, dan itu akan sangat berlaku dalam kasus itu).
Stéphane Chazelas

@Tim, lihat edit saya untuk koreksi pada suntingan Anda.
Stéphane Chazelas

@ StéphaneChazelas Terima kasih. Jadi itu karena pingkeluar dengan 0 setelah menerima SIGINT. Saya menemukan perilaku serupa ketika skrip bash berisi sudobukan ping, tetapi sudokeluar dengan 1 setelah menerima SIGINT. unix.stackexchange.com/questions/479023/…
Tim

13

Penjelasannya adalah bahwa bash mengimplementasikan WCE (tunggu dan keluar bersama) untuk SIGINT dan SIGQUIT per http://www.cons.org/cracauer/sigint.html . Itu berarti bahwa jika bash menerima SIGINT atau SIGQUIT sambil menunggu proses untuk keluar, itu akan menunggu sampai proses keluar dan akan keluar sendiri jika proses keluar pada sinyal itu. Ini memastikan bahwa program yang menggunakan SIGINT atau SIGQUIT di antarmuka penggunanya akan berfungsi seperti yang diharapkan (jika sinyal tidak menyebabkan program berhenti, skrip akan berlanjut secara normal).

Kelemahan muncul dengan program yang menangkap SIGINT atau SIGQUIT tetapi kemudian berhenti karena itu tetapi menggunakan keluar normal () alih-alih dengan mengirim ulang sinyal ke diri mereka sendiri. Mungkin tidak dapat mengganggu skrip yang memanggil program semacam itu. Saya pikir perbaikan nyata ada dalam program-program seperti ping dan ping6.

Perilaku serupa diimplementasikan oleh ksh93 dan FreeBSD / bin / sh, tetapi tidak oleh sebagian besar shell lainnya.


Terima kasih, itu masuk akal. Saya perhatikan FreeBSD sh tidak membatalkan baik ketika cmd keluar dengan keluar (130) baik, yang merupakan cara umum untuk melaporkan kematian oleh SIGINT seorang anak (mksh melakukan exit(130)misalnya misalnya jika Anda mengganggu mksh -c 'sleep 10;:').
Stéphane Chazelas

5

Ketika Anda menduga, ini karena SIGINT dikirim ke proses bawahan, dan shell berlanjut setelah proses itu keluar.

Untuk menangani ini dengan cara yang lebih baik, Anda dapat memeriksa status keluar dari perintah yang sedang berjalan. Kode pengembalian Unix mengkode metode yang keluar dari proses (panggilan atau sinyal sistem) dan nilai apa yang diteruskan exit()atau sinyal apa yang menghentikan proses. Ini semua agak rumit, tetapi cara tercepat untuk menggunakannya adalah mengetahui bahwa proses yang diakhiri oleh sinyal akan memiliki kode pengembalian yang tidak nol. Dengan demikian, jika Anda memeriksa kode kembali dalam skrip Anda, Anda dapat keluar sendiri jika proses anak dihentikan, menghilangkan kebutuhan akan kekurangan-kekurangan seperti sleeppanggilan yang tidak perlu . Cara cepat untuk melakukan ini di seluruh skrip Anda adalah dengan menggunakan set -e, meskipun mungkin memerlukan beberapa penyesuaian untuk perintah yang status keluarnya merupakan nol yang diharapkan.


1
Set -e tidak berfungsi dengan benar di bash kecuali Anda menggunakan bash-4
schily

Apa yang dimaksud "tidak berfungsi dengan benar"? Saya telah menggunakannya dengan sukses di bash 3, tetapi mungkin ada beberapa kasus tepi.
Tom Hunt

Dalam beberapa kasus sederhana, bash3 memang keluar karena kesalahan. Namun ini tidak terjadi dalam kasus umum. Sebagai hasil khas, make tidak berhenti ketika membuat target gagal dan ini dari makefile yang bekerja pada daftar target di subdirektori. David Korn dan saya harus mengirim berminggu-minggu dengan pengelola bash untuk meyakinkan dia untuk memperbaiki bug untuk bash4.
schily

4
Perhatikan bahwa masalahnya di sini adalah pingkembali dengan status keluar 0 setelah menerima SIGINT dan yang bashkemudian mengabaikan SIGINT yang diterimanya sendiri jika memang demikian. Menambahkan "set -e" atau memeriksa status keluar tidak akan membantu di sini. Menambahkan perangkap eksplisit pada SIGINT akan membantu.
Stéphane Chazelas

4

Terminal memperhatikan kontrol-c dan mengirimkan INTsinyal ke grup proses latar depan, yang di sini mencakup shell, karena pingbelum membuat grup proses latar depan baru. Ini mudah diverifikasi dengan menjebak INT.

#! /bin/bash
trap 'echo oh, I am slain; exit' INT
while true; do
  ping -c5 127.0.0.1
done

Jika perintah yang dijalankan telah membuat grup proses foreground baru, maka control-c akan pergi ke grup proses itu, dan bukan ke shell. Dalam hal ini, shell perlu memeriksa kode keluar, karena tidak akan diberi sinyal oleh terminal.

( INTPenanganan pada kulit dapat luar biasa rumit, dengan cara, sebagai shell kadang-kadang perlu untuk mengabaikan sinyal, dan kadang-kadang tidak Sumber menyelam jika penasaran, atau merenungkan:. tail -f /etc/passwd; echo foo)


Dalam hal ini, masalahnya bukan penanganan sinyal tetapi fakta bahwa bash melakukan jobcontrol dalam skrip meskipun seharusnya tidak, lihat jawaban saya untuk informasi lebih lanjut
schily

Agar SIGINT pergi ke grup proses baru, perintah juga harus melakukan ioctl () ke terminal untuk menjadikannya grup proses foreground terminal. pingtidak punya alasan untuk memulai grup proses baru di sini dan versi ping (iputils pada Debian) yang dengannya saya dapat mereproduksi masalah OP tidak membuat grup proses.
Stéphane Chazelas

1
Perhatikan bahwa bukan terminal yang mengirim SIGINT, melainkan disiplin garis perangkat tty (driver (kode dalam kernel) dari perangkat / dev / ttysomething) setelah menerima yang tidak terhapus (dengan lnext biasanya ^ V) ^ karakter C dari terminal.
Stéphane Chazelas

2

Yah, saya mencoba menambahkan sleep 1skrip bash, dan bang!
Sekarang saya bisa menghentikannya dengan dua Ctrl+C.

Saat menekan Ctrl+C, SIGINTsinyal dikirim ke proses yang saat ini dieksekusi, yang perintahnya dijalankan di dalam loop. Kemudian, proses subkulit terus mengeksekusi perintah berikutnya dalam loop, yang memulai proses lain. Untuk dapat menghentikan skrip perlu mengirim dua SIGINTsinyal, satu untuk mengganggu perintah saat ini dalam pelaksanaan dan satu untuk mengganggu proses subkulit .

Dalam skrip tanpa sleeppanggilan, menekan Ctrl+Csangat cepat dan berkali-kali sepertinya tidak berfungsi, dan tidak mungkin untuk keluar dari loop. Dugaan saya adalah bahwa menekan dua kali tidak cukup cepat untuk membuatnya tepat pada saat yang tepat antara gangguan proses yang dijalankan saat ini dan awal yang berikutnya. Setiap Ctrl+Cpenekanan akan mengirimkan SIGINTke proses yang dijalankan di dalam loop, tetapi tidak ke subshell .

Dalam skrip dengan sleep 1, panggilan ini akan menunda eksekusi selama satu detik, dan ketika terganggu oleh yang pertama Ctrl+C(pertama SIGINT), subkulit akan membutuhkan waktu lebih banyak untuk menjalankan perintah berikutnya. Jadi sekarang, yang kedua Ctrl+C(kedua SIGINT) akan menuju ke subkulit , dan eksekusi skrip akan berakhir.


Anda salah, pada shell yang berfungsi dengan benar, satu ^ C cukup melihat jawaban saya untuk latar belakang.
schily

Nah, mengingat Anda telah turun sebagai pilihan, dan saat ini jawaban Anda memiliki skor -1, saya tidak begitu yakin saya harus menganggap jawaban Anda dengan serius.
nephewtom

Fakta bahwa beberapa orang downvote tidak selalu terkait dengan kualitas balasan. Jika Anda perlu mengetik dua kali ^ c, Anda pasti adalah korban bug bash. Apakah Anda mencoba shell yang berbeda? Apakah Anda mencoba Bourne Shell yang asli?
schily

Jika shell jika berfungsi dengan benar, ia menjalankan semuanya dari skrip dalam grup proses yang sama dan kemudian satu ^ c sudah cukup.
schily

1
Perilaku @nephewtom yang dijelaskan dalam jawaban ini dapat dijelaskan dengan berbagai perintah dalam skrip yang berperilaku berbeda saat mereka menerima Ctrl-C. Jika ada sleep, kemungkinan besar Ctrl-C akan diterima saat sleep dijalankan (dengan asumsi semua yang lain dalam loop cepat). Tidur terbunuh, dengan nilai keluar 130. Induk tidur, kulit, pemberitahuan bahwa tidur dibunuh oleh sigint, dan keluar. Tetapi jika skrip berisi tidak tidur, maka Ctrl-C pergi ke ping sebagai gantinya, yang bereaksi dengan keluar dengan 0, sehingga shell induk menjalankan eksekusi perintah berikutnya.
Jonathan Hartley

0

Coba ini:

#!/bin/bash
while true; do
   echo "Ctrl-c works during sleep 5"
   sleep 5
   echo "But not during ping -c 5"
   ping -c 5 127.0.0.1
done

Sekarang ubah baris pertama menjadi:

#!/bin/sh

dan coba lagi - lihat apakah ping sekarang dapat disela.


0
pgrep -f process_name > any_file_name
sed -i 's/^/kill /' any_file_name
chmod 777 any_file_name
./any_file_name

misalnya pgrep -f firefoxakan membuat PID berjalan firefoxdan akan menyimpan PID ini ke file bernama any_file_name. Perintah 'sed' akan menambahkan killdi awal nomor PID dalam file 'any_file_name'. Baris ketiga akan any_file_namemengajukan file yang dapat dieksekusi. Sekarang baris keempat akan membunuh PID yang tersedia dalam file any_file_name. Menulis empat baris di atas dalam file dan menjalankan file itu dapat melakukan Control- C. Bekerja sangat baik untuk saya.


0

Jika ada yang tertarik pada perbaikan untuk bashfitur ini , dan tidak terlalu banyak dengan filosofi di baliknya , berikut ini adalah proposal:

Jangan menjalankan perintah yang bermasalah secara langsung, tetapi dari pembungkus yang a) menunggu sampai terminasi b) tidak mengacaukan sinyal dan c) tidak mengimplementasikan mekanisme WCE itu sendiri, tetapi mati begitu saja ketika menerima a SIGINT.

Pembungkus tersebut dapat dibuat dengan awk+ nya system()fungsi.

$ while true; do awk 'BEGIN{system("ping -c5 localhost")}'; done
PING localhost(localhost (::1)) 56 data bytes
64 bytes from localhost (::1): icmp_seq=1 ttl=64 time=0.082 ms
64 bytes from localhost (::1): icmp_seq=2 ttl=64 time=0.087 ms
^C
--- localhost ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1022ms
rtt min/avg/max/mdev = 0.082/0.084/0.087/0.009 ms
[3]-  Terminated              ping -c5 localhost

Masukkan skrip seperti OP:

#!/bin/bash
while true; do
        echo -e "\n*** DATE:" `date` " ***";
        echo "********************************************"
        awk 'BEGIN{system(ARGV[1])}' "ping -c5 ${1-localhost}"
done

-3

Anda adalah korban bug bash yang terkenal. Bash melakukan kontrol pekerjaan untuk skrip yang merupakan kesalahan.

Apa yang terjadi adalah bahwa bash menjalankan program eksternal dalam grup proses yang berbeda dari yang digunakan untuk skrip itu sendiri. Karena TTY processgroup diatur ke processgroup dari proses foreground saat ini, hanya proses foreground ini yang dimatikan dan loop dalam skrip shell berlanjut.

Untuk memverifikasi: Ambil dan kompilasi Bourne Shell baru-baru ini yang mengimplementasikan pgrp (1) sebagai program bawaan, kemudian tambahkan / bin / sleep 100 (atau / usr / bin / sleep tergantung pada platform Anda) ke loop skrip dan kemudian mulai Bourne Shell. Setelah Anda menggunakan ps (1) untuk mendapatkan ID proses untuk perintah sleep dan bash yang menjalankan skrip, panggil pgrp <pid>dan ganti "<pid>" dengan ID proses sleep dan bash yang menjalankan skrip. Anda akan melihat ID grup proses yang berbeda. Sekarang panggil sesuatu seperti pgrp < /dev/pts/7(ganti nama tty dengan tty yang digunakan oleh skrip) untuk mendapatkan grup proses tty saat ini. Grup proses TTY sama dengan grup proses dari perintah sleep.

Untuk memperbaikinya: gunakan shell yang berbeda.

Sumber-sumber Bourne Shell baru-baru ini ada dalam paket alat schily saya yang dapat Anda temukan di sini:

http://sourceforge.net/projects/schilytools/files/


Versi apa bashitu? AFAIK bashhanya melakukan itu jika Anda melewatkan opsi -m atau -i.
Stéphane Chazelas

Tampaknya ini tidak lagi berlaku untuk bash4 tetapi ketika OP memiliki masalah seperti itu, ia tampaknya menggunakan bash3
schily

Tidak dapat mereproduksi dengan bash3.2.48 atau bash 3.0.16 atau bash-2.05b (dicoba dengan bash -c 'ps -j; ps -j; ps -j').
Stéphane Chazelas

Ini pasti terjadi ketika Anda memanggil bash sebagai /bin/sh -ce. Saya harus menambahkan solusi buruk ke dalam smakeyang secara eksplisit membunuh kelompok proses untuk perintah yang sedang berjalan untuk mengizinkan ^Cuntuk membatalkan panggilan berlapis. Apakah Anda memeriksa apakah bash mengubah grup proses dari id grup proses yang diawali dengan?
schily

ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'tidak melaporkan pgid yang sama untuk ps dan bash di semua 3 ps doa. (ARGV0 = sh adalah zshcara untuk melewatkan argv [0]).
Stéphane Chazelas
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.