当前位置 : 主页 > 编程语言 > java >

字符设备驱动的几种写法

来源:互联网 收集:自由互联 发布时间:2022-09-29
随着linux内核的更新换代和计算机硬件的不断增多,字符设备驱动在不同内核版本下也呈现出了几种不同的写法,本文将具体随着linux发展的脚步详述字符设备驱动写法的更新。 在Linu


随着linux内核的更新换代和计算机硬件的不断增多,字符设备驱动在不同内核版本下也呈现出了几种不同的写法,本文将具体随着linux发展的脚步详述字符设备驱动写法的更新。

在Linux2.4以前,内核中所有已分配的字符设备编号都记录在一个名为 chrdevs ,元素个数为255的散列表里。该散列表中的每一个元素是一个 char_device_struct 结构,代表主设备号相同的一组设备。它在内核中的定义如下:

static struct char_device_struct {
struct char_device_struct *next; // 指向散列表中的下一个指针
unsigned int major; // 主设备号
unsigned int baseminor; // 起始次设备号
int minorct; // 设备编号数
char name[64]; // 设备驱动名
struct file_operations *fops; // 指向该设备对应的文件操作函数结构体指针
struct cdev *cdev; // 指向字符设备驱动程序描述符的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 ​​register_chrdev()​​​、​​register_chrdev_region()​​​和​​alloc_chrdev_region()​​​ 。这三个函数都会调用一个共用的 ​​__register_chrdev_region()​​函数来注册一组设备编号范围(即一个 char_device_struct 结构)。

1. __register_chrdev_region()

首先来看一下​​__register_chrdev_region​​函数的定义:

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);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
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;
ret = major;
}
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strncpy(cd->name,name, 64);
i = major_to_index(major);
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;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
  • 函数 __register_chrdev_region() 主要执行以下步骤:
  • 分配一个新的 char_device_struct 结构,并用 0 填充。
  • 如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。动态分配主设备号的原则是从散列表的最后一个桶向前寻找,如果那个桶是空的,主设备号就是相应散列桶的序号。所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
  • 根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。
  • 计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。如果设备编号范围有重复的话,则出错返回。
  • 将新的 char_device_struct 结构插入散列表中,并返回 char_device_struct 结构的地址。
  • 2. register_chrdev()

    在Linux2.4内核以前使用的是这种分配设备编号范围的函数。它每次都是粗粒度的分配一个单独主设备号和 0 ~ 255 的次设备号范围(如果申请的主设备号为 0 则动态分配一个)。该函数内部自动分配了一个新的 cdev 结构,我们另外还需传入一个 file_operations 结构的指针,用来和新建的char_device_struct结构体绑定,以后凡是相同主设备号(即所有256个共享该主设备号的次设备号设备)的设备均使用同一个file_operations,不管你实际使用了几个次设备号,默认都会将相应主设备号下的256个次设备号连续注册,造成了极大的浪费。其定义如下:

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

    cd = __register_chrdev_region(major, 0, 256, name);
    if (IS_ERR(cd))
    return PTR_ERR(cd);

    cdev = cdev_alloc();
    if (!cdev)
    goto out2;

    cdev->owner = fops->owner;
    cdev->ops = fops;
    kobject_set_name(&cdev->kobj, "%s", name);
    for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))
    *s = '!';

    err = cdev_add(cdev, MKDEV(cd->major, 0), 256);
    if (err)
    goto out;

    cd->cdev = cdev;

    return major ? 0 : cd->major;
    out:
    kobject_put(&cdev->kobj);
    out2:
    kfree(__unregister_chrdev_region(cd->major, 0, 256));
    return err;
    }
    • 其对应的设备注销函数为​​unregister_chrdev()​​​,内部调用了​​__unregister_chrdev_region()​​函数.
    • 为了避免资源的浪费,在linux2.4以后内核采用了register_chrdev_region或者alloc_chrdev_region函数来分配和注册字符设备。

    3. register_chrdev_region()

    看到region这个词相比大家应该知道,这是分配一个域,即指定的一个次设备号范围。当我们使用自定义的主设备号和次设备号范围时,使用该函数。

    如果申请的设备编号范围跨越了主设备号,它会把分配范围内的编号按主设备号分割成较小的子范围,并在每个子范围上调用 __register_chrdev_region() 。如果其中有一次分配失败的话,那会把之前成功分配的都全部退回。具体请看其实现代码:

    int register_chrdev_region(dev_t from, unsigned count, const char *name)
    {
    struct char_device_struct *cd;
    dev_t to = from + count;
    dev_t n, next;

    for (n = from; n < to; n = next) {
    next = MKDEV(MAJOR(n)+1, 0);
    if (next > to)
    next = to;
    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);
    }

    /*
    from: 注册的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0

    count:需要连续注册的次设备编号个数,比如: 起始次设备号为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations操作方法结构体上

    *name:字符设备名称
    */
    • 其对应的设备注销函数为​​unregister_chrdev_region()​​​,内部也同样调用了​​__unregister_chrdev_region()​​函数.

    4. alloc_chrdev_region()

    该函数用于动态申请设备编号范围,即我们事先不指定主设备号的情况下使用。它通过dev指针参数返回实际获得的起始设备编号。

    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;
    }

    /*
    *dev: 存放起始设备编号的指针,当注册成功, *dev就会等于分配到的起始设备编号,可以通过MAJOR()和MINNOR()函数来提取主次设备号

    baseminor:次设备号基地址,也就是起始次设备号

    count:需要连续注册的次设备编号个数,比如: 起始次设备号(baseminor)为0,baseminor=2,表示0~1的此设备号都要绑定在同一个file_operations操作方法结构体上

    *name:字符设备名称
    */
    • 其对应的设备注销函数也为​​unregister_chrdev_region()​​​,内部也同样调用了​​__unregister_chrdev_region()​​函数.

    5. 后续工作

    static struct cdev hello1_cdev; //定义字符设备

    cdev_init(&hello1_cdev, &hello1_fops); //为字符设备绑定文件操作符

    cdev_add(&hello1_cdev, MKDEV(major,0), 2); //为字符设备绑定设备号

    cls=class_create(THIS_MODULE, "hello"); //创建一种类,即创建/proc/class/hello

    class_device_create(cls,0, MKDEV(major,0), 0, "hello0"); //创建字符设备节点/dev/hello0

    一般字符设备的驱动需要以下几个步骤:


  • 为了更好的说明字符设备的编写步骤,下面举一个具体的程序,即在同一主设备号下创建两个设备域,并分别与不同的文件操作函数绑定。

    /*
    *创建两个字符设备,他们公用同一个主设备号;
    *但次设备号0~1对应第一个字符设备,使用hello1_fops文件操作符;
    * 次设备号2~3对应第二个字符设备,使用hello2_fops文件操作符;
    * 次设备号4不对应字符设备,不使用文件操作符;
    */

    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/fs.h>
    #include <linux/init.h>
    #include <linux/delay.h>
    #include <asm/irq.h>
    #include <asm/arch/regs-gpio.h>
    #include <asm/hardware.h>
    #include <asm/uaccess.h>
    #include <asm/io.h>
    #include <linux/list.h>
    #include <linux/cdev.h>

    static int hello_fops1_open(struct inode *inode, struct file *file)
    {
    printk("open_hello1\n");
    return 0;
    }



    static int hello_fops2_open (struct inode *inode, struct file *file)
    {
    printk("open_hello2\n");
    return 0;
    }


    /* 操作结构体1 */
    static struct file_operations hello1_fops={
    .owner=THIS_MODULE,
    .open =hello_fops1_open,
    };

    /* 操作结构体2 */
    static struct file_operations hello2_fops={
    .owner=THIS_MODULE,
    .open =hello_fops2_open,
    };


    static int major; //主设备
    static struct cdev hello1_cdev; //保存 hello1_fops操作结构体的字符设备
    static struct cdev hello2_cdev; //保存 hello2_fops操作结构体的字符设备
    static struct class *cls;

    static int chrdev_region_init(void)
    {
    dev_t devid;
    #if 0
    major = register_chrdev(0,"hello",&hello_fops); //以前采用这种形式
    #else
    if(major)
    {
    devid = MKDEV(major,0);
    register_chrdev_region(devid, 2, "hello");
    }else
    {
    alloc_chrdev_region(&devid, 0, 2,"hello"); //动态分配字符设备
    major=MAJOR(devid);
    }
    cdev_init(&hello1_cdev, &hello1_fops); //初始化cdev,绑定fops结构体
    cdev_add(&hello1_cdev, MKDEV(major,0), 2); //注册cdev,即绑定(major,0~1)

    devid = MKDEV(major,2);
    register_chrdev_region(devid, 2, "hello2");
    cdev_init(&hello2_cdev, &hello2_fops);
    cdev_add(&hello2_cdev,devid, 2); //注册cdev,即绑定(major,2~3)
    #endif

    cls=class_create(THIS_MODULE, "hello");
    /*创建字符设备节点*/
    class_device_create(cls,0, MKDEV(major,0), 0, "hello0"); //对应hello_fops1操作结构体
    class_device_create(cls,0, MKDEV(major,1), 0, "hello1"); //对应hello_fops1操作结构体
    class_device_create(cls,0, MKDEV(major,2), 0, "hello2"); //对应hello_fops2操作结构体
    class_device_create(cls,0, MKDEV(major,3), 0, "hello3"); //对应hello_fops2操作结构体
    class_device_create(cls,0, MKDEV(major,4), 0, "hello4"); //对应空

    return 0;
    }

    void chrdev_region_exit(void)
    {
    class_device_destroy(cls, MKDEV(major,4));
    class_device_destroy(cls, MKDEV(major,3));
    class_device_destroy(cls, MKDEV(major,2));
    class_device_destroy(cls, MKDEV(major,1));
    class_device_destroy(cls, MKDEV(major,0));

    class_destroy(cls);


    cdev_del(&hello1_cdev);
    unregister_chrdev_region(MKDEV(major,0), 2); //注销(major,0)~(major,1)

    cdev_del(&hello2_cdev);
    unregister_chrdev_region(MKDEV(major,2), 2); //注销(major,2)~(major,3)
    }

    module_init(chrdev_region_init);
    module_exit(chrdev_region_exit);
    MODULE_LICENSE("GPL");

    针对以上驱动,编写如下测试程序:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    void print_useg(char arg[])    //打印使用帮助信息
    {
    printf("useg: \n");
    printf("%s [dev]\n",arg);
    }

    int main(int argc,char **argv)
    {

    int fd;
    if(argc!=2)
    {
    print_useg(argv[0]);
    return -1;
    }

    fd=open(argv[1],O_RDWR);
    if(fd<0)
    printf("can't open %s \n",argv[1]);
    else
    printf("can open %s \n",argv[1]);
    return 0;
    }

    装载驱动后,进行测试,得到如下结果:

    # ls /dev/hello* -l
    crw-rw---- 1 0 0 252, 0 Jan 1 00:12 /dev/hello0
    crw-rw---- 1 0 0 252, 1 Jan 1 00:12 /dev/hello1
    crw-rw---- 1 0 0 252, 2 Jan 1 00:12 /dev/hello2
    crw-rw---- 1 0 0 252, 3 Jan 1 00:12 /dev/hello3
    crw-rw---- 1 0 0 252, 4 Jan 1 00:12 /dev/hello4

    #./a.out /dev/hello0 //打开/dev/hello0时,调用的是hello1_fops里的.open()
    open hello0
    #
    #
    #./a.out /dev/hello2 //打开/dev/hello2时,调用的是hello1_fops里的.open()
    open hello2
    #
    #
    #./a.out /dev/hello4 //打开无效,因为在驱动代码里没有分配次设备号4的操作结构体
    can't open /dev/hello4
    #


    上一篇:关于BITMAP位图结构体中的颜色平面的理解
    下一篇:没有了
    网友评论