1. 程式人生 > >Kernel字元裝置驅動框架

Kernel字元裝置驅動框架

    Linux裝置分為三大類:字元裝置,塊裝置和網路裝置,這三種裝置基於不同的裝置框架。相較於塊裝置和網路裝置,字元裝置在kernel中是最簡單的,也是唯一沒有基於裝置基礎框架(device結構)的裝置,因此字元裝置無法通過sysfs來訪問。那麼,使用者是如何來訪問字元裝置的?答案是通過裝置節點來訪問字元裝置。從這個角度來說,字元裝置的本質就是提供使用者介面,使得使用者可以通過 VFS 來訪問裝置節點,從而達到訪問字元裝置的目的。下面開始分析字元裝置框架的實現。

    1,字元裝置資料結構:

struct cdev {
    struct kobject kobj;    // 雖然內嵌了kobject結構,但是沒有加入kset連結串列,因此不會在sysfs中生效


    struct module *owner;    // 所屬模組
    const struct file_operations *ops;    // 檔案操作介面,使用者通過該介面訪問字元裝置驅動
    struct list_head list;    // 裝置連結串列節點
    dev_t dev;    // 裝置號,由主從裝置號組成。裝置號是裝置節點和cdev之間的紐帶
    unsigned int count;    // 從裝置個數

};

    2,字元設備註冊 register_chrdev():

    static inline int register_chrdev(unsigned int major, const char *name,
                  const struct file_operations *fops)
{
    return __register_chrdev

(major, 0, 256, name, fops);
}

int __register_chrdev(unsigned int major, unsigned int baseminor,
              unsigned int count, const char *name,
              const struct file_operations *fops)
{
    struct char_device_struct *cd;
    struct cdev *cdev;
    int err = -ENOMEM;

    cd = __register_chrdev_region(major, baseminor, count, name);    // 註冊裝置佔用的主從裝置號範圍
    if (IS_ERR(cd))
        return PTR_ERR(cd);

    cdev = cdev_alloc();    // 分配cdev結構
    if (!cdev)
        goto out2;

    cdev->owner = fops->owner;
    cdev->ops = fops;
    kobject_set_name(&cdev->kobj, "%s", name);    // 初始化cdev

    err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);    // 執行cdev的註冊
    if (err)
        goto out;

    cd->cdev = cdev;    // 將cdev關聯到裝置號結構

    return major ? 0 : cd->major;
out:
    kobject_put(&cdev->kobj);
out2:
    kfree(__unregister_chrdev_region(cd->major, baseminor, count));
    return err;
}
    從上面可以看出,註冊一個字元裝置包含兩個步驟:1,申請/註冊裝置號範圍。2,註冊cdev

    (1)申請/註冊裝置號範圍

        使用者通過裝置號訪問字元裝置,字元裝置號的分配就需要統一管理起來,防止同一個裝置號被重複分配。字元裝置號的分配通過__register_chrdev_region()介面完成。

static struct char_device_struct {
    struct char_device_struct *next;    // 下一個節點
    unsigned int major;    // 主裝置號
    unsigned int baseminor;    // 從裝置號起始

    int minorct;    // 從裝置個數
    char name[64];
    struct cdev *cdev;        /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor,
               int minorct, const char *name)
{
    struct char_device_struct *cd, **cp;
    int ret = 0;
    int i;

    cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);    // 申請 char_device_struct結構
    if (cd == NULL)
        return ERR_PTR(-ENOMEM);

    mutex_lock(&chrdevs_lock);

    /* temporary */
    if (major == 0) {    // 如果沒有指定主裝置號,由系統動態分配一個主裝置號
        for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
            if (chrdevs[i] == NULL)
                break;
        }

        if (i == 0) {
            ret = -EBUSY;
            goto out;
        }
        major = i;
    }

    // 初始化該char_device_struct

    cd->major = major;
    cd->baseminor = baseminor;
    cd->minorct = minorct;
    strlcpy(cd->name, name, sizeof(cd->name));

    i = major_to_index(major);    // chrdevs本質上是個hash陣列,計算hash code

    for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
        if ((*cp)->major > major ||
            ((*cp)->major == major &&
             (((*cp)->baseminor >= baseminor) ||
              ((*cp)->baseminor + (*cp)->minorct > baseminor))))
            break;

    /* Check for overlapping minor ranges.  */
    if (*cp && (*cp)->major == major) {
        int old_min = (*cp)->baseminor;
        int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
        int new_min = baseminor;
        int new_max = baseminor + minorct - 1;

        /* New driver overlaps from the left.  */
        if (new_max >= old_min && new_max <= old_max) {
            ret = -EBUSY;
            goto out;
        }

        /* New driver overlaps from the right.  */
        if (new_min <= old_max && new_min >= old_min) {
            ret = -EBUSY;
            goto out;
        }
    }

    cd->next = *cp;
    *cp = cd;    // 將新的char_device_struct加入hash陣列連結串列

    mutex_unlock(&chrdevs_lock);
    return cd;
out:
    mutex_unlock(&chrdevs_lock);
    kfree(cd);
    return ERR_PTR(ret);
}

    (2)cdev註冊:

    字元設備註冊,本質上就是將cdev加入一個全域性範圍的連結串列上,當用戶通過裝置號查詢裝置時,從該連結串列上找到相應的cdev,呼叫 cdev 下的file_operations對裝置進行操作。實際情況要比這複雜點,考慮到一個裝置可能有多個子裝置,即一次設備註冊請求的裝置號可能橫跨多個主裝置號,linux設計了一個大小為255的hash陣列,以主裝置號為hash key,以 kobj_map 為元素,將1個或連續的多個probe加入以主裝置號為key的hash陣列,其中每個probe都指向該cdev。當查詢某個裝置的時候,以主裝置號為key查詢hash陣列的衝突連結串列,找到目標裝置號落入的probe,取出關聯的cdev。

struct kobj_map {
    struct probe {
        struct probe *next;    // hash衝突連結串列
        dev_t dev;    // 首個裝置號
        unsigned long range;    // 子裝置個數
        struct module *owner;
        kobj_probe_t *get;    // 獲取cdev方法
        int (*lock)(dev_t, void *);
        void *data;    // 通常就是cdev
    } *probes[255];
    struct mutex *lock;
};

    裝置加入hash陣列的介面如下

int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
         struct module *module, kobj_probe_t *probe,
         int (*lock)(dev_t, void *), void *data)
{
    unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;    // 該裝置橫跨的主裝置號個數
    unsigned index = MAJOR(dev);    // 起始主裝置號
    unsigned i;
    struct probe *p;

    if (n > 255)
        n = 255;

    p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);    // 分配1個或多個連續的probe
    if (p == NULL)
        return -ENOMEM;

    for (i = 0; i < n; i++, p++) {
        p->owner = module;
        p->get = probe;
        p->lock = lock;
        p->dev = dev;
        p->range = range;
        p->data = data;
    }    // 每個probe都關聯該cdev

    mutex_lock(domain->lock);
    for (i = 0, p -= n; i < n; i++, p++, index++) {
        struct probe **s = &domain->probes[index % 255];
        while (*s && (*s)->range < range)
            s = &(*s)->next;
        p->next = *s;
        *s = p;
    }    // 每個probe加入到相應的hash衝突連結串列

    mutex_unlock(domain->lock);
    return 0;
}

    查詢裝置hash陣列的介面如下:

struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
{
    struct kobject *kobj;
    struct probe *p;
    unsigned long best = ~0UL;

retry:
    mutex_lock(domain->lock);
    for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) {    // 根據主裝置號找到hash衝突連結串列,迴圈訪問連結串列上的每個元素,找到目標裝置號落入的probe
        struct kobject *(*probe)(dev_t, int *, void *);
        struct module *owner;
        void *data;

        if (p->dev > dev || p->dev + p->range - 1 < dev)
            continue;    // 目標裝置號不在該probe範圍內,繼續尋找下一個

        if (p->range - 1 >= best)
            break;
        if (!try_module_get(p->owner))
            continue;
        owner = p->owner;
        data = p->data;
        probe = p->get;
        best = p->range - 1;
        *index = dev - p->dev;    // 找到目的probe

        if (p->lock && p->lock(dev, data) < 0) {
            module_put(owner);
            continue;
        }
        mutex_unlock(domain->lock);
        kobj = probe(dev, index, data);    // 取出probe指向的 cdev
        /* Currently ->owner protects _only_ ->probe() itself. */
        module_put(owner);
        if (kobj)
            return kobj;    // 返回 cdev

        goto retry;
    }
    mutex_unlock(domain->lock);
    return NULL;
}

    (3)字元裝置的開啟:   

static int chrdev_open(struct inode *inode, struct file *filp)
{
    const struct file_operations *fops;
    struct cdev *p;
    struct cdev *new = NULL;
    int ret = 0;

    spin_lock(&cdev_lock);
    p = inode->i_cdev;
    if (!p) {    // 判斷裝置是否已經被開啟,如果沒有被開啟過,查詢已經註冊的字元裝置
        struct kobject *kobj;
        int idx;
        spin_unlock(&cdev_lock);
        kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
        if (!kobj)
            return -ENXIO;
        new = container_of(kobj, struct cdev, kobj);    // 找到cdev
        spin_lock(&cdev_lock);
        /* Check i_cdev again in case somebody beat us to it while
           we dropped the lock. */
        p = inode->i_cdev;
        if (!p) {
            inode->i_cdev = p = new;
            list_add(&inode->i_devices, &p->list);
            new = NULL;    // 第一次開啟該字元裝置,將cdev賦值給inode,下次開啟時,不必再次查詢字元裝置連結串列

        } else if (!cdev_get(p))
            ret = -ENXIO;
    } else if (!cdev_get(p))
        ret = -ENXIO;
    spin_unlock(&cdev_lock);
    cdev_put(new);
    if (ret)
        return ret;

    ret = -ENXIO;
    fops = fops_get(p->ops);    // 取出cdev的file_operations
    if (!fops)
        goto out_cdev_put;

    replace_fops(filp, fops);    // 替換字元裝置的file_operations
    if (filp->f_op->open) {
        ret = filp->f_op->open(inode, filp);    // 呼叫字元裝置驅動的open()方法
        if (ret)
            goto out_cdev_put;
    }

    return 0;

 out_cdev_put:
    cdev_put(p);
    return ret;
}

    至此,我們已經分析了整個字元裝置的框架:從字元裝置的註冊,到字元裝置的查詢和開啟,框架架構簡單明瞭。字元裝置作為使用者訪問裝置的向上介面,通常跟i2c,spi,pci等向下介面驅動同時工作,以組成一個完整的硬體字元裝置。在這樣的裝置中,使用者通過字元裝置驅動介面獲取裝置,再通過i2c等驅動介面