การขัดจังหวะของการโทรของระบบเมื่อจับสัญญาณ


29

จากการอ่าน man page บนread()และการwrite()โทรปรากฏว่าการโทรเหล่านี้ถูกขัดจังหวะด้วยสัญญาณโดยไม่คำนึงว่าพวกเขาจะต้องปิดกั้นหรือไม่

โดยเฉพาะสมมติว่า

  • กระบวนการสร้างตัวจัดการสำหรับสัญญาณบางอย่าง
  • อุปกรณ์ถูกเปิด (พูดเทอร์มินัล) โดยที่O_NONBLOCK ไม่ได้ตั้งค่า (เช่นทำงานในโหมดบล็อก)
  • กระบวนการนี้ทำให้การread()เรียกระบบอ่านจากอุปกรณ์และผลลัพธ์จะดำเนินการพา ธ ควบคุมเคอร์เนลในเคอร์เนลพื้นที่
  • ในขณะที่กระบวนการดำเนินการอยู่read()ใน kernel-space สัญญาณที่ติดตั้งตัวจัดการไว้ก่อนหน้านี้จะถูกส่งไปยังกระบวนการนั้นและตัวจัดการสัญญาณถูกเรียกใช้

การอ่าน man man และส่วนที่เหมาะสมในSUSv3 'System Interfaces volume (XSH)' , หนึ่งพบว่า:

ผม. หาก a read()ถูกขัดจังหวะโดยสัญญาณก่อนที่จะอ่านข้อมูลใด ๆ (นั่นคือต้องปิดกั้นเพราะไม่มีข้อมูล) จะส่งกลับ -1 ด้วยการerrnoตั้งค่าเป็น [EINTR]

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

คำถาม A): ฉันถูกต้องหรือไม่ที่จะสมมติว่าในกรณีใด ๆ (บล็อก / ไม่มีบล็อก) การส่งและการจัดการสัญญาณไม่โปร่งใสอย่างสิ้นเชิงกับread()?

กรณีที่ ดูเหมือนจะเข้าใจได้เนื่องจากการบล็อกread()ปกติจะวางกระบวนการในTASK_INTERRUPTIBLEสถานะเพื่อให้เมื่อมีการส่งสัญญาณเคอร์เนลจะวางกระบวนการให้อยู่ในTASK_RUNNINGสถานะ

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

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

สิ่งนี้ทำให้ฉันถึงคำถามที่สอง (และครั้งสุดท้าย):

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

คำตอบ:


29

สรุป: คุณถูกต้องว่าการรับสัญญาณไม่โปร่งใสไม่ว่าในกรณีที่ i (ถูกขัดจังหวะโดยไม่ต้องอ่านอะไร) หรือในกรณีที่ ii (ถูกขัดจังหวะหลังจากการอ่านบางส่วน) ทำอย่างอื่นในกรณีที่ฉันต้องการเปลี่ยนแปลงพื้นฐานทั้งสถาปัตยกรรมของระบบปฏิบัติการและสถาปัตยกรรมของแอปพลิเคชัน

มุมมองการใช้งานระบบปฏิบัติการ

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

  • ยุติการเรียกระบบ รายงานจำนวนผู้ใช้ที่ทำไปแล้ว ขึ้นอยู่กับรหัสแอปพลิเคชันเพื่อเริ่มการโทรของระบบใหม่ในบางกรณีหากต้องการ นั่นเป็นวิธีที่ยูนิกซ์ทำงาน
  • บันทึกสถานะของการโทรของระบบและอนุญาตให้รหัสผู้ใช้เพื่อโทรกลับมา นี่เป็นปัญหาด้วยเหตุผลหลายประการ:
    • ขณะที่รหัสผู้ใช้กำลังทำงานมีบางอย่างที่อาจทำให้สถานะที่บันทึกไว้ไม่ถูกต้อง ตัวอย่างเช่นหากอ่านจากไฟล์ไฟล์อาจถูกตัดทอน ดังนั้นรหัสเคอร์เนลจะต้องใช้เหตุผลมากมายในการจัดการกรณีเหล่านี้
    • รัฐที่บันทึกไว้ไม่ได้รับอนุญาตให้ล็อคใด ๆ เพราะไม่มีการรับประกันว่ารหัสผู้ใช้จะกลับมาเป็น syscall อีกครั้งจากนั้นล็อคจะถูกเก็บไว้ตลอดไป
    • เคอร์เนลจะต้องเปิดเผยอินเทอร์เฟซใหม่เพื่อดำเนินการต่อหรือยกเลิก syscalls ต่อเนื่องนอกเหนือจากอินเทอร์เฟซปกติเพื่อเริ่ม syscall นี่เป็นภาวะแทรกซ้อนจำนวนมากสำหรับกรณีที่หายาก
    • สถานะที่บันทึกไว้จะต้องใช้ทรัพยากร (หน่วยความจำอย่างน้อย); ทรัพยากรเหล่านั้นจะต้องได้รับการจัดสรรและเก็บรักษาโดยเคอร์เนล แต่ถูกนับรวมกับการจัดสรรของกระบวนการ สิ่งนี้ไม่สามารถเอาชนะได้ แต่เป็นภาวะแทรกซ้อน
      • โปรดทราบว่าตัวจัดการสัญญาณอาจทำการเรียกระบบที่ตัวเองถูกขัดจังหวะ ดังนั้นคุณไม่สามารถมีการจัดสรรทรัพยากรคงที่ที่ครอบคลุม syscalls ที่เป็นไปได้ทั้งหมด
      • และถ้าทรัพยากรไม่สามารถจัดสรรได้ล่ะ? จากนั้นตึกระฟ้าจะต้องล้มเหลวอยู่ดี ซึ่งหมายความว่าแอปพลิเคชันจะต้องมีรหัสเพื่อจัดการกรณีนี้ดังนั้นการออกแบบนี้จะไม่ลดความซับซ้อนของรหัสแอปพลิเคชัน
  • อยู่ระหว่างดำเนินการ (แต่ถูกระงับ) สร้างเธรดใหม่สำหรับตัวจัดการสัญญาณ นี่เป็นปัญหาอีกครั้ง:
    • การใช้ระบบปฏิบัติการยูนิกซ์ก่อนหน้ามีเธรดเดียวต่อกระบวนการ
    • ตัวจัดการสัญญาณมีความเสี่ยงที่จะเกิดการโอเวอร์โหลดบนรองเท้าของ syscall นี่เป็นปัญหาอยู่แล้ว แต่ในการออกแบบยูนิกซ์ปัจจุบันมันมีอยู่
    • ทรัพยากรจะต้องได้รับการจัดสรรสำหรับเธรดใหม่ ดูด้านบน.

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

มุมมองออกแบบแอปพลิเคชัน

เมื่อแอปพลิเคชันถูกขัดจังหวะในระหว่างการเรียกของระบบ syscall ควรดำเนินการให้เสร็จสมบูรณ์หรือไม่ ไม่เสมอ. ตัวอย่างเช่นพิจารณาโปรแกรมเช่นเชลล์ที่อ่านบรรทัดจากเทอร์มินัลและผู้ใช้กดCtrl+C, เรียก SIGINT การอ่านจะต้องไม่สมบูรณ์นั่นคือสิ่งที่เป็นสัญญาณเกี่ยวกับ โปรดทราบว่าตัวอย่างนี้แสดงให้เห็นว่าreadsyscall จะต้องถูกขัดจังหวะแม้ว่าจะยังไม่มีการอ่านไบต์

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

การreadเรียกระบบเป็นวิธีที่เป็นเพราะมันเป็นสิ่งดั้งเดิมที่สมเหตุสมผลสำหรับการออกแบบทั่วไปของระบบปฏิบัติการ ความหมายคร่าวๆคือ“ อ่านให้มากที่สุดจนถึงขีด จำกัด (ขนาดบัฟเฟอร์) แต่หยุดถ้าสิ่งอื่นเกิดขึ้น” หากต้องการอ่านบัฟเฟอร์เต็มรูปแบบเกี่ยวข้องกับการทำงานreadในลูปจนกว่าจะอ่านจำนวนไบต์ได้มากที่สุด นี่เป็นฟังก์ชั่นระดับสูงกว่า, fread(3). ซึ่งแตกต่างจากread(2)ที่เป็นสายระบบเป็นฟังก์ชั่นห้องสมุดดำเนินการในพื้นที่ของผู้ใช้ที่ด้านบนของfread readเหมาะสำหรับแอปพลิเคชั่นที่อ่านไฟล์หรือพยายามตาย มันไม่เหมาะสำหรับล่ามบรรทัดคำสั่งหรือสำหรับโปรแกรมเครือข่ายที่ต้องทำการเชื่อมต่อเค้นอย่างหมดจดหรือสำหรับโปรแกรมเครือข่ายที่มีการเชื่อมต่อพร้อมกันและไม่ใช้เธรด

ตัวอย่างของการอ่านในลูปมีให้ในการเขียนโปรแกรมระบบ Linux ของ Robert Love:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

มันต้องใช้เวลาดูแลcase iและcase iiและอีกไม่กี่


ขอบคุณ Gilles เป็นอย่างมากสำหรับคำตอบที่กระชับและชัดเจนซึ่งยืนยันมุมมองที่คล้ายกันที่นำเสนอในบทความเกี่ยวกับปรัชญาการออกแบบ UNIX ดูเหมือนว่าฉันเชื่อว่าพฤติกรรมการขัดจังหวะ syscall นั้นเกี่ยวข้องกับการออกแบบ UNIX มากกว่าข้อ จำกัด ทางเทคนิคหรืออุปสรรค
darbehdar

@darbehdar เป็นทั้งสาม: ปรัชญาการออกแบบยูนิกซ์ (ที่นี่ส่วนใหญ่เป็นกระบวนการที่เชื่อถือได้น้อยกว่าเคอร์เนลและสามารถเรียกใช้รหัสโดยพลการนอกจากนี้กระบวนการและหัวข้อที่ไม่ได้สร้างขึ้นโดยปริยาย) ข้อ จำกัด ทางเทคนิค (ในการจัดสรรทรัพยากร) และการออกแบบแอปพลิเคชัน เป็นกรณีที่สัญญาณจะต้องยกเลิก syscall)
Gilles 'หยุดความชั่วร้าย'

2

เพื่อตอบคำถาม A :

read()ใช่การจัดส่งและการจัดการของสัญญาณไม่ได้ทั้งหมดโปร่งใสไป

การread()วิ่งครึ่งทางอาจครอบครองทรัพยากรบางอย่างในขณะที่สัญญาณถูกขัดจังหวะ และตัวจัดการสัญญาณของสัญญาณอาจเรียกอีกอันหนึ่งread()(หรือsyscalls ที่ปลอดภัยสัญญาณ asyncอื่น ๆ) เช่นกัน ดังนั้นread()สัญญาณที่ถูกขัดจังหวะโดยจะต้องหยุดก่อนเพื่อปล่อยทรัพยากรที่ใช้มิฉะนั้นตัวread()เรียกจากตัวจัดการสัญญาณจะเข้าถึงทรัพยากรเดียวกันและทำให้เกิดปัญหา reentrant

เนื่องจากระบบเรียกอื่น ๆ กว่าread()อาจจะเรียกว่าจากการจัดการสัญญาณและพวกเขายังอาจครอบครองชุดที่เหมือนกันของทรัพยากรread()ไม่ เพื่อหลีกเลี่ยงปัญหา reentrant ด้านบนการออกแบบที่ง่ายและปลอดภัยที่สุดคือการหยุดการขัดจังหวะread()ทุกครั้งที่มีสัญญาณเกิดขึ้นระหว่างการวิ่ง

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