OS · บทที่ 2 The Kernel Abstraction

หัวข้อ 5 · 15 นาที

Mode switch: interrupt, exception, system call

นึกภาพก่อน

กำลังอ่านหนังสืออยู่ มีสามเหตุที่ทำให้ต้องวางหนังสือ:

  • โทรศัพท์ดัง — เหตุจากข้างนอก ไม่เกี่ยวกับสิ่งที่กำลังอ่าน
  • เจอหน้าที่ฉีกขาดอ่านต่อไม่ได้ — เหตุจากสิ่งที่กำลังทำเอง และไม่ได้ตั้งใจ
  • ลุกไปถามบรรณารักษ์ — ตั้งใจขอความช่วยเหลือเอง

ทั้งสามกรณี ต้องคั่นหน้าไว้ก่อน แล้วกลับมาอ่านต่อจากจุดเดิมได้ สามเหตุนี้ตรงกับ interrupt, exception และ system call ซึ่งเป็นสามทางเดียวที่ CPU เปลี่ยนจาก user mode เข้า kernel mode

จาก user mode เข้า kernel mode

ทางถูกกระตุ้นโดยลักษณะ
Interrupttimer และ I/O deviceเหตุจากภายนอกโปรแกรม เกิดเมื่อใดก็ได้
Exceptionพฤติกรรมที่ไม่คาดคิดของโปรแกรม หรือพฤติกรรมมุ่งร้ายเหตุจากคำสั่งที่โปรแกรมกำลังทำ
System callโปรแกรมร้องขอให้ kernel ทำบางอย่างแทนตั้งใจ เรียกอีกชื่อว่า protected procedure call

system call มีทางเข้าได้เพียง จำนวนจำกัด และแต่ละทางเขียนอย่างระมัดระวังมาก โปรแกรมเลือกไม่ได้ว่าจะกระโดดเข้า kernel ตรงไหน เลือกได้แค่ว่าจะขอบริการหมายเลขใด

จาก kernel mode กลับ user mode

ทางทำอะไร
เริ่ม process ใหม่ / thread ใหม่jump ไปคำสั่งแรกของโปรแกรมหรือ thread
Return จาก interrupt, exception, system callresume การทำงานที่ถูกพักไว้
Process / thread context switchresume process อื่น
User-level upcall (UNIX signal)แจ้งเหตุการณ์แบบ asynchronous ไปยัง user program

สังเกตข้อสาม: เข้า kernel มาจาก process หนึ่ง แต่ออกไปเป็นอีก process หนึ่งได้ นี่คือวิธีที่ OS สลับงาน เช่น timer interrupt เกิดขณะ process A ทำงาน → kernel เก็บ context ของ A ลง PCB → เลือก B → โหลด context ของ B → กลับ user mode ที่ B

การสลับ mode อย่างปลอดภัย

kernel ไว้ใจ user program ไม่ได้ การออกแบบจึงต้องผ่านเงื่อนไขเหล่านี้:

  • kernel code ที่เขียนอย่างระมัดระวัง เก็บ state ของ user process แล้วพักไว้
  • ต้องรับมือ state ที่ ประหลาด มีบั๊ก หรือมุ่งร้าย ได้ เช่น
    • system call ที่ส่ง null pointer มา
    • return instruction ที่ชี้ออกนอกขอบเขต
    • user stack pointer ที่ชี้ออกนอกขอบเขต
  • ต้อง เป็นไปไม่ได้ ที่ user program ที่มีบั๊กหรือมุ่งร้ายจะทำให้ kernel ทำลายตัวเอง
  • user program ไม่ควรรู้ ว่ามี interrupt เกิดขึ้น (transparency)

รับ interrupt อย่างปลอดภัยด้วยอะไร

กลไกแก้ปัญหา
Interrupt vectorทางเข้า kernel มีจำนวนจำกัด
Kernel interrupt stackhandler ทำงานได้ไม่ว่า user code จะอยู่ในสภาพใด
Interrupt maskinghandler เป็นแบบ non-blocking
Atomic transfer of controlเปลี่ยน program counter, stack pointer, memory protection และ kernel/user mode เหมือนเป็นคำสั่งเดียว
Transparent restartable executionuser program ไม่รู้ว่าเกิด interrupt

Interrupt vector

ตารางที่เก็บ address และคุณสมบัติของ handler แต่ละตัว ดัชนีของตารางคือหมายเลข interrupt เมื่อเกิด interrupt หมายเลข i CPU เปิดตารางช่อง i แล้วกระโดดไป intrpHandler_i()

user program กำหนด address ปลายทางเองไม่ได้ ตารางนี้ kernel เป็นคนตั้งและอยู่ใน kernel memory

ทำไมต้อง atomic

ถ้าเปลี่ยนทีละอย่าง จะมีช่วงสั้น ๆ ที่ CPU อยู่ใน kernel mode แต่ PC ยังชี้ user code → user code ได้ทำงานด้วยสิทธิ์ kernel หรือ PC ชี้ kernel code แต่ยังเป็น user mode → kernel code ทำงานไม่ได้ จึงต้องเปลี่ยนทั้งสี่อย่างพร้อมกัน

Interrupt masking

  • interrupt handler ทำงานโดย ปิด interrupt ไว้ และเปิดคืนเมื่อ handler ทำเสร็จ
  • kernel ปิด interrupt เองได้ด้วย เช่น ตอนกำลังเลือก process/thread ถัดไป (เป็น atomic section)
  • บน x86: CLI ปิด interrupt, STI เปิด interrupt
  • มีผลเฉพาะ CPU ปัจจุบัน (ในเครื่อง multicore)
  • handler ต้อง non-blocking: ทำจนเสร็จ ไม่รออะไร งานหนักให้เก็บใส่คิวแล้วส่งต่อให้ OS thread ทำ (ปลุก OS thread ที่มีอยู่)
  • hardware อาจมี interrupt หลายระดับ จึง mask เฉพาะบางตัวได้ เช่น ตัวที่ priority ต่ำกว่า
  • Non-Maskable Interrupt (NMI) ปิดไม่ได้ เช่น kernel segmentation fault หรือไฟกำลังจะดับ

interrupt masking จะกลับมาอีกครั้งตอนสร้าง synchronization

System call handler ใน kernel

  1. Vector ผ่านทางเข้าที่กำหนดไว้ — ตารางที่ map หมายเลข system call ไปยัง handler
  2. หา argument — อยู่ใน register หรือบน user stack
  3. Copy argument จาก user memory เข้า kernel memory โดยตรวจตำแหน่งอย่างระวัง — กัน kernel จากโค้ดมุ่งร้ายที่พยายามหลบการตรวจ
  4. Validate argument — กัน kernel จากข้อผิดพลาดใน user code
  5. Copy ผลลัพธ์กลับ เข้า user memory โดยตรวจตำแหน่งอย่างระวังเช่นกัน

ทำไมต้อง copy ก่อน validate: ถ้า kernel ตรวจข้อมูลที่ยังอยู่ใน user memory แล้วค่อยใช้ทีหลัง อีก thread ของโปรแกรมเดียวกันอาจแก้ข้อมูลนั้นในช่วงระหว่าง "ตรวจ" กับ "ใช้" ได้ การ copy เข้ามาก่อนทำให้สิ่งที่ตรวจกับสิ่งที่ใช้เป็นตัวเดียวกัน

ตัวอย่างไล่ทีละขั้น

โจทย์: โปรแกรมเรียก read(fd, buf, 100) โดย buf ชี้ไปที่ address ใน kernel memory เกิดอะไรขึ้น

  1. library ใส่หมายเลข system call ของ read และ argument แล้วทำคำสั่ง trap → เข้า kernel mode ผ่านทางเข้าที่กำหนด
  2. kernel ใช้หมายเลขเปิดตาราง ไปที่ handler ของ read
  3. handler หา argument (fd, buf, 100) จาก register หรือ user stack แล้ว copy เข้า kernel
  4. validate: buf ถึง buf + 100 ต้องอยู่ใน memory ที่ process นี้เขียนได้ — ไม่ผ่าน เพราะชี้เข้า kernel memory
  5. kernel คืน error ให้โปรแกรม ไม่เขียนอะไรลง address นั้น

ถ้าไม่มีขั้น 4 โปรแกรมจะหลอกให้ kernel เขียนทับ memory ของ kernel เองได้

จุดที่มักพลาด

1. สลับ interrupt กับ exception

interrupt มาจากข้างนอก (timer, I/O) exception มาจากคำสั่งที่โปรแกรมทำ (ผิดพลาดหรือมุ่งร้าย)

2. คิดว่า system call คือการเรียกฟังก์ชันธรรมดา

เป็น protected procedure call: เปลี่ยน mode, เข้าได้เฉพาะทางที่กำหนด, kernel ตรวจ argument ทุกครั้ง

3. คิดว่า kernel กลับ user mode ได้ทางเดียวคือ return

มีสี่ทาง: เริ่ม process/thread ใหม่, return, context switch, upcall

4. คิดว่า CLI ปิด interrupt ทั้งเครื่อง

ปิดเฉพาะ CPU ปัจจุบัน

5. คิดว่า handler รอ I/O ได้

handler ต้อง non-blocking งานหนักส่งต่อให้ OS thread

ที่มา: 02-ProcessAndContextSwitch_Part-I_v3.pdf หน้า 27–32 · 03-ProcessAndContextSwitch_Part-II_v2.pdf หน้า 4–6, 16–19