Tại sao SIGINT không được truyền đến tiến trình con khi được gửi đến tiến trình cha của nó?


62

Đưa ra một quy trình shell (ví dụ sh) và quy trình con của nó (ví dụ cat), làm cách nào tôi có thể mô phỏng hành vi của Ctrl+ Cbằng cách sử dụng ID tiến trình của shell?


Đây là những gì tôi đã thử:

Chạy shrồi cat:

[user@host ~]$ sh
sh-4.3$ cat
test
test

Gửi SIGINTđến cattừ thiết bị đầu cuối khác:

[user@host ~]$ kill -SIGINT $PID_OF_CAT

cat nhận được tín hiệu và chấm dứt (như mong đợi).

Gửi tín hiệu đến tiến trình cha dường như không hoạt động. Tại sao tín hiệu không được truyền đến catkhi được gửi đến tiến trình cha của nó sh?

Điều này không hoạt động:

[user@host ~]$ kill -SIGINT $PID_OF_SH

1
Shell có cách bỏ qua các tín hiệu SIGINT không được gửi từ bàn phím hoặc thiết bị đầu cuối.
konsolebox

Câu trả lời:


86

Làm thế nào CTRL+ Ccông trình

Điều đầu tiên là hiểu cách CTRL+ Choạt động.

Khi bạn nhấn CTRL+ C, trình giả lập thiết bị đầu cuối của bạn sẽ gửi một ký tự ETX (cuối văn bản / 0x03).
TTY được cấu hình sao cho khi nhận được ký tự này, nó sẽ gửi SIGINT đến nhóm quy trình nền trước của thiết bị đầu cuối. Cấu hình này có thể được xem bằng cách làm sttyvà nhìn vào intr = ^C;. Đặc tả POSIX nói rằng khi nhận được INTR, nó sẽ gửi SIGINT đến nhóm quy trình nền trước của thiết bị đầu cuối đó.

Nhóm quá trình tiền cảnh là gì?

Vì vậy, bây giờ câu hỏi là, làm thế nào để bạn xác định nhóm quá trình tiền cảnh là gì? Nhóm quy trình tiền cảnh chỉ đơn giản là nhóm các quy trình sẽ nhận bất kỳ tín hiệu nào được tạo bởi bàn phím (SIGTSTOP, SIGINT, v.v.).

Cách đơn giản nhất để xác định ID nhóm quy trình là sử dụng ps:

ps ax -O tpgid

Cột thứ hai sẽ là ID nhóm quy trình.

Làm cách nào để gửi tín hiệu đến nhóm quy trình?

Bây giờ chúng ta đã biết ID nhóm quy trình là gì, chúng ta cần mô phỏng hành vi POSIX gửi tín hiệu đến toàn bộ nhóm.

Điều này có thể được thực hiện bằng killcách đặt -trước ID nhóm.
Ví dụ: nếu ID nhóm quy trình của bạn là 1234, bạn sẽ sử dụng:

kill -INT -1234

 


Mô phỏng CTRL+ Csử dụng số thiết bị đầu cuối.

Vì vậy, ở trên bao gồm làm thế nào để mô phỏng CTRL+ Cnhư là một quá trình thủ công. Nhưng nếu bạn biết số TTY và bạn muốn mô phỏng CTRL+ Ccho thiết bị đầu cuối đó thì sao?

Điều này trở nên rất dễ dàng.

Giả sử $ttylà thiết bị đầu cuối bạn muốn nhắm mục tiêu (bạn có thể có được điều này bằng cách chạy tty | sed 's#^/dev/##'trong thiết bị đầu cuối).

kill -INT -$(ps h -t $tty -o tpgid | uniq)

Điều này sẽ gửi SIGINT cho bất kỳ nhóm quy trình tiền cảnh nào $tty.


6
Thật đáng để chỉ ra rằng các tín hiệu đến trực tiếp từ kiểm tra quyền bỏ qua thiết bị đầu cuối, vì vậy Ctrl + C luôn thành công trong việc cung cấp tín hiệu trừ khi bạn tắt nó trong các thuộc tính đầu cuối, trong khi killlệnh có thể thất bại.
Brian Bi

4
+1, chosends a SIGINT to the foreground process group of the terminal.
andy

Đáng nói là nhóm quá trình của trẻ cũng giống như cha mẹ sau fork. Ví dụ C có thể chạy tối thiểu tại: unix.stackexchange.com/a/465112/32558
Ciro Santilli 改造

15

Như vinc17 nói, không có lý do gì để điều này xảy ra. Khi bạn nhập một chuỗi khóa tạo tín hiệu (ví dụ: Ctrl+ C), tín hiệu được gửi đến tất cả các quy trình được gắn vào (được liên kết với) thiết bị đầu cuối. Không có cơ chế như vậy cho các tín hiệu được tạo ra bởi kill.

Tuy nhiên, một lệnh như

kill -SIGINT -12345

sẽ gửi tín hiệu đến tất cả các quy trình trong nhóm quy trình 12345; xem giết (1)giết (2) . Trẻ em của vỏ thường nằm trong nhóm quy trình của vỏ (ít nhất, nếu chúng không đồng bộ), do đó, việc gửi tín hiệu đến âm của PID của vỏ có thể làm những gì bạn muốn.


Úi

Như vinc17 chỉ ra, điều này không hoạt động đối với các vỏ tương tác. Đây là một giải pháp thay thế có thể hoạt động:

giết -SIGINT - $ (echo $ (ps -p PID_of_shell o tpgid =))

ps -pPID_of_shellđược thông tin quá trình trên vỏ.  o tpgid=chỉ cho psđầu ra ID nhóm quá trình đầu cuối, không có tiêu đề. Nếu giá trị này nhỏ hơn 10000, pssẽ hiển thị nó với (các) không gian hàng đầu; đây $(echo …)là một mẹo nhanh để loại bỏ các không gian hàng đầu (và dấu).

Tôi đã làm điều này để làm việc trong thử nghiệm chữ thảo trên máy Debian.


1
Điều này không hoạt động khi quá trình được bắt đầu trong một vỏ tương tác (đó là những gì OP đang sử dụng). Tôi không có một tài liệu tham khảo cho hành vi này, mặc dù.
vinc17

12

Câu hỏi chứa câu trả lời của riêng nó. Gửi SIGINTđến các catquá trình với killmột mô phỏng hoàn hảo của những gì sẽ xảy ra khi bạn nhấn ^C.

Nói chính xác hơn, ký tự ngắt ( ^Ctheo mặc định) sẽ gửi SIGINTđến mọi tiến trình trong nhóm quy trình nền trước của thiết bị đầu cuối. Nếu thay vì catbạn đang chạy một lệnh phức tạp hơn liên quan đến nhiều quy trình, bạn sẽ phải giết nhóm quy trình để đạt được hiệu quả tương tự như ^C.

Khi bạn chạy bất kỳ lệnh bên ngoài nào mà không có &toán tử nền, shell sẽ tạo một nhóm quy trình mới cho lệnh và thông báo cho thiết bị đầu cuối rằng nhóm quy trình này hiện đang ở phía trước. Shell vẫn nằm trong nhóm quy trình riêng của nó, không còn ở phía trước. Sau đó, shell chờ lệnh thoát.

Đó là nơi mà bạn dường như đã trở thành nạn nhân bởi một quan niệm sai lầm phổ biến: ý tưởng rằng cái vỏ đang làm gì đó để tạo thuận lợi cho sự tương tác giữa quá trình con của nó và thiết bị đầu cuối. Điều đó không đúng. Khi nó đã thực hiện công việc thiết lập (tạo quy trình, cài đặt chế độ đầu cuối, tạo đường ống và chuyển hướng của các mô tả tệp khác và thực hiện chương trình đích), vỏ chỉ chờ . Những gì bạn nhập vào catsẽ không đi qua trình bao, cho dù đó là đầu vào bình thường hay ký tự đặc biệt tạo tín hiệu như thế nào ^C. Các catquá trình có thể truy cập trực tiếp đến các thiết bị đầu cuối thông qua file descriptor riêng của mình, và các thiết bị đầu cuối có khả năng gửi tín hiệu trực tiếp đến catquá trình bởi vì đó là nhóm quá trình foreground.

Sau khi catquá trình chết, shell sẽ được thông báo, vì đó là cha mẹ của catquá trình. Sau đó, vỏ sẽ hoạt động và đặt nó ở phía trước một lần nữa.

Đây là một bài tập để tăng sự hiểu biết của bạn.

Tại dấu nhắc shell trong thiết bị đầu cuối mới, hãy chạy lệnh này:

exec cat

Các exectừ khóa làm cho vỏ để thực hiện catmà không cần tạo một tiến trình con. Vỏ được thay thế bởi cat. PID trước đây thuộc về shell bây giờ là PID của cat. Xác nhận điều này với pstrong một thiết bị đầu cuối khác. Nhập một số dòng ngẫu nhiên và thấy rằng catlặp lại chúng cho bạn, chứng tỏ rằng nó vẫn hoạt động bình thường mặc dù không có quá trình vỏ như cha mẹ. Điều gì sẽ xảy ra khi bạn nhấn ^Cbây giờ?

Câu trả lời:

SIGINT được chuyển đến quá trình mèo, chết. Bởi vì đó là quá trình duy nhất trên thiết bị đầu cuối, phiên kết thúc, giống như bạn đã nói "thoát" tại dấu nhắc trình bao. Trong thực tế, con mèo vỏ của bạn trong một thời gian.


Vỏ đã thoát ra khỏi đường đi. +1
Piotr Dobrogost

Tôi không hiểu tại sao sau khi exec catnhấn ^Csẽ không ^Crơi vào mèo. Tại sao nó sẽ chấm dứt cái catmà bây giờ đã thay thế vỏ? Kể từ khi vỏ được thay thế, vỏ là thứ thực hiện logic gửi SIGINT cho con của nó khi nhận ^C.
Steven Lu

Vấn đề là cái vỏ không gửi SIGINT cho con của nó. SIGINT xuất phát từ trình điều khiển đầu cuối và được gửi đến tất cả các quy trình nền trước.

3

Không có lý do để tuyên truyền SIGINTcho đứa trẻ. Ngoài ra, system()đặc tả POSIX cho biết: "Hàm system () sẽ bỏ qua các tín hiệu SIGINT và SIGQUIT và sẽ chặn tín hiệu SIGCHLD, trong khi chờ lệnh kết thúc."

Nếu hệ vỏ truyền tín hiệu đã nhận SIGINT, ví dụ: theo Ctrl-C thực, điều này có nghĩa là quá trình con sẽ nhận được SIGINTtín hiệu hai lần, có thể có hành vi không mong muốn.


Shell không phải thực hiện điều này với system(). Nhưng bạn đã đúng, nếu nó bắt được tín hiệu (rõ ràng là như vậy) thì không có lý do gì để truyền tín hiệu xuống dưới.
goldilocks

@goldilocks Tôi đã hoàn thành câu trả lời của mình, có lẽ đưa ra lý do tốt hơn. Lưu ý rằng shell không thể biết liệu đứa trẻ đã nhận được tín hiệu hay chưa, do đó có vấn đề.
vinc17

1

setpgid Ví dụ tối thiểu nhóm quy trình POSIX C

Có thể dễ hiểu hơn với một ví dụ có thể chạy tối thiểu của API cơ bản.

Điều này minh họa cách tín hiệu được gửi đến trẻ, nếu trẻ không thay đổi nhóm quy trình setpgid.

C chính

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

volatile sig_atomic_t is_child = 0;

void signal_handler(int sig) {
    char parent_str[] = "sigint parent\n";
    char child_str[] = "sigint child\n";
    signal(sig, signal_handler);
    if (sig == SIGINT) {
        if (is_child) {
            write(STDOUT_FILENO, child_str, sizeof(child_str) - 1);
        } else {
            write(STDOUT_FILENO, parent_str, sizeof(parent_str) - 1);
        }
    }
}

int main(int argc, char **argv) {
    pid_t pid, pgid;

    (void)argv;
    signal(SIGINT, signal_handler);
    signal(SIGUSR1, signal_handler);
    pid = fork();
    assert(pid != -1);
    if (pid == 0) {
        is_child = 1;
        if (argc > 1) {
            /* Change the pgid.
             * The new one is guaranteed to be different than the previous, which was equal to the parent's,
             * because `man setpgid` says:
             * > the child has its own unique process ID, and this PID does not match
             * > the ID of any existing process group (setpgid(2)) or session.
             */
            setpgid(0, 0);
        }
        printf("child pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)getpgid(0));
        assert(kill(getppid(), SIGUSR1) == 0);
        while (1);
        exit(EXIT_SUCCESS);
    }
    /* Wait until the child sends a SIGUSR1. */
    pause();
    pgid = getpgid(0);
    printf("parent pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)pgid);
    /* man kill explains that negative first argument means to send a signal to a process group. */
    kill(-pgid, SIGINT);
    while (1);
}

GitHub ngược dòng .

Biên dịch với:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -Wpedantic -o setpgid setpgid.c

Chạy mà không có setpgid

Nếu không có bất kỳ đối số CLI nào, setpgidsẽ không được thực hiện:

./setpgid

Kết quả có thể xảy ra:

child pid, pgid = 28250, 28249
parent pid, pgid = 28249, 28249
sigint parent
sigint child

và chương trình bị treo.

Như chúng ta có thể thấy, pgid của cả hai quá trình là như nhau, vì nó được kế thừa qua fork.

Sau đó, bất cứ khi nào bạn nhấn:

Ctrl + C

Nó xuất ra một lần nữa:

sigint parent
sigint child

Điều này cho thấy làm thế nào:

  • để gửi tín hiệu đến toàn bộ nhóm quy trình với kill(-pgid, SIGINT)
  • Ctrl + C trên thiết bị đầu cuối gửi lệnh giết cho toàn bộ nhóm quy trình theo mặc định

Thoát khỏi chương trình bằng cách gửi tín hiệu khác nhau cho cả hai quá trình, ví dụ SIGQUIT với Ctrl + \.

Chạy với setpgid

Nếu bạn chạy với một đối số, ví dụ:

./setpgid 1

sau đó đứa trẻ thay đổi pgid của nó, và bây giờ chỉ có một sigint duy nhất được in mỗi lần từ cha mẹ:

child pid, pgid = 16470, 16470
parent pid, pgid = 16469, 16469
sigint parent

Và bây giờ, bất cứ khi nào bạn nhấn:

Ctrl + C

chỉ có cha mẹ nhận được tín hiệu là tốt:

sigint parent

Bạn vẫn có thể giết cha mẹ như trước với SIGQUIT:

Ctrl + \

tuy nhiên hiện tại trẻ có PGID khác và không nhận được tín hiệu đó! Điều này có thể thấy từ:

ps aux | grep setpgid

Bạn sẽ phải giết nó một cách rõ ràng với:

kill -9 16470

Điều này cho thấy rõ lý do tại sao các nhóm tín hiệu tồn tại: nếu không chúng ta sẽ có một loạt các quy trình còn sót lại để được làm sạch thủ công mọi lúc.

Đã thử nghiệm trên Ubuntu 18.04.

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.