Mengapa SIGINT tidak disebarkan ke proses anak saat dikirim ke proses induknya?


62

Mengingat proses shell (mis. sh) Dan proses turunannya (mis cat), bagaimana saya bisa mensimulasikan perilaku Ctrl+ Cmenggunakan ID proses shell?


Inilah yang saya coba:

Berjalan shlalu cat:

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

Mengirim SIGINTke catdari terminal lain:

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

cat menerima sinyal dan diakhiri (seperti yang diharapkan).

Mengirim sinyal ke proses induk tampaknya tidak berfungsi. Mengapa sinyal tidak disebarkan ke catsaat dikirim ke proses induknya sh?

Ini tidak bekerja:

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

1
Shell memiliki cara untuk mengabaikan sinyal SIGINT yang tidak dikirim dari keyboard atau terminal.
konsolebox

Jawaban:


86

Cara CTRL+ Cbekerja

Hal pertama adalah memahami cara CTRL+ Cbekerja.

Ketika Anda menekan CTRL+ C, emulator terminal Anda mengirimkan karakter ETX (akhir teks / 0x03).
TTY dikonfigurasi sedemikian rupa sehingga ketika menerima karakter ini, ia mengirimkan SIGINT ke grup proses foreground terminal. Konfigurasi ini dapat dilihat dengan melakukan sttydan melihat intr = ^C;. The spesifikasi POSIX mengatakan bahwa ketika INTR diterima, harus mengirim SIGINT untuk kelompok proses latar depan terminal itu.

Apa kelompok proses latar depan?

Jadi, sekarang pertanyaannya adalah, bagaimana Anda menentukan apa grup proses latar depan? Grup proses latar depan hanyalah grup proses yang akan menerima sinyal apa pun yang dihasilkan oleh keyboard (SIGTSTOP, SIGINT, dll.).

Cara termudah untuk menentukan ID grup proses adalah dengan menggunakan ps:

ps ax -O tpgid

Kolom kedua akan menjadi ID grup proses.

Bagaimana cara mengirim sinyal ke grup proses?

Sekarang kita tahu apa ID grup proses, kita perlu mensimulasikan perilaku POSIX mengirim sinyal ke seluruh grup.

Ini dapat dilakukan dengan killmeletakkan tanda -didepan ID grup.
Misalnya, jika ID grup proses Anda 1234, Anda akan menggunakan:

kill -INT -1234

 


Simulasikan CTRL+ Cmenggunakan nomor terminal.

Jadi di atas mencakup cara mensimulasikan CTRL+ Csebagai proses manual. Tetapi bagaimana jika Anda tahu nomor TTY, dan Anda ingin mensimulasikan CTRL+ Cuntuk terminal itu?

Ini menjadi sangat mudah.

Mari kita asumsikan $ttyterminal yang ingin Anda targetkan (Anda bisa mendapatkannya dengan menjalankannya tty | sed 's#^/dev/##'di terminal).

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

Ini akan mengirimkan SIGINT ke grup proses foreground apa pun $tty.


6
Perlu ditunjukkan bahwa sinyal yang datang langsung dari terminal memeriksa izin bypass, jadi Ctrl + C selalu berhasil mengirimkan sinyal kecuali Anda mematikannya di atribut terminal, sedangkan killperintah mungkin gagal.
Brian Bi

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

Layak disebutkan bahwa kelompok proses anak sama dengan orang tua setelah fork. Minimal runnable C misalnya di: unix.stackexchange.com/a/465112/32558
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

15

Seperti yang dikatakan vinc17, tidak ada alasan untuk hal ini terjadi. Saat Anda mengetik urutan kunci penghasil sinyal (mis., Ctrl+ C), Sinyal dikirim ke semua proses yang terhubung ke (terkait dengan) terminal. Tidak ada mekanisme untuk sinyal yang dihasilkan oleh kill.

Namun, perintahnya seperti

kill -SIGINT -12345

akan mengirim sinyal ke semua proses dalam grup proses 12345; lihat membunuh (1) dan membunuh (2) . Anak-anak dari shell biasanya berada dalam grup proses shell (setidaknya, jika mereka tidak sinkron), jadi mengirimkan sinyal ke negatif PID shell dapat melakukan apa yang Anda inginkan.


Ups

Seperti yang ditunjukkan vinc17, ini tidak berfungsi untuk shell interaktif. Inilah alternatif yang mungkin berhasil:

kill -SIGINT - $ (echo $ (ps -p PID_of_shell o tpgid =))

ps -pPID_of_shellmendapat informasi proses pada shell.  o tpgid=memberitahu psuntuk menampilkan hanya ID grup proses terminal, tanpa header. Jika ini kurang dari 10.000, psakan menampilkannya dengan ruang terdepan; yang $(echo …)adalah trik cepat untuk menanggalkan terkemuka (dan trailing) spasi.

Saya mendapatkan ini bekerja dalam pengujian sepintas pada mesin Debian.


1
Ini tidak berfungsi ketika proses dimulai dalam shell interaktif (yang digunakan OP). Saya tidak punya referensi untuk perilaku ini.
vinc17

12

Pertanyaannya berisi jawabannya sendiri. Mengirimkan SIGINTke catproses dengan killadalah simulasi sempurna dari apa yang terjadi ketika Anda menekan ^C.

Untuk lebih tepatnya, karakter interupsi ( ^Csecara default) mengirim SIGINTke setiap proses dalam grup proses latar depan terminal. Jika alih-alih catAnda menjalankan perintah yang lebih rumit yang melibatkan banyak proses, Anda harus membunuh grup proses untuk mencapai efek yang sama dengan ^C.

Ketika Anda menjalankan perintah eksternal apa pun tanpa &operator latar belakang, shell membuat grup proses baru untuk perintah dan memberi tahu terminal bahwa grup proses ini sekarang berada di latar depan. Shell masih dalam grup prosesnya sendiri, yang tidak lagi berada di latar depan. Kemudian shell menunggu perintah untuk keluar.

Di situlah Anda tampaknya telah menjadi korban oleh kesalahpahaman umum: gagasan bahwa shell sedang melakukan sesuatu untuk memfasilitasi interaksi antara proses anaknya dan terminal. Itu tidak benar. Setelah melakukan pekerjaan pengaturan (proses pembuatan, pengaturan mode terminal, pembuatan pipa dan pengalihan file deskriptor lainnya, dan menjalankan program target) shell hanya menunggu . Apa yang Anda ketik cattidak akan melalui shell, apakah itu input normal atau karakter khusus penghasil sinyal ^C. The catproses memiliki akses langsung ke terminal melalui deskriptor file sendiri, dan terminal memiliki kemampuan untuk mengirim sinyal langsung ke catproses karena kelompok proses latar depan.

Setelah catproses mati, shell akan diberi tahu, karena itu adalah induk dari catproses. Kemudian shell menjadi aktif dan menempatkan dirinya di latar depan lagi.

Inilah latihan untuk meningkatkan pemahaman Anda.

Pada prompt shell di terminal baru, jalankan perintah ini:

exec cat

Kata execkunci menyebabkan shell dieksekusi cattanpa membuat proses anak. Shell diganti oleh cat. PID yang sebelumnya milik shell sekarang menjadi PID dari cat. Verifikasi ini dengan psdi terminal yang berbeda. Ketik beberapa baris acak dan lihat yang catmengulanginya kembali kepada Anda, membuktikan bahwa itu masih berperilaku normal meskipun tidak memiliki proses shell sebagai orangtua. Apa yang akan terjadi ketika Anda menekan ^Csekarang?

Menjawab:

SIGINT dikirim ke proses kucing, yang mati. Karena itu adalah satu-satunya proses di terminal, sesi berakhir, sama seperti jika Anda mengatakan "keluar" pada prompt shell. Akibatnya kucing adalah cangkang Anda untuk sementara waktu.


Shell sudah keluar dari jalan. +1
Piotr Dobrogost

Saya tidak mengerti mengapa setelah exec catmenekan ^Ctidak hanya akan ^Cmenjadi kucing. Mengapa itu mengakhiri catyang sekarang telah menggantikan shell? Karena shell telah diganti, shell adalah hal yang mengimplementasikan logika mengirim SIGINT kepada anak-anaknya setelah menerima ^C.
Steven Lu

Intinya adalah bahwa shell tidak mengirim SIGINT ke anak-anaknya. SIGINT berasal dari driver terminal, dan dikirim ke semua proses latar depan.

3

Tidak ada alasan untuk menyebarkan SIGINTke anak. Selain itu system()spesifikasi POSIX mengatakan: "Fungsi sistem () harus mengabaikan sinyal SIGINT dan SIGQUIT, dan akan memblokir sinyal SIGCHLD, sambil menunggu perintah untuk berakhir."

Jika shell menyebarkan diterima SIGINT, misalnya mengikuti Ctrl-C nyata, ini berarti bahwa proses anak akan menerima SIGINTsinyal dua kali, yang mungkin memiliki perilaku yang tidak diinginkan.


Shell tidak harus mengimplementasikan ini dengan system(). Tapi Anda benar, jika itu menangkap sinyal (jelas itu benar) maka tidak ada alasan untuk menyebarkannya ke bawah.
goldilocks

@goldilocks Saya telah menyelesaikan jawaban saya, mungkin memberikan alasan yang lebih baik. Perhatikan bahwa shell tidak dapat mengetahui apakah anak telah menerima sinyal, karena itu masalahnya.
vinc17

1

setpgid Contoh minimal grup proses POSIX C

Mungkin lebih mudah untuk memahami dengan contoh API yang bisa dijalankan yang minimal bisa dijalankan.

Ini menggambarkan bagaimana sinyal dikirim ke anak, jika anak tidak mengubah grup prosesnya setpgid.

main.c

#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 hulu .

Kompilasi dengan:

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

Jalankan tanpa setpgid

Tanpa argumen CLI, setpgidtidak akan dilakukan:

./setpgid

Hasil yang mungkin:

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

dan programnya hang.

Seperti yang bisa kita lihat, pgid dari kedua proses itu sama, seperti yang diwarisi fork.

Lalu setiap kali Anda menekan:

Ctrl + C

Output lagi:

sigint parent
sigint child

Ini menunjukkan bagaimana:

  • untuk mengirim sinyal ke seluruh grup proses dengan kill(-pgid, SIGINT)
  • Ctrl + C pada terminal mengirimkan kill ke seluruh grup proses secara default

Tutup program dengan mengirimkan sinyal berbeda ke kedua proses, misalnya SIGQUIT dengan Ctrl + \.

Jalankan dengan setpgid

Jika Anda menjalankan argumen, misalnya:

./setpgid 1

kemudian anak mengubah pgid-nya, dan sekarang hanya satu sigint yang dicetak setiap waktu dari induknya saja:

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

Dan sekarang, setiap kali Anda menekan:

Ctrl + C

hanya orang tua yang menerima sinyal juga:

sigint parent

Anda masih dapat membunuh orang tua seperti sebelumnya dengan SIGQUIT:

Ctrl + \

namun anak sekarang memiliki PGID yang berbeda, dan tidak menerima sinyal itu! Ini bisa dilihat dari:

ps aux | grep setpgid

Anda harus membunuhnya secara eksplisit dengan:

kill -9 16470

Ini memperjelas mengapa grup sinyal ada: jika tidak kita akan mendapatkan banyak proses yang tersisa untuk dibersihkan secara manual setiap saat.

Diuji pada Ubuntu 18.04.

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.