Linux字符设备驱动程序

!Warning:请确保能够访问图床Imgur,以正常显示图片


源码基线:linux-4.0-rc1

字符设备驱动程序

图6.1所示为字符设备驱动的结构,字符设备驱动与字符设备,以及字符设备驱动与用户空间访问该设备的程序之间的有关系。

在Linux内核中,字符设备使用struct cdev的一个结构体实例来表示。

  • dev_t:表示设备号,共32位,12位为主设备号,20位为次设备号。dev_t可以通过MAJORMINOR两个宏获得主设备号与次设备号;通过MKDEV宏可以通过主设备号与次设备号构造dev_t
  • file_operations:定义了字符设备驱动提供给虚拟文件系统的接口函数。
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops; /* 文件系统操作接口 */
    struct list_head list;
    dev_t dev; /* 设备号 */
    unsigned int count;
};

#define MINORBITS   20
#define MINORMASK   ((1U << MINORBITS) - 1)
#define MAJOR(dev)  ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)  ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

内核提供了一组函数以用于操作cdev结构体:

  • cdev_init():用于初始化struct cdev
  • cdev_alloc():用于动态申请一个cdev结构体。
  • cdev_add():向系统添加一个cdev,完成字符设备的注册。通常在字符设备驱动模块加载函数中调用。
  • cdev_del():向系统删除一个cdev,完成字符设备的注销。通常在字符设备驱动模块卸载函数中调用。

分配和释放设备号

在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()alloc_chrdev_region()向系统申请设备号。

  • register_chrdev_region():用于已知设备的起始设备号的情况。可能造成设备号冲突。
  • alloc_chrdev_region():用于未知设备号,向系统动态申请未被占用的设备号的情况,得到系统分配的设备号。能够避免设备号冲突。
  • unregister_chrdev_region():释放申请的设备号。

设备驱动分析

从内存中分配一片大小为4K的空间,作为globalmem虚拟字符设备。我们来看下,设备号的申请、设备的注册以及文件系统如何对设备进行管理(open、read、wirtet等操作)。

模块初始化

#define GLOBALMEM_SIZE  0x1000
#define MEM_CLEAR   0x1
#define GLOBALMEM_MAJOR 230

static int globalmem_major = GLOBALMEM_MAJOR;

static int __init globalmem_init(void)
{
    int ret;
    dev_t devno = MKDEV(globalmem_major, 0); /* 主设备号=230,次设备号=0 */

    if (globalmem_major)
        /* 分配指定设备号 */
        ret = register_chrdev_region(devno, 1, "globalmem");
    else {
        /* 动态分配设备号 */
        ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
        globalmem_major = MAJOR(devno);
    }
    if (ret < 0)
        return ret;

    globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
    if (!globalmem_devp) {
        ret = -ENOMEM;
        goto fail_malloc;
    }

    globalmem_setup_cdev(globalmem_devp, 0); /* 设备注册 */
    mutex_init(&globalmem_devp->mutex);
    return 0;

 fail_malloc:
    unregister_chrdev_region(devno, 1);
    return ret;
}
module_init(globalmem_init);

globalmem_init()是globalmem虚拟字符设备的驱动初始化函数,主要完成设备号分配,设备注册的工作。

设备号分配

#define CHRDEV_MAJOR_HASH_SIZE  255

static struct char_device_struct {
    struct char_device_struct *next; /* 主设备号%CHRDEV_MAJOR_HASH_SIZE后结果相
        同的设备链表,按照主设备号从小到大的顺序排列 */
    unsigned int major; /* 主设备号 */
    unsigned int baseminor; /* 次设备号 */
    int minorct; /* 当前主设备号需要分配多少个次设备号,baseminor+minorct意味着
        设备分配的主设备号对应的连续设备号,单个主设备号对应最多2^20个设备号 */
    char name[64]; /* 设备名称 */
    struct cdev *cdev;      /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

全局变量chrdevs用来保存字符设备的设备号节点信息,主设备号有2^12个,如果把所有主设备号定义成数组chrdevs[2^12],则需浪费比较大的内存。如果全部定义成链表,则算法搜索复杂度造成效率低下。因此采用数组+链表的方式来表示。

设备号指定分配

我们先来看下,设备号的分配过程。可以指定设备号进行分配,也可以不指定设备号,由系统动态分配。我们先来看下指定分配的过程。

/* 在建立一个字符设备之前,需要先获得设备的编号。手动申请设备号容易出现设备号
   冲突,导致申请失败。
   from:是要分配的设备编号范围的起始值
   count:是所请求的连续设备编号的个数
   name:是与设备编号关联的设备名称,出现在/proc/devices和sysfs中 */
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
    struct char_device_struct *cd;
    dev_t to = from + count; /* 如果count非常大,则所请求的范围可能和下一个主
                主设备事情重叠。但只要所请求的范围是可用的,这不会有任何问题 */
    dev_t n, next;

    /* 连续申请的设备号可能跨越主设备号,则以循环迭代的方式,一次只处理一个主
       设备号 */
    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0); /* next指向下一个主设备号的起始次设备号0 */
        if (next > to) 
            next = to; /* 次设备号有20位,意味着一个主设备号对应的设备号池最多
                可以分配2^20个设备号。如果主设备号对应的设备号池还还没用完,则
                下一个分配的设备号的主设备号还是此次的主设备号,不会递增 */
        cd = __register_chrdev_region(MAJOR(n), MINOR(n),
                   next - n, name);
        if (IS_ERR(cd))
            goto fail;
    }
    return 0;
fail:
    to = n;
    /* 在连续分配设备号时,只要有一次申请失败,则注销所有已经申请的设备号 */
    for (n = from; n < to; n = next) {
        next = MKDEV(MAJOR(n)+1, 0);
        kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
    }
    return PTR_ERR(cd);
}

/* 建立设备号数据结构struct char_device_struct节点,并添加到chrdevs[]链表 */
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);
    mutex_lock(&chrdevs_lock);

    /* 如果主设备号是0,则表示自动分配设备号 */
    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; /* 从chrdevs[]中获取一个未使用的字符设备实例 */ 
    }

    cd->major = major; /* 主设备号 */
    cd->baseminor = baseminor; /* 次设备号 */
    cd->minorct = minorct;
    strlcpy(cd->name, name, sizeof(cd->name)); /* 设备名称 */

    i = major_to_index(major); /* chrdevs只有255个数组成员,而主设备号
                共有4096个,取模操作选取对应的chrdevs */

    /* 遍历chrdevs[],按照设备编号大小顺序,查找新设备号节点的插入位置 */                    
    for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
        if ((*cp)->major > major || /* 如果cp主设备号大于申请的主设备号 */
            ((*cp)->major == major && /* 如果cp主设备号相等,且次设备
                                    起始编号大于申请的次设备号,或者次设备的
                                    最大编号大于申请的次设备号 */
             (((*cp)->baseminor >= baseminor) || 
              ((*cp)->baseminor + (*cp)->minorct > baseminor)))) 
            break;

    /* 如果新申请的设备号节点与已有的设备号节点发生了重叠,则表示出错了,设备号
       申请失败 */
    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;
    mutex_unlock(&chrdevs_lock);
    return cd;
out:
    mutex_unlock(&chrdevs_lock);
    kfree(cd);
    return ERR_PTR(ret);
}

因为指定了分配的设备起始号globalmem_major,所以由__register_chrdev_region()负责建立设备号数据结构struct char_device_struct节点,计算索引值i = 主设备号 % 255,添加节点到chrdevs[i]链表。有几种情况:

  • 如果chrdevs[i]是空节点,则把新的节点直接插入。
  • 如果chrdevs[i]不是空节点,则需要比较节点链表的节点与新节点的设备号信息,进行排序插入。
    • 主设备号小的节点排列在前面。
    • 如果主设备号相同,则次设备号小(如果是连续分配设备号,则以最大的设备号作比较)的节点,或者排列在前面。

假设有3个struct char_device_struct节点A、B、C,则其排序后的结果如图所示。虽然A节点的次设备起始编号小于B节点,但由于A节点的次设备最大编号大于B节点,所以其被放置在B节点之后。

chrdevs[]链表是用来给用户空间查看已注册的设备的信息(主设备号、设备名称),可以通过/proc/devices来查看chrdevs[]链表信息。

chin@linux:~$ cat /proc/devices 
Character devices:
  1 mem
  4 /dev/vc/0
230 globalmem

设备号动态分配

我们再来看另一个动态分配设备设备号的函数alloc_chrdev_region(),这个函数与register_chrdev_region()其实是差不多的,唯一的区别是主设备号被指定为0,因此在__register_chrdev_region()中将进行设备号的动态查找分配。

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
{
    struct char_device_struct *cd;
    cd = __register_chrdev_region(0, baseminor, count, name);
    if (IS_ERR(cd))
        return PTR_ERR(cd);
    *dev = MKDEV(cd->major, cd->baseminor);
    return 0;
}

设备注册

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
    int err, devno = MKDEV(globalmem_major, index);

    cdev_init(&dev->cdev, &globalmem_fops);
    dev->cdev.owner = THIS_MODULE;
    err = cdev_add(&dev->cdev, devno, 1);
    if (err)
        printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}

cdev_init()完成字符设备结构体对象struct cdev的初始化。

/* 主要完成kobject的初始化,及ops指针的注册 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    kobject_init(&cdev->kobj, &ktype_cdev_default);
    cdev->ops = fops;
}

globalmem_devp的初始化状态:

cdev_add()把字符设备加入到kobj_map链表。

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    int error;

    p->dev = dev; /* globalmem_dev.cdev.dev = 设备号 */
    p->count = count; /* globalmem_dev.cdev.count = 连续分配个数 */

    error = kobj_map(cdev_map, dev, count, NULL,
             exact_match, exact_lock, p);
    if (error)
        return error;

    kobject_get(p->kobj.parent);

    return 0;
}

这里先介绍下cdev_map这个全局变量,这个全局变量是由struct kobj_map定义的,用来管理所有的字符设备。

struct kobj_map {
    struct probe {
        struct probe *next;
        dev_t dev; /* 设备号 */
        unsigned long range; /* 设备号范围 */
        struct module *owner;
        kobj_probe_t *get;
        int (*lock)(dev_t, void *);
        void *data; /* 指向struc cdev实例对象 */
    } *probes[255];
    struct mutex *lock;
};

static struct kobj_map *cdev_map;

void __init chrdev_init(void)
{
    cdev_map = kobj_map_init(base_probe, &chrdevs_lock);
}

/* 初始化全局变量cdev_map,cdev_map.probes[] = base_probe()*/
struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct mutex *lock)
{
    struct kobj_map *p = kmalloc(sizeof(struct kobj_map), GFP_KERNEL);
    struct probe *base = kzalloc(sizeof(*base), GFP_KERNEL);
    int i;

    if ((p == NULL) || (base == NULL)) {
        kfree(p);
        kfree(base);
        return NULL;
    }

    base->dev = 1;
    base->range = ~0;
    base->get = base_probe;
    for (i = 0; i < 255; i++)
        p->probes[i] = base;
    p->lock = lock;
    return p;
}

static struct kobject *base_probe(dev_t dev, int *part, void *data)
{
    if (request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev)) > 0)
        /* Make old-style 2.4 aliases work */
        request_module("char-major-%d", MAJOR(dev));
    return NULL;
}

cdev_mapkobj_map_init()进行初始化,初始化状态如图所示。

现在再来看下kobj_map(),建立struct probe对象表示字符设备,并加入到cdev_map字符设备管理链表里。

* domain = cdev_map
   dev = 设备号
   range = 连续分配设备号个数
   probe = exact_match()
   lock = exact_lock()
   data = globalmem_dev.cdev
*/
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) /* 限制最大只允许255个主设备号 */
        n = 255;

    p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);

    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; /* 指向字符设备对象globalmem_dev.cdev */
    }
    mutex_lock(domain->lock);
    for (i = 0, p -= n; i < n; i++, p++, index++) {
        struct probe **s = &domain->probes[index % 255];
        /* 如果主设备号对应的probes已经存在struct probe对象,则按照连续设备号个
           数range从小到大进行排序 */
        while (*s && (*s)->range < range)
            s = &(*s)->next;
        p->next = *s; /* 插入链表 */
        *s = p;
    }
    mutex_unlock(domain->lock);
    return 0;
}

操作设备

用户空间对设备进行open()操作的时候,由于globalmem是字符设备,因此最终会调用到chrdev_open(),通过设备号搜索设备注册时加入的probes[]链表,找到230设备号链表里对应的struct probe *p那个节点,最终获取到globalmem_devp。把globalmem_devp.ops与文件节点挂接struct file.f_op = globalmem_devp.ops,之后用户空间就可以通过用户空间的文件操作,调用到globalmem_devp.ops的设备操作了。

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);
        /* 获取globalmem_devp.cdev.kobj */
        kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
        if (!kobj)
            return -ENXIO;
        /* 获取globalmem_devp.cdev */
        new = container_of(kobj, struct cdev, kobj);
        spin_lock(&cdev_lock);
        p = inode->i_cdev;
        if (!p) {
            inode->i_cdev = p = new;
            list_add(&inode->i_devices, &p->list);
            new = NULL;
        } 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); /* 获取globalmem_fops */
    if (!fops)
        goto out_cdev_put;

    replace_fops(filp, fops); /* 挂接文件操作指针。后面对该文件的read()、write()
                等操作,将会调用到globalmem_fops.read()、globalmem_fops.write()*/
    if (filp->f_op->open) {
        ret = filp->f_op->open(inode, filp); /* 执行globalmem_open() */
        if (ret)
            goto out_cdev_put;
    }

    return 0;

 out_cdev_put:
    cdev_put(p);
    return ret;
}

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) {
        struct kobject *(*probe)(dev_t, int *, void *);
        struct module *owner;
        void *data;

        if (p->dev > dev || p->dev + p->range - 1 < dev)
            continue;
        if (p->range - 1 >= best)
            break;
        if (!try_module_get(p->owner))
            continue;
        owner = p->owner;
        data = p->data; /* globalmem_devp.cdev */
        probe = p->get; /* exact_match() */
        best = p->range - 1;
        *index = dev - p->dev;
        if (p->lock && p->lock(dev, data) < 0) {
            module_put(owner);
            continue;
        }
        mutex_unlock(domain->lock);
        kobj = probe(dev, index, data); /* 获取globalmem_devp.cdev.kobj */
        module_put(owner);
        if (kobj)
            return kobj;
        goto retry;
    }
    mutex_unlock(domain->lock);
    return NULL;
}

static struct kobject *exact_match(dev_t dev, int *part, void *data)
{
    struct cdev *p = data;
    return &p->kobj; /* globalmem_devp.cdev.kobj */
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值