在 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 接上不同的 driver 場景中相當常見。

Linux kernel 內的封裝

C++ 使用 class 關鍵字來實現封裝,但是 c 語言中沒有 class 關鍵字,但可以使用 struct 來模擬一個 class ,雖然 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 ,將資料與函式封裝到一個 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 或 function 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_node
{
    int val;
    ...
    struct list_head list;
};

利用 Private pointer 模擬繼承

當 derived class 和 base class 差異不大時,可以採取這個方式。

透過 struct type 的定義來宣告出各個 struct type variable ,每一 struct type variable 可以看做是一個 derived class ,各個 derived class 可以透過 private pointer 來擴展自己的屬性和方法。

也就是說,struct 內含有 pointer 成員,這個 pointer 成員通常是 void * 型別,pointer 交由各個 derived class 來處理。

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

https://docs.huihoo.com/doxygen/linux/kernel/3.7/structnet__device.html

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

多重繼承與 interface

如下圖,B 跟 C 都繼承 A ,到這裡並沒有問題,但如果 D 又繼承了 B 跟 C ,因為 B 跟 C 都繼承於 A ,就會產生衝突。

multiple_inheritence

該如何解決這樣的問題呢? 將多重繼承改為單一繼承,另一個繼承以 interface 來代替,這樣就可以解決衝突。

以下是 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 ,一切將變得更有系統化的去理解。