36 คะแนน โดย GN⁺ 2025-08-29 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • แพตเทิร์นการออกแบบเชิงวัตถุ สามารถนำมาใช้ใน เคอร์เนลที่เขียนด้วยภาษา 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;  
      };  
      
  • อุปกรณ์ต่างชนิดกัน (เช่น 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;  
      };  
      
  • แต่ละบริการสามารถมีพฤติกรรมเฉพาะของตัวเองได้ ขณะที่คำสั่ง 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 *);  
          ...  
      };  
      
  • ไม่ว่าจะเป็น 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 ความคิดเห็น

 
GN⁺ 2025-08-29
ความคิดเห็นบน Hacker News
  • มีการพูดคุยถึงบทความที่อธิบายว่า แม้ Linux kernel จะเขียนด้วยภาษา C แต่ก็รับเอาหลักการเชิงวัตถุมาใช้ เช่น การใช้ function pointer ใน struct เพื่อทำ polymorphism เทคนิคแบบนี้มีมาก่อนการเขียนโปรแกรมเชิงวัตถุเสียอีก และมักเรียกว่า 'abstract data type (ADT)' หรือ data abstraction ความต่างสำคัญระหว่าง ADT กับ OOP คือ ใน ADT สามารถละเว้นการ implement ฟังก์ชันได้ แต่ใน OOP ต้องมี implementation เสมอ ถ้าต้องการฟังก์ชันแบบเลือกได้ใน OOP ก็ต้องสร้างคลาสเพิ่มสำหรับแต่ละฟังก์ชันเลือกได้ แล้วทุกครั้งที่ implement ก็ต้องสืบทอดร่วมกันผ่าน multiple inheritance และตรวจตอนรันไทม์ว่าอ็อบเจ็กต์นั้นเป็นอินสแตนซ์ของคลาสเสริมนั้นหรือไม่ ซึ่งยุ่งยาก ขณะที่ใน ADT แค่ตรวจว่า function pointer เป็น NULL หรือไม่ก็พอ
    • ใน Smalltalk และ Objective-C วิธีแบบ OOP ดั้งเดิมคือเช็กได้ง่าย ๆ ตอนรันไทม์ว่าอ็อบเจ็กต์ตอบสนองต่อข้อความนั้นได้หรือไม่ น่าเสียดายที่แก่นของ OOP ถูกบิดเบือนไปเพราะรูปแบบการออกแบบที่ยึดคลาสเป็นศูนย์กลางมากเกินไปใน C++ และ Java
    • ส่วนใหญ่เห็นด้วย และบอกว่าใน C ก็ใช้แพตเทิร์นนี้เช่นกัน โดยใน OOP แบบดั้งเดิม แนวทางที่พบได้บ่อยคือใส่ implementation แบบ default หรือ stub ไว้ที่ base ส่วนใน OOP ยุคใหม่หรือภาษาแนว concept-oriented ก็มีวิธี cast ไปยัง interface ที่ใช้เพียงบางส่วนของ API ที่ต้องการได้ Go เป็นตัวอย่างที่ดี
    • สำหรับข้ออ้างว่าเทคนิคนี้มาก่อน OOP นั้น อยากอธิบายว่า OOP เป็นการทำให้แพตเทิร์นและกระบวนทัศน์ที่มีอยู่ก่อนแล้วกลายเป็นรูปแบบที่ชัดเจนเป็นทางการมากขึ้น
    • ในภาษา OOP ส่วนใหญ่ เช่น Java, C# ตอนนี้ก็ใช้ lambda ได้แล้ว จึงทำแบบเดียวกับใน C ได้เลย เพราะ lambda ก็เป็นเพียง function pointer และสามารถกำหนดตรงให้กับตัวแปรอินสแตนซ์ได้ (Java ใช้เวลากว่าสิบปีกว่าจะเพิ่ม lambda และ Sun Microsystems ก็เคยถึงขั้นฟ้อง Microsoft จากความพยายามจะเพิ่ม lambda เข้าไปใน Java ซึ่งเป็นเกร็ดเก่า ๆ ที่ชวนขำ)
    • inheritance ไม่ใช่สิ่งจำเป็น ใช้ composite pattern แทนได้ Python ก็คล้ายกันตรงที่ต้องส่ง self/this/object pointer อย่างชัดเจน จึงคล้าย data abstraction แบบ C
  • เมื่อหลายปีก่อน Peterpaul เคยพัฒนาระบบเชิงวัตถุแบบเบา ๆ ที่ใช้งานบน C ได้อย่างสะดวก (repo) ไม่ต้องส่งอ็อบเจ็กต์อย่างชัดเจนเอง เอกสารยังไม่มาก แต่มีชุดทดสอบครบถ้วน (ทดสอบ1, ทดสอบ2)
    • ถ้าอยากเห็นว่าหน้าตาเป็นอย่างไรเมื่อไม่มี syntactic sugar ของ carbon ดูได้ที่นี่ ดูเหมือนว่าจะไม่รองรับ parametric polymorphism
    • คิดว่า Vala ก็เป็นความพยายามที่เหมาะกับพื้นที่เฉพาะทางนี้เช่นกัน
  • ผู้แสดงความเห็นบอกว่าตนไม่ค่อยรู้เรื่องนี้มากนัก แต่ดูเหมือน OP จะทำไม่เหมือนสิ่งที่นักพัฒนา kernel ทำ ถ้าอ่านบทความที่ OP ลิงก์ไว้ จะเห็นว่าใน vtable มี type function pointer แต่ OP ให้ความรู้สึกเหมือนใช้ void pointer อีกทั้งข้อดีหลักที่บทความของนักพัฒนา kernel กล่าวถึงคือ การประหยัดหน่วยความจำด้วยการมีเพียง vtable pointer ตัวเดียว แทนที่จะมี function pointer หลายตัวในแต่ละอินสแตนซ์ของ struct กล่าวคือประเด็นหลักคือการประหยัดหน่วยความจำ แต่ OP ใช้ vtable นี้เป็นชั้น indirect สำหรับสลับเมธอดตอนรันไทม์และทำ polymorphism ซึ่งเป็นคนละแพตเทิร์นกับที่นักพัฒนา kernel พูดถึง
    • OP ไม่ได้หมายถึง void pointer แต่หมายถึง void (ไม่มีอาร์กิวเมนต์และไม่มีค่าที่คืนกลับ) vtable ใช้เพื่อทำ polymorphism ถ้าไม่มี polymorphism ก็ไม่ใช้ vtable เลย จึงยิ่งประหยัดหน่วยความจำกว่าเดิม
  • ต่อความเห็นที่ว่าการต้องส่งอ็อบเจ็กต์อย่างชัดเจนทุกครั้งไม่สะดวก ผู้แสดงความเห็นบอกว่าตนกลับไม่ชอบการใช้ this แบบปริยาย เพราะจริง ๆ แล้วก็มีการส่งอินสแตนซ์ this อยู่ตลอด การมี this แบบชัดเจนทำให้ไม่สับสนว่าตัวแปรเป็นของอินสแตนซ์ เป็น global หรือมาจากที่อื่น
    • คิดว่าหนึ่งในความผิดพลาดใหญ่ของไวยากรณ์ OOP ใน C++ (และ Java) คือการไม่บังคับให้เขียน this เมื่ออ้างถึงสมาชิกของอินสแตนซ์
    • คิดว่าผู้เขียนกำลังชี้ให้เห็นส่วนที่ต้องระบุอ็อบเจ็กต์สองครั้งใน object->ops->start(object) คือครั้งหนึ่งเพื่อ resolve vtable และอีกครั้งเพื่อส่งอ็อบเจ็กต์ให้กับ implementation ของฟังก์ชันใน C
    • เพื่อให้สังกัดของตัวแปรชัดเจน มักใช้ naming convention อย่าง mFoo, m_Foo, foo_ เป็นต้น โดยชอบ foo_ มากกว่า this->foo เพราะสั้นกว่า แน่นอนว่าใน C++ ก็สามารถเขียน this แบบชัดเจนได้
    • this แบบปริยายช่วยให้โค้ดสั้นลง และถ้าใช้เมธอดจริง ก็ไม่ต้องเขียนคำนำหน้า struct ซ้ำในทุกฟังก์ชัน เช่น mystruct_dosmth(s); จะดูเป็นธรรมชาติกว่าเมื่อเขียนเป็น s->dosmth();
    • ใช้ macro ก็ช่วยจัดการให้ฉลาดขึ้นได้อีก
  • ผู้แสดงความเห็นบอกว่าได้เรียนรู้แพตเทิร์นนี้ครั้งแรกจากสไลด์นำเสนอของ Tmux (เอกสาร) และตนก็เคยเขียนบทความสรุปแนวคิดนี้ไว้ด้วย (บทความเรื่องคำสั่งเชิงวัตถุของ tmux)
  • สมัยเรียนมหาวิทยาลัยเคยลองทำแนวทางนี้ในโปรเจ็กต์เล็ก ๆ หลายงาน การทำให้ C มีความรู้สึกคล้าย OOP นั้นสนุกดี แต่ถ้าไม่ระวังก็พังหนักได้อย่างรวดเร็ว
  • ควรสังเกตว่านี่เป็นแพตเทิร์นที่ใช้ interface (ก็คือ vtable หรือ table ของ function pointer) ไม่ใช่ตัวอ็อบเจ็กต์ทั้งหมด ฟีเจอร์เชิงวัตถุอื่น ๆ อย่างคลาสหรือ inheritance กลับมีต้นทุนสูงและทำตามได้ยากกว่า
    • inheritance ท้ายที่สุดก็คือรูปแบบหนึ่งของการ compose vtable ส่วนคลาสก็เป็นเพียงการรวมกันของ vtable และตัวแปรใน scope
    • ใน C ถ้า cast struct ที่เป็นสมาชิกตัวแรก ก็ทำ field inheritance ได้ค่อนข้างเป็นธรรมชาติอย่างน่าประหลาด
    • ปกติใน vtable จะมีฟังก์ชันที่รับ this pointer ตัวอย่าง struct file_operations เป็น function pointer ที่ไม่รับ this pointer จึงมองว่าไม่ค่อยใช่ vtable จริง ๆ
  • มีการทำ inline wrapper ให้กับฟังก์ชันใน vtable เพื่อให้เขียน foo(thing, ...) แทน thing->vtable->foo(thing, ...) ได้
  • มีคนสงสัยมาตลอดว่าทำไมแพตเทิร์นแบบนี้ไม่ถูกใส่ไว้ในมาตรฐาน C ใหม่ ทั้งที่ชัดเจนว่าหลายคนเขียนซ้ำรูปแบบเดิมกันอยู่เรื่อย ๆ
    • ถ้าเพิ่ม syntactic sugar เข้าไป ก็ต้องมีทั้งวิธีใช้งานที่รองรับอย่างเป็นทางการและ fallback ที่ให้ความรู้สึกเหมือนยังขาดอะไรไปพร้อมกัน จุดแข็งของ C คือไม่ซ่อนความซับซ้อนแบบไดนามิก เมื่อมี dynamic dispatch เกิดขึ้นจะเห็นได้ชัดเสมอ แม้หลายภาษาจะทำให้สิ่งนี้เป็นทางการไว้แล้ว แต่ข้อดีเฉพาะของ C คือความซับซ้อนยังปรากฏอยู่ จึงมักถูกใช้เมื่อจำเป็นต้องมี dynamic dispatch จริง ๆ เท่านั้น และไวยากรณ์ก็ไม่ได้ยากอยู่แล้ว
    • ดูเหมือนว่าทางฝั่ง High C Compiler อาจเคยมีความพยายามไปในทิศทางนี้อยู่บ้าง
  • มีคำเตือนอย่างหนักแน่นจากประสบการณ์ตรงว่าอย่าใช้แพตเทิร์นนี้เด็ดขาด เพราะเคยทุกข์กับการดูแลโค้ดขนาดใหญ่ที่ใช้โครงสร้างแบบนี้ มันเป็นฝันร้ายทั้งในแง่การบำรุงรักษา อ่านยากมาก คอมไพเลอร์ก็ optimize การเรียกผ่าน pointer ไม่ได้ และเครื่องมือก็ไม่รองรับเลย ไวยากรณ์ก็ประหลาด วิศวกรใหม่แทบต้องเข้าใจภายในของคอมไพเลอร์ C++ ถึงจะอ่านโค้ดออก ที่สำคัญที่สุดคือ เทียบกับประโยชน์ที่ยังน่าสงสัยของการนำ OOP มาใช้แล้ว มันอาจทำลายการบำรุงรักษาระยะยาวได้ ถ้าจำเป็นจริง ๆ ก็ควรใช้ C++ ไปเลย
    • เมื่อถูกถามว่าฝันร้ายตรงไหนอย่างเจาะจง ก็มีคนโต้ว่าแบบที่มี syntactic sugar น้อยกลับช่วยให้เห็นชัดว่าการเรียกฟังก์ชันใดเป็น dynamic dispatch จึงอ่านง่ายกว่า และทำให้จำกัดการใช้งานไว้เฉพาะจุดที่ต้องการ dynamic dispatch ได้ นอกจากนี้ยังเคยอ่านบทความในบล็อกที่บอกว่าโค้ดไดนามิกใน C optimize ได้ง่ายกว่าเพราะมี function pointer น้อยกว่า ไม่ได้หมายความว่าต้องไป reimplement คอมไพเลอร์ C++ ทั้งหมด แค่เข้าใจแก่นของ OOP ก็ทำแบบนี้ได้ตามธรรมชาติ สุดท้าย สำหรับข้ออ้างว่า 'อย่าทำให้ C กลายเป็น C++ ห่วย ๆ' นั้น มุมมองนี้กลับเห็นว่านี่ต่างหากคือวิธีแบบ C และเป็นเหตุผลที่เลือกใช้ เพราะสามารถใส่ความ dynamic ได้ตรงจุดตามต้องการ