Không thể dừng tập lệnh bash bằng Ctrl + C


42

Tôi đã viết một tập lệnh bash đơn giản với một vòng lặp để in ngày và ping đến một máy từ xa:

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

Khi tôi chạy nó từ một thiết bị đầu cuối, tôi không thể dừng nó với Ctrl+C. Có vẻ như nó gửi ^Cthiết bị đầu cuối, nhưng kịch bản không dừng lại.

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

Không có vấn đề bao nhiêu lần tôi nhấn nó hoặc tôi làm nó nhanh như thế nào. Tôi không thể ngăn chặn nó.
Làm bài kiểm tra và nhận ra bởi chính mình.

Là một giải pháp phụ, tôi dừng nó lại Ctrl+Z, rồi dừng lại và sau đó kill %1.

Chính xác thì chuyện gì đang xảy ra ở đây ^C?

Câu trả lời:


26

Điều gì xảy ra cả bashpingnhận được SIGINT ( bashkhông tương tác, cả hai pingbashchạy trong nhóm quá trình tương tự đã được tạo ra và thiết lập như nhóm quá trình foreground của thiết bị đầu cuối bằng vỏ tương tác bạn chạy mà kịch bản từ).

Tuy nhiên, bashxử lý SIGINT không đồng bộ, chỉ sau khi lệnh hiện đang chạy đã thoát. bashchỉ thoát khi nhận được SIGINT đó nếu lệnh hiện đang chạy bị chết của SIGINT (nghĩa là trạng thái thoát của nó chỉ ra rằng nó đã bị SIGINT giết).

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

Trên, bash, shsleepnhận SIGINT khi tôi nhấn Ctrl-C, nhưng shlối ra bình thường với một mã số 0 xuất cảnh, nên bashbỏ qua SIGINT, đó là lý do chúng ta thấy "ở đây".

ping, ít nhất là một từ iputils, hành xử như vậy. Khi bị gián đoạn, nó sẽ in các số liệu thống kê và thoát với trạng thái thoát 0 hoặc 1 tùy thuộc vào việc ping của nó có được trả lời hay không. Vì vậy, khi bạn nhấn Ctrl-C trong khi pingđang chạy, bashlưu ý rằng bạn đã nhấn Ctrl-Ctrong trình xử lý SIGINT của nó, nhưng vì pingthoát bình thường, bashkhông thoát.

Nếu bạn thêm một sleep 1vòng lặp đó và nhấn Ctrl-Ctrong khi sleepđang chạy, vì sleepkhông có trình xử lý đặc biệt nào trên SIGINT, nó sẽ chết và báo cáo bashrằng nó đã chết vì SIGINT, và trong trường hợp đó bashsẽ thoát ra (nó thực sự sẽ tự chết bằng SIGINT để báo cáo sự gián đoạn cho cha mẹ của nó).

Về lý do tại sao bashhành xử như vậy, tôi không chắc chắn và tôi lưu ý rằng hành vi không phải lúc nào cũng mang tính quyết định. Tôi vừa hỏi câu hỏi trong bashdanh sách gửi thư phát triển ( Cập nhật : @Jilles hiện đã đóng đinh lý do trong câu trả lời của anh ấy ).

Vỏ khác duy nhất tôi thấy có hành vi tương tự là ksh93 (Cập nhật, như được đề cập bởi @Jilles, FreeBSD cũng vậysh ). Ở đó, SIGINT dường như bị bỏ qua rõ ràng. Và ksh93thoát ra bất cứ khi nào một lệnh bị giết bởi SIGINT.

Bạn có hành vi tương tự như bashtrên nhưng cũng:

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

Không xuất "kiểm tra". Đó là, nó thoát ra (bằng cách tự giết bằng SIGINT ở đó) nếu lệnh mà nó đang chờ chết của SIGINT, ngay cả khi nó, chính nó đã không nhận được SIGINT đó.

Một công việc xung quanh sẽ là thêm một:

trap 'exit 130' INT

Ở đầu tập lệnh buộc bashphải thoát khi nhận được SIGINT (lưu ý rằng trong mọi trường hợp, SIGINT sẽ không được xử lý đồng bộ, chỉ sau khi lệnh hiện đang chạy).

Lý tưởng nhất là chúng tôi muốn báo cáo với cha mẹ rằng chúng tôi đã chết vì SIGINT (để nếu đó là một bashtập lệnh khác chẳng hạn, bashtập lệnh đó cũng bị gián đoạn). Làm một cái exit 130không giống như chết của SIGINT (mặc dù một số shell sẽ được đặt $?thành cùng một giá trị cho cả hai trường hợp), tuy nhiên, nó thường được sử dụng để báo cáo cái chết của SIGINT (trên các hệ thống có SIGINT là 2).

Tuy nhiên bash, ksh93hoặc FreeBSD sh, không hoạt động. Tình trạng thoát 130 đó không được coi là một cái chết bởi SIGINT và tập lệnh gốc sẽ không hủy bỏ ở đó.

Vì vậy, một giải pháp thay thế tốt hơn có thể là giết chính chúng ta bằng SIGINT khi nhận được SIGINT:

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

1
Câu trả lời của jilles giải thích về vấn đề tại sao. Như một ví dụ minh họa , xem xét  for f in *.txt; do vi "$f"; cp "$f" newdir; done. Nếu người dùng gõ Ctrl + C trong khi chỉnh sửa một trong các tệp, vichỉ cần hiển thị một thông báo. Có vẻ hợp lý rằng vòng lặp nên tiếp tục sau khi người dùng hoàn thành chỉnh sửa tệp. (Và vâng, tôi biết rằng bạn có thể nói vi *.txt; cp *.txt newdir; Tôi chỉ gửi forvòng lặp làm ví dụ.)
Scott

@ Hủy, điểm tốt. Mặc dù vi( vimít nhất) cũng không tắt tty isigkhi chỉnh sửa ( :!cmdmặc dù nó không rõ ràng khi bạn chạy , và điều đó sẽ áp dụng rất nhiều trong trường hợp đó).
Stéphane Chazelas

@Tim, xem chỉnh sửa của tôi để chỉnh sửa về chỉnh sửa của bạn.
Stéphane Chazelas

@ StéphaneChazelas Cảm ơn. Vì vậy, đó là vì pingthoát với 0 sau khi nhận được SIGINT. Tôi đã tìm thấy một hành vi tương tự khi tập lệnh bash chứa sudothay vì ping, nhưng sudothoát với 1 sau khi nhận được SIGINT. unix.stackexchange.com/questions/479023/ trên
Tim

13

Giải thích là bash thực hiện WCE (chờ đợi và thoát hợp tác) cho SIGINT và SIGQUIT mỗi http://www.cons.org/cracauer/sigint.html . Điều đó có nghĩa là nếu bash nhận SIGINT hoặc SIGQUIT trong khi chờ quá trình thoát, nó sẽ đợi cho đến khi quá trình thoát và sẽ tự thoát nếu quá trình thoát khỏi tín hiệu đó. Điều này đảm bảo rằng các chương trình sử dụng SIGINT hoặc SIGQUIT trong giao diện người dùng của họ sẽ hoạt động như mong đợi (nếu tín hiệu không khiến chương trình kết thúc, tập lệnh sẽ tiếp tục bình thường).

Một nhược điểm xuất hiện với các chương trình bắt SIGINT hoặc SIGQUIT nhưng sau đó chấm dứt vì nó nhưng sử dụng lối thoát bình thường () thay vì gửi lại tín hiệu cho chính chúng. Có thể không thể làm gián đoạn các tập lệnh gọi các chương trình như vậy. Tôi nghĩ rằng sửa chữa thực sự có trong các chương trình như ping và ping6.

Hành vi tương tự được thực hiện bởi ksh93 và FreeBSD's / bin / sh, nhưng không phải bởi hầu hết các shell khác.


Cảm ơn, điều đó rất có ý nghĩa. Tôi lưu ý FreeBSD sẽ không hủy bỏ khi cmd thoát ra khỏi lối ra (130), đây là cách phổ biến để báo cáo cái chết bằng SIGINT của một đứa trẻ ( exit(130)ví dụ như mksh làm gián đoạn mksh -c 'sleep 10;:').
Stéphane Chazelas

5

Khi bạn phỏng đoán, điều này là do SIGINT được gửi đến quy trình cấp dưới và lớp vỏ tiếp tục sau khi quá trình đó thoát ra.

Để xử lý việc này theo cách tốt hơn, bạn có thể kiểm tra trạng thái thoát của các lệnh đang chạy. Mã trả về Unix mã hóa cả phương thức mà một quá trình đã thoát (cuộc gọi hệ thống hoặc tín hiệu) và giá trị nào được truyền đến exit()hoặc tín hiệu nào kết thúc quá trình. Điều này khá phức tạp, nhưng cách sử dụng nhanh nhất là để biết rằng một quá trình bị chấm dứt bởi tín hiệu sẽ có mã trả về khác không. Do đó, nếu bạn kiểm tra mã trả về trong tập lệnh của mình, bạn có thể tự thoát nếu quá trình con bị chấm dứt, loại bỏ sự cần thiết của các lệnh không liên quan như sleepcác cuộc gọi không cần thiết . Một cách nhanh chóng để thực hiện điều này trong toàn bộ tập lệnh của bạn là sử dụng set -e, mặc dù nó có thể yêu cầu một vài điều chỉnh cho các lệnh có trạng thái thoát là một giá trị khác.


1
Đặt -e không hoạt động chính xác trong bash trừ khi bạn đang sử dụng bash-4
schily

Điều gì có nghĩa là "không hoạt động chính xác"? Tôi đã sử dụng nó trên bash 3 thành công, nhưng có lẽ có một số trường hợp cạnh.
Tom Hunt

Trong một vài trường hợp đơn giản, bash3 đã thoát lỗi. Điều này đã không xảy ra trong trường hợp chung. Kết quả là, việc thực hiện không dừng lại khi tạo mục tiêu thất bại và điều này là từ một tệp tạo tệp hoạt động trong danh sách các mục tiêu trong các thư mục con. David Korn và tôi đã phải gửi thư nhiều tuần với người bảo trì bash để thuyết phục anh ấy sửa lỗi cho bash4.
schily

4
Lưu ý rằng vấn đề ở đây là pingtrả về với trạng thái thoát 0 khi nhận SIGINT và bashsau đó bỏ qua SIGINT mà nó nhận được nếu đó là trường hợp. Thêm "set -e" hoặc kiểm tra trạng thái thoát sẽ không giúp đỡ ở đây. Thêm một cái bẫy rõ ràng trên SIGINT sẽ giúp.
Stéphane Chazelas

4

Thiết bị đầu cuối thông báo control-c và gửi INTtín hiệu đến nhóm quy trình nền trước, ở đây bao gồm cả vỏ, vì pingchưa tạo ra một nhóm quy trình tiền cảnh mới. Điều này là dễ dàng để xác minh bằng cách bẫy INT.

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

Nếu lệnh đang chạy đã tạo ra một nhóm quy trình tiền cảnh mới, thì control-c sẽ chuyển đến nhóm quy trình đó chứ không phải vào trình bao. Trong trường hợp đó, shell sẽ cần kiểm tra mã thoát, vì nó sẽ không được báo hiệu bởi thiết bị đầu cuối.

( INTbằng cách này, việc xử lý trong các shell có thể rất phức tạp, vì shell đôi khi cần bỏ qua tín hiệu, và đôi khi không. Nguồn lặn nếu tò mò hoặc suy ngẫm tail -f /etc/passwd; echo foo:)


Trong trường hợp này, vấn đề không được tín hiệu xử lý nhưng thực tế là bash không jobcontrol trong kịch bản mặc dù nó không nên, xem câu trả lời của tôi để biết thêm thông tin
Schily

Để SIGINT đi đến nhóm quy trình mới, lệnh cũng sẽ phải thực hiện một ioctl () đến thiết bị đầu cuối để biến nó thành nhóm quy trình tiền cảnh của thiết bị đầu cuối. pingkhông có lý do để bắt đầu một nhóm quy trình mới ở đây và phiên bản ping (iputils trên Debian) mà tôi có thể tái tạo vấn đề của OP không tạo ra một nhóm quy trình.
Stéphane Chazelas

1
Lưu ý rằng đó không phải là thiết bị đầu cuối gửi SIGINT, đó là kỷ luật dòng của thiết bị tty (trình điều khiển (mã trong kernel) của thiết bị / dev / ttys Something) khi nhận được ký tự không được giải mã (bởi lnext thường ^ V) ^ C ký tự từ thiết bị đầu cuối.
Stéphane Chazelas

2

Chà, tôi đã cố gắng thêm một sleep 1tập lệnh bash, và bang!
Bây giờ tôi có thể ngăn chặn nó với hai Ctrl+C.

Khi nhấn Ctrl+C, SIGINTtín hiệu được gửi đến quá trình hiện đang được thực thi, lệnh nào được chạy bên trong vòng lặp. Sau đó, quá trình subshell tiếp tục thực hiện lệnh tiếp theo trong vòng lặp, bắt đầu một quá trình khác. Để có thể dừng tập lệnh, cần phải gửi hai SIGINTtín hiệu, một để làm gián đoạn lệnh hiện tại trong thực thi và một để làm gián đoạn quá trình subshell .

Trong tập lệnh mà không có sleepcuộc gọi, nhấn Ctrl+Cthực sự nhanh và nhiều lần dường như không hoạt động và không thể thoát khỏi vòng lặp. Tôi đoán là nhấn hai lần là không đủ nhanh để làm cho nó chỉ trong một thời điểm thích hợp giữa sự gián đoạn của quá trình thực hiện hiện tại và bắt đầu quá trình tiếp theo. Mỗi lần Ctrl+Cnhấn sẽ gửi một SIGINTđến một quá trình được thực hiện bên trong vòng lặp, nhưng không gửi đến lớp con .

Trong kịch bản với sleep 1, cuộc gọi này sẽ tạm dừng thực thi trong một giây và khi bị gián đoạn bởi lần đầu tiên Ctrl+C(lần đầu tiên SIGINT), lớp con sẽ mất nhiều thời gian hơn để thực hiện lệnh tiếp theo. Vì vậy, bây giờ, thứ hai Ctrl+C(thứ hai SIGINT) sẽ đi đến lớp con và việc thực thi tập lệnh sẽ kết thúc.


Bạn đã nhầm, trên một vỏ hoạt động chính xác, một ^ C là đủ để xem câu trả lời của tôi cho nền.
schily

Chà, xem xét bạn đã bỏ phiếu và hiện tại câu trả lời của bạn có điểm -1, tôi không tin lắm nên tôi nghiêm túc trả lời.
cháu trai

Thực tế là một số người downvote không phải lúc nào cũng liên quan đến chất lượng của một câu trả lời. Nếu bạn cần gõ hai lần ^ c, bạn chắc chắn là nạn nhân của lỗi bash. Bạn đã thử một vỏ khác? Bạn đã thử Bourne Shell thực sự?
schily

Nếu shell nếu hoạt động chính xác, nó sẽ chạy mọi thứ từ một tập lệnh trong cùng một nhóm quy trình và sau đó một ^ c là đủ.
schily

1
Hành vi @nephewtom mô tả trong câu trả lời này có thể được giải thích bằng các lệnh khác nhau trong tập lệnh hành xử khác nhau khi chúng nhận được Ctrl-C. Nếu có một giấc ngủ, rất có khả năng Ctrl-C sẽ được nhận trong khi giấc ngủ đang thực thi (giả sử mọi thứ khác trong vòng lặp đều nhanh). Giấc ngủ bị giết, với giá trị thoát 130. Cha mẹ của giấc ngủ, một cái vỏ, thông báo rằng giấc ngủ đã bị giết bởi sigint và thoát ra. Nhưng nếu tập lệnh không có chế độ ngủ, thì Ctrl-C sẽ chuyển sang ping, nó sẽ phản ứng bằng cách thoát bằng 0, do đó, trình bao cha mẹ thực hiện lệnh tiếp theo.
Jonathan Hartley

0

Thử đi:

#!/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

Bây giờ thay đổi dòng đầu tiên thành:

#!/bin/sh

và thử lại - xem liệu ping có bị gián đoạn không.


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

ví dụ pgrep -f firefoxsẽ grep bộ vi xử lý đang chạy firefoxvà sẽ lưu bộ PID này vào một tệp có tên any_file_name. Lệnh 'sed' sẽ thêm killvào đầu số PID trong tệp 'any_file_name'. Dòng thứ ba sẽ any_file_nametập tin thực thi. Bây giờ dòng tiếp theo sẽ giết chết PID có sẵn trong tệp any_file_name. Viết bốn dòng trên vào một tệp và thực thi tệp đó có thể thực hiện Control- C. Làm việc hoàn toàn tốt cho tôi.


0

Nếu bất kỳ ai quan tâm đến việc sửa lỗi cho bashtính năng này và không quá quan tâm đến triết lý đằng sau nó , thì đây là một đề xuất:

Đừng chạy trực tiếp lệnh có vấn đề, nhưng từ một trình bao bọc mà a) chờ nó kết thúc b) không gây rối với tín hiệu và c) không tự thực hiện cơ chế WCE, mà chỉ chết khi nhận được a SIGINT.

Một trình bao bọc như vậy có thể được thực hiện với awk+ system()chức năng của nó .

$ 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

Đặt một tập lệnh như của OP:

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

-3

Bạn là nạn nhân của một lỗi bash nổi tiếng. Bash không kiểm soát công việc cho các kịch bản đó là một sai lầm.

Điều xảy ra là bash chạy các chương trình bên ngoài trong một nhóm quy trình khác với việc sử dụng cho chính tập lệnh. Vì nhóm quy trình TTY được đặt thành nhóm quy trình của quy trình tiền cảnh hiện tại, chỉ có quy trình tiền cảnh này bị hủy và vòng lặp trong tập lệnh shell tiếp tục.

Để xác minh: Tìm nạp và biên dịch Shell Bourne gần đây thực hiện pgrp (1) dưới dạng chương trình dựng sẵn, sau đó thêm / bin / ngủ 100 (hoặc / usr / bin / ngủ tùy thuộc vào nền tảng của bạn) vào vòng lặp tập lệnh Vỏ Bourne. Sau khi bạn sử dụng ps (1) để lấy ID tiến trình cho lệnh ngủ và bash chạy tập lệnh, hãy gọi pgrp <pid>và thay thế "<pid>" bằng ID tiến trình của chế độ ngủ và bash chạy tập lệnh. Bạn sẽ thấy ID nhóm quy trình khác nhau. Bây giờ gọi một cái gì đó như pgrp < /dev/pts/7(thay thế tên tty bằng tty được sử dụng bởi tập lệnh) để có được nhóm quy trình tty hiện tại. Nhóm quy trình TTY bằng với nhóm quy trình của lệnh ngủ.

Để khắc phục: sử dụng một vỏ khác.

Các nguồn Bourne Shell gần đây nằm trong gói công cụ schily của tôi mà bạn có thể tìm thấy ở đây:

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


Phiên bản bashđó là gì? AFAIK bashchỉ làm điều đó nếu bạn vượt qua tùy chọn -m hoặc -i.
Stéphane Chazelas

Có vẻ như điều này không còn áp dụng cho bash4 nhưng khi OP gặp vấn đề như vậy, anh ta dường như sử dụng bash3
schily

Không thể sao chép bằng bash3.2.48 hoặc bash 3.0.16 cũng như bash-2.05b (đã thử với bash -c 'ps -j; ps -j; ps -j').
Stéphane Chazelas

Điều này chắc chắn xảy ra khi bạn gọi bash là /bin/sh -ce. Tôi đã phải thêm một cách giải quyết xấu xí vào smakeđó rõ ràng sẽ giết chết nhóm quy trình cho một lệnh hiện đang chạy để cho phép ^Chủy bỏ một cuộc gọi thực hiện nhiều lớp. Bạn có kiểm tra xem bash có thay đổi nhóm quy trình từ id nhóm quy trình mà nó được khởi tạo không?
schily

ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'không báo cáo pgid giống nhau cho ps và bash trong cả 3 yêu cầu ps. (ARGV0 = sh là zshcách để vượt qua argv [0]).
Stéphane Chazelas
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.