Linux内核编程(补)字符设备驱动编写(字符设备框架)


  

一、杂项设备框架和字符设备框架的区别

   我们在编写字符设备时,使用杂项设备框架会比使用字符设备框架编写更为简便。但是由于使用杂项设备框架会使得注册的设备变为杂项设备,虽然功能都一样,但是分类会不同。如果分类严格,则我们需要使用字符设备驱动框架来编写,这样注册的设备就被分到了字符设备类中。
   如果正在开发的设备需要明确地归类到字符设备中(例如在 /sys/class/char 中),你需要对设备号进行特殊的管理,那么使用字符设备框架是更合适的选择。如果对设备分类要求不高,杂项设备框架则可以帮助简化开发流程。

二、字符设备驱动框架相关知识

1. 设备号

   Linux2.6 版本以后,使用 dev_t 来表示一个设备号,dev_t 实际上是一个 u32 类型。其中高 12 位是主设备号,低 20 位是次设备号。
   设备号使用前需要先申请: register_chrdev_region(静态分配)或 alloc_chrdev_region(动态分配)设备号函数申请。这里我们常使用动态申请的方式。

●内核提供了设备号合成与分解的宏方便开发者使用。
(1)合成设备号:

dev_t dev_num;
unsigned int ma = 240; // 示例主设备号
unsigned int mi = 0;   // 示例次设备号

dev_num=MKDEV(ma,mi); //ma:主设备号;mi:次设备号

(2)分解设备号:

dev_t dev_num;
MAJOR(dev_num); //从设备号 dev 中分解出主设备号
MINOR(dev_num); //从设备号 dev 中分解出次设备号

2. 特性

(1)设备驱动模块安装后,不会自动创建/dev/目录下设备文件节点,需要开发者在/dev/目录下手动使用 mknod 命令创建或者在驱动程序中调用自动创建设备文件的函数进行创建。mknod命令如下:

mknod /dev/file-name  c 247 1
/*
    c: 这个参数指定了设备文件的类型为“字符设备”。在 Linux 中,设备文件可以是字符设备(c)或块设备(b)。
	247:主设备号
	1:次设备号
*/

(2)调用 cdev_add 注册后,只占用指定数量的次设备号。

3. 常用API

(1)动态申请设备号

头文件:#include <linux/fs.h>
返回值:成功:0;失败:返回负数。

int alloc_chrdev_region(dev_t *dev,  unsigned baseminor,  unsigned count,const char *name);
/*
	dev:存放分配到的第一个设备(包含主次设备号)。
	baseminor:要分配起始次设备号。
	count:连续的次设备号数量。
	name:设备名,不需要和/dev/的设备文件名相同。
*/

(2)注销设备号

void unregister_chrdev_region( dev_t from, unsigned count);
//from:起始设备号(主、次) (包含主次设备号) 。
//count:连续的次设备号数量。

(3)字符设备结构体初始化

头文件:#include <linux/ cdev.h>
功能:初始化核心结构,具体做的是清零核心结构,初始化核心结构的 list,kobj,ops 成员。

void cdev_init(struct cdev *cdev, const struct file_operations *fops);
/*
	cdev:需要初始化的核心结构指针。
	fops:文件操作方法结构指针。
*/

(4)注册字符设备

头文件:#include <linux/ cdev.h>
返回值:成功:返回 0; 失败:返回负数

int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);
/*
 cdev:需要注册的核心结构指针(已完成初始化)。
 dev_t dev:设备号。
 unsigned count :连续次设备号数量。
*/

(5)注销字符设备

void cdev_del(struct cdev *cdev);

(6)创建设备类

用于创建一个设备类。设备类在 /sys/class 中表示设备的类型,使得设备管理更加方便。

struct class *class_create(struct module *owner, const char *name);
//struct module *owner :通常用于指定模块的所有权。如果该类是由内核模块创建的,通常会传递 THIS_MODULE 宏。
//const char *name :这是类的名称,通常是一个字符串。这个名称将出现在 /sys/class/ 目录中,表示该类下的所有设备。

(7)注销设备类

void class_destroy(struct class *cls);

(8)创建设备节点

创建并注册一个设备。该设备通常与设备类(struct class)相关联,并且会在 /dev 目录下生成相应的设备节点。

struct device *device_create(struct class *class, struct device *parent,
                             dev_t devt, void *drvdata, const char *fmt, ...);
/*
	struct class *class :指向 struct class 的指针,表示设备所属的类。这是在调用 class_create 时创建的设备类。
	parent:指向父设备的指针,如果设备没有父设备,可以传递 NULL。
	dev_t devt :设备号(dev_t),这是一个组合了主设备号和次设备号的值。
	void *drvdata:驱动程序数据指针。这通常是驱动程序为该设备维护的私有数据。
	const char *fmt:设备的名称格式字符串。
	... (可变参数):用于 fmt 的可变参数,可以指定设备名称中的可变部分。
*/

(9)注销设备节点

void device_destroy(struct class *class, dev_t devt);
/*
	struct class *class :指向 struct class 的指针,表示设备所属的类。
	dev_t devt :设备号。
*/

三、字符设备驱动框架编写流程

1. 操作总流程

  1. 为指针/结构体分配内存空间。(在内核中使用kzalloc函数申请)
  2. 动态申请设备号。
  3. 初始化字符设备结构体。
  4. 注册已经初始化好的字符设备。
  5. 创建设备类。
  6. 创建设备节点。
  7. 在驱动出口函数中完成内存释放、注销等功能。

2. 具体代码(使用命令创建设备节点文件)

   优化封装管理:因为我们用到了设备号、字符设备结构体等变量,这些变量都属于字符设备。我们可以将这些变量进行封装,方便操作管理。具体代码如下所示。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>

#define DEVICE_NAME "chrdev-linux-26"

struct cdev_obj {
    dev_t dev_num;           // 设备号
    struct cdev cdev;        // 字符设备结构体
};

struct cdev_obj *cdev_one;  // 初始化封装的结构体

const struct file_operations my_fops = {
    .read = NULL,
    .write = NULL,
    .open = NULL,
};

static int __init linux26_device_init(void)
{
    int ret;

    // 1. 使用kzalloc分配 cdev_obj 结构空间
    cdev_one = kzalloc(sizeof(struct cdev_obj), GFP_KERNEL);
    if (cdev_one == NULL) {
        printk("kzalloc error\n");
        ret = -ENOMEM;
        goto error0;
    }

    // 2. 申请设备号: 动态 。次设备号的范围为0~2
    ret = alloc_chrdev_region(&cdev_one->dev_num, 0, 2, DEVICE_NAME);
    if (ret < 0) {
        printk("alloc_chrdev_region error\n");
        goto error1;
    }

    // 3. 初始化 cdev 结构
    cdev_init(&cdev_one->cdev, &my_fops);

    // 4. 注册已经初始化好的 cdev 结构,2个次设备号
    ret = cdev_add(&cdev_one->cdev, cdev_one->dev_num, 2);
    if (ret < 0) {
        printk("cdev_add error\n");
        goto error2;
    }
    
    return 0;

error2:  //要释放掉前面成功的资源
    unregister_chrdev_region(cdev_one->dev_num, 2);
error1:
    kfree(cdev_one);
error0:
    return ret;
}

static void __exit linux26_device_exit(void)
{
    // 1. 注销 cdev 结构
    cdev_del(&cdev_one->cdev);
    // 2. 释放设备号
    unregister_chrdev_region(cdev_one->dev_num, 2);
    // 3. 释放 cdev 结构空间
    kfree(cdev_one);
}
module_init(linux26_device_init);
module_exit(linux26_device_exit);
MODULE_LICENSE("GPL");

   代码中申请了 2 个次设备号,且次设备号的开始编号从0开始(代码中设置)。这里我们通过使用命令来创建设备节点文件。由于需要知道主设备号,因此在代码中使用分解设备号的宏来获取并打印出主设备号。使用dmesg命令查看。
在这里插入图片描述

   由于我们将模块注册到内核以后,并不会在/dev/目录下生成设备节点,所以需要我们手动创建设备节点文件。这里由于我们申请了两个次设备号范围,则我们可以选择次设备号0或1。具体使用命令:sudo mknod /dev/QJLdev_test c 246 0,或使用sudo mknod /dev/QJLdev_test c 246 1。这里的次设备号不能使用超过我们申请的设备号范围!否则在操作设备节点时会报错!
在这里插入图片描述

3. 具体代码(自动创建设备节点文件)

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>

#define DEVICE_NAME "QJLdev_test"

struct cdev_obj {
    dev_t dev_num;           // 设备号
    struct cdev cdev;        // 字符设备结构体
    struct class *myclass;  // 设备类
    struct device *devfile; // 设备文件
};

struct cdev_obj *cdev_one;  // 初始化封装的结构体

const struct file_operations my_fops = {
    .read = NULL,
    .write = NULL,
    .open = NULL,
};

static int __init linux26_device_init(void)
{
    int ret;

    // 1. 使用kzalloc分配 cdev_obj 结构空间
    cdev_one = kzalloc(sizeof(struct cdev_obj), GFP_KERNEL);
    if (cdev_one == NULL) {
        printk(KERN_ERR "kzalloc error\n");
        ret = -ENOMEM;
        goto error0;
    }

    // 2. 申请设备号: 动态 。次设备号的范围为0~2
    ret = alloc_chrdev_region(&cdev_one->dev_num, 0, 2, DEVICE_NAME);
    if (ret < 0) {
        printk(KERN_ERR "alloc_chrdev_region error\n");
        goto error1;
    }

    // 3. 初始化 cdev 结构,2个次设备号
    cdev_init(&cdev_one->cdev, &my_fops);

    // 4. 注册已经初始化好的 cdev 结构
    ret = cdev_add(&cdev_one->cdev, cdev_one->dev_num, 2);
    if (ret < 0) {
        printk(KERN_ERR "cdev_add error\n");
        goto error2;
    }
    printk(KERN_INFO "主设备号为:%d\n", MAJOR(cdev_one->dev_num));

    // 5. 创建设备类
    cdev_one->myclass = class_create(THIS_MODULE, "class-qjl");
    if (IS_ERR(cdev_one->myclass)) {
        printk(KERN_ERR "class_create error\n");
        ret = PTR_ERR(cdev_one->myclass);
        goto error3;
    }

    // 6. 创建设备节点。
    cdev_one->devfile = device_create(cdev_one->myclass, NULL, cdev_one->dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(cdev_one->devfile)) {
        printk(KERN_ERR "device_create error\n");
        ret = PTR_ERR(cdev_one->devfile);
        goto error4;
    }

    return 0;

error4:
    class_destroy(cdev_one->myclass);
error3:
    cdev_del(&cdev_one->cdev);
error2:
    unregister_chrdev_region(cdev_one->dev_num, 2);
error1:
    kfree(cdev_one);
error0:
    return ret;
}

static void __exit linux26_device_exit(void)
{
    // 1. 注销设备节点
    device_destroy(cdev_one->myclass, cdev_one->dev_num);
    // 2. 注销设备类
    class_destroy(cdev_one->myclass);
    // 3. 注销 cdev 结构
    cdev_del(&cdev_one->cdev);
    // 4. 释放设备号
    unregister_chrdev_region(cdev_one->dev_num, 2);
    // 5. 释放 cdev 结构空间
    kfree(cdev_one);
}

module_init(linux26_device_init);
module_exit(linux26_device_exit);
MODULE_LICENSE("GPL");

/dev/目录下生成的设备节点。
在这里插入图片描述

/sys/class/目录下生成的设备类。
在这里插入图片描述

四、涉及c语言知识点–使用指针变量前必须为其分配内存空间

   使用结构体来举例:如果你在代码中定义了一个结构体变量(普通变量),例如 struct MyStruct myStruct;,那么内存会在定义时自动为这个结构体分配。你不需要手动为它分配内存。如果你定义了一个结构体指针(指针变量),例如 struct MyStruct *pMyStruct;,那么指针本身只占用指针的内存空间(通常是4或8字节,取决于系统)。但是,结构体本身的内存并没有自动分配。你需要通过 malloc 等方式为这个指针分配内存,例如 pMyStruct = (struct MyStruct*)malloc(sizeof(struct MyStruct));

   知识点:当我们在结构体内部定义指针变量时,如果我们要使用该指针,也必须为其申请内存空间!因为在为结构体分配内存时,只会为结构体本身的成员分配内存,但不会为结构体中的指针指向的内存分配空间。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>  // 需要包含这个头文件以使用 strcpy

struct data {
    int a;
    char *b;
};

struct data *my_data;

int main() 
{
    my_data = (struct data *)malloc(sizeof(struct data));   // 为结构体分配内存
    my_data->a = 13;  //直接操作普通变量
   
    my_data->b = (char *)malloc(sizeof(char) * 4);  //操作指针,则需要分配内存, 
    strcpy(my_data->b, "hhh"); // 使用 strcpy 将字符串复制到分配的内存中
    
    printf("%d, %s\n", my_data->a, my_data->b);
    
    // 释放分配的内存
    free(my_data->b);  // 先释放内部指针
    free(my_data);     // 再释放结构体本身
    
    return 0;
}

   如果函数返回值为指针类型,通常意味着函数内部已经为该指针分配了内存。这种情况下,无需手动分配内存。在某些框架或库中,可能会提供专门的API函数用于释放内存。这种情况下,需要根据API的文档来确定是否需要手动调用释放函数。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值