ย้อนกลับไปที่ "ข่าวสำหรับผู้พัฒนา"

Signals in prod: dangers and pitfalls

ในบล็อกโพสต์นี้ Chris Down วิศวกร Kernel ที่ Meta จะพูดถึงข้อผิดพลาดของการใช้สัญญาณ Linux ในสภาพแวดล้อมการใช้งานจริงของ Linux และเหตุผลที่ผู้พัฒนาควรหลีกเลี่ยงการใช้สัญญาณทุกครั้งหากเป็นไปได้

สัญญาณ Linux คืออะไร

สัญญาณคือเหตุการณ์ที่ระบบ Linux สร้างขึ้นเพื่อตอบสนองต่อเงื่อนไขบางประการ โดยสัญญาณอาจถูกส่งจาก kernel ไปยังกระบวนการหนึ่ง ถูกส่งจากกระบวนการหนึ่งไปอีกกระบวนการหนึ่ง หรือถูกส่งจากกระบวนการหนึ่งไปยังตัวมันเอง เมื่อได้รับสัญญาณแล้ว กระบวนการก็อาจดำเนินการได้

สัญญาณเป็นส่วนสำคัญของสภาพแวดล้อมการดำเนินงานแบบ Unix และเป็นสิ่งที่มีมาอย่างน้อยก็ตั้งแต่ช่วงแรกเริ่มแล้ว โดยเป็นเหมือนระบบประปาสำหรับองค์ประกอบหลักหลายอย่างของระบบปฏิบัติการ อาทิเช่น การถ่ายโอนข้อมูลหลัก (core dump), การจัดการวงจรชีวิตของกระบวนการ ฯลฯ และโดยทั่วไปแล้วก็ทำหน้าที่ได้ค่อนข้างดีตลอดระยะเวลาห้าสิบกว่าปีที่เราใช้งานมา ด้วยเหตุนี้ เมื่อมีคนชี้ให้เห็นว่าการใช้สัญญาณเพื่อการสื่อสารระหว่างกระบวนการ (IPC) อาจเป็นอันตรายได้จึงทำให้บางคนอาจคิดว่าคำพูดเหล่านี้เป็นเพียงความเห็นเลื่อนลอยของผู้ที่ต้องการคิดค้นวิธีที่ซ้ำซ้อนกับวิธีที่ดีอยู่แล้ว อย่างไรก็ตาม บทความนี้มีวัตถุประสงค์เพื่อสาธิตกรณีที่สัญญาณเป็นสาเหตุของปัญหาการใช้งานจริง และเสนอวิธีบรรเทาผลกระทบที่อาจเกิดขึ้นรวมถึงทางเลือกอื่นๆ

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

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

เหตุการณ์

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

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

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

Logrotate เข้ามามีบทบาท

Logrotate เป็นเครื่องมือมาตรฐานสำหรับการหมุนเวียนบันทึกเมื่อใช้งาน Linux ซึ่งเครื่องมือนี้มีมาเกือบสามสิบปีแล้ว และมีแนวคิดเรียบง่าย กล่าวคือ จัดการวงจรชีวิตของบันทึกด้วยการหมุนเวียนและลบบันทึกเหล่านั้น

Logrotate ไม่ได้ส่งสัญญาณใดๆ ด้วยตัวเอง ดังนั้นคุณจะไม่พบข้อมูลอะไรเกี่ยวกับสัญญาณเหล่านี้ในหน้าหลักของ Logrotate หรือเอกสารประกอบมากนัก อย่างไรก็ตาม Logrotate สามารถใช้คำสั่งใดก็ได้เพื่อดำเนินการก่อนหรือหลังการหมุนเวียน ดังเช่นตัวอย่างพื้นฐานจากการกำหนดค่าเริ่มต้นของ Logrotate ใน CentOS ซึ่งคุณสามารถดูการกำหนดค่าได้ดังนี้

/var/log/cron
/var/log/maillog
/var/log/messages
/var/log/secure
/var/log/spooler
{
    sharedscripts
    postrotate
        /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
    endscript
}

ตัวอย่างที่ยกมานี้ค่อนข้างเปราะบาง แต่เราจะละไว้และถือว่าการกำหนดค่านี้ใช้งานได้ตามที่ตั้งใจไว้ การกำหนดค่านี้บอกว่าหลังจาก Logrotate หมุนเวียนไฟล์ใดก็ตามที่อยู่ในรายการ Logrotate ควรส่ง SIGHUP ไปยัง PID ที่มีอยู่ใน /var/run/syslogd.pid ซึ่งควรเป็นของอินสแตนซ์ syslogd ที่ทำงานอยู่

การกำหนดค่านี้ดีสำหรับเครื่องมือที่มี API สาธารณะที่เสถียร เช่น syslog แต่กับเครื่องมือภายในที่การใช้งาน SIGHUP เป็นรายละเอียดการใช้งานภายในที่สามารถเปลี่ยนแปลงได้ตลอดเวลาล่ะ

ประวัติของการหยุดชะงัก

ปัญหาอย่างหนึ่งที่เกิดขึ้นก็คือ ความหมายเชิงนัยของสัญญาณนั้นขึ้นอยู่กับการตีความและตั้งโปรแกรมของผู้พัฒนาแอพพลิเคชั่นและผู้ใช้ ยกเว้นสัญญาณที่ไม่สามารถตรวจจับได้ในพื้นที่ของผู้ใช้และมีความหมายเดียว เช่น SIGKILL และ SIGSTOP ในบางกรณี ความแตกต่างส่วนใหญ่เป็นไปในทางทฤษฎี เช่น SIGTERM ซึ่งเข้าใจกันโดยทั่วไปว่าหมายถึง "ยุติอย่างสง่างามโดยเร็วที่สุด" อย่างไรก็ตาม ในกรณีของ SIGHUP ความหมายจะมีความชัดเจนน้อยกว่ากันมาก

SIGHUP ถูกคิดค้นขึ้นมาสำหรับการสื่อสารแบบอนุกรม (serial lines) และเดิมทีใช้เพื่อระบุว่าปลายทางอีกด้านหนึ่งของการเชื่อมต่อได้มีการส่งข้อมูลมา หากเปรียบเทียบกับการที่เรายังคงสืบสายเลือดของเรามาจนถึงทุกวันนี้ SIGHUP ก็ยังคงถูกส่งไปยังที่ซึ่งเทอร์มินัลเทียม (pseudo terminal) หรือเทอร์มินัลเสมือนถูกปิด (จึงมีการใช้เครื่องมืออย่าง nohup ที่ทำการ mask)

ในยุคแรกๆ ของ Unix มีความจำเป็นต้องทำการรีโหลด daemon ซึ่งโดยปกติแล้ว อย่างน้อยๆ จะประกอบด้วยการกำหนดค่า/ไฟล์บันทึกที่เปิดใหม่โดยไม่ต้องรีสตาร์ท และสัญญาณดูเหมือนจะเป็นวิธีที่ปราศจากการพึ่งพาเพื่อให้บรรลุเป้าหมายดังกล่าว แน่นอนว่าไม่มีสัญญาณสำหรับอะไรแบบนั้น แต่เนื่องจาก daemon เหล่านี้ไม่มีเทอร์มินัลการควบคุม จึงไม่มีเหตุผลที่จะรับ SIGHUP ดังนั้นสัญญาณนี้จึงดูเหมือนจะเป็นสัญญาณที่สะดวกต่อการแอบเข้าถึงโดยไม่มีผลข้างเคียงใดๆ ที่ชัดเจน

แต่ถึงอย่างนั้นแผนนี้ก็มีปัญหาอยู่เล็กน้อย โดยสถานะเริ่มต้นสำหรับสัญญาณไม่ได้เป็นแบบ "ละเว้น" แต่เป็นแบบเฉพาะสัญญาณ ตัวอย่างเช่น โปรแกรมไม่จำเป็นต้องกำหนดค่า SIGTERM ด้วยตนเองเพื่อยุติแอพพลิเคชั่นของตน ตราบใดที่ไม่ได้มีการตั้งค่าตัวจัดการสัญญาณอื่นๆ Kernel ก็จะยุติโปรแกรมของพวกเขาฟรีโดยไม่จำเป็นต้องมีโค้ดใดๆ ในพื้นที่ผู้ใช้ ซึ่งนับว่าสะดวกมาก

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

แน่นอนว่ามันจะเป็นการดีถ้าหากเราลบทุกที่ที่อาจส่ง SIGHUP ไปยังโปรแกรมออกไปให้หมด ซึ่งปัญหามีอยู่ว่าในโค้ดเบสขนาดใหญ่ที่เติบโตเต็มที่นั้นลบได้ยาก SIGHUP ไม่เหมือนกับการเรียก IPC ที่ควบคุมอย่างเข้มงวดซึ่งคุณสามารถใช้คำสั่ง grep โค้ดเบสได้อย่างง่ายดาย สัญญาณนั้นมาได้จากทุกที่ ทุกเวลา และมีการตรวจสอบการทำงานของสัญญาณเพียงเล็กน้อย (นอกเหนือจากการตรวจสอบขั้นพื้นฐานที่สุดอย่าง "คุณคือผู้ใช้รายนี้หรือมี CAP_KILL") ประเด็นสำคัญที่สุดก็คือ มันยากที่จะระบุได้ว่าสัญญาณจะมาจากไหนได้บ้าง แต่ IPC ที่ชัดเจนกว่านี้จะทำให้เรารู้ว่าสัญญาณนี้ไม่ได้มีความหมายอะไรสำหรับเราและควรได้รับการเพิกเฉย

จากการหยุดชะงักสู่ภัยอันตราย

ถึงตอนนี้ ผมคิดว่าคุณน่าจะเริ่มเดาได้แล้วว่าเกิดอะไรขึ้น การเปิดตัว LogDevice เริ่มต้นในบ่ายวันหนึ่งที่โชคชะตาฟ้าลิขิต โดยมีการเปลี่ยนแปลงโค้ดตามที่ได้พูดถึงไปแล้วก่อนหน้านี้ ในตอนแรกไม่มีอะไรผิดปกติ แต่พอถึงเที่ยงคืนของวันรุ่งขึ้น ทุกสิ่งทุกอย่างก็เริ่มพังทลายลงอย่างลึกลับ โดยสาเหตุก็คือชุดคำสั่งต่อไปนี้ในการกำหนดค่า Logrotate ของแมชชีนซึ่งส่ง SIGHUP ที่ไม่สามารถจัดการได้ในขณะนี้ (และส่งผลร้ายแรง) ไปยัง daemon ของ LogDevice

/var/log/logdevice/audit.log {
  daily
  # [...]
  postrotate
    pkill -HUP logdeviced
  endscript
}

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

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

อันตรายของสัญญาณ

สัญญาณเหมาะกับอะไร

อย่างแรก การใช้สัญญาณเป็นกลไกที่ส่งผลต่อการเปลี่ยนแปลงในสถานะกระบวนการของระบบปฏิบัติการ เป็น รากฐานที่ดี ซึ่งประกอบด้วยสัญญาณอย่าง SIGKILL ซึ่งเป็นไปไม่ได้ที่จะติดตั้งตัวจัดการสัญญาณนี้ แล้วทำสิ่งที่คุณคาดหวังทุกประการ และลักษณะการทำงานเริ่มต้นของ Kernel ของ SIGABRT, SIGTERM, SIGINT, SIGSEGV และ SIGQUIT รวมถึงสัญญาณอื่นๆ ที่คล้ายกัน ซึ่งโดยทั่วไปแล้วผู้ใช้และโปรแกรมเมอร์จะเข้าใจเป็นอย่างดี

สิ่งที่สัญญาณเหล่านี้มีเหมือนกันก็คือ เมื่อคุณได้รับสัญญาณแล้ว สัญญาณทั้งหมดจะเข้าสู่สถานะของเทอร์มินัลปลายทางภายใน Kernel เอง กล่าวคือ จะไม่มีการดำเนินการตามขั้นตอนของพื้นที่ผู้ใช้อีกต่อไปเมื่อคุณได้รับ SIGKILL หรือ SIGTERM ที่ไม่มีตัวจัดการสัญญาณพื้นที่ผู้ใช้

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

ลักษณะการทำงานเริ่มต้นที่เป็นอันตราย

คุณอาจสังเกตเห็นว่าเราไม่ได้พูดถึงสัญญาณอื่นๆ บางสัญญาณที่ยุติการใช้งานโดยค่าเริ่มต้นเหมือนกัน ต่อไปนี้คือรายชื่อสัญญาณมาตรฐานทั้งหมดที่ยุติการใช้งานโดยค่าเริ่มต้น (ยกเว้นสัญญาณการถ่ายโอนข้อมูลหลัก (core dump) อย่าง SIGABRT หรือ SIGSEGV เนื่องจากสัญญาณเหล่านี้สมเหตุสมผลทั้งหมด):

  • SIGALRM
  • SIGEMT
  • SIGHUP
  • SIGINT
  • SIGIO
  • SIGKILL
  • SIGLOST
  • SIGPIPE
  • SIGPOLL
  • SIGPROF
  • SIGPWR
  • SIGSTKFLT
  • SIGTERM
  • SIGUSR1
  • SIGUSR2
  • SIGVTALRM

เมื่อมองแวบแรก สัญญาณเหล่านี้อาจดูสมเหตุสมผล แต่ขอให้ดูความผิดปกติต่อไปนี้

  • SIGHUP: หากใช้สัญญาณนี้ตามที่ตั้งใจไว้แต่แรก การกำหนดให้การยุติเป็นค่าเริ่มต้นจะถือว่าสมเหตุสมผล แต่ด้วยการใช้งานแบบผสมผสานในปัจจุบันที่หมายถึง "เปิดไฟล์อีกครั้ง" สัญญาณนี้จึงนับว่าอันตราย
  • SIGPOLL และ SIGPROF: ทั้งสองสัญญาณนี้จัดอยู่ในประเภทที่ "ควรให้ฟังก์ชั่นมาตรฐานจัดการเป็นการภายในมากกว่าโปรแกรมของคุณ" แต่ถึงแม้ว่าอาจจะไม่อันตราย แต่ลักษณะการทำงานเริ่มต้นในการยุติยังคงดูไม่ค่อยเหมาะเท่าใดนัก
  • SIGUSR1 และ SIGUSR2: ทั้งสองสัญญาณนี้คือ "สัญญาณที่ผู้ใช้กำหนด" ซึ่งแน่นอนว่าคุณสามารถใช้งานได้ตามที่คุณต้องการ แต่เนื่องจากสัญญาณเหล่านี้เป็นเทอร์มินัลโดยค่าเริ่มต้น หากคุณใช้ USR1 สำหรับความต้องการบางอย่างที่เฉพาะเจาะจงและไม่ต้องการสิ่งนั้นในภายหลัง คุณจะไม่สามารถลบโค้ดออกได้อย่างปลอดภัย คุณจะต้องคิดอย่างมีสติในการเพิกเฉยสัญญาณอย่างชัดแจ้ง ทั้งนี้ การเพิกเฉยจะไม่ชัดเจนเท่าใดนัก แม้กระทั่งกับโปรแกรมเมอร์ที่มีประสบการณ์ก็ตาม

และที่กล่าวมาข้างต้นคือสัญญาณเทอร์มินัลเกือบหนึ่งในสามที่เป็นปัญหาที่สุด และที่แย่ที่สุดคืออันตรายเป็นอย่างยิ่งเมื่อความต้องการของโปรแกรมเปลี่ยนไป ที่แย่ไปกว่านั้น แม้แต่สัญญาณที่ "ผู้ใช้กำหนด" ที่คาดคะเนไว้ก็เป็นหายนะที่รอวันเกิดเมื่อมีคนลืม SIG_IGN สัญญาณอย่างชัดแจ้ง หรือแม้กระทั่ง SIGUSR1 หรือ SIGPOLL ที่ไม่มีพิษภัยก็อาจทำให้เกิดปัญหาได้

นี่ไม่ใช่แค่คำถามเกี่ยวกับความคุ้นเคย ไม่ว่าคุณจะรู้วิธีการทำงานของสัญญาณดีแค่ไหน การเขียนโค้ดที่สัญญาณถูกต้องในครั้งแรกก็ยังเป็นเรื่องยากมากอยู่ดี เพราะแม้ว่าสัญญาณจะมีหน้าตาที่ชัดเจน แต่ก็มีความซับซ้อนกว่าที่เห็นมาก

ลำดับขั้นตอนของโค้ด การทำงานพร้อมๆ กัน และความเชื่อผิดๆ เกี่ยวกับ SA_RESTART

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

ผมไม่ได้พูดถึงกรณี "เล็กน้อย" เช่น ความปลอดภัยในฟังก์ชั่นการจัดการสัญญาณ ซึ่งส่วนใหญ่แก้ไขได้ด้วยการบัมพ์ sig_atomic_t หรือใช้รั้วสัญญาณแบบครบหน่วย (atomic signal fence) ของ C++ ด้วยซ้ำ ซึ่งส่วนใหญ่ค้นหาได้ง่ายและน่าจดจำในฐานะข้อผิดพลาดของใครก็ตามที่เพิ่งผ่านการจัดการสัญญาณครั้งแรกซึ่งมีความยุ่งยาก แต่สิ่งที่ยากกว่ากันมากคือการให้เหตุผลเกี่ยวกับลำดับขั้นตอนของโค้ดในส่วนเล็กๆ ของโปรแกรมที่ซับซ้อนเมื่อได้รับสัญญาณ การทำเช่นนี้ต้องมีการคิดอย่างต่อเนื่องและชัดเจนเกี่ยวกับสัญญาณในทุกส่วนของวงจรชีวิตแอพพลิเคชั่น (เดี๋ยวก่อน แล้ว EINTR ล่ะ SA_RESTART เพียงพอหรือเปล่า เราควรดูลำดับขั้นตอนอะไรถ้าสัญญาณนี้ยุติการใช้งานก่อนเวลาอันควร ตอนนี้ฉันมีโปรแกรมที่ทำงานพร้อมกัน ความหมายของมันคืออะไร) หรือการตั้งค่า sigprocmask หรือ pthread_setmask สำหรับบางส่วนของวงจรชีวิตแอพพลิเคชั่นของคุณแล้วภาวนาให้ลำดับขั้นตอนของโค้ดไม่มีการเปลี่ยนแปลง (ซึ่งแน่นอนว่าไม่ใช่การคาดเดาที่ดีในบรรยากาศของการพัฒนาที่รวดเร็ว) signalfd หรือการรัน sigwaitinfo ในเธรดเฉพาะอาจจะพอช่วยได้บ้าง แต่ทั้งสองสัญญาณนี้มีกรณีที่เกิดข้อผิดพลาดและข้อกังวลด้านการใช้งานที่เยอะมากจนยากที่จะแนะนำให้ใช้

เราเชื่อว่าโปรแกรมเมอร์ที่มีประสบการณ์ส่วนใหญ่คงรู้อยู่แล้วว่าแม้แต่ตัวอย่างคร่าวๆ ของการเขียนโค้ดที่ปลอดภัยต่อเธรดอย่างถูกต้องก็เป็นเรื่องที่ยากมาก และถ้าคุณคิดว่าการเขียนโค้ดที่ปลอดภัยต่อเธรดอย่างถูกต้องเป็นเรื่องยาก สัญญาณก็ยิ่งยากกว่ากันมาก ตัวจัดการสัญญาณต้องอาศัยโค้ดที่ไม่มีการล็อกอย่างเคร่งครัดกับโครงสร้างข้อมูลแบบครบหน่วยเท่านั้น ทั้งนี้เนื่องจากลำดับขั้นตอนหลักของการดำเนินการถูกระงับและเราไม่รู้ว่ามันถือตัวล็อกใดอยู่ และเนื่องจากลำดับขั้นตอนหลักของการดำเนินการอาจกำลังปฏิบัติการแบบไม่ครบหน่วย (non-atomic operations) ตามลำดับ นอกจากนี้ตัวจัดการสัญญาณต้องกลับเข้ามาใหม่อย่างเต็มรูปแบบ กล่าวคือ จะต้องสามารถซ้อนอยู่ภายในตัวเองได้ เนื่องจากตัวจัดการสัญญาณสามารถทับซ้อนกันได้หากมีการส่งสัญญาณหลายครั้ง (หรือแม้แต่สัญญาณเดียวด้วย SA_NODEFER) ซึ่งเป็นสาเหตุหนึ่งที่ทำให้คุณไม่สามารถใช้ฟังก์ชั่นต่างๆ เช่น printf หรือ malloc ในตัวจัดการสัญญาณได้ เนื่องจากฟังก์ชั่นเหล่านี้พึ่งพา global mutex ในการซิงค์ หากคุณกำลังถือตัวล็อกดังกล่าวไว้เมื่อได้รับสัญญาณ แล้วเรียกใช้ฟังก์ชั่นที่จำเป็นต้องมีตัวล็อกนั้นอีกครั้ง แอพพลิเคชั่นของคุณจะหยุดชะงัก ซึ่งเป็นเรื่องที่อธิบายได้ยากจริงๆ ด้วยเหตุนี้ หลายคนจึงเขียนคำสั่งต่อไปนี้ให้เป็นตัวจัดการสัญญาณของตัวเอง

static volatile sig_atomic_t received_sighup; 

static void sighup(int sig __attribute__((unused))) { received_sighup = 1; }

static int configure_signal_handlers(void) {
  return sigaction(
    SIGHUP,
    &(const struct sigaction){.sa_handler = sighup, .sa_flags = SA_RESTART},
    NULL);
}

int main(int argc, char *argv[]) {
  if (configure_signal_handlers()) {
       /* failed to set handlers */
  }

  /* usual program flow */

  if (received_sighup) {
    /* reload */
    received_sighup = 0;
  }

  /* usual program flow */
}

ปัญหาคือแม้ว่า signalfd หรือความพยายามอื่นๆ ในการจัดการสัญญาณแบบไม่ซิงค์อาจดูค่อนข้างเรียบง่ายและมีประสิทธิภาพ แต่คำสั่งนี้ละเลยความจริงที่ว่าจุดหยุดชะงักมีความสำคัญพอๆ กับการดำเนินการที่กระทำหลังจากได้รับสัญญาณ ตัวอย่างเช่น สมมติว่าโค้ดพื้นที่ผู้ใช้ของคุณกำลังทำ I/O หรือเปลี่ยนเมตาดาต้าของอ็อบเจ็กต์ที่มาจาก Kernel (เช่น Inode หรือ FD) ในกรณีนี้ จริงๆ แล้วคุณอาจอยู่ในสแต็กพื้นที่ของ Kernel ในขณะที่เกิดการหยุดชะงัก ตัวอย่างต่อไปนี้แสดงให้เห็นลักษณะของเธรดที่อาจเป็นไปได้เมื่อพยายามปิดตัวอธิบายไฟล์

# cat /proc/2965230/stack
 [<0>] schedule+0x43/0xd0
 [<0>] io_schedule+0x12/0x40
 [<0>] wait_on_page_bit+0x139/0x230
 [<0>] filemap_write_and_wait+0x5a/0x90
 [<0>] filp_close+0x32/0x70
 [<0>] __x64_sys_close+0x1e/0x50
 [<0>] do_syscall_64+0x4e/0x140
 [<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9

คุณจะเห็นว่า __x64_sys_close คือตัวแปร x86_64 ของการเรียกใช้ระบบ close ซึ่งปิดตัวอธิบายไฟล์ เมื่อการดำเนินการมาถึงจุดนี้ เราจะรอให้พื้นที่เก็บข้อมูลสำรองได้รับการอัปเดต (ซึ่งก็คือคำสั่ง wait_on_page_bit นี้) เนื่องจากการทำงานของ I/O มักจะช้ากว่าการดำเนินการอื่นๆ หลายลำดับ schedule นี้จึงเป็นวิธีบอกใบ้ให้กับตัวกำหนดเวลา CPU ของ Kernel โดยสมัครใจว่าเรากำลังจะดำเนินการที่มีเวลาแฝงสูง (เช่น ดิสก์หรือเครือข่าย I/O) และควรพิจารณาหากระบวนการอื่นในการกำหนดเวลาแทนกระบวนการปัจจุบันไปก่อน ซึ่งนับเป็นวิธีที่ดี เนื่องจากช่วยให้เราสามารถส่งสัญญาณไปยัง Kernel ว่าควรทำการเลือกกระบวนการที่จะใช้ CPU ให้เป็นประโยชน์จริงๆ แทนที่จะเสียเวลากับกระบวนการที่ไม่สามารถดำเนินการต่อได้จนกว่าจะเสร็จสิ้นการรอการตอบสนองจากสิ่งที่อาจต้องใช้เวลาสักพัก

ให้ลองนึกภาพว่าเราส่งสัญญาณไปยังกระบวนการที่เรากำลังใช้งานอยู่ สัญญาณที่เราส่งไปมีตัวจัดการพื้นที่ผู้ใช้ในเธรดที่จะรับสัญญาณดังกล่าว ดังนั้นเราจะดำเนินการต่อในพื้นที่ผู้ใช้ วิธีหนึ่งที่การแข่งขันนี้จะสามารถจบลงได้ก็คือ Kernel จะพยายามออกจาก schedule คลายสแต็กเพิ่มเติมและส่งคืน errno ของ ESYSRESTART หรือ EINTR ไปยังพื้นที่ผู้ใช้เพื่อระบุว่าเราถูกขัดจังหวะ แต่เราปิดได้มากแค่ไหน สถานะของตัวอธิบายไฟล์ในตอนนี้เป็นอย่างไร

เนื่องจากเรากลับมายังพื้นที่ผู้ใช้แล้ว เราก็จะใช้งานตัวจัดการสัญญาณ เมื่อตัวจัดการสัญญาณออกจากระบบ เราจะเผยแพร่ข้อผิดพลาดไปยังตัวกำหนดขอบเขต close ของ libc พื้นที่ผู้ใช้ จากนั้นเผยแพร่ไปยังแอพพลิเคชั่น ซึ่งในทางทฤษฎีแล้วสามารถดำเนินการบางอย่างเกี่ยวกับสถานการณ์ที่พบได้ ที่เราพูดว่า "ในทางทฤษฎี" ก็เพราะเป็นเรื่องยากมากที่จะรู้ว่าเราต้องทำอะไรกับหลายๆ สถานการณ์เหล่านี้โดยใช้สัญญาณ และบริการจำนวนมากในการใช้งานจริงไม่สามารถจัดการกับกรณีที่เกิดข้อผิดพลาดได้ดีเท่าใดนัก วิธีดังกล่าวอาจใช้ได้ในบางแอพพลิเคชั่นที่ความสมบูรณ์ของข้อมูลไม่สำคัญขนาดนั้น อย่างไรก็ตาม ในแอพพลิเคชั่นการใช้งานจริงที่ใส่ใจ อย่างจริงจัง ในเรื่องของความสอดคล้องและความสมบูรณ์ของข้อมูล วิธีนี้ทำให้เห็นปัญหาที่สำคัญ กล่าวคือ Kernel ไม่ได้เปิดเผยวิธีอย่างละเอียดใดๆ ที่จะทำให้เข้าใจว่ามันไปได้ไกลแค่ไหน มีอะไรบ้างที่ทำสำเร็จและไม่สำเร็จ และอะไรคือสิ่งที่เราควรทำจริงๆ เกี่ยวกับสถานการณ์นั้นๆ ที่แย่ไปกว่านั้น ถ้าหากคำสั่ง close กลับมาพร้อมกับคำสั่ง EINTR สถานะของตัวอธิบายไฟล์จะไม่ได้รับการระบุ ดังนี้

“หากคำสั่ง close() ถูกสัญญาณ [...] ขัดจังหวะ สถานะของ [ตัวอธิบายไฟล์] จะไม่ได้รับการระบุ”

ขอให้โชคดีกับการพยายามให้เหตุผลเกี่ยวกับวิธีจัดการคำสั่งดังกล่าวอย่างปลอดภัยในแอพพลิเคชั่นของคุณ โดยทั่วไปแล้ว การจัดการ EINTR แม้กระทั่งกับการเรียกใช้ระบบที่เป็นไปด้วยดีก็มีความซับซ้อน มีปัญหาเล็กๆ น้อยๆ จำนวนมากเป็นสาเหตุหลักที่ทำให้คำสั่ง SA_RESTART ไม่เพียงพอ ทั้งนี้ การเรียกใช้ระบบไม่ได้รีสตาร์ทได้เหมือนกันทั้งหมด และคาดหวังว่าผู้พัฒนาแอพพลิเคชั่นของคุณทุกคนจะเข้าใจและลดความแตกต่างเชิงลึกของการรับสัญญาณสำหรับทุกการเรียกใช้ระบบ ณ จุดการเรียกใช้ทุกจุดที่ขอให้มีการหยุดทำงานได้ ต่อไปนี้คือข้อความจาก man 7 signal

“อินเทอร์เฟซต่อไปนี้จะไม่รีสตาร์ทหลังจากถูกตัวจัดการสัญญาณขัดจังหวะ โดยไม่คำนึงถึงการใช้งาน SA_RESTART ซึ่งมักจะล้มเหลวโดยมีข้อผิดพลาด EINTR [...]”

ในทำนองเดียวกัน การใช้ sigprocmask และการคาดหวังว่าลำดับขั้นตอนของโค้ดจะยังคงที่ก็ทำให้เกิดปัญหา เนื่องจากโดยปกติแล้ว ผู้พัฒนามักจะไม่เอาเวลามาคิดเกี่ยวกับขอบเขตของการจัดการสัญญาณ หรือคิดหาวิธีสร้างหรือรักษาโค้ดที่สัญญาณถูกต้อง เช่นเดียวกับการจัดการสัญญาณในเธรดเฉพาะที่มี sigwaitinfo ซึ่งอาจต้องจบด้วย GDB และเครื่องมือคล้ายกันที่ไม่สามารถแก้ไขจุดบกพร่องของกระบวนการได้ ลำดับขั้นตอนของโค้ดที่ผิดพลาดเล็กๆ น้อยๆ หรือการจัดการข้อผิดพลาดอาจส่งผลให้เกิดจุดบกพร่อง เกิดการหยุดทำงาน แก้ไขจุดที่เสียหายได้ยาก เกิดการหยุดชะงัก และปัญหาอื่นๆ อีกมากมายที่จะนำคุณไปสู่การใช้เครื่องมือจัดการเหตุการณ์ที่คุณต้องการ

ความซับซ้อนสูงในสภาพแวดล้อมแบบมัลติเธรด

หากคุณคิดว่าการทำงานพร้อมกัน การกลับเข้ามาใหม่ และการใช้สัญญาณแบบครบหน่วยที่ได้พูดถึงกันไปนั้นแย่มากพอแล้ว การรวมการประมวลผลแบบมัลติเธรดเข้ามาด้วยยิ่งทำให้สิ่งต่างๆ ซับซ้อนขึ้นไปอีก เรื่องนี้มีความสำคัญอย่างยิ่งเมื่อพิจารณาถึงข้อเท็จจริงที่ว่าแอพพลิเคชั่นที่ซับซ้อนจำนวนมากใช้งานเธรดแยกกันอย่างชัดเจน ตัวอย่างเช่น ใช้งานเธรดเป็นส่วนหนึ่งของ jemalloc, GLib หรือคล้ายกัน ไลบรารีเหล่านี้บางรายการถึงขั้นติดตั้งตัวจัดการสัญญาณด้วยตัวเอง ซึ่งส่งผลให้เกิดความซับซ้อนยิ่งขึ้น

man 7 signal ได้กล่าวถึงเรื่องนี้ไว้ดังนี้

“สัญญาณอาจถูกสร้างขึ้น (และกำลังรอดำเนินการ) สำหรับเธรดทั้งหมด (เช่น เมื่อส่งโดยใช้ kill(2)) หรือสำหรับเธรดใดเธรดหนึ่ง [...] หากมีเธรดที่สัญญาณไม่ถูกบล็อกมากกว่าหนึ่งเธรดขึ้นไป Kernel จะเลือกเธรดที่ต้องการเพื่อส่งสัญญาณ”

หรือกล่าวให้ชัดเจนขึ้นก็คือ "สำหรับสัญญาณส่วนใหญ่แล้ว Kernel จะส่งสัญญาณไปยังเธรดใดก็ตามที่ไม่ได้บล็อกสัญญาณด้วย sigprocmask" SIGSEGV, SIGILL และคำสั่งที่คล้ายกันทำหน้าที่คล้ายกับดัก และให้สัญญาณนั้นๆ มุ่งเป้าไปที่เธรดที่ละเมิดอย่างชัดเจน แต่ถึงแม้ใครจะคิดอย่างไรก็ตาม สัญญาณส่วนใหญ่ก็ไม่สามารถส่งสัญญาณไปยังเธรดเดียวในกลุ่มเธรดได้อย่างชัดเจน ไม่เว้นแม้กระทั่งการใช้ tgkill หรือ pthread_kill

นั่นหมายความว่าคุณไม่สามารถเปลี่ยนลักษณะของการจัดการสัญญาณโดยรวมแม้เพียงเล็กน้อยทันทีที่คุณมีชุดของเธรด หากบริการจำเป็นต้องบล็อกสัญญาณเป็นระยะด้วย sigprocmask ในเธรดหลัก คุณก็จำเป็นต้องสื่อสารกับเธรดอื่นๆ เป็นการภายนอกว่าเธรดเหล่านั้นควรจัดการการบล็อกสัญญาณอย่างไร มิฉะนั้นสัญญาณอาจถูกเธรดอื่นกลืน และจะไม่มีใครเห็นสัญญาณนั้นอีกเลย แน่นอนว่าคุณสามารถบล็อกสัญญาณในเธรดย่อยเพื่อหลีกเลี่ยงเหตุการณ์นี้ได้ แต่ถ้าหากเธรดเหล่านี้จำเป็นต้องจัดการสัญญาณของตัวเอง แม้แต่กับสัญญาณพื้นฐานอย่าง waitpid ก็จะทำให้สิ่งต่างๆ เกิดความซับซ้อน

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

ขาดคำจำกัดความและการสื่อสารว่าสำเร็จหรือล้มเหลว

สัญญาณต่างๆ ได้รับการเผยแพร่แบบไม่ซิงค์กันใน Kernel การเรียกใช้ระบบด้วยคำสั่ง kill จะกลับมาทันทีที่มีการบันทึกสัญญาณที่รอดำเนินการสำหรับกระบวนการหรือ task_struct ของเธรดที่เป็นปัญหา ดังนั้นจึงไม่มีการรับประกันว่าจะมีการส่งสัญญาณอย่างทันท่วงที แม้ว่าสัญญาณจะไม่ถูกบล็อกก็ตาม

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

ใครก็ตามที่ใช้ Linux มานานพอต้องเคยพบกรณีที่ตัวเองต้องการหยุดบางกระบวนการโดยไม่ลังเล แต่กลับพบว่ากระบวนการนั้นไม่ตอบสนองแม้แต่กับสัญญาณที่ควรจะได้ผลชะงัดอย่าง SIGKILL ทั้งนี้ ปัญหาคือการชวนให้เข้าใจผิด จุดประสงค์ของคำสั่ง kill(1) นั้นไม่ใช่เพื่อหยุดกระบวนการ แต่มีไว้เพียงเพื่อจัดคิวคำขอไปยัง Kernel (โดยไม่มีข้อบ่งชี้ว่าจะได้รับบริการเมื่อใด) ว่ามีคนขอให้มีการดำเนินการบางอย่างเท่านั้น

หน้าที่ของการเรียกใช้ระบบด้วยคำสั่ง kill คือการทำเครื่องหมายสัญญาณว่ารอดำเนินการในเมตาดาต้างานของ Kernel ซึ่งทำได้สำเร็จแม้ว่างาน SIGKILL จะไม่หยุดก็ตาม โดยเฉพาะในกรณีของ SIGKILL นั้น Kernel รับประกันว่าจะไม่มีการดำเนินการตามขั้นตอนในโหมดผู้ใช้อีกต่อไป แต่เราอาจยังต้องดำเนินการตามขั้นตอนในโหมด Kernel เพื่อให้การดำเนินการที่ถ้าหากปล่อยไว้อาจส่งผลให้ข้อมูลเสียหายนั้นเสร็จสิ้น หรือเพื่อปล่อยทรัพยากร ด้วยเหตุนี้ เราจึงยังคงประสบความสำเร็จแม้ว่าสถานะจะเป็น D (ไม่สามารถขัดจังหวะการพักเครื่องได้) ก็ตาม ซึ่งการหยุดการทำงานของตัวเองจะไม่ล้มเหลวเว้นแต่คุณจะให้สัญญาณที่ไม่ถูกต้อง คุณไม่ได้รับอนุญาตให้ส่งสัญญาณนั้น หรือไม่มี PID ที่คุณร้องขอให้ส่งสัญญาณไปให้ ซึ่งส่งผลให้การเผยแพร่สถานะไม่ปลาย (non-terminal state) ไปยังแอพพลิเคชั่นอย่างมีประสิทธิภาพนั้นไม่มีประโยชน์

สรุปสาระสำคัญ

  • สัญญาณนั้นใช้ได้กับสถานะเทอร์มินัลที่จัดการใน Kernel ล้วนๆ โดยไม่มีตัวจัดการพื้นที่ผู้ใช้ สำหรับสัญญาณที่คุณต้องการให้หยุดการทำงานของโปรแกรมในทันที ให้คุณปล่อยสัญญาณเหล่านั้นไว้เพื่อให้ Kernel จัดการ ซึ่งยังหมายความว่า Kernel อาจสามารถออกจากงานของตัวเองก่อนเวลาได้ ทำให้ทรัพยากรโปรแกรมของคุณว่างเร็วขึ้น ในขณะที่คำขอ IPC ของพื้นที่ผู้ใช้จะต้องรอให้ส่วนของพื้นที่ผู้ใช้เริ่มดำเนินการอีกครั้ง
  • วิธีหลีกเลี่ยงปัญหาการจัดการสัญญาณคืออย่าจัดการกับสัญญาณเหล่านั้นเลยจะเป็นการดีที่สุด อย่างไรก็ตาม สำหรับแอพพลิเคชั่นที่จัดการการประมวลผลสถานะซึ่งต้องดำเนินบางอย่างเกี่ยวกับกรณีต่างๆ เช่น SIGTERM ควรใช้ API ระดับสูง เช่น folly::AsyncSignalHandler ซึ่งได้มีการทำให้คุณสมบัติจำนวนหนึ่งใช้งานได้ง่ายอยู่แล้ว

  • หลีกเลี่ยงการสื่อสารคำขอของแอพพลิเคชั่นด้วยสัญญาณ ให้ใช้การแจ้งเตือนที่จัดการด้วยตนเอง (เช่น inotify) หรือ RPC ของพื้นที่ผู้ใช้ที่มีส่วนเฉพาะของวงจรชีวิตแอพพลิเคชั่นเพื่อจัดการ แทนการอาศัยการขัดจังหวะแอพพลิเคชั่น
  • หากเป็นไปได้ ให้จำกัดขอบเขตของสัญญาณไว้ที่ส่วนย่อยของโปรแกรมหรือเธรดด้วย sigprocmask ซึ่งจะเป็นการลดจำนวนโค้ดที่ต้องผ่านการตรวจสอบเป็นประจำเพื่อความถูกต้องของสัญญาณ ทั้งนี้โปรดทราบว่าหากลำดับขั้นตอนของโค้ดหรือกลยุทธ์การสร้างเธรดเปลี่ยนไป คำสั่ง mask อาจไม่ให้ผลลัพธ์ตามที่คุณต้องการ
  • เมื่อ daemon เริ่มทำงาน ให้ mask สัญญาณเทอร์มินัลที่ไม่ได้เข้าใจไปในทางเดียวกันและสามารถปรับเปลี่ยนได้ในบางจุดในโปรแกรมของคุณ เพื่อหลีกเลี่ยงไม่ให้กลับไปเป็นลักษณะการทำงานเริ่มต้นของ Kernel ซึ่งผมแนะนำให้กำหนดค่าดังต่อไปนี้
signal(SIGHUP, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGUSR1, SIG_IGN);
signal(SIGUSR2, SIG_IGN);

ลักษณะการทำงานของสัญญาณมีความซับซ้อนอย่างยิ่งในการให้เหตุผลแม้กระทั่งในโปรแกรมที่ได้รับการออกแบบมาอย่างดี และการใช้งานสัญญาณทำให้เกิดความเสี่ยงที่ไม่จำเป็นในแอพพลิเคชั่นที่สามารถใช้ทางเลือกอื่นๆ ได้ โดยทั่วไปแล้ว เราไม่แนะนำให้คุณใช้สัญญาณในการสื่อสารกับส่วนของพื้นที่ผู้ใช้ของโปรแกรม แต่ควรให้โปรแกรมจัดการเหตุการณ์ต่างๆ เองอย่างโปร่งใส (ด้วย inotify เป็นต้น) หรือใช้การสื่อสารในพื้นที่ของผู้ใช้ที่สามารถรายงานข้อผิดพลาดกลับไปยังผู้ออกสัญญาณ และสามารถสร้าง enum รวมถึงพิสูจน์ได้ในขณะรวบรวม เช่น Thrift, gRPC หรือการสื่อสารที่คล้ายกัน

ผมหวังว่าบทความนี้จะแสดงให้คุณเห็นว่า ถึงแม้สัญญาณอาจดูผิวเผินเหมือนเรียบง่าย แต่ในความเป็นจริงแล้วไม่ได้เป็นเช่นนั้นเลย ซึ่งสุนทรียศาสตร์ของความเรียบง่ายที่ส่งเสริมการใช้งานในฐานะ API สำหรับซอฟต์แวร์พื้นที่ผู้ใช้นั้นปฏิเสธการตัดสินใจในการออกแบบโดยนัยที่ไม่เหมาะกับกรณีการใช้งานในระยะใช้งานจริงส่วนใหญ่ในยุคปัจจุบัน

สรุปชัดๆ ก็คือ มีกรณีการใช้งานที่ถูกต้องสำหรับสัญญาณอยู่ ซึ่งสัญญาณนั้นใช้ได้ดีกับการสื่อสารขั้นพื้นฐานกับ Kernel เกี่ยวกับสถานะกระบวนการที่ต้องการเมื่อไม่มีองค์ประกอบของพื้นที่ผู้ใช้ เช่น ในกรณีที่กระบวนการควรหยุดทำงาน อย่างไรก็ตาม การเขียนโค้ดที่สัญญาณถูกต้องในครั้งแรกเมื่อคาดว่าสัญญาณจะติดอยู่ในพื้นที่ผู้ใช้นั้นเป็นเรื่องยาก

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

เรียนรู้เพิ่มเติมเกี่ยวกับโอเพนซอร์สของ Meta ได้ที่เว็บไซต์โอเพนซอร์สของเรา, สมัครติดตามช่อง YouTube ของเรา, หรือติดตามเราที่ Twitter, Facebook และ LinkedIn