- 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; };
- Ví dụ: struct cho thao tác của thiết bị
- 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 nhaunetdev.ops->start()gọi hành vi của thiết bị mạng, còndisk.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; };
- Struct dịch vụ:
- 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
- Giao diện được đơn giản hóa thành
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”
- Ví dụ: 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 *); ... };
- Ví dụ: https://elixir.bootlin.com/linux/v6.15/source/include/linux/fs.h
- 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 = ... }
- Phải truyền đối tượng một cách tường minh như
- Ư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
Ý kiến trên Hacker News
NULLhay khôngvoid. 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óivoidmà làvoidtheo 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ữathisngầm. Trên thực tế vẫn luôn đang truyền instancethis, vàthistườ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ácthiskhi tham chiếu thành viên instance là một trong những sai lầm lớnobject->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 CmFoo,m_Foo,foo_v.v. Có người thíchfoo_hơnthis->foovì ngắn gọn hơn. Dĩ nhiên trong C++ vẫn có thể viếtthismột cách tường minhthisngầ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ànhs->dosmth();this. Ví dụstruct file_operationslại là các con trỏ hàm không nhận con trỏthis, nên khó xem đó là vtable đúng nghĩafoo(thing, ...)thay vìthing->vtable->foo(thing, ...)