在 linux kernel 中的 OOP 設計思維

我在 trace linux kernel 的 source code ,面對龐大且複雜的架構,時常覺得無所適從,經常看到許多 function pointer 或是 struct ,各種指來指去,讓你迷失在複雜的程式碼中,後來陸續看過一些文章與書籍後,才了解到其實背後都是 OOP (Object-Oriented Prigramming) 的設計思維,本篇文章想要來講 linux kernel 中是如何利用 C 語言來實現物件導向程式的封裝、繼承與多型。

為什麼 Linux kernel 中要採用 OOP 語法

物件導向程式設計 (OOP) 使用「物件」來做設計,物件是程式設計的基本單位,資料和程式被封裝在物件內,這樣的程式設計開發的風格,帶來 code reuse 的好處,並有靈活的擴充性與可維護性,在 Linux kernel 這種大型專案中,這樣的設計風格被廣為使用。

在 kernel 中,透過分層設計,各層實現各自的功能,各層之間通過 interface 來溝通,每一層都是對其下面一層的封裝,並留出 API 來為上一層服務。軟體分層設計有利於 code reuse,避免重造輪子,同時使得軟體的層次架構更加清晰,更易於管理和維護。各層之間統一接口,可以適應不同的平台和設備,提高軟體的跨平台兼容性,這樣的用法在為不同廠牌的 device 接上不同的 device driver 場景中相當常見。

Linux kernel 內的封裝

C++ 使用 class 關鍵字來實現封裝, class 內封裝了 data 跟 member function ,但是 c 語言中沒有 class 關鍵字,C 語言使用 struct 來模擬一個 class ,struct 內可以宣告不同型別的變數,就如同 C++ class 內的 data 一樣。

但 struct 內部不能像 class 一樣可以直接定義 member function,但可以透過在 struct 內嵌 function pointer 來模擬 class 中的 member function 。

struct animal
{
    itn age;
    int weight;
    void (*fp)(void);
};

如果一個 struct 中需要內嵌多個 function pointer ,更可以把這些 function pointer 進一步封裝到一個 struct 內。

struct func_operations
{
    void (*fp1)(void);
    void (*fp2)(void);
    void (*fp3)(void);
}

struct animal
{
    int age;
    int weight;
    struct func_operations fp;
};

透過以上封裝的實現,就能夠仿照 C++ 的 class ,將資料與 function 封裝到一個 struct 內。

Linux Kernel 內的繼承

內嵌 struct 模擬繼承

struct cat
{
    struct animal *p;
    struct animal ani;
    char sex;
    void (*eat)(void);
};

在型別 struct cat 內嵌型別 struct animal ,這樣的 struct cat 相當於是一個繼承 struct animal 的 derived class ,struct animal 相當於 base class 。 C 語言透過在 struct 內再內嵌一個 struct 或一個指向 struct 的 pointer 來模擬 class 的繼承。

linked list 是 linux kernel 內很常出現的一種動態資料結構,在 kernel 中使用它,就是封裝跟繼承的實現。

一般在寫 linked list ,每個 linked list node 往往包含 data 和 pointer :

struct list_node
{
    int data;
    struct list_node* prev;
    struct list_node* next;
};

linux kernel 為了實現對 linked list 的 code reuse ,定義許多 linked list 相關的操作

struct list_head
{
    struct list_head *next, *prev;
};

void INIT_LIST_HEAD (struct list_head *list);
int list_empty(const struct list_head *head);
list_add, list_del, list_replace, list_move...

可以將 struct list_head 和相關的操作函示當成一個 base class ,如果想要繼承這些操作,只要將 list_head 內嵌到自己的 struct 內即可。

struct my_list
{
    int val;
    ...
    struct list_head list;
};

利用 Private pointer 模擬繼承

這種方法適用於各個子類之間有許多共通的成員變數,例如 linux kernel 中的 network card device ,不同廠牌的網卡其實讀寫操作都一樣,唯一不同的可能是 I/O register 等硬體關聯的屬性,遇到這種 base class 和 dervied class 之間差異不大的狀況,不必為每個類型的網卡都定義出 struct ,將各個網卡的共同屬性拿出來共同定義成一個 struct即可, struct net_device 就是這樣的 struct :

https://elixir.bootlin.com/linux/v5.4/source/include/linux/netdevice.h#L1556

在 struct net_device 中的 pointer 成員 void* ml_priv ,就是留給各類型網卡來配置自己的 private data 用的。

struct net_device {
  char   name[IFNAMSIZ];
  const struct net_device_ops *netdev_ops;
    int    ifindex;
    void   *ml_priv;
};

利用上述類型的 struct 來定義出 struct type 變數,每一 struct type 變數可以看做是一個 derived class ,各個 derived class 可以透過 private pointer 來擴展自己的屬性和方法。

換句話說,struct 內含有 pointer 成員,這個 pointer 成員通常是 void * 型別,pointer 交由各個 derived class 來自行處理。

多重繼承與 interface

如下圖,B 跟 C 都繼承 A ,到這裡並沒有問題,但如果 D 又繼承了 B 跟 C ,因為 B 跟 C 都繼承於 A ,就會造成 D 會有兩個 A 。

multiple_inheritence

該如何解決這樣的問題呢? 將多重繼承改為單一繼承,另一個繼承以 interface 來代替,這樣就可以解決衝突,這邊講的 interface 是指可以供其他人呼叫的 API 。

以下以 RTL8150 USB 網路卡 driver 為例,他分別繼承 struct usb_devicestruct net_device 兩種 base class ,這兩個 struct 又都包含 struct device ,就形成如上圖的多重繼承。

struct rtl8150 {
    unsigned long flags;
    struct usb_device *udev;
    struct tasklet_struct tl;
    struct net_device *netdev;
    struct urb *rx_urb, *tx_urb, *intr_urb;
    struct sk_buff *tx_skb, *rx_skb;
    struct sk_buff *rx_skb_pool[RX_SKB_POOL_SIZE];
    spinlock_t rx_pool_lock;
    struct usb_ctrlrequest dr;
    int intr_interval;
    u8 *intr_buff;
    u8 phy;
};

觀察 rtl8150_probe() ,可以得到以下兩點觀察:


static int rtl8150_probe(struct usb_interface *intf,
             const struct usb_device_id *id)
{
    struct usb_device *udev = interface_to_usbdev(intf);
    rtl8150_t *dev;
    struct net_device *netdev;
    netdev = alloc_etherdev(sizeof(rtl8150_t));
    if (!netdev)
        return -ENOMEM;
    dev = netdev_priv(netdev);

    dev->udev = udev;
    dev->netdev = netdev;
  • interface_to_usbdev () 透過 input intf 得到 struct usb_device 這個 struct ,而不是以繼承的方式 。
  • net_device 就是透過繼承的方式,完整宣告包含 struct net_devicestruct rtl8150 ,之後使用的就是後者內的 struct net_device

Linux Kernel 內的多型

在 derived class 繼承 base class 的過程中,一個 interface 在不同的 derived class 中就可以有不同的實現,通過 base class pointer 去呼叫 derived class 中的不同實現,就形成多型。

struct net_device 為例,作為一個 base class ,各廠牌的網路卡會有各自的 init, open, stop 等實作,當一個 struct net_device 型別的 pointer 去指向不同廠牌網卡變數時,就會呼叫特定 derived class 的操作,藉此實現多型。

結語

面對 linux kernel 中一層又一層的 nested struct ,每個 struct 又包含長長的定義,若還是以 procedure programming 方式去分析,很快就會迷失在錯綜複雜的資料結構內。若是你知道原來透過 C 語言也可以實現 OOP ,並以 OOP 的思維去 trace code ,一切將變得更有系統化的去理解。

Reference

  1. 嵌入式 C 語言自我修養
  2. Object-oriented design patterns in the kernel, part 1
  3. Object-oriented design patterns in the kernel, part 2

Updated on 2022-11-27 19:26:55 星期日

在〈在 linux kernel 中的 OOP 設計思維〉中有 2 則留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *