- แพตเทิร์นการออกแบบเชิงวัตถุ สามารถนำมาใช้ใน เคอร์เนลที่เขียนด้วยภาษา C เพื่อทำให้เกิด polymorphism และความเป็นโมดูลาร์ ช่วยให้การออกแบบระบบมีความยืดหยุ่นมากขึ้น
- ใช้ vtable (ตารางฟังก์ชันเสมือน) เพื่อทำให้อินเทอร์เฟซของอุปกรณ์และบริการเป็นมาตรฐาน และรองรับ การเปลี่ยนพฤติกรรมแบบไดนามิกขณะรันไทม์
- บริการของเคอร์เนลและ scheduler ใช้อินเทอร์เฟซผ่าน vtable เพื่อรองรับการเริ่ม หยุด และเริ่มใหม่อย่างสอดคล้องกัน พร้อมซ่อนรายละเอียดการติดตั้งใช้งานไว้ภายใน
- เมื่อทำงานร่วมกับ kernel module จะรองรับการโหลดไดรเวอร์แบบไดนามิก และขยายระบบได้โดยไม่ต้องคอมไพล์ใหม่
- แนวทางนี้ให้ทั้ง ความยืดหยุ่น และอิสระในการทดลอง แต่ข้อเสียคือไวยากรณ์ซับซ้อนและมีความจุกจิกจากการต้องส่งอ็อบเจ็กต์อย่างชัดเจน
อิสระและแพตเทิร์นเชิงวัตถุในการพัฒนา OS
- การพัฒนา OS ของตัวเองเปิดโอกาสให้ทดลองได้อย่างอิสระ โดยไม่ติดข้อจำกัดจากการทำงานร่วมกับผู้อื่นหรือการใช้งานจริง
- ไม่ต้องกังวลเรื่องช่องโหว่ด้านความปลอดภัย การบำรุงรักษาโค้ด หรือภาระในการออกรีลีส
- นี่คือเสน่ห์ของการพัฒนา OS ที่ทำให้สามารถสำรวจแพตเทิร์นการเขียนโปรแกรมนอกมาตรฐานได้
- บทความ LWN “Object-oriented design patterns in the kernel” ยกตัวอย่างการที่เคอร์เนล Linux นำ หลักการเชิงวัตถุ มาใช้ในภาษา C
- ใช้ struct ที่มี function pointer เพื่อสร้าง polymorphism
- อาศัย encapsulation, modularity และ extensibility เพื่อดึงข้อดีของแนวคิดเชิงวัตถุมาใช้แม้ในเคอร์เนลระดับล่าง
แนวคิดพื้นฐานของ vtable
- vtable คือ struct ที่บรรจุ function pointer เพื่อกำหนดอินเทอร์เฟซของอ็อบเจ็กต์
- ตัวอย่าง: struct สำหรับพฤติกรรมของอุปกรณ์
struct device_ops { void (*start)(void); void (*stop)(void); }; struct device { const char *name; const struct device_ops *ops; };
- ตัวอย่าง: struct สำหรับพฤติกรรมของอุปกรณ์
- อุปกรณ์ต่างชนิดกัน (เช่น
netdev,disk) ใช้ API เดียวกัน แต่มีการติดตั้งใช้งานต่างกันnetdev.ops->start()จะเรียกการทำงานของอุปกรณ์เครือข่าย ส่วนdisk.ops->start()จะเรียกการทำงานของดิสก์
- การเปลี่ยนแปลงขณะรันไทม์: สามารถสลับ vtable แบบไดนามิกเพื่อเปลี่ยนพฤติกรรมได้โดยไม่ต้องแก้โค้ดฝั่งที่เรียกใช้
- หากซิงโครไนซ์อย่างเหมาะสม ก็จะช่วยให้พฤติกรรมไดนามิกเปลี่ยนแปลงได้อย่างเป็นระเบียบ
ตัวอย่างการใช้งานใน OS
การจัดการบริการ
- จัดการบริการของเคอร์เนล (เช่น networking manager, worker pool, window server) ผ่าน อินเทอร์เฟซที่สอดคล้องกัน
- struct ของบริการ:
struct service_ops { void (*start)(void); void (*stop)(void); void (*restart)(void); }; struct service { pid_t pid; const struct service_ops *ops; };
- struct ของบริการ:
- แต่ละบริการสามารถมีพฤติกรรมเฉพาะของตัวเองได้ ขณะที่คำสั่ง start/stop/restart จากเทอร์มินัลยังทำงานในรูปแบบมาตรฐานเดียวกัน
- ลด ระดับการพึ่งพากัน ระหว่างโค้ดกับบริการ และทำให้การจัดการง่ายขึ้น
Scheduler
- scheduler รองรับนโยบายหลากหลาย เช่น round-robin, shortest-job-first, FIFO และ priority scheduling
- อินเทอร์เฟซถูกย่อให้เหลือ
yield,block,add,next - กำหนดผ่าน vtable จึงสามารถสลับนโยบายการจัดตารางขณะรันไทม์ได้
- เปลี่ยนนโยบายทั้งระบบได้โดยไม่ต้องแก้ส่วนอื่นของเคอร์เนล
- อินเทอร์เฟซถูกย่อให้เหลือ
การทำ abstraction ของไฟล์
- struct file_operations ของ Linux คือการทำให้แนวคิด “ทุกอย่างคือไฟล์” เกิดขึ้นจริง
- ตัวอย่าง: https://elixir.bootlin.com/linux/v6.15/source/include/linux/fs.h
struct file_operations { struct module *owner; loff_t (*llseek)(struct file *, loff_t, int); ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); ... };
- ตัวอย่าง: https://elixir.bootlin.com/linux/v6.15/source/include/linux/fs.h
- ไม่ว่าจะเป็น socket, อุปกรณ์ หรือไฟล์ข้อความ ต่างก็ใช้อินเทอร์เฟซ read/write แบบเดียวกัน
- โค้ดใน user space จึงทำงานได้อย่างสม่ำเสมอ โดยไม่จำเป็นต้องรู้รายละเอียดการติดตั้งใช้งาน
การทำงานร่วมกับ kernel module
- kernel module รองรับการโหลดไดรเวอร์หรือ hook แบบไดนามิกผ่านการสลับ vtable
- คล้ายกับ Linux module ที่ขยายเคอร์เนลได้โดยไม่ต้องคอมไพล์ใหม่หรือรีบูต
- เมื่อต้องการเพิ่มความสามารถใหม่ ก็เพียงอัปเดต vtable ของ struct เดิม
ข้อเสีย
- ความซับซ้อนของไวยากรณ์:
- ต้องส่งอ็อบเจ็กต์อย่างชัดเจน เช่น
object->ops->start(object) - ดูจุกจิกกว่าเมื่อเทียบกับการส่งแบบแฝงของ C++
- function signature ก็ยาวและซับซ้อนกว่า:
static void object_start(struct object* this) { this->id = ... }
- ต้องส่งอ็อบเจ็กต์อย่างชัดเจน เช่น
- ข้อดี: การส่งแบบชัดเจนทำให้เห็น dependency ของฟังก์ชันได้ชัด และความเชื่อมโยงระหว่างอ็อบเจ็กต์กับพฤติกรรมก็โปร่งใสมากขึ้น
- สำหรับโค้ดเคอร์เนล นี่คือ tradeoff ที่เหมาะสมระหว่างความซับซ้อนกับความชัดเจน
ประเด็นที่น่าสนใจ
- vtable เป็นวิธีที่เรียบง่ายในการรักษา ความยืดหยุ่น พร้อมลดความซับซ้อน
- เปลี่ยนพฤติกรรมขณะรันไทม์ได้ รักษาอินเทอร์เฟซให้สม่ำเสมอ และเพิ่มความสามารถใหม่ได้ง่าย
- เป็นอีกแนวทางหนึ่งในการทำ การออกแบบเชิงวัตถุ ด้วยภาษา C และตอกย้ำความสนุกเชิงทดลองของการพัฒนา OS
- ข้อมูลเพิ่มเติม: โครงการ xine (https://xine.sourceforge.net/hackersguide#id324430) แสดงวิธีจัดการตัวแปร private ด้วย vtable
- การพัฒนา OS คือพื้นที่ของ การทดลองอย่างสร้างสรรค์ และพิสูจน์ว่าแพตเทิร์นเชิงวัตถุเป็นเครื่องมือที่ทรงพลังได้แม้ในระบบระดับล่าง
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
object->ops->start(object)คือครั้งหนึ่งเพื่อ resolve vtable และอีกครั้งเพื่อส่งอ็อบเจ็กต์ให้กับ implementation ของฟังก์ชันใน Cfoo_มากกว่าthis->fooเพราะสั้นกว่า แน่นอนว่าใน C++ ก็สามารถเขียน this แบบชัดเจนได้mystruct_dosmth(s);จะดูเป็นธรรมชาติกว่าเมื่อเขียนเป็นs->dosmth();struct file_operationsเป็น function pointer ที่ไม่รับ this pointer จึงมองว่าไม่ค่อยใช่ vtable จริง ๆfoo(thing, ...)แทนthing->vtable->foo(thing, ...)ได้