36 điểm bởi GN⁺ 2025-08-29 | 1 bình luận | Chia sẻ qua WhatsApp
  • Mẫu thiết kế hướng đối tượng vẫn có thể hiện thực tính đa hình và tính mô-đun ngay cả trong kernel viết bằng C, từ đó cho phép thiết kế hệ thống linh hoạt hơn
  • Sử dụng vtable (bảng hàm ảo) để chuẩn hóa giao diện của thiết bị và dịch vụ, đồng thời hỗ trợ nhiều hành vi khác nhau thông qua thay đổi động lúc chạy
  • Các dịch vụ kernel và scheduler cung cấp giao diện nhất quán cho các thao tác như khởi động, dừng, khởi động lại thông qua vtable, đồng thời đóng gói chi tiết triển khai
  • Kết hợp với kernel module để hỗ trợ nạp driver động, cho phép mở rộng hệ thống mà không cần biên dịch lại
  • Cách tiếp cận này mang lại tính linh hoạt và sự tự do thử nghiệm, nhưng có nhược điểm là cú pháp phức tạp và sự rườm rà do phải truyền đối tượng tường minh

Tự do trong phát triển OS và các mẫu hướng đối tượng

  • Việc tự phát triển OS cho phép thử nghiệm tự do mà không bị ràng buộc bởi cộng tác hay ứng dụng thực tế
    • Không bị áp lực bởi lỗ hổng bảo mật, bảo trì mã nguồn hay quy trình phát hành
    • Đây là một sức hút của phát triển OS, vì có thể khám phá các mẫu lập trình phi tiêu chuẩn
  • Bài viết LWN “Object-oriented design patterns in the kernel” giới thiệu các ví dụ Linux kernel hiện thực nguyên lý hướng đối tượng bằng C
    • Hiện thực tính đa hình bằng struct chứa con trỏ hàm
    • Tận dụng lợi ích của hướng đối tượng ngay cả trong kernel mức thấp thông qua đóng gói, tính mô-đun và khả năng mở rộng

Khái niệm cơ bản về vtable

  • vtable là một struct chứa các con trỏ hàm, dùng để định nghĩa giao diện của đối tượng
    • Ví dụ: struct cho thao tác của thiết bị
      struct device_ops {  
          void (*start)(void);  
          void (*stop)(void);  
      };  
      struct device {  
          const char *name;  
          const struct device_ops *ops;  
      };  
      
  • Các thiết bị khác nhau (ví dụ: netdev, disk) dùng cùng một API nhưng phần triển khai khác nhau
    • netdev.ops->start() gọi hành vi của thiết bị mạng, còn disk.ops->start() gọi hành vi của thiết bị đĩa
  • Thay đổi lúc chạy: có thể thay thế vtable động để đổi hành vi mà không cần sửa mã phía gọi
    • Khi đồng bộ hóa phù hợp, cách này cho phép tiến hóa hành vi động một cách gọn gàng

Các trường hợp áp dụng trong OS

Quản lý dịch vụ

  • Quản lý các dịch vụ kernel (trình quản lý mạng, worker pool, window server, v.v.) bằng giao diện nhất quán
    • Struct dịch vụ:
      struct service_ops {  
          void (*start)(void);  
          void (*stop)(void);  
          void (*restart)(void);  
      };  
      struct service {  
          pid_t pid;  
          const struct service_ops *ops;  
      };  
      
  • Mỗi dịch vụ hiện thực hành vi riêng, nhưng từ terminal có thể khởi động/dừng/khởi động lại theo cách chuẩn hóa
  • Giảm độ kết dính giữa mã và dịch vụ, giúp việc quản lý đơn giản hơn

Scheduler

  • Scheduler có thể hỗ trợ nhiều chiến lược như round-robin, shortest-job-first, FIFO, lập lịch theo độ ưu tiên
    • Giao diện được đơn giản hóa thành yield, block, add, next
    • Định nghĩa bằng vtable để có thể thay đổi chính sách lập lịch lúc chạy
    • Có thể thay đổi toàn bộ chính sách mà không cần sửa phần còn lại của kernel

Trừu tượng hóa file

  • Struct file_operations của Linux hiện thực triết lý “mọi thứ đều là file”
  • Socket, thiết bị và file văn bản đều cung cấp cùng một giao diện read/write
  • Mã ở user space không cần biết chi tiết triển khai mà vẫn có thể hoạt động theo cách nhất quán

Kết hợp với kernel module

  • Kernel module hỗ trợ nạp driver hoặc hook động thông qua việc thay thế vtable
    • Tương tự Linux module, có thể mở rộng kernel mà không cần biên dịch lại hoặc khởi động lại
    • Khi thêm tính năng mới, chỉ cần cập nhật vtable của struct hiện có

Nhược điểm

  • Độ phức tạp cú pháp:
    • Phải truyền đối tượng một cách tường minh như object->ops->start(object)
    • Rườm rà hơn so với cách truyền ngầm trong C++
    • Chữ ký hàm cũng dài dòng hơn:
      static void object_start(struct object* this) {  
          this->id = ...  
      }  
      
  • Ưu điểm: việc truyền tường minh giúp làm rõ phụ thuộc của hàm, và mối liên kết giữa đối tượng với hành vi trở nên minh bạch hơn
    • Đây là một tradeoff hợp lý giữa độ phức tạp và tính rõ ràng trong mã kernel

Hàm ý

  • vtable cung cấp một cách đơn giản để giảm độ phức tạp mà vẫn giữ được tính linh hoạt
    • Dễ thay thế hành vi lúc chạy, duy trì giao diện nhất quán và thêm tính năng mới
  • Mang đến một cách mới để hiện thực thiết kế hướng đối tượng bằng C, đồng thời nhấn mạnh niềm vui thử nghiệm trong phát triển OS
  • Tài liệu bổ sung: dự án xine (https://xine.sourceforge.net/hackersguide#id324430) giới thiệu cách quản lý biến riêng tư bằng vtable
  • Phát triển OS là sân chơi của thử nghiệm sáng tạo, và các mẫu hướng đối tượng chứng minh rằng đây là công cụ mạnh mẽ ngay cả trong các hệ thống mức thấp

1 bình luận

 
GN⁺ 2025-08-29
Ý kiến trên Hacker News
  • Bài viết bàn về việc dù Linux kernel được viết bằng C, nó vẫn tiếp nhận các nguyên lý hướng đối tượng như hiện thực đa hình bằng cách sử dụng con trỏ hàm trong struct. Kỹ thuật này đã tồn tại từ rất lâu trước cả lập trình hướng đối tượng và thường được gọi là 'kiểu dữ liệu trừu tượng (ADT)' hoặc trừu tượng hóa dữ liệu. Khác biệt cốt lõi giữa ADT và OOP là trong ADT có thể bỏ qua việc hiện thực hàm, còn trong OOP thì luôn cần hiện thực. Nếu cần các hàm tùy chọn trong OOP, phải tạo thêm class cho từng hàm tùy chọn, rồi mỗi lần hiện thực lại phải kế thừa kèm qua đa kế thừa và kiểm tra lúc runtime xem đối tượng đó có phải là instance của class bổ sung ấy hay không, khá phiền phức. Trong khi đó với ADT, chỉ cần kiểm tra đơn giản xem con trỏ hàm có phải NULL hay không
    • Trong Smalltalk và Objective-C, cách làm OOP truyền thống là có thể dễ dàng kiểm tra lúc runtime xem một đối tượng có phản hồi một message hay không. Thật đáng tiếc khi bản chất của OOP bị méo mó bởi các pattern thiết kế quá thiên về class trong C++ và Java
    • Phần lớn là đồng ý, và cho biết trong C cũng dùng kiểu pattern này, còn trong OOP truyền thống thì cách tiếp cận phổ biến là đặt hiện thực mặc định (default) hoặc stub trong lớp cơ sở. Trong OOP hiện đại hay các ngôn ngữ hướng khái niệm, cũng có thể cast sang interface chỉ dùng một tập con của API cần thiết. Go là ví dụ điển hình
    • Về nhận định rằng kỹ thuật này có trước lập trình hướng đối tượng, có người muốn diễn đạt rằng OOP đúng hơn là sự chính thức hóa (formalization) của các pattern và paradigm đã có từ trước
    • Ngay cả trong phần lớn ngôn ngữ OOP như Java, C# v.v. hiện nay cũng có thể dùng lambda nên hoàn toàn có thể hiện thực giống hệt trong C. Lambda thực chất chỉ là con trỏ hàm nên có thể gán trực tiếp vào biến instance. (Java mất hơn 10 năm mới đưa lambda vào, và còn có giai thoại cũ khá buồn cười là Sun Microsystems từng kiện Microsoft vì nỗ lực thêm lambda vào Java)
    • Kế thừa không phải bắt buộc. Có thể dùng pattern hợp thành (composite). Python cũng tương tự ở chỗ phải truyền rõ ràng con trỏ self/this/object, nên khá giống với kiểu trừu tượng hóa dữ liệu theo phong cách C
  • Vài năm trước Peterpaul từng phát triển một hệ thống hướng đối tượng nhẹ có thể dùng khá dễ chịu trên nền C (repo). Không cần truyền đối tượng một cách tường minh, tài liệu còn thiếu nhưng có bộ kiểm thử đầy đủ (test1, test2)
    • Nếu muốn xem nó trông ra sao khi không có cú pháp đường mật của carbon, có thể xem ở đây. Có vẻ nó không hỗ trợ đa hình tham số
    • Cũng có ý kiến rằng Vala đang thử sức khá phù hợp với thị trường ngách này
  • Có người nói mình không rành lắm phần này, nhưng OP có vẻ đang làm khác với điều các lập trình viên kernel đã làm. Đọc bài mà OP dẫn thì vtable có chứa con trỏ hàm theo kiểu định kiểu rõ ràng, còn OP tạo cảm giác đang dùng con trỏ void. Ngoài ra lợi ích chính được bài viết của phía kernel nhắc tới là tiết kiệm bộ nhớ bằng cách không đặt nhiều con trỏ hàm trong từng instance của struct mà chỉ cần một con trỏ vtable. Tức là điểm chính là tiết kiệm bộ nhớ, trong khi OP lại dùng vtable như một lớp gián tiếp để thay thế method lúc runtime và hiện thực đa hình. Pattern này khác với điều các lập trình viên kernel đang nói
    • Có người phản hồi rằng OP không nói đến con trỏ void mà là void theo nghĩa hàm không tham số, không trả về giá trị. Vtable được dùng để hiện thực đa hình. Nếu không có đa hình thì vốn dĩ cũng chẳng dùng vtable, nên còn tiết kiệm bộ nhớ hơn nữa
  • Trước ý kiến rằng việc phải truyền đối tượng một cách tường minh mỗi lần là bất tiện, có người nói ngược lại rằng họ ghét việc dùng this ngầm. Trên thực tế vẫn luôn đang truyền instance this, và this tường minh giúp không bị nhầm một biến là của instance, toàn cục hay đến từ nơi khác
    • Có người cho rằng trong cú pháp OOP của C++ (và Java), việc không bắt buộc dùng this khi tham chiếu thành viên instance là một trong những sai lầm lớn
    • Có người nghĩ tác giả đang chỉ ra chỗ phải ghi đối tượng hai lần trong object->ops->start(object): một lần để phân giải vtable, một lần để truyền đối tượng vào hiện thực hàm C
    • Để làm rõ biến thuộc về đâu, người ta thường dùng quy ước đặt tên cho biến thành viên như mFoo, m_Foo, foo_ v.v. Có người thích foo_ hơn this->foo vì ngắn gọn hơn. Dĩ nhiên trong C++ vẫn có thể viết this một cách tường minh
    • this ngầm giúp việc viết code ngắn gọn hơn, và khi dùng method thực thụ thì không phải lặp lại tiền tố struct cho mọi hàm. Ví dụ mystruct_dosmth(s); trở nên tự nhiên hơn thành s->dosmth();
    • Cũng có thể xử lý khéo hơn một chút bằng macro
  • Có người cho biết mình học được pattern này lần đầu từ tài liệu thuyết trình của Tmux (tài liệu). Họ cũng có một bài viết tự tổng hợp về khái niệm này (bài về lệnh hướng đối tượng trong tmux)
  • Có người từng hiện thực cách này trong vài dự án nhỏ thời đại học. Cảm giác tạo ra được thứ giống OOP trong C khá thú vị, nhưng nếu không cẩn thận thì vấn đề có thể phình to rất nhanh
  • Cần lưu ý đây là pattern tận dụng interface (tức vtable, bảng con trỏ hàm), chứ không phải toàn bộ mô hình object. Các tính năng hướng đối tượng khác như class, kế thừa v.v. lại có chi phí cao hơn và cũng khó tuân theo hơn
    • Suy cho cùng, kế thừa chỉ là một dạng hợp thành của vtable. Còn class cũng chỉ là sự kết hợp giữa vtable và các biến trong phạm vi mà thôi
    • Trong C, nếu cast struct ở thành viên đầu tiên thì việc kế thừa field lại tự nhiên hơn tưởng tượng
    • Vtable thường chứa các hàm nhận con trỏ this. Ví dụ struct file_operations lại là các con trỏ hàm không nhận con trỏ this, nên khó xem đó là vtable đúng nghĩa
  • Có người tạo các wrapper inline cho hàm trong vtable để có thể viết foo(thing, ...) thay vì thing->vtable->foo(thing, ...)
  • Có người luôn thắc mắc vì sao pattern này không được đưa vào tiêu chuẩn C mới. Rõ ràng rất nhiều người đang lặp đi lặp lại cùng một kiểu hiện thực
    • Nếu thêm cú pháp đường mật (các yếu tố cú pháp giúp bớt rườm rà), thì sẽ phải đồng thời tồn tại cả cách dùng được cho phép chính thức lẫn một kiểu fallback có cảm giác như còn thiếu gì đó. Điểm mạnh của C là không che giấu sự phức tạp động. Mỗi khi có dynamic dispatch xảy ra thì luôn rất rõ ràng. Đã có nhiều ngôn ngữ chính thức hóa chuyện này rồi, nhưng ưu điểm riêng của C là sự phức tạp được phơi bày ra. Vì vậy người ta chỉ dùng khi thực sự cần dynamic dispatch. Ngoài ra cú pháp này cũng không quá khó
    • Có lẽ phía High C Compiler đã từng thử đi theo hướng này ở mức độ nào đó
  • Đây là lời khuyên rất mạnh mẽ từ kinh nghiệm thực tế: tuyệt đối đừng dùng pattern này. Có người từng ám ảnh vì phải bảo trì một codebase lớn được viết theo cấu trúc này. Độ dễ đọc thì tệ hại, compiler không thể tối ưu tốt các lời gọi qua con trỏ, tooling cũng không hỗ trợ gì cả. Cú pháp thì gượng gạo, người mới muốn đọc được code gần như phải hiểu tường tận nội bộ compiler C++. Quan trọng hơn hết, so với những lợi ích đáng ngờ của việc đưa OOP vào, về lâu dài nó có thể phá hỏng khả năng bảo trì. Nếu thật sự cần thì cứ dùng C++
    • Khi được hỏi cụ thể phần nào là cơn ác mộng, có người nói họ lại cho rằng ít cú pháp đường mật hơn thì tốt hơn cho độ dễ đọc vì giúp thấy rõ lời gọi hàm nào là dynamic dispatch. Nhờ vậy có thể giới hạn nó đúng ở những chỗ cần. Họ cũng từng đọc một bài blog nói rằng code động trong C dễ tối ưu hơn vì có ít con trỏ hàm. Ý không phải là phải tái hiện nguyên xi compiler C++, mà chỉ cần hiểu bản chất OOP thì có thể hiện thực một cách tự nhiên. Cuối cùng, trước lập luận 'đừng biến C thành một thứ C++ vụng về', có người cho rằng đây mới chính là cách làm đậm chất C, và lý do chọn nó là vì có thể chèn tính động đúng vào những nơi mong muốn.