Linux驅動程序開發007 - 設備驅動模型初探

序言
從這一章開始,我們將詳細的介紹Linux的設備驅動模型。Linux設備驅動模型是一個相當複雜的系統,對於初學者來說真有些無從入手。而且更加困難的是,隨着新的Linux Kernel的release,Linux的設備驅動模型總會有或大或小的變化,我們將盡量展現 Linux Kernel 的這種變化。

早期的Linux內核(版本2.4之前)並沒有實現一個統一的設備模型,設備節點的創建一般是mknod命令手動創建或利用devfs文件系統創建。早期的Linux發行版一般會採用手動創建的方式預先把通常用到的節點都創建出來,而嵌入式系統則會採用devfs的方式。起初Linux 2.6 內核還支持devfs,但從2.6.18開始,內核完全移除了devfs系統而採用的udev的方式動態的創建設備節點。因此,新的Linux發行版都採用udev的方式管理設備節點文件。(關於udev的詳細信息,請參考:http://www.kernel.org/pub/linux/utils/kernel/hotplug/udev.html)。

Linux2.6設備驅動模型的基本元素是Class、Bus、Device、Driver,下面我們分別介紹各個部分。

Class 和Class Device
驅動模型最基本的概念是設備及其類別,Linux中使用struct class 和struct class_device來管理不同類別的設備。由於設備驅動模型是一個複雜的系統,我們還是從一個簡單的例子開始介紹,然後在逐步展開。其實實現設備節點的動態創建是一個很簡單的事情,並不需要太多的代碼。我們修改我們的驅動初始化函數如下:

#include <linux/device.h>
#define DEVNAME "hello"

static dev_t dev;
static struct class *hello_class;
static struct cdev *hello_cdev;
static int __init hello_init(void)
{
    int error;

    error = alloc_chrdev_region(&dev, 0, 2, "hello");
    if (error)
    {
        printk("hello: alloc_chardev_region failed!\n");
        goto out;
    }
    hello_cdev = cdev_alloc();
    if (hello_cdev == NULL)
    {
        printk("hello: alloc cdev failed!\n");
        error = -ENOMEM;
        goto out_chrdev;
    }
    hello_cdev->ops = &hello_fops;
    hello_cdev->owner = THIS_MODULE;
    error = cdev_add(hello_cdev, dev, 1);
    if (error)
    {
        printk("hello: cdev_add failed!\n");
        goto out_cdev;
    }
    hello_class = class_create(THIS_MODULE, DEVNAME);
    if (IS_ERR(hello_class))
    {
        error = PTR_ERR(hello_class);
        goto out_chrdev;
    }
    class_device_create(hello_class, NULL, dev, NULL, DEVNAME);
    memset (hello_buf, 0, sizeof(hello_buf));
    memcpy(hello_buf, DEFAULT_MSG, sizeof(DEFAULT_MSG));
    printk("hello: Hello World!\n");
    return 0;

out_cdev:
    cdev_del(hello_cdev);
out_chrdev:
    unregister_chrdev_region(hello_cdev->dev, 2);
out:
    return error;
}
static void __exit hello_exit(void)
{
    class_device_destroy(hello_class, dev);
    class_destroy(hello_class);
    unregister_chrdev_region(hello_cdev->dev, 2);
    cdev_del(hello_cdev);
    printk("hello: Goodbye World\n");
}


重新編譯這個驅動程序,當加載這個驅動到內核中時,系統(一般是hotplug和udev系統)就會自動的創建我們指定的設備名字:/dev/hello,同時,你也可以發現在sysfs系統中添加了新的文件:/sys/class/hello/hello/。
當然並不需要把所有的代碼都貼到這裏,但是這樣做可能更加清楚。我們把添加的代碼顯示成藍色,這樣你可以清楚的看到代碼的簡單程度。這裏主要用到了class_create 和class_device_create函數,它們定義在<linux/device.h>頭文件中。

extern struct class *class_create(struct module *owner, const char *name);
extern void class_destroy(struct class *cls);
extern struct class_device *class_device_create(struct class *cls,
                        struct class_device *parent,
                        dev_t devt,
                        struct device *device,
                        const char *fmt, ...)
                    __attribute__((format(printf,5,6)));
extern void class_device_destroy(struct class *cls, dev_t devt);


Linux是通過設備與設備類來管理設備的,當你調用這些函數向系統註冊設備及其類的時候,內核會自動的在sysfs文件系統中創建對應的文件。如果想了解更多的關於設備及其類的函數接口,請你閱讀Linux kernel的源文件(include/device.h頭文件和drivers/base/class.c實現文件)。這裏簡單說明class_device_create函數,它的最後一個參數是一個變參,類似於printf函數參數,用於指定添加設備的名稱也就是顯示在/dev/目錄下設備文件名稱。

創建的class_device設備會自動註冊到系統中,這樣對於給定的設備,系統會自動找到匹配的設備驅動。

你可以在設備類或設備目錄下(/sys/class)創建文件,這個文件提供了內核同用戶空間程序的交互接口。內核提供了設備、設備類的內核函數接口,這些接口也定義在<include/device.h>頭文件中。

int class_create_file(struct class *cls, const struct class_attribute *attr);
void class_remove_file(struct class *cls, const struct class_attribute *attr);
int class_device_create_file(struct class_device *class_dev,
                 const struct class_device_attribute *attr);
void class_device_remove_file(struct class_device *class_dev,
                  const struct class_device_attribute *attr);
int class_device_create_bin_file(struct class_device *class_dev,
                 struct bin_attribute *attr);
void class_device_remove_bin_file(struct class_device *class_dev,
                  struct bin_attribute *attr);


其實,這些函數僅僅是簡單的封裝了sysfs文件系統函數,但它爲設備及設備類提供了統一的函數接口。

Drivers
當一個設備註冊到系統中時,它就向系統表明了哪個驅動匹配這個設備。Linux內核會在註冊的設備驅動中查找匹配的驅動並調用對應的探測函數(probe)來初始化設備。設備驅動接口也定義在<linux/device.h>頭文件中。

struct device_driver {
    const char        *name;
    struct bus_type        *bus;
    struct module        *owner;
    const char         *mod_name;   
    int (*probe) (struct device *dev);
    int (*remove) (struct device *dev);
    void (*shutdown) (struct device *dev);
    int (*suspend) (struct device *dev, pm_message_t state);
    int (*resume) (struct device *dev);
    struct attribute_group **groups;
    struct driver_private *p;
};


其中,比較重要的數據是name,Linux內核就是根據name來匹配設備與驅動的。probe函數用於設備的探測及初始化,remove函數在設備移出系統時被觸發調用。

一般來說,我們的驅動需要實現name、module、probe、remove函數。

Bus
通常我們的驅動並不需要實現Bus接口,也沒有這個必要,因此你完全可以跳過這段,除非你想添加一個新的總線到系統中。

在Linux2.6內核中,struct bus_type描述了一個bus對象,它定義在<linux/device.h>頭文件中。(你發現沒有,到目前Linux設備模型接口都定義在這個文件中)

struct bus_type {
    const char        *name;
    struct bus_attribute    *bus_attrs;
    struct device_attribute    *dev_attrs;
    struct driver_attribute    *drv_attrs;

    int (*match)(struct device *dev, struct device_driver *drv);
    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
    int (*probe)(struct device *dev);
    int (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);

    int (*suspend)(struct device *dev, pm_message_t state);
    int (*suspend_late)(struct device *dev, pm_message_t state);
    int (*resume_early)(struct device *dev);
    int (*resume)(struct device *dev);

    struct bus_type_private *p;
};

extern int __must_check bus_register(struct bus_type *bus);
extern void bus_unregister(struct bus_type *bus);

extern int __must_check bus_create_file(struct bus_type *,
                    struct bus_attribute *);
extern void bus_remove_file(struct bus_type *, struct bus_attribute *);


在這個結構中,name描述了bus的名字,如它會顯示在/sys/bus/中。match函數用於匹配屬於這個bus的設備和驅動,uevent用於處理Linux uevent事件。probe和remove類似與driver的函數接口,主要用於設備的Hotplug處理。其他的函數是Power相關的函數接口。

同樣,bus也需要註冊到系統中,並可以在sysfs中創建文件。

sysfs文件系統
sysfs類似於proc文件系統,用於用戶空間程序和內核空間交互數據的接口。但sysfs提供了更多的功能,其中之一就是顯示Linux驅動程序模型的分層結構關係。Ubuntu 804的sysfs文件系統的目錄顯示如下:
           block  bus  class  devices  firmware  fs  kernel  module  power  slab
當你瀏覽這個文件系統的時候,你會發現裏面有很多鏈接文件,其實正是這些鏈接文件展現了Linux驅動模型各個組成部分之間的關係。
sysfs文件系統中,最重要的就是struct attribute結構,它被用來管理內核sysfs文件的接口(名字,屬性,讀寫函數等)。內核sysfs提供了基本的attribute接口,不同的設備如bus、device在基本attribute的基礎上定義了自己的讀寫函數,sysfs提供了對應的宏來簡化屬性的操作。請參考<linux/sysfs.h>頭文件中。

struct attribute {  
    const char        *name;
    struct module     *owner;
    mode_t            mode;
};

#define __ATTR(_name,_mode,_show,_store) { \
    .attr = {.name = __stringify(_name), .mode = _mode },    \
    .show    = _show,                    \
    .store    = _store,                    \
}

int __must_check sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
int __must_check sysfs_create_dir(struct kobject *kobj);


我們看到,sysfs的struct attribute結構本身並不包含讀寫訪問函數,驅動模型的各個部分都會擴展這個結構並定義自己的屬性結構來引入各自的操作函數,如 class:(這個結構定義在<linux/device.h>頭文件中)。

struct class_attribute {
    struct attribute    attr;
    ssize_t (*show)(struct class *, char * buf);
    ssize_t (*store)(struct class *, const char * buf, size_t count);
};
#define CLASS_ATTR(_name, _mode, _show, _store)            \
struct class_attribute class_attr_##_name = __ATTR(_name, _mode, _show, _store)


關於sysfs的更多信息,請參考 Linux內核源代碼樹中的Documentation/filesystems/sysfs.txt文件。

Platform總線
platform總線是Linux內核中的一個虛擬總線,它使得設備的管理更加簡單化。目前大部分的驅動都是用platform總線來寫的。platform總線模型的各個部分都是繼承自Device模型(姑且這麼說吧),它在系統內實現了個虛擬的總線,即platform_bus,如果你的設備需要platform總線管理,那麼就需要向系統中註冊platform設備及其驅動程序。就像前面所介紹的那樣,platform總線分爲platform_bus, platform_device 和platform_driver幾個部分,他們的接口定義在<linux/platform.h>頭文件中。
  • platform bus
我們先來看看platform_bus的定義:

struct device platform_bus = {
    .bus_id        = "platform",
};

struct bus_type platform_bus_type = {
    .name        = "platform",
    .dev_attrs    = platform_dev_attrs,
    .match        = platform_match,
    .uevent        = platform_uevent,
    .suspend    = platform_suspend,
    .suspend_late    = platform_suspend_late,
    .resume_early    = platform_resume_early,
    .resume        = platform_resume,
};

int __init platform_bus_init(void)
{
    int error;

    error = device_register(&platform_bus);
    if (error)
        return error;
    error =  bus_register(&platform_bus_type);
    if (error)
        device_unregister(&platform_bus);
    return error;
}


platform_bus數據結構描述了platform bus設備,platform_bus_type描述了platform bus總線,它提供了platform總線設備和驅動的匹配函數。platform總線是由函數platform_bus_init(void)初始化的。
對於Linux我們一般的設備驅動程序來說,就像前面Bus一段提到的那樣,我們不需要關心platform總線本身,我們只要調用我們的設備和驅動接口就可以了。

  • Platform Device
如果你想讓platform總線來管理設備,那麼,你需要先向platform系統註冊設備,這個過程是通過下面的函數接口來實現的:

int platform_device_add(struct platform_device *pdev);
int platform_device_register(struct platform_device *pdev);


我們一般需要調用platform_device_register函數來向系統添加platform設備。這兩個函數唯一的差別就是platform_device_register在添加設備前會初始化platform_device的dev數據成員,它是一個struct device類型數據。當一個platform_device添加到platform總線中後,platform總線就會爲它找到匹配的設備驅動程序,很顯然,在這之前,你需要向系統註冊platform_driver。
  • Platform Driver
我們先來看看platform總線設備驅動的結構:

struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*suspend_late)(struct platform_device *, pm_message_t state);
    int (*resume_early)(struct platform_device *);
    int (*resume)(struct platform_device *);
    struct device_driver driver;
};

extern int platform_driver_register(struct platform_driver *);


很顯然,它“繼承”自struct device_driver,同樣類似於struct device_driver,一般我們需要實現probe函數,及指定platform_driver能驅動的設備的名字。

  • 使用Platform總線
下面這個例子告訴你如何使用platoform總線,這是一個Android Goldfish GPIO驅動程序。它本身就是一個platform設備驅動(goldfish-gpio),同時,它又會向系統注新的設備(android-timed-gpio),這個新設備又被timed_output.c驅動程序驅動。

......
#include <linux/platform_device.h>

struct platform_device timed_gpio_device = {
    .name  = "android-timed-gpio",
    .id    = -1,
    .dev.platform_data = &timed_gpio_platform_data,
};

static int goldfish_gpio_probe(struct platform_device *pdev)
{
    struct goldfish_gpio_data *gpio_data;
    ......
    error = platform_device_register(&timed_gpio_device);
    ......
    return 0;
}

static int goldfish_gpio_remove(struct platform_device *pdev)
{
    int i;
    struct goldfish_gpio_data *gpio_data;
    ......
        platform_device_unregister(&timed_gpio_device);
    ......
    return 0;
}

static struct platform_driver goldfish_gpio_driver = {
    .probe    = goldfish_gpio_probe,
    .remove   = goldfish_gpio_remove,
    .driver = {
        .name = "goldfish-gpio"
    }
};

static int __init goldfish_gpio_init(void)
{
    return platform_driver_register(&goldfish_gpio_driver);
}

static void __exit goldfish_gpio_exit(void)
{
    platform_driver_unregister(&goldfish_gpio_driver);
}


這個新註冊的設備(timed_gpio_device)由timed_output驅動管理,通過瀏覽這段代碼,你應該對如何使用platform總線有個全面的瞭解。(本想把全部code放在這裏,但超過最大字數限制!)

static struct class *timed_gpio_class;
struct timed_gpio_data {
    struct device *dev;
    struct hrtimer timer;
    spinlock_t lock;
    unsigned     gpio;
    int         max_timeout;
    u8         active_low;
};
......

static int android_timed_gpio_probe(struct platform_device *pdev)
{
    struct timed_gpio_platform_data *pdata = pdev->dev.platform_data;
    struct timed_gpio *cur_gpio;
    struct timed_gpio_data *gpio_data, *gpio_dat;
    int i, ret = 0;
    ......
}

static int android_timed_gpio_remove(struct platform_device *pdev)
{
}

static struct platform_driver android_timed_gpio_driver = {
    .probe        = android_timed_gpio_probe,
    .remove        = android_timed_gpio_remove,
    .driver        = {
        .name        = "android-timed-gpio",
        .owner        = THIS_MODULE,
    },
};

static int __init android_timed_gpio_init(void)
{
    timed_gpio_class = class_create(THIS_MODULE, "timed_output");
    if (IS_ERR(timed_gpio_class))
        return PTR_ERR(timed_gpio_class);
    return platform_driver_register(&android_timed_gpio_driver);
}

static void __exit android_timed_gpio_exit(void)
{
    class_destroy(timed_gpio_class);
    platform_driver_unregister(&android_timed_gpio_driver);
}


Kobject和kset
提到Linux的設備模型,就不得不提kobject和kset這兩個內核對象,他們纔是Linux內核設備模型的最基礎的結構,但講解他們卻是一個枯燥過程,限於篇幅,這個就不作介紹了,請參考Linux文檔<documentation/kobject.txt>。

後記
在這裏,我們簡單的介紹了Linux的設備模型,包括基本總線、設備、驅動的關係,同時也簡單的介紹了Linux2.6內核的platform總線。這些內容應該足夠讓你瞭解如何使用Linux設備模型來管理設備了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章