Design Pattern kinh điển trong Linux Kernel (3/3): kiến trúc và checklist review¶
Phần cuối trong loạt 3 phần: các pattern kiến trúc giúp driver cắm vào kernel, bảng đối chiếu với GoF/C++ và DPDK, checklist review và bài tập.
Phần này tiếp nối phần 2 (dữ liệu intrusive và concurrency). Các pattern ở phần 1 và 2 được nhắc lại trong bảng tổng hợp và checklist.
5. Kiến trúc và mở rộng¶
Nhóm này trả lời câu hỏi: làm sao để hàng nghìn driver và subsystem do các nhóm khác nhau viết có thể cắm vào kernel mà không ai phải sửa code trung tâm.
5.1 Registration framework — driver không gọi ai, core gọi driver¶
Vấn đề. Kernel không thể biết trước có những driver nào, còn device thì có thể xuất hiện trước hoặc sau driver.
Cách làm. Driver khai báo một struct mô tả mình (match table và callback) rồi đăng ký với bus. Driver core lo việc ghép cặp và gọi probe() khi tìm thấy device phù hợp. Đây là inversion of control, tương đương Template Method hoặc Plugin.
static const struct of_device_id mydev_of_match[] = {
{ .compatible = "vendor,mydev" },
{ }
};
MODULE_DEVICE_TABLE(of, mydev_of_match);
static struct platform_driver mydev_driver = {
.probe = mydev_probe,
.remove = mydev_remove,
.driver = {
.name = "mydev",
.of_match_table = mydev_of_match,
},
};
module_platform_driver(mydev_driver);
Cần nhớ. probe() có thể chạy nhiều lần (nhiều instance) và có thể bị hoãn bằng -EPROBE_DEFER khi phụ thuộc chưa sẵn sàng. Vì vậy không được giữ state trong biến global, mọi thứ phải nằm trong struct private của từng device. Cùng một pattern lặp lại ở PCI, I2C, SPI, USB, chỉ khác tên struct.
5.2 Notifier chain — Observer pattern¶
Vấn đề. Nhiều module cần biết khi một sự kiện xảy ra (interface up/down, CPU hotplug, reboot) mà nơi phát sự kiện không cần biết họ.
Cách làm. Mỗi bên quan tâm đăng ký một notifier_block vào chain. Nơi phát sự kiện gọi lần lượt từng callback theo thứ tự priority.
static int mydev_netdev_event(struct notifier_block *nb,
unsigned long event, void *ptr)
{
struct net_device *ndev = netdev_notifier_info_to_dev(ptr);
if (event == NETDEV_UP)
netdev_info(ndev, "link up\n");
return NOTIFY_DONE;
}
static struct notifier_block mydev_nb = {
.notifier_call = mydev_netdev_event,
};
register_netdevice_notifier(&mydev_nb); /* trong init */
unregister_netdevice_notifier(&mydev_nb); /* trong exit: bắt buộc */
Cần nhớ. Có bốn loại chain: atomic (callback không được sleep), blocking (được sleep), raw (caller tự lock) và SRCU. Quên unregister khi unload module là lỗi kinh điển: chain sẽ gọi vào vùng code đã bị giải phóng.
5.3 Initcall và linker section — đăng ký lúc link¶
Vấn đề. Hàng trăm hàm init của các subsystem phải được gọi lúc boot theo đúng thứ tự, nhưng không ai muốn duy trì một danh sách trung tâm.
Cách làm. Macro đặt con trỏ hàm vào một section riêng của ELF. Linker script gom các section đó thành mảng liên tiếp có symbol đầu và cuối, và kernel chỉ cần duyệt mảng đó.
/* Khi built-in, module_init(fn) tương đương device_initcall(fn):
* con trỏ fn được đặt vào section .initcall6.init */
/* Thứ tự các mức:
* pure(0) -> core(1) -> postcore(2) -> arch(3)
* -> subsys(4) -> fs(5) -> device(6) -> late(7) */
static int __init mydev_init(void) { /* ... */ }
module_init(mydev_init);
Cùng một mẹo được dùng cho EXPORT_SYMBOL (bảng __ksymtab), MODULE_DEVICE_TABLE (để udev tự load module), tham số boot __setup(), và cả phần __init: toàn bộ code init bị giải phóng sau khi boot xong. DPDK dùng ý tưởng tương tự qua RTE_INIT và RTE_PMD_REGISTER_*, nhưng dựa vào constructor của ELF thay vì section tự định nghĩa.
5.4 Library vs midlayer¶
Vấn đề. Khi nhiều driver làm việc giống nhau, ta muốn gom phần chung lại. Cách "tự nhiên" là viết một midlayer đứng giữa, nắm toàn bộ luồng xử lý và chỉ gọi xuống driver ở vài điểm.
Cách kernel chọn. Neil Brown gọi đó là "midlayer mistake". Midlayer áp đặt luồng xử lý, lock và thứ tự thao tác lên mọi driver, nên driver nào có phần cứng khác biệt sẽ phải lách. Kernel ưu tiên cách ngược lại: driver tự nắm luồng, phần chung được cung cấp dưới dạng library helper mà driver tự chọn gọi hay không.
regmapcung cấp cache thanh ghi, khóa và bus I2C/SPI/MMIO, nhưng driver vẫn quyết định khi nào đọc ghi.phylink/phylib, NAPI và các helperdevm_*đều là thư viện để driver dùng, không phải khung bắt buộc.- Ops table với callback
NULL(mục 1.1, phần 1) thường đi kèm một helper mặc định, ví dụgeneric_file_read_iter. Driver có thể điền helper đó vào, hoặc tự viết hàm của mình.
Bài học cho code của team: khi thiết kế framework nội bộ (ví dụ M-Plane hay HAL cho FPGA), hãy hỏi xem nó đang là thư viện hay đang là midlayer giành quyền kiểm soát.
6. Bảng tổng hợp và checklist review¶
Hầu hết pattern trong kernel đều có bản tương đương trong C++ và trong DPDK. Nắm được một bên thì đọc bên kia rất nhanh.
| Pattern trong kernel | Tương đương GoF / C++ | Tương đương DPDK |
|---|---|---|
Ops table (file_operations) |
Virtual function, Strategy | eth_dev_ops |
Struct nhúng + container_of |
Kế thừa | container_of trong rte_common.h |
| kobject / device model | Composite, smart pointer | rte_device, rte_bus |
kref / refcount_t |
std::shared_ptr |
refcnt trong rte_mbuf |
Goto cleanup, cleanup.h |
RAII, destructor | Goto cleanup |
ERR_PTR |
std::expected |
rte_errno |
devres (devm_*) |
RAII gắn với owner | Không có tương đương trực tiếp |
kmem_cache |
Object Pool | rte_mempool |
list_head, hlist |
boost::intrusive |
TAILQ (sys/queue.h) |
| RCU | Copy-on-write, std::atomic<shared_ptr> |
rte_rcu_qsbr |
| Per-CPU data | thread_local |
RTE_PER_LCORE |
| Seqlock | — | rte_seqlock |
| Deferred work | Active Object, thread pool | Ring + worker lcore |
| Static key | — | — |
| Registration framework | Template Method, Plugin | RTE_PMD_REGISTER_PCI |
| Notifier chain | Observer | rte_eth_dev_callback_register |
| Initcall / linker section | Static registration | RTE_INIT |
Checklist khi review một driver¶
- [ ] Bảng ops có khai báo
constkhông? - [ ] Mọi
container_ofcó đảm bảo con trỏ thật sự nằm trong struct đó không? - [ ] Object có kobject hoặc
struct devicecó được giải phóng trongrelease()thay vìkfree()trực tiếp không? - [ ] Mỗi chỗ cất con trỏ có
gettương ứng, và mỗi nhánh thoát cóputkhông? - [ ] Goto ladder có giải phóng đúng thứ tự ngược, và
remove()có khớp vớiprobe()không? - [ ] API trả về
NULLhayERR_PTR, và code có kiểm tra đúng loại không? - [ ] Có vùng nhớ
devm_nào còn được truy cập sau khi unbind không? ISR có thể chạy trong lúcremove()không? - [ ] Có xóa phần tử trong lúc duyệt list mà không dùng bản
_safekhông? - [ ] Có sleep trong
rcu_read_lock()hay giữ con trỏ RCU saurcu_read_unlock()không? - [ ] Có việc nào sleep (mutex,
GFP_KERNEL, I2C) đang chạy trong hard IRQ, softirq hay tasklet không? - [ ] Driver có giữ state trong biến global thay vì struct private của từng device không?
- [ ] Mọi notifier và callback đã đăng ký có được hủy đăng ký khi unload không?
7. Bài tập và tài liệu tham khảo¶
Bài tập cho team¶
- Chọn một driver team đang maintain. Liệt kê mọi ops table, mọi chỗ dùng
container_ofvà mọi cặpget/put, sau đó chạy checklist ở mục 6. - Chuyển
probe()của một driver từ goto ladder sangdevm_*. Kiểm tra kỹ thứ tự giải phóng và ISR trong lúcremove(). - Viết một module nhỏ giữ config bằng RCU. Một kthread đọc liên tục, một file sysfs để cập nhật. Bật
CONFIG_PROVE_RCUvà cố ý viết sai để xem lockdep bắt lỗi. - Đặt
eth_dev_opscủa một PMD DPDK cạnhnet_device_opscủa driver kernel cho cùng NIC. So sánh callback nào có bên này mà không có bên kia, và giải thích vì sao.
Tài liệu tham khảo¶
- Neil Brown, Linux kernel design patterns — part 1 (reference counting), LWN, 6/2009.
- Neil Brown, Linux kernel design patterns — part 2 (list, rbtree, cấu trúc dữ liệu), LWN, 6/2009.
- Neil Brown, Linux kernel design patterns — part 3 (midlayer mistake), LWN, 6/2009.
- Neil Brown, Object-oriented design patterns in the kernel, part 1, LWN, 6/2011.
- Kernel docs: Coding style (chương về goto cleanup), kobject, kref, What is RCU?, devres.
- Elixir Bootlin để tra cứu mọi struct và hàm nhắc đến trong bài.
Phần trước: Phần 2 · Mục lục cả loạt