Linux 字符设备驱动结构—— cdev 结构体、设备号相关知识解析及创建字符设备示例

18 篇文章 8 订阅

一、字符设备基础知识

1、设备驱动分类

      linux系统将设备分为3类:字符设备、块设备、网络设备。使用驱动程序的方式如下:

  • 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
  • 块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

        每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

2、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系

     如上图所示。

    在Linux内核中:

  • 使用cdev结构体来描述字符设备;
  •  通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;
  •  通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;

     在Linux字符设备驱动中:

  •  模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;
  • 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;
  •  模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;

     用户空间访问该设备的程序:

  •  通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;

3、字符设备驱动模型

 二、cdev 结构体解析

      在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:

<include/linux/cdev.h>
 
struct cdev { 
    struct kobject kobj;                  //内嵌的内核对象.
    struct module *owner;                 //该字符设备所在的内核模块的对象指针.
    const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
    struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.
    dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.
    unsigned int count;                   //隶属于同一主设备号的次设备号的个数.
};

内核给出的操作struct cdev结构的接口主要有以下几个:
(1)void cdev_init(struct cdev *, const struct file_operations *);

其源代码如代码清单如下:

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

      该函数主要对struct cdev结构体做初始化, 最重要的就是建立cdev 和 file_operations之间的连接:

  • 将整个结构体清零; 
  • 初始化list成员使其指向自身;
  • 初始化kobj成员;
  • 初始化ops成员;

(2)struct cdev *cdev_alloc(void);

     该函数主要分配一个struct cdev结构,动态申请一个cdev内存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在调用cdev_alloc后,显式的做初始化即: .ops=xxx_ops).

其源代码清单如下:

struct cdev *cdev_alloc(void)
{
    struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
    if (p) {
        INIT_LIST_HEAD(&p->list);
        kobject_init(&p->kobj, &ktype_cdev_dynamic);
    }
    return p;
}


     在上面的两个初始化的函数中,我们没有看到关于owner成员、dev成员、count成员的初始化;其实,owner成员的存在体现了驱动程序与内核模块间的亲密关系,struct module是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化 .owner = THIS_MODULE, 该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而dev成员和count成员则在cdev_add中才会赋上有效的值。

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

       该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了。

当然这里还需提供两个参数:

  • 设备号 dev,
  • 和该设备关联的设备编号的数量。

这两个参数直接赋值给struct cdev 的dev成员和count成员。

(4)void cdev_del(struct cdev *p);

     该函数向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经不可以使用了。

     从上述的接口讨论中,我们发现对于struct cdev的初始化和注册的过程中,我们需要提供几个东西

  • struct file_operations结构指针;
  • dev设备号;
  • count次设备号个数。

但是我们依旧不明白这几个值到底代表着什么,而我们又该如何去构造这些值!

三、设备号相应操作

1 -- 主设备号和次设备号(二者一起为设备号):

      一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

  linux内核中,设备号用dev_t来描述,2.6.28中定义如下:

  typedef u_long dev_t;

  在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。

内核也为我们提供了几个方便操作的宏实现dev_t(仅仅是生成1个dev_t的值,此时并未与任何文件联系起来,只是一个数值而已):

  • 从设备号中提取major和minor
MAJOR(dev_t dev);                              

MINOR(dev_t dev);
  • 通过major和minor构建设备号
MKDEV(int major,int minor);


注:这只是构建设备号。并未注册,需要调用 register_chrdev_region 静态申请;

//宏定义:
#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))</span>


2、分配设备号(两种方法):

  • 静态申请:int register_chrdev_region(dev_t from, unsigned count, const char *name);

其源代码清单如下:

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);
}
  • 动态分配:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

其源代码清单如下:

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


        可以看到二者都是调用了__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);
 
    /* 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;
        ret = major;
    }
 
    cd->major = major;
    cd->baseminor = baseminor;
    cd->minorct = minorct;
    strlcpy(cd->name, name, sizeof(cd->name));
 
    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和  alloc_chrdev_region 的区别,register_chrdev_region直接将Major 注册进入,而 alloc_chrdev_region从Major = 0 开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去;

        除了前面两个函数,还加了一个register_chrdev 函数,可以发现这个函数的应用非常简单,只要一句就可以搞定前面函数所做之事;

        上述3者应用可以简单总结如下(对应的代码效果等同):

register_chrdev_region                                                alloc_chrdev_region register_chrdev
devno = MKDEV(major,minor);
  ret = register_chrdev_region(devno, 1, "hello"); 
    cdev_init(&cdev,&hello_ops);
    ret = cdev_add(&cdev,devno,1);
alloc_chrdev_region(&devno, minor, 1, "hello");
    major = MAJOR(devno);
    cdev_init(&cdev,&hello_ops);
    ret = cdev_add(&cdev,devno,1)
register_chrdev(major,"hello",&hello_ops);

 下面分析一下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);
}

        调用了 __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();
    if (!cdev)
        goto out2;
 
    cdev->owner = fops->owner;
    cdev->ops = fops;
    kobject_set_name(&cdev->kobj, "%s", name);
 
    err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
    if (err)
        goto out;
 
    cd->cdev = cdev;
 
    return major ? 0 : cd->major;
out:
    kobject_put(&cdev->kobj);
out2:
    kfree(__unregister_chrdev_region(cd->major, baseminor, count));
    return err;
}

        可以看到这个函数不只帮我们注册了设备号,还帮我们做了cdev 的初始化以及cdev 的注册;

3、注销设备号:

void unregister_chrdev_region(dev_t from, unsigned count);

4、创建设备文件:

     利用cat /proc/devices查看申请到的设备名,设备号。

  • 使用mknod手工创建:

        在命令行通过命令创建:mknod filename type major minor

  • 自动创建设备节点:

    利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动用加入对udev 的支持主要做的就是:在驱动初始化的代码里调用class_create(...)为该设备创建一个class,再为每个设备调用device_create(...)创建对应的设备。

    内核中定义的struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create(…)函数来在/dev目录下创建相应的设备节点。

     这样,加载模块的时候,用户空间中的udev会自动响应 device_create()函数,去/sysfs下寻找对应的类从而创建设备节点。

class_create(...) 函数

功能:创建一个类;

下面是具体定义:

#define class_create(owner, name)        \
({                        \
    static struct lock_class_key __key;    \
    __class_create(owner, name, &__key);    \
})

owner:THIS_MODULE
name  : 名字

调用的__class_create(owner, name, &__key)源代码如下:

struct class *__class_create(struct module *owner, const char *name,
                 struct lock_class_key *key)
{
    struct class *cls;
    int retval;
 
    cls = kzalloc(sizeof(*cls), GFP_KERNEL);
    if (!cls) {
        retval = -ENOMEM;
        goto error;
    }
 
    cls->name = name;
    cls->owner = owner;
    cls->class_release = class_create_release;
 
    retval = __class_register(cls, key);
    if (retval)
        goto error;
 
    return cls;
 
error:
    kfree(cls);
    return ERR_PTR(retval);
}
EXPORT_SYMBOL_GPL(__class_create);

对应的销毁函数:void class_destroy(struct class *cls)

void class_destroy(struct class *cls)
{
    if ((cls == NULL) || (IS_ERR(cls)))
        return;
 
    class_unregister(cls);
}

device_create(...) 函数
struct device *device_create(struct class *class, struct device *parent,
                 dev_t devt, void *drvdata, const char *fmt, ...)

功能:创建一个字符设备文件

参数:

      struct class *class  :类
      struct device *parent:NULL
     dev_t devt  :设备号
     void *drvdata  :null、
     const char *fmt  :名字

返回:

    struct device *
下面是源码解析:

struct device *device_create(struct class *class, struct device *parent,
                 dev_t devt, void *drvdata, const char *fmt, ...)
{
    va_list vargs;
    struct device *dev;
 
    va_start(vargs, fmt);
    dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
    va_end(vargs);
    return dev;
}

调用的device_create_vargs(class, parent, devt, drvdata, fmt, vargs)解析如下:

struct device *device_create_vargs(struct class *class, struct device *parent,
                   dev_t devt, void *drvdata, const char *fmt,
                   va_list args)
{
    return device_create_groups_vargs(class, parent, devt, drvdata, NULL,
                      fmt, args);
}

现在就不继续往下跟了,大家可以继续往下跟;

四 生成字符设备驱动的示例

下面是一个实例,由一个模块hello和对应的测试程序组成:

hello.c


#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
 
static int major = 250;
static int minor=0;
static dev_t devno;
static struct class *cls;
static struct device *test_device;
 
static int hello_open (struct inode *inode, struct file *filep)
{
    printk("hello_open \n");
    return 0;
}
static struct file_operations hello_ops=
{
    .open = hello_open,
};
 
static int hello_init(void)
{
    int ret;    
    printk("hello_init \n");
 
 
    devno = MKDEV(major,minor);
    ret = register_chrdev(major,"hello",&hello_ops);
 
    cls = class_create(THIS_MODULE, "myclass");
    if(IS_ERR(cls))
    {
        unregister_chrdev(major,"hello");
        return -EBUSY;
    }
    test_device = device_create(cls,NULL,devno,NULL,"hello");//mknod /dev/hello
    if(IS_ERR(test_device))
    {
        class_destroy(cls);
        unregister_chrdev(major,"hello");
        return -EBUSY;
    }    
    return 0;
}
static void hello_exit(void)
{
    device_destroy(cls,devno);
    class_destroy(cls);    
    unregister_chrdev(major,"hello");
    printk("hello_exit \n");
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);

test.c

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
 
 
main()
{
    int fd;
 
 
    fd = open("/dev/hello",O_RDWR);
    if(fd<0)
    {
        perror("open fail \n");
        return ;
    }
 
 
    close(fd);
}

Makefile

ifneq  ($(KERNELRELEASE),)
obj-m:=hello.o
$(info "2nd")
else
KDIR := /lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
all:
    $(info "1st")
    make -C $(KDIR) M=$(PWD) modules
clean:
    rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order
endif

        先执行make编译生成hello.ko文件,然后执行sudo insmod hello.ko将模块安装到内核中。使用lsmod |grep hello查看是否正常工作,查看/dev/hello节点是否存在。

         编译test.c文件生成测试程序,并进行验证(肯定无法打开啦),查看dmesg是否输出hello_init。

 参考链接:

https://blog.csdn.net/zqixiao_09/article/details/50849735
https://blog.csdn.net/zqixiao_09/article/details/50839042

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值