เทคนิคที่เชื่อถือได้สำหรับการฆ่ากระบวนการพื้นหลังในการยกเลิกสคริปต์คืออะไร?


5

ฉันใช้เชลล์สคริปต์เพื่อตอบสนองต่อเหตุการณ์ของระบบและสถานะการอัปเดตจะแสดงในตัวจัดการหน้าต่างของฉัน ตัวอย่างเช่นหนึ่งสคริปต์กำหนดสถานะ wifi ปัจจุบันโดยการฟังหลายแหล่ง:

  1. เชื่อมโยง / แยกเหตุการณ์จาก wpa_supplicant
  2. การเปลี่ยนแปลงที่อยู่จาก ip (ดังนั้นฉันรู้ว่าเมื่อ dhcpcd ได้กำหนดที่อยู่)
  3. กระบวนการตัวจับเวลา (ดังนั้นความแรงของสัญญาณจะอัปเดตเป็นครั้งคราว)

เพื่อให้ได้มัลติเพล็กซ์ฉันต้องวางไข่กระบวนการพื้นหลัง:

{ wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while sleep 30; do echo; done } |
while read line; do update_wifi_status; done &

เช่นการตั้งค่าคือเมื่อใดก็ตามที่แหล่งที่มาของเหตุการณ์ส่งออกเป็นเส้นสถานะ wifi ของฉันจะอัปเดต ไปป์ไลน์ทั้งหมดทำงานในพื้นหลัง (สุดท้าย '& amp;') เพราะฉันดูแหล่งของเหตุการณ์อื่นที่ทำให้สคริปต์ของฉันสิ้นสุดลง:

wait_for_termination
kill $!

การฆ่าควรจะทำความสะอาดกระบวนการพื้นหลัง แต่ในรูปแบบนี้มันไม่ทำงาน กระบวนการ 'wpa_cli' และ 'ip' อยู่รอดอย่างน้อยที่สุดและไม่ตายในเหตุการณ์ถัดไปของพวกเขา (ในทางทฤษฎีแล้วพวกเขาควรจะได้รับ SIGPIPE ฉันคิดว่ากระบวนการอ่านจะต้องมีชีวิตอยู่ด้วย)

คำถามคือจะทำ [และอย่างหรูหรา!] ให้ทำความสะอาดกระบวนการพื้นหลังทั้งหมดที่เกิดขึ้นได้อย่างไร


1
กลุ่ม cg ทั้งหมดนั้นแม่นยำสำหรับการจัดการกับสถานการณ์เช่นนี้ บางที serverwatch.com/tutorials/article.php/3920051/... และ serverwatch.com/tutorials/article.php/3921001/... มีความช่วยเหลือ
vonbrand

ขอบคุณ! ลิงก์นั้นเน้นไปที่ประสิทธิภาพของเดสก์ท็อป แต่มีการตรวจสอบอีกเล็กน้อยมันก็ชัดเจนว่าจะใช้กลุ่ม cg สำหรับกรณีนี้ได้อย่างไร
sqweek

คำตอบ:


7

ทางออกที่ง่ายสุดคือการเพิ่มสิ่งนี้ในตอนท้ายของสคริปต์:

kill -- -$$

คำอธิบาย:

$$ ให้ PID ของเชลล์ที่ทำงาน ดังนั้น, kill $$ จะส่ง SIGTERM ไปยังกระบวนการเชลล์ อย่างไรก็ตามถ้าเรา ลบล้าง PID kill ส่ง SIGTERM ไปที่ ทุกกระบวนการในกลุ่มกระบวนการ . เราต้องการ -- ก่อนดังนั้น kill รู้ว่า -$$ เป็น ID กลุ่มกระบวนการและไม่ใช่แฟล็ก

โปรดทราบว่าสิ่งนี้อาศัยเชลล์ที่รันอยู่ซึ่งเป็นหัวหน้ากลุ่มกระบวนการ! มิฉะนั้น, $$ (PID) จะไม่ตรงกับ ID กลุ่มกระบวนการและท้ายที่สุดคุณก็ส่งสัญญาณไปยังผู้ที่รู้ว่าที่ไหนดี (อาจจะไม่มีที่ไหนเลยเพราะไม่น่าจะเป็นกลุ่มกระบวนการที่มี ID ตรงกันหากเราไม่ใช่หัวหน้ากลุ่ม) .

เมื่อเชลล์เริ่มต้นมันจะสร้างกลุ่มกระบวนการใหม่ [1] ทุกกระบวนการแยกกลายเป็นสมาชิกของกลุ่มกระบวนการนั้นเว้นแต่พวกเขาจะเปลี่ยนกลุ่มกระบวนการอย่างชัดเจนผ่าน syscall ( setpgid )

วิธีที่ง่ายที่สุดในการรับประกันสคริปต์เฉพาะจะทำงานในฐานะหัวหน้ากลุ่มกระบวนการคือเรียกใช้โดยใช้ setsid. ตัวอย่างเช่นฉันมีสคริปต์สถานะเหล่านี้บางตัวที่ฉันเรียกใช้จากสคริปต์หลัก:

#!/bin/sh
wifi_status &
bat_status &

เขียนเช่นนี้สคริปต์ wifi และ battery ทำงานกับกลุ่มกระบวนการเดียวกันกับสคริปต์หลักและ kill -- -$$ ไม่ทำงาน การแก้ไขคือ:

#!/bin/sh
setsid wifi_status &
setsid bat_status &

ฉันพบ pstree -p -g มีประโยชน์ในการสร้างภาพกระบวนการ & amp; ID กลุ่มกระบวนการ

ขอบคุณทุกคนที่มีส่วนและทำให้ฉันขุดลึกลงไปอีกนิดฉันได้เรียนรู้สิ่งต่าง ๆ ! :)

[1] มีสถานการณ์อื่นที่เชลล์สร้างกลุ่มกระบวนการหรือไม่ เช่น. ในการเริ่ม subshell? ฉันไม่รู้ ...


ทำ kill -- -$$ ฆ่าตัวตายด้วยเหรอ? ฉันสนใจที่จะสร้างบทที่สามารถฆ่าเด็ก ๆ ได้ แต่ไม่ใช่ตัวของตัวเอง
Craig McQueen

ใช่มันยังฆ่าตัวตาย หากคุณต้องการฆ่าเด็ก ๆ เท่านั้นคุณจะต้องติดตาม PID ของพวกเขาหลังจากเปิดตัว (หรือดูคำตอบอื่น ๆ ที่ยกระดับเชลล์) jobs builtin) หากเด็ก ๆ อาจแยกคุณยังอาจพบว่าเทคนิคนี้มีประโยชน์ กล่าวคือ ใช้ setsid เพื่อให้กระบวนการเด็กแต่ละกระบวนการกลายเป็นผู้นำกลุ่มแล้ว kill -- -$CHILD เพื่อฆ่าเด็กและลูกหลาน
sqweek

1

ตกลงฉันคิดวิธีแก้ปัญหาที่เหมาะสมที่ไม่ได้ใช้ cgroups มันจะไม่ทำงานเมื่อเผชิญกับกระบวนการฟอร์กเพราะ Leonardo Dagnino ชี้ให้เห็น

ปัญหาอย่างหนึ่งของการติดตาม ID กระบวนการด้วยตนเองผ่านทาง $! เพื่อฆ่าพวกเขาในภายหลังคือสภาพการแข่งขันโดยธรรมชาติ - หากกระบวนการเสร็จสิ้นก่อนที่คุณจะฆ่ามันสคริปต์จะส่งสัญญาณไปยังกระบวนการที่ไม่มีอยู่จริงหรืออาจไม่ถูกต้อง

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

ในการค้นหาวิธีการแก้ปัญหาข้างต้นฉันได้พบกับ งาน คำสั่งซึ่งก่อนหน้านี้ฉันคิดว่าใช้ได้เฉพาะกับเชลล์แบบโต้ตอบเท่านั้น ปรากฎว่ามันทำงานได้ดีมันสคริปต์และติดตามกระบวนการพื้นหลังที่เราเปิดตัวโดยอัตโนมัติ - ถ้ากระบวนการถูกยกเลิกมันจะไม่ปรากฏในรายการงานอีกต่อไป

ดังนั้นคำสั่ง:

trap 'kill $(jobs -p)' EXIT

เพียงพอที่จะทำให้แน่ใจได้ว่า - ในกรณีง่าย ๆ - การยกเลิกกระบวนการพื้นหลังเมื่อเชลล์ปัจจุบันออก

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

{ trap 'kill $(jobs -p)' EXIT
wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while echo; do sleep 30; done } |
while read line; do update_wifi_status; done &

สุดท้าย งาน -p ให้เพียง pid ของกระบวนการสุดท้ายในไปป์ไลน์ (เช่นเดียวกับ $!) อย่างที่คุณเห็นฉันกำลังวางไข่กระบวนการพื้นหลังใน เป็นครั้งแรก กระบวนการของไปป์ไลน์พื้นหลังดังนั้นฉันต้องการสัญญาณที่จ่ายเช่นกัน

pid ของกระบวนการแรกสามารถรับได้จาก งาน แต่ฉันไม่แน่ใจว่าสิ่งนี้จะประสบความสำเร็จได้อย่างไร ด้วยการใช้ bash ฉันจะได้ผลลัพธ์ในลักษณะนี้:

$ sleep 20 | sleep 20 &
$ jobs -l
[1]+ 25180 Running                 sleep 20
     25181                       | sleep 20 &

ดังนั้นโดยใช้คำสั่ง kill ที่แก้ไขเล็กน้อยจากสคริปต์แม่ฉันสามารถส่งสัญญาณกระบวนการทั้งหมดในไปป์ไลน์:

wait_for_termination
kill $(jobs -l |awk '$2 == "|" {print $1; next} {print $2}')

1
ครั้งแรก jobs -p ให้ PID ของกระบวนการแรกในไปป์ผู้นำกลุ่มกระบวนการไม่ใช่กระบวนการสุดท้าย ประการที่สอง jobs -l รายการ PID ของกระบวนการที่ถูกฆ่าหรือออกด้วยเช่นกันดังนั้นสภาพการแข่งขันที่เป็นไปได้ของคุณก็จะมีอีก ทำไมไม่ใส่ท่อเข้าไปใน subshell จริงเปิดด้วย (...)? jobs -p ส่งคืน PID ของเชลล์ย่อยนี้ หรือใช้ ps ซึ่งในคอลัมน์ PPID จะแสดงรายการ PID หลัก (เช่นของคุณ $$ ) หมายเหตุยัง $PPID ตัวแปร Bash
Andreas Spindler

1
ขอบคุณสำหรับการแก้ไข! ฉันไม่แน่ใจว่าสิ่งที่ฉันจะได้รับจากการใช้ subshell จริง - มันไม่ได้ทำให้ฉันเข้าใกล้กระบวนการเบื้องหลังที่เริ่มต้นทางด้านซ้ายของไปป์ พูดตามตรงนะ ps เป็นประโยชน์อย่างมากป่องๆนั่นคือ GNU psซึ่งฉันไม่เคยเรียนรู้ที่จะใช้ดีสำหรับการเขียนสคริปต์เพื่อประโยชน์ของเครื่องมือ saner มันฟังดูเป็นวิธีที่ดีแม้ว่า ... สำหรับกรณีของฉันมันดูเหมือน pgrep -g $$ ให้สิ่งที่ฉันต้องการอย่างแน่นอน (PID ของกระบวนการทั้งหมดที่อยู่ในกลุ่มกระบวนการเดียวกันกับ $$) ... ฉันไม่แน่ใจ 100 $ ว่าขอบเขต "กลุ่มกระบวนการ" คืออะไร
sqweek

ที่จริงฉันคิดว่ามันง่ายกว่า ... การเรียกเปลือก setpgid() เมื่อมันเริ่มขึ้นเพื่อสร้างกลุ่มกระบวนการใหม่และคุณสามารถใช้เก่าปกติ kill เพื่อส่งสัญญาณกลุ่มกระบวนการโดยใช้จำนวนลบ ดังนั้น... kill -$$ ควรฆ่าทุก ๆ งานพื้นหลัง - ตราบใดที่ไม่มีพวกมันเริ่มกลุ่มกระบวนการของตัวเอง :)
sqweek

0

คุณสามารถได้รับ PID ของพวกเขาแต่ละคนโดยการใส่สิ่งที่ชอบ

PID1=$!

หลังจาก wpa_cli

PID2=$!

หลังจาก ip และในตอนท้ายของสคริปต์ที่คุณฆ่าพวกเขาทั้งสอง:

kill $PID1
kill $PID2

อย่างไรก็ตามนั่นจะไม่ทำงานหากกระบวนการแยก จากนั้นกลุ่ม cg จะเป็นทางออกที่ดีที่สุด


ขวา. ภาวะแทรกซ้อนในกรณีนี้กระบวนการที่เป็นปัญหาจะเปิดตัวใน subshell ซึ่งคือ (a) ในพื้นหลังและ (b) ส่วนหนึ่งของไปป์ไลน์ ดังนั้นจึงไม่ใช่เรื่องง่ายเลยที่จะเผยแพร่ตัวแปร PID ที่บันทึกไว้กลับไปที่ parent shell (ซึ่งเป็นเชลล์ตัวเดียวที่รู้ว่าเมื่อไรจะยุติ) ฉันคิดว่าโครงสร้างที่สร้างจาก "Trap EXIT" จะช่วยให้ฉันสามารถทำความสะอาดกระบวนการพื้นหลังที่เปิดตัวใน subshell ได้อย่างสวยงามและจากนั้นฉันต้องการวิธีที่เชื่อถือได้ในการส่งสัญญาณการยุติลงใน subshell (ซึ่งอาจจะง่ายเหมือน kill $ !, ไม่แน่ใจ). ข้อดีของกระบวนการฟอร์กกิ้ง
sqweek
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.