ทำไม SIGINT ถึงไม่แพร่กระจายไปยังกระบวนการลูกเมื่อส่งไปยังกระบวนการแม่


62

ด้วยกระบวนการเชลล์ (เช่นsh) และกระบวนการลูก (เช่นcat) ฉันจะจำลองพฤติกรรมของCtrl+ Cโดยใช้ ID กระบวนการของเชลล์ได้อย่างไร


นี่คือสิ่งที่ฉันได้ลอง:

วิ่งshแล้วcat:

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

ส่งSIGINTไปcatจากสถานีอื่น:

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

cat รับสัญญาณและยกเลิก (ตามที่คาดไว้)

ดูเหมือนว่าการส่งสัญญาณไปยังกระบวนการหลักนั้นไม่ทำงาน ทำไมสัญญาณไม่แพร่กระจายไปcatเมื่อถูกส่งไปยังกระบวนการแม่sh?

สิ่งนี้ไม่ทำงาน:

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

1
เชลล์มีวิธีละเว้นสัญญาณ SIGINT ที่ไม่ได้ส่งจากแป้นพิมพ์หรือเทอร์มินัล
konsolebox

คำตอบ:


86

วิธีCTRL+ Cงาน

สิ่งแรกคือการเข้าใจว่าCTRL+ Cทำงานอย่างไร

เมื่อคุณกดCTRL+ Cเทอร์มินัลอีมูเลเตอร์ของคุณจะส่งอักขระ ETX (สิ้นสุดข้อความ / 0x03)
TTY ได้รับการกำหนดค่าเช่นนั้นเมื่อได้รับตัวละครนี้มันจะส่ง SIGINT ไปยังกลุ่มกระบวนการพื้นหน้าของเทอร์มินัล การกำหนดค่านี้สามารถดูได้โดยการทำและกำลังมองหาstty ข้อกำหนด POSIXกล่าวว่าเมื่ออินทร์ได้รับก็ควรส่ง SIGINT ไปยังกลุ่มกระบวนการเบื้องหน้าของอาคารว่าintr = ^C;

กลุ่มกระบวนการพื้นหน้าคืออะไร?

ดังนั้นตอนนี้คำถามคือคุณจะกำหนดกลุ่มกระบวนการพื้นหน้าได้อย่างไร กลุ่มกระบวนการพื้นหน้าเป็นเพียงกลุ่มของกระบวนการที่จะรับสัญญาณใด ๆ ที่สร้างขึ้นโดยแป้นพิมพ์ (SIGTSTOP, SIGINT, ฯลฯ )

วิธีที่ง่ายที่สุดในการพิจารณา ID กลุ่มกระบวนการคือการใช้ps:

ps ax -O tpgid

คอลัมน์ที่สองจะเป็นรหัสกลุ่มกระบวนการ

ฉันจะส่งสัญญาณไปยังกลุ่มกระบวนการได้อย่างไร

ตอนนี้เรารู้ว่า ID กลุ่มกระบวนการคืออะไรเราจำเป็นต้องจำลองพฤติกรรม POSIX ในการส่งสัญญาณไปยังกลุ่มทั้งหมด

สิ่งนี้สามารถทำได้ด้วยkillการใส่-รหัสประจำตัวด้านหน้า
ตัวอย่างเช่นถ้า ID กลุ่มกระบวนการของคุณคือ 1234 คุณจะใช้:

kill -INT -1234

 


จำลองCTRL+ Cโดยใช้หมายเลขเทอร์มินัล

ดังนั้นข้างต้นครอบคลุมถึงวิธีการจำลองCTRL+ Cเป็นกระบวนการด้วยตนเอง แต่ถ้าคุณรู้หมายเลข TTY และคุณต้องการจำลองCTRL+ Cสำหรับเทอร์มินัลนั้น

กลายเป็นเรื่องง่ายมาก

สมมติว่า$ttyเป็นเทอร์มินัลที่คุณต้องการกำหนดเป้าหมาย (คุณสามารถรับสิ่งนี้ได้จากการรันtty | sed 's#^/dev/##'ในเทอร์มินัล)

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

นี้จะส่ง SIGINT เพื่อสิ่งที่กลุ่มกระบวนการเบื้องหน้าของ$ttyคือ


6
เป็นค่าที่ชี้ให้เห็นว่าสัญญาณที่มาโดยตรงจากการตรวจสอบการอนุญาตข้ามเทอร์มินัลดังนั้น Ctrl + C จะประสบความสำเร็จในการส่งสัญญาณเสมอเว้นแต่คุณจะปิดมันในคุณสมบัติของเทอร์มินัลในขณะที่killคำสั่งอาจล้มเหลว
Brian Bi

4
+1 สำหรับsends a SIGINT to the foreground process group of the terminal.
andy

forkมูลค่าการกล่าวขวัญว่ากระบวนการกลุ่มของเด็กที่เป็นเช่นเดียวกับผู้ปกครองหลัง ตัวอย่าง C ที่รันได้
Ciro Santilli 事件改造中心中心法轮功六四事件

15

ตามที่ vinc17 พูดว่าไม่มีเหตุผลอะไรที่จะเกิดขึ้น เมื่อคุณพิมพ์ลำดับคีย์การสร้างสัญญาณ (เช่นCtrl+ C) สัญญาณจะถูกส่งไปยังกระบวนการทั้งหมดที่เชื่อมต่อกับ (เชื่อมโยงกับ) เทอร์มินัล killไม่มีกลไกดังกล่าวสำหรับสัญญาณที่สร้างขึ้นโดยเป็น

อย่างไรก็ตามคำสั่งเช่น

kill -SIGINT -12345

จะส่งสัญญาณไปยังกระบวนการทั้งหมดในกลุ่มกระบวนการ 12345; ดูฆ่า (1) และฆ่า (2) โดยทั่วไปแล้วลูก ๆ ของเชลล์จะอยู่ในกลุ่มกระบวนการของเชลล์ (อย่างน้อยถ้าพวกมันไม่ใช่แบบอะซิงโครนัส) ดังนั้นการส่งสัญญาณไปยังค่าลบ PID ของเชลล์อาจทำสิ่งที่คุณต้องการ


อุ่ย

ในขณะที่ vinc17 ชี้ให้เห็นสิ่งนี้จะไม่ทำงานสำหรับเชลล์แบบโต้ตอบ นี่คือทางเลือกที่อาจใช้งานได้:

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

ps -pPID_of_shellรับข้อมูลกระบวนการบนเชลล์  o tpgid=บอกpsให้เอาต์พุตเฉพาะ ID กลุ่มกระบวนการเทอร์มินัลโดยไม่มีส่วนหัว หากน้อยกว่า 10,000 psจะแสดงด้วยพื้นที่ว่างนำหน้า $(echo …)เป็นเคล็ดลับที่รวดเร็วในการเปลื้องผ้าชั้นนำ (และต่อท้าย) ช่องว่าง

ฉันได้รับสิ่งนี้เพื่อทำงานในการทดสอบคร่าวๆบนเครื่องเดเบียน


1
สิ่งนี้ไม่ทำงานเมื่อเริ่มกระบวนการในเชลล์แบบโต้ตอบ (ซึ่งเป็นสิ่งที่ OP ใช้) แม้ว่าฉันจะไม่มีการอ้างอิงสำหรับพฤติกรรมนี้
vinc17

12

คำถามมีคำตอบของตัวเอง ส่งSIGINTไปยังcatกระบวนการที่มีเป็นแบบจำลองที่สมบูรณ์แบบของสิ่งที่เกิดขึ้นเมื่อคุณกดkill^C

เพื่อความแม่นยำมากขึ้นอักขระขัดจังหวะ ( ^Cโดยค่าเริ่มต้น) จะส่งSIGINTไปยังทุกกระบวนการในกลุ่มกระบวนการพื้นหน้าของเทอร์มินัล หากcatคุณใช้คำสั่งที่ซับซ้อนกว่าซึ่งเกี่ยวข้องกับหลาย ๆ กระบวนการแทนที่จะต้องฆ่ากลุ่มกระบวนการเพื่อให้ได้ผลเช่น^Cเดียวกัน

เมื่อคุณรันคำสั่งภายนอกที่ไม่มีตัว&ดำเนินการพื้นหลังเชลล์จะสร้างกลุ่มกระบวนการใหม่สำหรับคำสั่งและแจ้งให้เทอร์มินัลว่ากลุ่มกระบวนการนี้อยู่ในเบื้องหน้า เชลล์ยังคงอยู่ในกลุ่มกระบวนการของตัวเองซึ่งไม่ได้อยู่ในเบื้องหน้าอีกต่อไป จากนั้นเชลล์จะรอให้คำสั่งออก

นั่นคือสิ่งที่คุณดูเหมือนจะตกเป็นเหยื่อจากความเข้าใจผิดที่พบบ่อย: ความคิดที่ว่าเชลล์กำลังทำอะไรบางอย่างเพื่ออำนวยความสะดวกในการโต้ตอบระหว่างกระบวนการลูก (es) และเทอร์มินัล นั่นไม่ใช่ความจริง เมื่อมีการทำผลงานติดตั้ง (การสร้างกระบวนการการตั้งค่าโหมด terminal, การสร้างท่อและเปลี่ยนเส้นทางของอธิบายไฟล์อื่น ๆ และดำเนินการโปรแกรมเป้าหมาย) เปลือกเพียงแค่รอ สิ่งที่คุณพิมพ์ลงจะไม่ผ่านเปลือกไม่ว่าจะเป็นการป้อนข้อมูลปกติหรือสัญญาณที่สร้างตัวละครพิเศษเช่นcat กระบวนการมีการเข้าถึงโดยตรงไปยังสถานีผ่านอธิบายไฟล์ของตัวเองและ terminal มีความสามารถในการส่งสัญญาณโดยตรงกับกระบวนการเพราะมันเป็นกลุ่มกระบวนการเบื้องหน้า^Ccatcat

หลังจากcatกระบวนการตายไปเชลล์จะได้รับแจ้งเนื่องจากเป็นพาเรนต์ของcatกระบวนการ จากนั้นเชลล์จะทำงานและทำให้ตัวเองอยู่เบื้องหน้าอีกครั้ง

นี่คือแบบฝึกหัดเพื่อเพิ่มความเข้าใจของคุณ

ที่พร้อมต์เชลล์ในเทอร์มินัลใหม่ให้รันคำสั่งนี้:

exec cat

execคำหลักที่ทำให้เกิดเปลือกในการดำเนินการcatโดยไม่ต้องสร้างกระบวนการเด็ก catเปลือกจะถูกแทนที่ด้วย หมายเลขผลิตภัณฑ์ที่เดิมเป็นเปลือกที่เป็นตอนนี้ PID catของ ตรวจสอบสิ่งนี้ด้วยpsในเทอร์มินัลอื่น พิมพ์บรรทัดสุ่มและดูว่าcatมันกลับมาหาคุณซ้ำเพื่อพิสูจน์ว่ามันยังคงทำงานได้ตามปกติแม้จะไม่มีกระบวนการเชลล์ในฐานะผู้ปกครอง จะเกิดอะไรขึ้นเมื่อคุณกด^Cตอนนี้?

ตอบ:

SIGINT ถูกส่งไปยังกระบวนการของแมวซึ่งตาย เนื่องจากเป็นกระบวนการเดียวในเทอร์มินัลเซสชันจึงสิ้นสุดลงราวกับว่าคุณได้พูดว่า "ออก" ที่พรอมต์เชลล์ เอฟเฟกต์เป็นแมวของคุณในขณะที่


เปลือกหลุดออกจากทางแล้ว +1
Piotr Dobrogost

ฉันไม่เข้าใจว่าทำไมหลังจากexec catกด^Cไม่ได้เป็นเพียง^Cแมว ทำไมมันจะยุติสิ่งcatที่แทนที่เชลล์แล้ว? ตั้งแต่เปลือกถูกแทนที่ด้วยเปลือกเป็นสิ่งที่ใช้ตรรกะของการส่ง SIGINT ^Cกับเด็กของตนเมื่อได้รับ
Steven Lu

ประเด็นก็คือเชลล์ไม่ได้ส่ง SIGINT ให้ลูกของมัน SIGINT มาจากไดรเวอร์เทอร์มินัลและถูกส่งไปยังกระบวนการพื้นหน้าทั้งหมด

3

ไม่มีเหตุผลที่จะเผยแพร่SIGINTไปยังเด็ก ยิ่งไปกว่านั้นsystem()ข้อมูลจำเพาะ POSIX บอกว่า: "ฟังก์ชั่น system () จะไม่สนใจสัญญาณ SIGINT และ SIGQUIT และจะบล็อกสัญญาณ SIGCHLD ในขณะที่รอคำสั่งให้ยุติการทำงาน"

หากเชลล์แพร่กระจายสิ่งที่ได้รับSIGINTเช่นการทำตามจริง Ctrl-C นี่หมายความว่ากระบวนการลูกจะรับSIGINTสัญญาณสองครั้งซึ่งอาจมีพฤติกรรมที่ไม่พึงประสงค์


system()เปลือกไม่ได้มีการดำเนินการนี้ด้วย แต่คุณพูดถูกถ้าจับสัญญาณ (เห็นได้ชัดว่ามันเกิดขึ้น) ก็ไม่มีเหตุผลที่จะถ่ายทอดมันลง
goldilocks

@ goldilocks ฉันได้ทำคำตอบแล้วอาจจะให้เหตุผลที่ดีกว่าก็ได้ โปรดทราบว่าเปลือกไม่สามารถรู้ได้ว่าเด็กได้รับสัญญาณแล้วจึงเป็นปัญหา
vinc17

1

setpgid ตัวอย่างขั้นต่ำสุดของกลุ่มกระบวนการ POSIX C

มันอาจจะง่ายกว่าที่จะเข้าใจด้วยตัวอย่างที่รันได้น้อยที่สุดของ API ที่ซ่อนอยู่

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 ต้นน้ำ

รวบรวมกับ:

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

ทำงานโดยไม่ต้อง setpgid

หากไม่มีอาร์กิวเมนต์ CLI ใด ๆsetpgidจะไม่ทำ:

./setpgid

ผลลัพธ์ที่เป็นไปได้:

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

และโปรแกรมหยุดทำงาน

ในฐานะที่เราสามารถมองเห็น pgid forkของกระบวนการทั้งสองจะเหมือนกันตามที่ได้รับสืบทอดมาทั่ว

จากนั้นเมื่อใดก็ตามที่คุณกดปุ่ม:

Ctrl + C

มันออกมาอีกครั้ง:

sigint parent
sigint child

นี่แสดงให้เห็นว่า:

  • เพื่อส่งสัญญาณไปยังกลุ่มกระบวนการทั้งหมดด้วย kill(-pgid, SIGINT)
  • Ctrl + C บนเทอร์มินัลส่งการฆ่าไปยังกลุ่มกระบวนการทั้งหมดโดยค่าเริ่มต้น

ออกจากโปรแกรมโดยการส่งสัญญาณที่แตกต่างกันไปทั้งกระบวนการเช่น SIGQUIT Ctrl + \กับ

ทำงานด้วย setpgid

หากคุณมีข้อโต้แย้งเช่น:

./setpgid 1

ดังนั้นเด็กจึงเปลี่ยน pgid และตอนนี้จะพิมพ์ sigint เพียงครั้งเดียวทุกครั้งจาก parent เท่านั้น:

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

และตอนนี้เมื่อใดก็ตามที่คุณกด:

Ctrl + C

ผู้ปกครองเท่านั้นที่รับสัญญาณเช่นกัน:

sigint parent

คุณยังสามารถฆ่าผู้ปกครองเหมือนเดิมด้วย SIGQUIT:

Ctrl + \

อย่างไรก็ตามตอนนี้เด็กมี PGID ที่แตกต่างกันและไม่ได้รับสัญญาณนั้น! สามารถเห็นได้จาก:

ps aux | grep setpgid

คุณจะต้องฆ่ามันอย่างชัดเจนด้วย:

kill -9 16470

สิ่งนี้ทำให้ชัดเจนว่าทำไมกลุ่มสัญญาณจึงมีอยู่: ไม่เช่นนั้นเราจะได้รับกระบวนการที่เหลือเพื่อทำความสะอาดด้วยตนเองตลอดเวลา

ทดสอบบน Ubuntu 18.04

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.