Skip to content

Design Pattern kinh điển trong Linux Kernel (3/3): kiến trúc và checklist review

Design Patterns 3/3

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.

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.

  • regmap cung 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 helper devm_* đề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 const không?
  • [ ] Mọi container_of có đảm bảo con trỏ thật sự nằm trong struct đó không?
  • [ ] Object có kobject hoặc struct device có được giải phóng trong release() thay vì kfree() trực tiếp không?
  • [ ] Mỗi chỗ cất con trỏ có get tương ứng, và mỗi nhánh thoát có put không?
  • [ ] Goto ladder có giải phóng đúng thứ tự ngược, và remove() có khớp với probe() không?
  • [ ] API trả về NULL hay ERR_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úc remove() không?
  • [ ] Có xóa phần tử trong lúc duyệt list mà không dùng bản _safe không?
  • [ ] Có sleep trong rcu_read_lock() hay giữ con trỏ RCU sau rcu_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

  1. Chọn một driver team đang maintain. Liệt kê mọi ops table, mọi chỗ dùng container_of và mọi cặp get/put, sau đó chạy checklist ở mục 6.
  2. Chuyển probe() của một driver từ goto ladder sang devm_*. Kiểm tra kỹ thứ tự giải phóng và ISR trong lúc remove().
  3. 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_RCU và cố ý viết sai để xem lockdep bắt lỗi.
  4. Đặt eth_dev_ops của một PMD DPDK cạnh net_device_ops củ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


Phần trước: Phần 2 · Mục lục cả loạt