当前位置: 首页 > news >正文

Linux字符设备驱动模型

一、Linux字符设备简介

学习 Linux 驱动开发时,经常会接触到各种各样的设备,比如 LED 灯、按键、EEPROM、LCD 屏等等。
那这些设备和我们的上层应用程序是怎么打交道的呢?答案就是——通过驱动程序中的字符设备接口


💡 为什么叫“字符设备”?

“字符设备(Character Device)”其实指的是一种以字节流(character stream)方式进行数据传输的设备。
比如我们平时操作的串口(UART)、EEPROM、键盘、鼠标等,数据是一字节一字节传输的。
和它相对的是“块设备”(如硬盘),那种是以“块”为单位传输的。

所以呢,像 AT24C02 这种 EEPROM,它存储和读写都是按字节来的,因此驱动设计时会采用字符设备驱动模型

在 Linux 系统中,发展到现在为止,大体有三种不同的字符设备驱动框架或模型:

🧩 Linux中字符设备的三种驱动模型

在 Linux 系统中,字符设备驱动的实现方式主要有三种:

杂项设备驱动模型(Misc Device)——重点、最常用
这个模型使用起来最简单,注册也最方便,常用于一些简单的外设,比如:LED、蜂鸣器、按键等等。
很多开发板上最基础的驱动,都是通过杂项设备模型实现的。

早期经典字符设备驱动模型
这是比较老的写法,比如 Linux 2.4 时代经常用。
它的接口是 register_chrdev() 这种函数,现在已经比较少用了,因为功能比较有限,不能灵活分配设备号,也不太方便管理多个设备。

Linux 2.6 标准字符设备驱动模型
这是我们现在主流使用的模型,功能最完善,也最灵活。
它是通过 cdev_init()cdev_add() 等函数完成注册和管理的,支持动态分配设备号,还能配合 udev 自动创建设备节点。
所以,如果我们后面要写比较完整的驱动,比如 I2C、SPI、LCD 等设备驱动,基本都会基于这个模型来实现。


🔗 三种模型的共同点

不管是哪一种模型,它们的核心结构其实都一样:

① 都需要定义一个 struct file_operations 类型的结构体。
里面定义了设备的各种操作接口,比如:

struct file_operations {.open = xxx_open,       // 打开设备.read = xxx_read,       // 读设备.write = xxx_write,     // 写设备.release = xxx_release, // 关闭设备
};

这个结构体的作用就像“设备说明书”,告诉内核,当应用程序对设备执行 open()read()write() 这些系统调用时,应该调用驱动中的哪个函数。

② 另外,所有的字符设备驱动都必须在使用前注册(register),在不使用时注销(unregister)
也就是说,我们的驱动程序要先告诉内核:
“我是谁(主设备号、次设备号)”,“我能做什么(操作函数)”。
只有注册成功,用户空间才能通过设备文件去访问我们这个设备。


⚙️ 三种模型的不同点

不同点主要有两方面:

使用的主设备号和次设备号不同

  • 杂项设备通常共用主设备号 10,次设备号由系统自动分配;

  • 标准字符设备和早期模型则需要自己申请主次设备号。

注册与注销的方法不同

  • 杂项设备用 misc_register()misc_deregister()

  • 早期模型用 register_chrdev()unregister_chrdev()

  • 标准模型则用一系列 cdev 相关函数:cdev_init()cdev_add()cdev_del() 等。


📚 补充说明

注意一点,注册字符设备驱动的本质工作,其实就是:
👉 向内核注册一个主设备号 + 次设备号 + 文件操作方法。
至于其他的一些信息,比如模块描述、作者、版本号之类的,更多是附属属性,属于辅助说明。

二、Linux文件操作函数

open —— 打开设备或创建文件

📘 头文件:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

📗 函数原型:

int open(const char *pathname, int flags, mode_t mode);

📙 功能:
以指定的方式打开或创建一个文件(包括设备文件)。

📒 参数说明:

  • pathname:文件路径,比如 /dev/led

  • flags:打开方式,比如 O_RDWRO_RDONLYO_WRONLYO_CREAT

  • mode:权限位(当创建文件时才有用,比如 0666

📘 返回值:
成功返回一个文件描述符(fd),失败返回 -1

📖 说明:
打开 /dev/xxx 时,内核其实会调用驱动中的 open() 函数。
比如:

int fd = open("/dev/led", O_RDWR);

就会进入我们驱动里定义的 .open = led_open 函数。


pread —— 从指定位置读取数据

📘 头文件:

#include <unistd.h>

📗 函数原型:

ssize_t pread(int fd, void *buf, size_t count, off_t offset);

📙 功能:
从文件中指定偏移量位置读取 count 个字节到缓冲区 buf

📒 参数说明:

  • fd:文件描述符

  • buf:用户缓存区地址

  • count:期望读取的字节数

  • offset:相对文件头的偏移位置

📘 返回值:
成功返回实际读取到的字节数,失败返回 -1

📖 讲解示例:
比如我们要从 EEPROM 某个地址读 16 字节:

pread(fd, buffer, 16, 0x20); // 从偏移0x20开始读16字节


pwrite —— 向指定位置写数据

📘 头文件:

#include <unistd.h>

📗 函数原型:

ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

📙 功能:
buf 中取出 count 个字节,写入文件中指定的偏移位置。

📒 参数说明:

  • fd:文件描述符

  • buf:用户空间要写入的数据缓冲区

  • count:要写入的字节数

  • offset:写入位置偏移量

📘 返回值:
成功返回写入的字节数,失败返回 -1

📖 讲解示例:
例如向 EEPROM 地址 0x40 写 8 字节数据:

pwrite(fd, buffer, 8, 0x40);


ioctl —— 控制设备行为的万能接口

📘 头文件:

#include <sys/ioctl.h>

📗 函数原型:

int ioctl(int fd, unsigned long request, ...);

📙 功能:
通过命令码控制设备执行特定的操作。
可以理解为 Linux 提供的一个“万能控制口”,让我们可以自定义命令让驱动执行各种任务。

📒 参数说明:

  • fd:设备对应的文件描述符

  • request:命令码(系统预定义或用户自定义)

  • ...:可选参数,根据命令不同传不同内容

📘 返回值:
成功返回 0 或正数,失败返回 -1。

📖 讲解示例:

#define LED_ON   0x01
#define LED_OFF  0x02
ioctl(fd, LED_ON);  // 打开LED

在驱动中会进入 .unlocked_ioctl = led_ioctl 函数,根据命令执行不同的操作。


poll —— 监控设备状态是否就绪

📘 头文件:

#include <poll.h>

📗 函数原型:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

📙 功能:
用来监控一个或多个文件描述符是否可读、可写或有异常。
在驱动开发中,常用于中断事件检测按键输入等待

📒 参数说明:

struct pollfd {int fd;        // 要监测的文件描述符short events;  // 期望的事件,例如 POLLIN(可读)、POLLOUT(可写)short revents; // 实际返回的事件
};

📘 返回值:

  • 0:有就绪的文件描述符数量

  • 0:超时

  • -1:出错

📖 讲解示例:

struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN;
int ret = poll(&fds, 1, 3000); // 等待3秒

如果设备可读,则返回1;否则超时。

三、杂项字符设备驱动模型

特征:

杂项字符设备(misc device)主要用于一些简单、轻量级的字符设备,比如传感器(DHT11)、LED 灯、蜂鸣器等。

特点如下:

1️⃣ 服务单一设备
杂项设备一般只对应一个具体的硬件设备(例如一个传感器或控制模块),结构简单,适合小型设备驱动。

2️⃣ 主设备号固定为 10
在 Linux 中,字符设备由“主设备号 + 次设备号”唯一标识。
主设备号用于标识驱动程序类型,而次设备号标识该驱动下的具体设备。
杂项设备驱动的主设备号是固定的 10

3️⃣ 次设备号范围:0~255
每注册一个杂项设备,会自动分配一个唯一的次设备号,用于区分不同的杂项设备实例。

4️⃣ 内核自动创建设备节点文件
驱动加载成功后,内核会自动在 /dev/ 目录下创建一个与 .name 同名的设备节点文件,例如 /dev/misc
这意味着开发者不再需要手动执行 mknod 命令,非常方便。

缺点:

虽然简单易用,但杂项设备模型也有一定局限性:

① 主设备号固定为 10,不可更改,灵活性较差。
② 可用的次设备号仅 256 个(0~255),资源有限。
③ 驱动加载时只能创建一个设备节点文件,无法同时服务多个设备实例
④ 节点由内核自动创建,应用层不便于统一管理 /dev 目录下的设备文件。

因此,杂项设备模型更适合“单一功能的小模块驱动”,不适合复杂设备系统。

核心数据结构:

在内核源码中,杂项设备的核心结构体为 struct miscdevice

#include <linux/miscdevice.h>struct miscdevice {int minor;                        // 次设备号(0~255),也可以设置为 MISC_DYNAMIC_MINOR 由系统自动分配const char *name;                 // 设备名,内核会自动在 /dev/ 下创建同名设备节点文件const struct file_operations *fops; // 指向文件操作方法结构体的指针struct list_head list;struct device *parent;struct device *this_device;const struct attribute_group **groups;const char *nodename;umode_t mode;
};

其中最关键的三个字段是:

  • minor:次设备号

  • name:设备节点文件名

  • fops:文件操作方法结构体(struct file_operations

其余的字段要么是:

  • 由内核自动维护的(如 listthis_device

  • 或是高级功能扩展(如 groupsparent

所以,普通驱动开发者 几乎不用管其他字段

关键 API函数:

misc_register()

用于注册杂项设备(misc device)

① 头文件

#include <linux/miscdevice.h>

② 函数原型

int misc_register(struct miscdevice *misc);

③ 参数说明

参数

类型

说明

misc

struct miscdevice *

指向杂项设备结构体的指针,必须事先填好 minornamefops

struct miscdevice —— 杂项字符设备核心结构体

头文件

#include <linux/miscdevice.h>

结构体原型

struct miscdevice {int minor;const char *name;const struct file_operations *fops;struct list_head list;struct device *parent;struct device *this_device;const char *nodename;umode_t mode;
};

成员字段详细说明

1)minor —— 次设备号

int minor;

含义:

  • 指定杂项设备使用的 次设备号

  • 主设备号固定为 10MISC_MAJOR = 10

  • 次设备号可以为:

    • MISC_DYNAMIC_MINOR(=255):表示动态自动分配

    • 固定的次设备号,例如:1、2、5 等

推荐使用:

.minor = MISC_DYNAMIC_MINOR

2)name —— 设备名

const char *name;

功能:

  • 决定设备节点名称 /dev/<name>

  • 必须唯一,否则注册失败

例如:

.name = "my_misc",

对应 /dev/my_misc


3)fops —— 文件操作结构体

const struct file_operations *fops;

说明:

  • 指向驱动的 file_operations 操作集合

  • 决定驱动的功能:

系统调用

对应驱动回调

open()

fops->open

read()

fops->read

write()

fops->write

ioctl()

fops->unlocked_ioctl

release()

fops->release

示例:

.fops = &misc_fops,

4)list —— 内核用于管理 misc 的链表节点

struct list_head list;

说明:

  • 驱动无需管理

  • 内核用于维护 /dev 下所有 misc 设备的链表


5)parent —— 父设备

struct device *parent;

说明:

  • 一般填 NULL

  • 如果挂接在一个更具体的设备下,可设置此字段


6)this_device —— 注册后生成的 device 对象指针

struct device *this_device;

说明:

  • 注册后由内核填写

  • 表示 /sys/class/misc/<name> 对应的设备对象

驱动不需要手动设置。


7)nodename —— 节点名称(可选)

const char *nodename;

功能:

  • 强制指定 /dev 下的节点名

  • 不建议使用,一般用 .name


8)mode —— 设备权限(可选)

umode_t mode;

例如:

.mode = 0666,

表示 /dev/<name> 可读可写。

struct file_operations —— 字符设备/杂项设备的操作方法集合

头文件

#include <linux/fs.h>

结构体原型

struct file_operations {struct module *owner;int (*open)(struct inode *, struct file *);ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);loff_t (*llseek)(struct file *, loff_t, int);long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);int (*release)(struct inode *, struct file *);
};

结构体成员

字段类型功能说明
ownerstruct module *通常填 THIS_MODULE,用于模块引用计数,防止模块被卸载。
openint (*)(struct inode *, struct file *)open("/dev/xxx") 时调用,设备打开。
readssize_t (*)(struct file *, char __user *, size_t, loff_t *)read() 调用,驱动向用户空间提供数据。
writessize_t (*)(struct file *, const char __user *, size_t, loff_t *)write() 调用,用户写数据到驱动。
llseekloff_t (*)(struct file *, loff_t, int)lseek() 调用,移动文件指针。
unlocked_ioctllong (*)(struct file *, unsigned int, unsigned long)ioctl() 调用,执行用户命令。
releaseint (*)(struct inode *, struct file *)close() 调用时执行,释放资源。

④ 返回值

返回值

含义

0

注册成功

<0(负值)

注册失败,通常为标准 Linux 错误码,如 -EBUSY-ENOMEM


⑤ 函数功能总结

misc_register() 是杂项设备的核心注册函数,其主要功能包括:

  1. 为设备分配次设备号(根据 misc->minor,可以动态分配)

  2. 将设备加入内核的 misc 设备链表

  3. 自动创建设备节点 /dev/<name>

  4. 为设备创建并初始化 struct device(即 misc->this_device

  5. 若设置了 .mode,设置设备节点权限

  6. 若设置了 .groups,创建 sysfs 属性

  7. 完成字符设备的注册工作,并使其可被用户空间访问

简单总结:自动完成字符设备驱动大部分繁琐的注册流程。

misc_deregister()

用于注销杂项设备

① 头文件

#include <linux/miscdevice.h>

② 函数原型

void misc_deregister(struct miscdevice *misc);

③ 参数说明

参数

类型

说明

misc

struct miscdevice *

要注销的杂项设备结构体指针(必须是之前注册成功的设备)


④ 返回值

返回值

含义

无返回值(void)

始终成功执行,不需要检查返回值


⑤ 函数功能总结

misc_deregister() 是注销杂项设备的函数,其主要功能包括:

  1. 从 misc 设备链表中移除设备

  2. 删除 /dev/<name> 设备节点

  3. 注销字符设备,回收系统资源

  4. 销毁与本设备关联的 this_device 设备模型对象

  5. 若有 sysfs 属性(由 .groups 定义),一并移除

简单总结:反向操作,撤销 misc_register() 做的所有工作。

杂项字符设备驱动模型示例代码

驱动代码misc_drv.c实现:

📝 杂项字符设备驱动代码完整流程总览
步骤功能代码位置/示例作用注意点
第一步引入头文件#include <linux/module.h>提供模块初始化/退出、文件操作结构体、杂项设备结构体、内核与用户空间安全拷贝接口忘记 <linux/uaccess.h> 会导致 copy_to_user / copy_from_user 报错;缺 <linux/miscdevice.h> 会找不到 miscdevice
第二步构建驱动模块框架misc_init() / misc_exit() + module_init/module_exit提供模块加载和卸载的入口与出口函数忘记 module_init / module_exit 会导致模块无法自动执行;MODULE_LICENSE 防止内核 taint
第三步构建杂项设备核心结构体并注册设备1. 定义操作函数 misc_open/read/write/release
2. 定义 file_operations misc_fops
3. 定义 miscdevice misc_dev
4. 在入口函数 misc_init() 调用 misc_register(&misc_dev)
注册设备到内核,使内核创建 /dev/misc_demo 并绑定文件操作接口操作函数必须在 file_operations 之前定义;file_operations 必须在 miscdevice 之前定义;copy_to_user / copy_from_user 防止内核访问用户空间出错
第四步注销杂项设备misc_exit() 调用 misc_deregister(&misc_dev)卸载模块时移除设备节点 /dev/misc_demo 并释放内核资源必须与 misc_register() 配对使用;确保模块卸载时资源释放干净
①引入头文件
#include <linux/module.h>       // 模块初始化/退出、模块信息
#include <linux/init.h>         // __init / __exit 修饰符
#include <linux/fs.h>           // 文件操作结构体 file_operations
#include <linux/miscdevice.h>   // 杂项设备核心结构体与注册接口
#include <linux/uaccess.h>      // 提供内核与用户空间安全数据拷贝相关的函数和宏

每个头文件的作用详解

1. <linux/module.h>

  • 提供模块加载与卸载接口(module_init、module_exit)

  • 提供模块信息声明宏(MODULE_LICENSE 等)

👉 没有它,驱动模块无法正常加载到内核中。


2. <linux/init.h>

  • 定义 __init__exit 修饰符

  • 用于明确入口函数和退出函数在内核内存中的位置与释放机制

👉 驱动的入口函数和出口函数必须用它的修饰符。


3. <linux/fs.h>

  • 定义 struct file_operations

  • 包含 open/read/write/ioctl 等文件操作接口原型

👉 字符设备一定要用 file_operations,所以这个必须包含。


4. <linux/miscdevice.h>

  • 杂项设备的核心头文件

  • 提供 struct miscdevice 定义

  • 提供 misc_register()misc_deregister() 接口

👉 不包含这个头文件就无法使用杂项设备模型。


5. <linux/uaccess.h>

  • 提供用户空间与内核空间之间安全拷贝的函数:

    • copy_to_user()

    • copy_from_user()

  • 防止越界访问导致内核崩溃

👉 所有 read/write 中的数据传输都需要它。

②构建杂项设备驱动框架
#include <linux/module.h>       // 模块初始化/退出、模块信息
#include <linux/init.h>         // __init / __exit 修饰符
#include <linux/fs.h>           // 文件操作结构体 file_operations
#include <linux/miscdevice.h>   // 杂项设备核心结构体与注册接口
#include <linux/uaccess.h>      // 提供内核与用户空间安全数据拷贝相关的函数和宏//==================== 模块入口 ====================//
static int __init misc_init(void) {return 0;
}//==================== 模块出口 ====================//
static void __exit misc_exit(void) {}module_init(misc_init);
module_exit(misc_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Liao");
MODULE_DESCRIPTION("Misc device demo");

1.实现入口函数(module init)

static int __init misc_init(void) {return 0;
}

作用:

  • 模块加载时执行,类似“驱动的构造函数”

  • 后面会在这里加入:

    • misc_register() 注册杂项设备

    • 也可以创建资源、初始化变量

目前只是一个空框架,表示正常加载。

2.实现出口函数(module exit)

static void __exit misc_exit(void) {}

作用:

  • 模块卸载时执行,类似“驱动的析构函数”

  • 后面会在这里加入:

    • misc_deregister() 注销杂项设备

    • 释放资源、删除节点

当前为空框架,表示正常卸载。

3.注册入口和出口函数

module_init(misc_init);
module_exit(misc_exit);

将这两个函数注册给内核,让 insmod/rmmod 可以自动调用。

  • 内核模块加载时自动执行 misc_init()

  • 模块卸载时自动执行 misc_exit()

4.模块元信息声明

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Liao");
MODULE_DESCRIPTION("Misc device demo");

作用:

  • 声明模块许可(GPL)

  • 作者信息

  • 模块描述信息

  • 保证模块加载时不被内核 taint,同时便于维护和调试

③构建杂项设备核心结构体并注册设备
#include <linux/module.h>       // 模块初始化/退出、模块信息
#include <linux/init.h>         // __init / __exit 修饰符
#include <linux/fs.h>           // 文件操作结构体 file_operations
#include <linux/miscdevice.h>   // 杂项设备核心结构体与注册接口
#include <linux/uaccess.h>      // 提供内核与用户空间安全数据拷贝相关的函数和宏static char kernel_buf[64] = "Hello from kernel!";//==================== 文件操作函数 ====================//
static int misc_open(struct inode *inode, struct file *file) {printk("misc: device opened\n");return 0;
}static ssize_t misc_read(struct file *file, char __user *buf,size_t size, loff_t *offset) {printk("misc: read invoked\n");return copy_to_user(buf, kernel_buf, strlen(kernel_buf));
}static ssize_t misc_write(struct file *file, const char __user *buf,size_t size, loff_t *offset) {printk("misc: write invoked\n");memset(kernel_buf, 0, sizeof(kernel_buf));copy_from_user(kernel_buf, buf, size);printk("misc: kernel_buf = %s\n", kernel_buf);return size;
}static int misc_release(struct inode *inode, struct file *file) {printk("misc: device closed\n");return 0;
}//==================== 文件操作结构体 ====================//
static struct file_operations misc_fops = {.owner   = THIS_MODULE,.open    = misc_open,.read    = misc_read,.write   = misc_write,.release = misc_release,
};//==================== 杂项设备结构体 ====================//
static struct miscdevice misc_dev = {.minor = 255,       // 固定次设备号.name  = "misc_demo",  // 自动生成 /dev/misc_demo.fops  = &misc_fops,
};//==================== 模块入口 ====================//
static int __init misc_init(void) {int ret = misc_register(&misc_dev);if (ret)printk("misc device register failed!\n");elseprintk("misc device register success!\n");return ret;
}//==================== 模块出口 ====================//
static void __exit misc_exit(void) {}module_init(misc_init);
module_exit(misc_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Liao");
MODULE_DESCRIPTION("Misc device demo");

1️⃣ 模块入口注册杂项字符设备

static int __init misc_init(void) {int ret = misc_register(&misc_dev);if (ret)printk("misc device register failed!\n");elseprintk("misc device register success!\n");return ret;
}

✔ 作用

  • 将杂项设备注册到内核

  • 内核自动创建设备节点 /dev/misc_demo

  • 后续用户空间可通过 open/read/write 访问设备

✔ 注意点

  • misc_register() 必须在 miscdevice 结构体定义之后调用

  • 返回值非 0 表示注册失败,需要打印日志或处理

2️⃣ 定义杂项设备核心结构体 miscdevice

static struct miscdevice misc_dev = {.minor = 255,          // 固定次设备号.name  = "misc_demo",  // 设备节点名称.fops  = &misc_fops,   // 文件操作集合
};

✔ 作用

  • minor:指定次设备号,可固定或用 MISC_DYNAMIC_MINOR 自动分配

  • name:决定生成的 /dev/ 节点名称

  • fops:指向文件操作集合,实现设备操作接口

✔ 注意点

  • miscdevice 必须在入口函数调用 misc_register 之前定义

  • fops 指针必须指向已定义的 file_operations 结构体


3️⃣ 定义文件操作集合 file_operations

static struct file_operations misc_fops = {.owner   = THIS_MODULE,.open    = misc_open,.read    = misc_read,.write   = misc_write,.release = misc_release,
};

✔ 作用

  • 描述驱动对用户空间操作的接口映射

  • 内核在用户调用 open/read/write/close 时会查找此结构体执行相应函数

✔ 注意点

  • owner = THIS_MODULE 用于防止模块被卸载

  • 成员函数必须在此之前定义或至少声明


4️⃣ 文件操作函数

static int misc_open(...) { ... }
static ssize_t misc_read(...) { ... }
static ssize_t misc_write(...) { ... }
static int misc_release(...) { ... }
//具体代码看完整版misc_drv.c

✔ 作用

  • misc_open:设备打开时执行

  • misc_read:用户 read 时,将内核缓冲区拷贝到用户空间

  • misc_write:用户 write 时,将数据拷贝到内核缓冲区

  • misc_release:设备关闭时执行

✔ 注意点

  • 使用 copy_to_user / copy_from_user 进行内核/用户空间数据安全传输

  • 不要直接访问用户空间指针,否则可能导致内核崩溃

④注销杂项字符设备

调用 misc_deregister(),确保驱动卸载时正确删除杂项设备节点并释放系统资源。

static void __exit misc_exit(void) {misc_deregister(&misc_dev);          // 注销杂项设备printk("misc device deregister success!\n");
}

✔ 作用

  • misc_deregister() 将设备从内核的杂项设备链表中移除

  • 自动删除对应的 /dev/misc_demo 设备节点

  • 释放内核资源,防止模块卸载后留下“死设备”


✔ 运行时效果

  • 当使用 rmmod 卸载模块时,misc_exit() 被自动执行

  • 设备节点 /dev/misc_demo 被删除

  • 内核日志中输出:

misc device deregister success!
  • 用户空间无法再访问该设备


✔ 注意点

  • 必须在模块卸载时调用,否则内核中仍保留设备信息

  • 注销顺序必须在模块资源释放前完成

  • misc_register() 配对使用,确保驱动加载与卸载一致

⑤完整驱动代码misc_drv.c
#include <linux/module.h>       // 模块初始化/退出、模块信息
#include <linux/init.h>         // __init / __exit 修饰符
#include <linux/fs.h>           // 文件操作结构体 file_operations
#include <linux/miscdevice.h>   // 杂项设备核心结构体与注册接口
#include <linux/uaccess.h>      // 提供内核与用户空间安全数据拷贝相关的函数和宏static char kernel_buf[64] = "Hello from kernel!";//==================== 文件操作函数 ====================//
static int misc_open(struct inode *inode, struct file *file) {printk("misc: device opened\n");return 0;
}static ssize_t misc_read(struct file *file, char __user *buf,size_t size, loff_t *offset) {printk("misc: read invoked\n");return copy_to_user(buf, kernel_buf, strlen(kernel_buf));
}static ssize_t misc_write(struct file *file, const char __user *buf,size_t size, loff_t *offset) {printk("misc: write invoked\n");memset(kernel_buf, 0, sizeof(kernel_buf));copy_from_user(kernel_buf, buf, size);printk("misc: kernel_buf = %s\n", kernel_buf);return size;
}static int misc_release(struct inode *inode, struct file *file) {printk("misc: device closed\n");return 0;
}//==================== 文件操作结构体 ====================//
static struct file_operations misc_fops = {.owner   = THIS_MODULE,.open    = misc_open,.read    = misc_read,.write   = misc_write,.release = misc_release,
};//==================== 杂项设备结构体 ====================//
static struct miscdevice misc_dev = {.minor = 255,       // 固定次设备号.name  = "misc_demo",  // 自动生成 /dev/misc_demo.fops  = &misc_fops,
};//==================== 模块入口 ====================//
static int __init misc_init(void) {int ret = misc_register(&misc_dev);if (ret)printk("misc device register failed!\n");elseprintk("misc device register success!\n");return ret;return 0;
}//==================== 模块出口 ====================//
static void __exit misc_exit(void) {misc_deregister(&misc_dev);printk("misc device deregister success!\n");
}module_init(misc_init);
module_exit(misc_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Liao");
MODULE_DESCRIPTION("Misc device demo");

应用代码app.c实现:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main(void) {int fd;char buf[64] = {0};fd = open("/dev/misc_demo", O_RDWR);if (fd < 0) {perror("open failed");return -1;}printf("open success, fd=%d\n", fd);// 读取内核消息read(fd, buf, sizeof(buf));printf("read from driver: %s\n", buf);// 写入数据write(fd, "Hello kernel!", 13);close(fd);return 0;
}
  • open()

    • 打开设备节点 /dev/misc_demo

    • 成功返回文件描述符 fd

    • 触发驱动的 misc_open()

  • read()

    • 从内核缓冲区读取数据到用户空间

    • 触发驱动的 misc_read()

    • 内核日志打印 misc: read invoked

    • 数据被拷贝到 buf

  • write()

    • 将用户空间数据写入内核缓冲区

    • 触发驱动的 misc_write()

    • 内核缓冲区 kernel_buf 更新为 "Hello kernel!"

    • 内核打印更新后的数据

  • close()

    • 关闭设备

    • 触发驱动的 misc_release()

程序执行流程:

用户空间程序│ open("/dev/misc_demo")▼
驱动 misc_open()│
read(fd) ------------------> misc_read() -> copy_to_user() -> buf
write(fd, "Hello kernel!") --> misc_write() -> copy_from_user() -> kernel_buf│
close(fd) -----------------> misc_release()

Makefile编译杂项设备驱动和应用程序:

obj-m += misc_drv.o# 内核源码目录(根据你的环境修改)
KDIR ?= /home/liao/rk3399/nanopc-t4/kernel-rockchip
PWD := $(shell pwd)all:make -C $(KDIR) M=$(PWD) modulesaarch64-linux-gnu-gcc app.c -o appclean:make -C $(KDIR) M=$(PWD) cleanrm -f app

1️⃣ obj-m += misc_drv.o

  • 作用:告诉内核构建系统,这里有一个模块 misc_drv.o 需要编译为内核模块(.ko)

  • 注意点misc_drv.c 必须在当前目录存在


2️⃣ KDIR 和 PWD

KDIR ?= /home/liao/rk3399/nanopc-t4/kernel-rockchip
PWD := $(shell pwd)
  • KDIR:指向你的内核源码路径

  • PWD:当前目录路径

  • 用途:内核模块编译时使用 make -C $(KDIR) M=$(PWD) 将模块源码交给内核构建系统


3️⃣ all 目标

all:make -C $(KDIR) M=$(PWD) modulesaarch64-linux-gnu-gcc app.c -o app
  • make -C $(KDIR) M=$(PWD) modules

    • 调用内核的 Make 系统编译模块

    • 生成 misc_drv.ko

  • aarch64-linux-gnu-gcc app.c -o app

    • 编译用户空间应用程序 app.c

    • 生成可执行文件 app


4️⃣ clean 目标

clean:make -C $(KDIR) M=$(PWD) cleanrm -f app
  • 清理模块生成的中间文件和 .ko

  • 删除应用程序可执行文件

  • 保持源码目录干净


操作流程:

① 编译驱动和应用程序
make

成功后会生成:

misc_drv.ko
app

② 加载驱动模块

查看内核日志:

③ 查看自动创建设备节点
④ 运行应用程序

若出现报错没有权限

-bash: ./app: Permission denied

执行:

chmod +x app

添加可执行权限。

查看内核打印:

⑤ 卸载模块

四、早期经典字符设备驱动模型

特征:

一个驱动可管理多个同类字符设备(群驱动)

注册一次驱动(一个主设备号)即可管理最多 256 个次设备节点,适合同类设备的统一管理。
例如:

  • 多个鼠标

  • 多个 LED 灯

  • 多个数模转换器等

应用层可创建多个设备节点,例如:

/dev/mouse0  
/dev/mouse1  
...


② 没有独立的核心数据结构(原始模型)

不同于:

  • miscdevice(杂项设备)

  • cdev(现代模型)

早期模型没有统一结构体,只依赖 file_operations


③ 主设备号范围有限(0~255,不含 10)

  • 主设备号用于标识驱动本身

  • 内核将主设备号 10 保留用于杂项设备


④次设备号范围:0~255(一次性占满)

注册成功后,驱动占用 256 个次设备号全部区间
因此同一个主设备号下可以管理最多 256 个设备节点。


⑤内核不会自动在 /dev 下创建设备节点

必须由用户手动创建:

mknod /dev/xxx c 主设备号 次设备号

⭐ 补充:驱动代码可分层

典型的 Linux 驱动体系:

  • 主控(controller)驱动:管理总线、设备发现

  • 子设备(slave)驱动:控制具体硬件设备

早期字符设备驱动同样可这样拆分。

缺点:

❌(1)没有核心数据结构

不如 cdev 或 misc 规范化 → 可维护性差

❌(2)主设备号、次设备号数量有限

随着设备数增多,容易冲突,无法满足现代系统需求。

手动创建字符设备节点:

命令格式:

mknod /dev/设备名 c 主设备号 次设备号

参数解释:

参数说明
设备名任意命名,与驱动名无关
c字符设备
主设备号必须与 register_chrdev 返回值一致
次设备号0~255 任意可用一次设备号

示例:

mknod /dev/early0 c 240 0

驱动本身不会创建节点,必须依靠用户或自动创建机制。

关键 API函数:


register_chrdev() —— 注册字符设备驱动


① 头文件

#include <linux/fs.h>

② 函数原型

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);

③ 参数说明

参数名类型说明
majorunsigned int要申请的主设备号。若为 0:让内核自动分配一个可用主设备号。
nameconst char*字符设备名称,显示在 /proc/devices 中,与 /dev/xxx 节点无直接关系。
fopsstruct file_operations *指向已经定义好的驱动操作方法集合,如 open/read/write/release 等。

④ 返回值

返回值含义
> 0成功,返回主设备号
< 0失败,返回负错误码

⑤ 功能总结

向内核注册一个字符设备,使其获得主设备号,并由 file_operations 与系统层建立绑定关系,成为正式的字符设备驱动。注册后可在 /proc/devices 看到设备名称。


unregister_chrdev() —— 注销字符设备


① 头文件

#include <linux/fs.h>

② 函数原型

void unregister_chrdev(unsigned int major, const char *name);

③ 参数说明

参数名类型说明
majorunsigned int之前 register_chrdev() 获得的主设备号
nameconst char*设备名称,必须与注册时的 name 一致

④ 返回值

无返回值

⑤ 功能总结

注销由 register_chrdev() 注册的字符设备,释放主设备号,使其从 /proc/devices 中消失。


class_create() —— 创建一个类(class)


① 头文件

#include <linux/device.h>

② 函数原型

struct class *class_create(struct module *owner, const char *name);

③ 函数参数

参数名

类型

说明

owner

struct module *

一般写 THIS_MODULE;表示属于哪个模块

name

const char *

类名称,将出现在 /sys/class/<name>/

④ 返回值

返回值

含义

struct class*

指向新创建的 class

ERR_PTR(err)

失败,需要使用 IS_ERR() 判断

⑤ 功能总结

创建一个系统类,并在 /sys/class/<name>/ 生成对应目录。
这是 Linux 字符设备自动创建设备节点的第一步,必须与 device_create() 配合使用。


device_create() —— 自动创建设备节点 /dev/xxx


① 头文件

#include <linux/device.h>

② 函数原型

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

③ 函数参数

参数名

类型

说明

class

struct class *

class_create() 的返回值,用于指定设备属于哪个类

parent

struct device *

父设备,通常填 NULL

devt

dev_t

设备号,由 MKDEV(major, minor) 生成

drvdata

void *

驱动私有数据,用不到一般填 NULL

fmt

const char *

设备节点的名称格式(例如 "mydev"),节点将自动创建为:
/dev/mydev

...

可变参数

配合 fmt 用,可忽略

④ 返回值

返回值

含义

struct device*

成功 返回设备结构体指针

ERR_PTR(err)

失败

⑤ 功能总结

根据设备号 devt 自动创建 /dev/xxx 设备节点
无需 mknod,是现代 Linux 驱动创建设备节点的主要方式。


class_destroy() —— 删除类


① 头文件

#include <linux/device.h>

② 函数原型

void class_destroy(struct class *cls);

③ 参数

参数名

类型

说明

cls

struct class *

class_create() 的返回值

④ 返回值

无返回值

⑤ 功能总结

删除 class 对象,从 /sys/class/xxx/ 中移除对应目录。


device_destroy() —— 删除 /dev/xxx 节点


① 头文件

#include <linux/device.h>

② 函数原型

void device_destroy(struct class *class, dev_t devt);

③ 参数(详细)

参数名

类型

说明

class

struct class *

class_create() 的返回值

devt

dev_t

设备号,一般 MKDEV(major, minor)

④ 返回值

⑤ 功能总结

删除通过 device_create() 创建的设备节点 /dev/xxx,同时移除 sysfs 下对应的设备信息。

早期经典字符设备驱动模型示例代码:

手动添加设备节点版本(mknod 手动创建 /dev 节点)

驱动代码(early_chr.c)— 手动 mknod 版本
// early_chr.c —— 早期经典字符设备驱动(手动 mknod 版)#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>//===================== 文件操作方法 =====================//static int early_open(struct inode *inode, struct file *file)
{printk("early_open() 被调用\n");return 0;
}static ssize_t early_read(struct file *file, char __user *buf,size_t size, loff_t *offset)
{printk("early_read() 被调用\n");return 0;
}static ssize_t early_write(struct file *file, const char __user *buf,size_t size, loff_t *offset)
{printk("early_write() 被调用\n");return size;  // 假装写成功
}static int early_release(struct inode *inode, struct file *file)
{printk("early_release() 被调用\n");return 0;
}//===================== 文件操作结构体 =====================//static struct file_operations early_fops = {.owner   = THIS_MODULE,.open    = early_open,.read    = early_read,.write   = early_write,.release = early_release,
};//===================== 全局变量 =====================//
static int major = 0;   // 主设备号(0 表示自动分配)//===================== 入口函数 =====================//static int __init early_init(void)
{// 注册字符设备驱动major = register_chrdev(0, "early_chr", &early_fops);if (major < 0) {printk("register_chrdev 失败!\n");return major;}printk("早期字符设备驱动注册成功!主设备号 = %d\n", major);printk("请手动创建设备节点:  mknod /dev/early_chr c %d 0\n", major);return 0;
}//===================== 出口函数 =====================//static void __exit early_exit(void)
{unregister_chrdev(major, "early_chr");printk("early_chr 驱动已卸载\n");
}module_init(early_init);
module_exit(early_exit);
MODULE_LICENSE("GPL");
应用层测试程序(app.c)
// app.c — 用户层测试程序#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd;char buf[10] = {0};// 打开设备文件fd = open("/dev/early_chr", O_RDWR);if (fd < 0) {printf("open /dev/early_chr 失败!\n");return -1;}printf("打开成功,fd = %d\n", fd);read(fd, buf, 1);write(fd, "A", 1);close(fd);return 0;
}
Makefile(驱动 + 应用一键编译)
obj-m += early_chr.o# 内核源码路径
KDIR ?= /home/liao/rk3399/nanopc-t4/kernel-rockchip   # ←根据你的系统修改 !!!PWD := $(shell pwd)all:make -C $(KDIR) M=$(PWD) modulesaarch64-linux-gnu-gcc app.c -o appclean:make -C $(KDIR) M=$(PWD) cleanrm -f app
 操作流程:
① 编译驱动和应用程序

在 Ubuntu 下执行:

make

成功后会生成:

  • early_chr.ko(驱动)

  • app(应用程序)


②安装驱动模块

查看主设备号:

③手动创建设备节点

查看自动创建设备节点:

④运行测试程序查看 dmesg:
⑤卸载驱动

自动创建设备节点版本

驱动代码(early_chr_auto.c)— 自动创建设备节点
// early_chr_auto.c — 自动创建设备节点版本(class_create + device_create)#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>static int major = 0;
static struct class *early_class;
static struct device *early_device;/************************ 文件操作方法 ************************/static int early_open(struct inode *inode, struct file *file)
{printk("early_open() 调用\n");return 0;
}static ssize_t early_read(struct file *file, char __user *buf,size_t size, loff_t *offset)
{printk("early_read() 调用\n");return 0;
}static ssize_t early_write(struct file *file, const char __user *buf,size_t size, loff_t *offset)
{printk("early_write() 调用\n");return size;
}static int early_release(struct inode *inode, struct file *file)
{printk("early_release() 调用\n");return 0;
}/************************ 文件操作结构体 ************************/static struct file_operations early_fops = {.owner   = THIS_MODULE,.open    = early_open,.read    = early_read,.write   = early_write,.release = early_release,
};/************************ 模块入口函数 ************************/static int __init early_init(void)
{// 注册字符设备,自动分配主设备号major = register_chrdev(0, "early_chr_auto", &early_fops);if (major < 0) {printk("register_chrdev 失败\n");return major;}printk("早期字符设备驱动(自动节点)注册成功,主设备号 = %d\n", major);// 创建设备类 /sys/class/early_chr_class/early_class = class_create(THIS_MODULE, "early_chr_class");if (early_class != NULL) {printk("成功创建设备类 /sys/class/early_chr_class/\n");}// 创建设备节点 /dev/early_chr_autoearly_device = device_create(early_class, NULL,MKDEV(major, 0),NULL,"early_chr_auto");if (early_device != NULL) {class_destroy(early_class);printk("成功创建设备节点 /dev/early_chr_auto\n");}return 0;
}/************************ 模块出口函数 ************************/static void __exit early_exit(void)
{device_destroy(early_class, MKDEV(major, 0));class_destroy(early_class);unregister_chrdev(major, "early_chr_auto");printk("early_chr_auto 驱动已卸载\n");
}module_init(early_init);
module_exit(early_exit);
MODULE_LICENSE("GPL");
应用程序(app.c)

与手动节点版相同,只是设备名字不同:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd;char c;fd = open("/dev/early_chr_auto", O_RDWR);if (fd < 0) {printf("open /dev/early_chr_auto 失败!\n");return -1;}printf("打开成功 fd=%d\n", fd);read(fd, &c, 1);write(fd, "A", 1);close(fd);return 0;
}
Makefile
obj-m += early_chr_auto.oKDIR ?= /home/liao/rk3399/nanopc-t4/kernel-rockchip   # ←根据你的系统修改 !!!
PWD := $(shell pwd)all:make -C $(KDIR) M=$(PWD) modulesaarch64-linux-gnu-gcc app.c -o appclean:make -C $(KDIR) M=$(PWD) cleanrm -f app

操作流程参考上述例程。

五、Linux2.6标准字符设备模型

Linux 2.6 开始,字符设备驱动的注册方式全面升级,引入了 cdev 核心结构体 + 动态设备号机制。这是目前 Linux 内核最标准、最规范、最推荐的字符设备驱动模型。

特征:

标准字符设备模型与之前两个模型(杂项模型、早期经典模型)相比,具有以下重要特征:


① 一个驱动可以服务多个设备(批量设备)

  • 一个驱动可一次性注册多个连续的设备号,例如:

    • 驱动有能力管理 10000 个鼠标

    • 主设备号固定为一个

    • 次设备号连续分配 0~9999

这使得驱动非常适合管理 大量相同类型的设备


② 使用 dev_t(32 位)描述设备号

dev_t 本质为 u32,被拆分为:

位宽

含义

高 12 位

主设备号(0~4095,不包含 10——杂项设备保留)

低 20 位

次设备号(0~1,048,575≈1M)

因此:

  • 主设备号范围更大

  • 次设备号从原来的 0~255 扩展到 0~1M,更能容纳大量设备


③ 次设备号必须连续分配(不可分散)

标准字符设备注册(无论动态/静态)都要求:

  • 仅能注册一段连续区间的次设备号

  • 不能分散注册多个离散的次设备号段

如果要注册多个离散段,需要多次 register/alloc,但每段必须连续。


④ 驱动加载后不会自动创建 /dev 节点

与杂项设备不同:

  • 内核不会自动创建 /dev 设备文件

  • 由用户层通过 mknod 手动创建:

例如:

sudo mknod /dev/demo c 240 0

当然,在较新内核中,可使用 class_create + device_create 自动创建节点,不过本模型强调的是“标准 cdev 驱动模型”,它本质上不负责自动创建设备文件。

核心数据结构:

内核头文件:

#include <linux/cdev.h>


struct cdev — 标准字符设备的核心结构体

struct cdev {struct kobject kobj;       // 内核对象,用于 sysfs 管理,内核自动维护struct module *owner;      // 指向本驱动模块const struct file_operations *ops; // 文件操作方法struct list_head list;     // 内核链表,自动维护dev_t dev;                 // 起始设备号(包含主次设备号)unsigned int count;        // 该驱动占用的连续设备号个数
};

这里必须由驱动程序填充的成员:

  1. .ops

  2. .dev

  3. .count

结构体本身由开发者维护,其他复杂结构由内核自动处理。

关键API函数:

cdev_alloc()------在堆空间动态申请一个struct cdev对象空间

①头文件

#include <linux/cdev.h>

②函数原型

struct cdev *cdev_alloc(void);

③函数参数

④返回值

返回值含义
struct cdev *返回动态申请的 cdev 对象指针
NULL申请失败(可能内存不足)
struct cdev 结构体 ------标准字符设备的核心结构体
struct cdev {struct kobject kobj;            // 表示内核设备对象,提供 sysfs 支持struct module *owner;           // 拥有此 cdev 的模块(一般是 THIS_MODULE)const struct file_operations *ops; // 文件操作方法struct list_head list;          // cdev 链表dev_t dev;                      // 设备号(包含主+次设备号)unsigned int count;             // 次设备号数量
};

1. struct kobject kobj

  • 作用:

    • 把 cdev 纳入 Linux 核心对象系统(kobject、kset 等)

    • 让字符设备可以在 sysfs 中出现,如:

      /sys/dev/char/<major>:<minor>/
      
    • 提供引用计数(防止设备被错误释放)

📌 简单理解:
kobject 是内核万物之父,所有设备对象都需要它。


2. struct module *owner

  • 常见使用方式:

cdev->owner = THIS_MODULE;
  • 用途:

    • 避免在设备还在使用时卸载模块

    • 若用户打开 /dev/xxx,内核会增加此模块引用计数

    • 文件关闭后才允许卸载

📌 没有 owner,模块可能在使用途中被卸载,导致内核崩溃(Oops)。


3. const struct file_operations *ops

这是字符设备的灵魂,用户对设备的所有操作都要从这里进去。

典型结构:

struct file_operations {int (*open)(...);ssize_t (*read)(...);ssize_t (*write)(...);int (*release)(...);long (*unlocked_ioctl)(...);...
};

;

📌 ops 把 cdev 连接到驱动操作函数。

没有 ops,字符设备就是一个“空壳”。


4. struct list_head list

  • 在系统中可能存在多个 cdev,内核使用链表管理它们

  • 这个字段使 cdev 支持多个次设备号(count > 1)


5. dev_t dev

保存字符设备的设备号:

MKDEV(major, minor)

包含:

  • 主设备号(major)

  • 次设备号(minor)

例如:

dev_t dev = MKDEV(240, 0);

这是驱动在 /dev 中的唯一标识。


6. unsigned int count

表示 cdev 所涵盖的连续次设备号数量

例如:

register_chrdev_region(MKDEV(240, 0), 3, "mydev");

则你可以创建:

  • 240:0

  • 240:1

  • 240:2

count = 3

📌 若 count 是 1,则是单一设备号。

⑤函数功能

cdev_alloc() 的作用是:在堆空间动态申请并初始化一个 struct cdev 对象。

//内核源码实现(简化理解):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;
}

具体做了哪些事:

  1. 使用 kzalloc 申请 cdev 结构体内存并清零

  2. 初始化内部链表(INIT_LIST_HEAD)

  3. 初始化 kobject(用于 sysfs 管理)

  4. 返回可直接使用的 cdev 对象

该 cdev 对象接下来通常需要:

  • 设置 ops(文件操作方法)

  • 调用 cdev_add() 加入内核

  • 卸载时调用 cdev_del() 并手动 kfree()

register_chrdev_region()------静态注册一个指定范围的设备号

①头文件

#include <linux/fs.h>

②函数原型

int register_chrdev_region(dev_t from, unsigned count, const char *name);

③函数参数

参数类型说明
fromdev_t起始设备号(包含主设备号 + 次设备号)。由 MKDEV(major, minor) 构造
countunsigned请求的连续设备号数量
nameconst char*设备或驱动名称,会显示在 /proc/devices

④返回值

返回值含义
0注册成功
<0注册失败(常见:-EBUSY,表示设备号被占用)

⑤函数功能

register_chrdev_region() 用于静态注册一个指定范围的设备号。

即:

  • 你自己指定主设备号与起始次设备号

  • 内核验证该设备号区间是否空闲

  • 空闲则注册,否则失败

适用于 你明确知道要使用的主设备号 的情况:

  • 厂家要求固定主设备号

  • 驱动文档规定主设备号

  • 某些商业驱动规定固定 major

  • 需要和老版本驱动兼容

⚠️ 如果不知道该用哪个主设备号,不要用这个 API,应使用 alloc_chrdev_region()

⑥示例:

dev_t devno;
int ret;/* 指定主设备号 200,次设备号从 0 开始,连续申请 2 个设备号 */
devno = MKDEV(200, 0);ret = register_chrdev_region(devno, 2, "mydriver");
if (ret < 0) {printk("register_chrdev_region failed!\n");return ret;
}printk("register success: major=%d minor=%d\n",MAJOR(devno), MINOR(devno));

常见错误与注意事项

Ⅰ 主设备号不能为 10
主设备号 10 由 杂项设备模型 exclusively 使用。

设备号冲突(-EBUSY)
若主设备号已被其他驱动占用,会注册失败。

Ⅲ count 必须为连续数量
不能申请离散设备号,只能申请一个区间。

alloc_chrdev_region()------自动分配主设备号,并注册一段连续的字符设备号。

①头文件

#include <linux/fs.h>

②函数原型

int alloc_chrdev_region(dev_t *dev, unsigned baseminor,unsigned count, const char *name);

③函数参数

参数类型说明
devdev_t*输出参数,用于存放分配得到的起始设备号(主设备号 & 次设备号)
baseminorunsigned起始次设备号,一般设为 0
countunsigned申请的连续设备号数量
nameconst char*驱动名称,用于 /proc/devices 中显示

④返回值

返回值含义
0注册成功
<0注册失败,通常为 -EBUSY 或 -EINVAL

⑤函数功能

alloc_chrdev_region() 的作用:自动分配主设备号,并注册一段连续的字符设备号。

它完成两件重要的事:


内核自动分配主设备号

无需指定主设备号,也不用担心与系统已有驱动冲突。

例如:

dev_t dev;
alloc_chrdev_region(&dev, 0, 2, "mydev");

假设系统分配的主设备号为 245,则:

dev = MKDEV(245, 0)


为驱动预留 count 个连续的次设备号

如果 count=2,则系统同时分配:

主设备号

次设备号

245

0

245

1

 将结果保存到 dev 中

可以通过宏解析主次设备号:

int major = MAJOR(dev);
int minor = MINOR(dev);

适用于:

  • 标准字符设备驱动模型(Linux 2.6+)强烈推荐使用

  • 当你不确定一个主设备号是否被占用时使用

  • 当驱动中需要多个设备节点时(如 led0、led1)

⑥示例:

dev_t devno;
int ret;/* 申请 1 个设备号,起始次设备号 0 */
ret = alloc_chrdev_region(&devno, 0, 1, "led");
if (ret < 0) {printk("alloc_chrdev_region failed\n");return ret;
}printk("major = %d, minor = %d\n", MAJOR(devno), MINOR(devno));

cdev_init()------初始化一个 struct cdev 结构体,并将其与 file_operations 绑定

①头文件

#include <linux/cdev.h>

②函数原型

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

③函数参数

参数类型说明
cdevstruct cdev *指向要初始化的 cdev 结构体对象
fopsconst struct file_operations *指向文件操作集(open/read/write/ioctl 等)

④返回值

返回值含义
无返回值该函数永远不会失败,但必须保证 cdev 指针有效

⑤函数功能

cdev_init() 用于初始化一个 struct cdev 结构体,并将其与 file_operations 绑定。

主要功能:

  1. cdev->ops = fops

  2. 设置 owner 字段为 THIS_MODULE(在 cdev_add 时完成)

  3. 初始化 cdev 的内部链表

  4. 为后续的 cdev_add() 做准备

❗注意:cdev_init 不会分配内存,你必须确保 cdev 已经存在(静态或动态分配)。

使用场景:

  • 当你已经分配好了一个 struct cdev(通常是静态或使用 kmalloc 申请)

  • 在调用 cdev_add() 注册前,必须先进行初始化

  • 旧式字符设备模型的核心步骤之一

⑥示例

① 静态分配方式:

static struct cdev my_cdev;cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;cdev_add(&my_cdev, devno, 1);


② 动态分配方式:

struct cdev *cdev;cdev = cdev_alloc();  // 申请结构体空间
cdev_init(cdev, &my_fops);
cdev->owner = THIS_MODULE;cdev_add(cdev, devno, 1);

cdev_add()------将初始化后的 cdev 正式注册到内核

①头文件

#include <linux/cdev.h>

②函数原型

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

③函数参数

参数类型说明
cdevstruct cdev *已初始化的 cdev 对象(必须先 cdev_init)
devdev_t起始设备号(包含主+次设备号)
countunsigned int需要注册的连续设备号数量

④返回值

返回值含义
0注册成功
< 0注册失败(例如:设备号冲突、cdev 指针无效等)

⑤函数功能

cdev_add() 用于将初始化后的 cdev 正式注册到内核,使其成为可用的字符设备。

其主要功能包括:

  1. 注册 cdev 对象到内核

  2. 绑定设备号 dev(主次设备号)

  3. 启用该字符设备,让用户空间程序可以 open/read/write

  4. 内核会在 /proc/devices 中显示该设备

这是字符设备驱动真正生效的关键步骤
没有 cdev_add(),驱动等于没注册。

⑥示例:

1)静态 cdev 使用示例

static struct cdev my_cdev;dev_t dev_no;  // 已经通过 register/alloc_chrdev_region 获得的设备号cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;if (cdev_add(&my_cdev, dev_no, 1) < 0) {printk("cdev_add error\n");
}

2)动态 cdev 使用示例

struct cdev *cdev;cdev = cdev_alloc();
cdev_init(cdev, &my_fops);
cdev->owner = THIS_MODULE;if (cdev_add(cdev, dev_no, 2) < 0) {printk("cdev_add failed\n");
}

⑦注意事项

必须先 cdev_init() 再 cdev_add(),顺序不能反
否则内核会崩溃或 oops。

正确顺序必须是:

① 分配设备号   register_chrdev_region / alloc_chrdev_region
② 分配 cdev     (静态 or cdev_alloc)
③ 初始化 cdev    cdev_init()
④ 添加 cdev      cdev_add()

卸载驱动时:

① cdev_del()
② unregister_chrdev_region()

设备号 dev 必须是有效且已申请的
否则会返回 -EINVAL / -EBUSY。

count 表示连续设备号数量
例如 count=2 表示注册 dev、dev+1 两个设备号。

⚠ 设置 owner 非常重要:

cdev->owner = THIS_MODULE;

否则模块可能被正在使用时卸载,导致 crash。


cdev_del()------将已经注册到内核的 cdev 从内核中注销

①头文件

#include <linux/cdev.h>

②函数原型

void cdev_del(struct cdev *cdev);

③函数参数

参数类型说明
cdevstruct cdev *需要从内核注销的 cdev 对象指针(之前已通过 cdev_add 注册)

④返回值

返回值含义
无(void)此函数没有返回值

虽然没有返回值,但如果传入无效指针,内核可能产生 OOPS,因此需要确保 cdev 有效。

⑤函数功能

cdev_del() 用于将已经注册到内核的 cdev 从内核中注销。

具体功能:

  1. 将 cdev 从内核字符设备表中删除

  2. 释放 cdev 对应的内核资源

  3. 使设备不可再被用户空间访问

  4. 和 cdev_add() 相反,是卸载驱动时必须执行的清理动作

必须在驱动退出函数中调用,否则会出现设备号资源泄漏。

⑥示例:

● 驱动入口(注册字符设备)

static struct cdev my_cdev;
dev_t dev_no;alloc_chrdev_region(&dev_no, 0, 1, "mydev");cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;cdev_add(&my_cdev, dev_no, 1);

● 驱动退出(注销字符设备)

cdev_del(&my_cdev);
unregister_chrdev_region(dev_no, 1);

⑦注意事项

  • ✔ 必须确保 cdev_add() 已经成功执行过,才能调用 cdev_del()

  • ✔ 通常只删除一次,否则可能造成内核崩溃

  • ✔ 如果 cdev 是通过 cdev_alloc() 动态申请的,需要在 cdev_del() 后手动 kfree(cdev)

unregister_chrdev_region()------注销之前注册的一段设备号区域

①头文件

#include <linux/fs.h>

②函数原型

void unregister_chrdev_region(dev_t from, unsigned int count);

③函数参数

参数类型说明
fromdev_t起始设备号(主设备号 + 次设备号)
countunsigned int需要注销的连续设备号数量

④返回值

⑤函数功能

unregister_chrdev_region() 用于注销之前注册的一段设备号区域。

具体功能:

  1. 释放之前申请的设备号(无论是 alloc_chrdev_region 还是 register_chrdev_region 获得的)

  2. 将设备号从内核设备表中移除

  3. 必须在驱动退出函数中调用,否则设备号会泄漏

⑥示例:

● 驱动入口(申请设备号)

dev_t dev_no;
alloc_chrdev_region(&dev_no, 0, 2, "mydev");

● 驱动退出(释放设备号)

unregister_chrdev_region(dev_no, 2);

常见错误及注意事项

错误原因
不调用 unregister_chrdev_region设备号泄漏,导致下次加载驱动失败
count 填写错误部分设备号未释放,导致冲突
from 不是申请得到的设备号会释放错误区域,引发不可预测后果

http://www.dtcms.com/a/617792.html

相关文章:

  • C++ List 容器详解:迭代器失效、排序与高效操作
  • 婚纱网站wordpress微商模板
  • GPT问答:泛型、哈希表与缓存、命名参数。251116
  • 免费学软件的自学网站保健品网站建设流程
  • 网络访问流程:HTTPS + TCP + IP
  • 智能体AI、技术浪潮与冲浪哲学
  • 基于 PyTorch + BERT 意图识别与模型微调
  • 沃尔沃公司网站建设微信官方网站建设
  • 网站备案域名怎么买找在农村适合的代加工
  • 42 解决一些问题
  • Claude Code 功能+技巧
  • 基于人脸识别和 MySQL 的考勤管理系统实现
  • AUTOSAR_CP_OS-Operating System for Multi-Core:多核操作系统
  • 什么是 “信任模型” 和 “安全假设”?
  • 【秣厉科技】LabVIEW工具包——HIKRobot(海康机器人系列)
  • 网易UU远程全功能技术解构:游戏级性能突围与安全边界探析
  • 蓝桥杯第八届省赛单片机设计完全入门(零基础保姆级教程)
  • 搭建网站分类建立名词
  • 没有域名的网站wordpress占用资源
  • RPA+AI双剑合璧!小红书商品笔记自动发布,效率提升2000%[特殊字符]
  • 19.传输层协议UDP
  • linux服务-rsync+inotify文件同步-rsync
  • 机器学习之ravel()的作用
  • Wi-Fi 7路由器性能分析:从传输速率到多设备协同全面解析
  • 【Java手搓RAGFlow】-1- 环境准备
  • 审计部绩效考核关键指标与综合评估方法
  • Photoshop - Photoshop 工具栏(29)钢笔工具
  • 营销型网站策划方案大德通众包做网站怎么样
  • 使用 Web Workers 提升前端性能:让 JavaScript 不再阻塞 UI
  • HTTP与HTTPS深度解析:从明文传输到安全通信