目录
一、概念
核心特点
典型应用场景
与块设备的区别
二、代码实现
1、设备号
1.1 组成:主设备号 + 次设备号
1.2 设备号两种分配方式:静态分配、动态分配
1.2.1 静态分配(手动指定固定主设备号)
1.2.2 动态分配(推荐,内核自动分配空闲主号)
2、stuct cdev (字符设备核心结构体)
2.1 结构体原型(内核源码简化版)
2.2 配套核心函数(固定配对流程)
2.2.1 cdev_init
2.2.2 cdev_add
2.2.3. cdev_del
2.3 标准使用流程
3、创建设备容器
函数作用
4、创建单个设备实例,触发自动生成 /dev 节点
函数作用
只调用 device_create 不先 class_create?
三、完整实现
一、概念
字符设备是Linux系统中一种以字符为单位进行数据传输的设备类型,与块设备(以固定大小的数据块为单位)相对。字符设备通常用于需要逐字节或非结构化数据流传输的场景,例如键盘、鼠标、串口、终端等。
核心特点
- 按字节访问:数据以字符流形式传输,不支持随机访问(如直接跳转到指定位置)。
- 无缓存机制:数据通常直接传输,不经过系统缓冲区(少数例外可通过设置实现)。
- 实时性高:适用于对延迟敏感的设备,如传感器或交互式输入设备。
典型应用场景
- 输入设备:键盘、鼠标、触摸屏。
- 输出设备:串口终端、打印机。
- 虚拟设备:
/dev/null、/dev/random等特殊文件。
与块设备的区别
| 特性 | 字符设备 | 块设备 |
|---|---|---|
| 数据传输单位 | 字节(字符流) | 固定大小的数据块(如512B) |
| 访问方式 | 顺序访问 | 支持随机访问 |
| 缓存机制 | 通常无缓存 | 通常带缓存 |
| 典型设备 | 串口、终端 | 硬盘、SSD |
二、代码实现
1、设备号
1.1 组成:主设备号 + 次设备号
内核中每个字符设备唯一标识 = 设备号 dev_t
dev_t 是 32 位无符号整数:
- 高 12 位:主设备号 major
- 低 20 位:次设备号 minor
宏操作(内核代码)
MAJOR(dev_t dev); // 提取主设备号 unsigned int MINOR(dev_t dev); // 提取次设备号 unsigned int MKDEV(maj, min); // 拼接主次生成dev_t dev_t1.2 设备号两种分配方式:静态分配、动态分配
1.2.1 静态分配(手动指定固定主设备号)
核心api
// 拼接主次号 dev_t MKDEV(unsigned int maj, unsigned int min); int register_chrdev_region(dev_t from, unsigned count, const char *name);参数说明:
from:起始设备号(用 MKDEV 拼接好)count:连续占用多少个次设备name:驱动名,存到 /proc/devices
使用步骤
- 自己选一个未被占用的主设备号(查看
/proc/devices) MKDEV(指定主号, 起始次号)生成 dev_t- 调用 register_chrdev_region 注册
#define MY_MAJ 200 dev_t devno = MKDEV(MY_MAJ, 0); // 占用次设备0,共1个设备 register_chrdev_region(devno, 1, "static_dev");释放接口
void unregister_chrdev_region(dev_t from, unsigned count);优缺点
- 优点:设备号固定,不用 mknod 每次换号;
- 缺点:容易和系统已有驱动主设备号冲突,加载失败,嵌入式不推荐。
1.2.2 动态分配(推荐,内核自动分配空闲主号)
核心 api
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);参数说明:
dev:输出参数,内核回填分配好的起始 dev_t;baseminor:起始次设备号,一般填 0;count:连续次设备数量;name:驱动名称。
返回值:成功 0,失败负错误码。
使用步骤
dev_t devno; // 自动分配主设备,次设备从0开始,1个设备 int ret = alloc_chrdev_region(&devno, 0, 1, "auto_dev"); if(ret < 0) return ret; // 提取打印主次号 printk("major:%d minor:%d", MAJOR(devno), MINOR(devno));释放同样用
unregister_chrdev_region(devno, 1);优缺点
- 优点:不会冲突,不用手动查空闲主设备号,通用驱动首选;
- 缺点:每次开机加载主设备号可能变化,搭配
device_create自动生成 /dev 节点可规避该问题。
2、stuct cdev (字符设备核心结构体)
2.1 结构体原型(内核源码简化版)
struct cdev { struct kobject kobj; // 内核对象,sysfs 驱动管理 const struct file_operations *ops; // 绑定读写open/read/write接口 struct module *owner; // 所属模块 THIS_MODULE dev_t dev; // 该设备对应的完整设备号 unsigned int count; // 占用次设备数量 };2.2 配套核心函数(固定配对流程)
2.2.1 cdev_init
void cdev_init(struct cdev *cdev, const struct file_operations *fops);功能:只做结构体初始化,把file_operations函数集绑定到cdev->ops
2.2.2 cdev_add
int cdev_add(struct cdev *p, dev_t dev, unsigned count);功能:把 cdev 注册进内核,系统能识别该字符设备
返回值:成功返回 0;失败负数错误码
2.2.3. cdev_del
void cdev_del(struct cdev *p);功能:从内核注销 cdev
2.3 标准使用流程
// 1. 定义全局cdev对象 struct cdev mycdev; dev_t devno; // 初始化文件操作集 struct file_operations my_fops = { .open = dev_open, .read = dev_read, .write = dev_write, .release = dev_release, }; static int __init drv_init(void) { // 分配设备号 alloc_chrdev_region(&devno, 0, 1, "mydev"); // 2. 初始化cdev,绑定fops cdev_init(&mycdev, &my_fops); mycdev.owner = THIS_MODULE; // 标记所属模块,防卸载崩溃 // 3. 注册cdev到内核 cdev_add(&mycdev, devno, 1); return 0; } static void __exit drv_exit(void) { // 注销cdev cdev_del(&mycdev); // 释放设备号 unregister_chrdev_region(devno, 1); }3、创建设备容器
函数作用
- 在
/sys/class/下创建一个分类文件夹,用来归类同一类型的设备; - 生成
struct class结构体,是device_create必须依赖的父容器; - 向内核设备模型注册设备分类,为后续自动生成
/dev节点做前置准备。
版本区分
// Linux 5.x / 6.2及更早 struct class *cls = class_create(THIS_MODULE, "my_class"); // Linux 6.3+ struct class *cls = class_create("my_class");执行后生成目录:
/sys/class/my_class/
销毁配对
class_destroy(cls);4、创建单个设备实例,触发自动生成 /dev 节点
必须依赖 class_create 返回的 class 指针才能调用。
函数作用
- 在上面创建好的 class 分类下,新建一个具体设备;
- 内核发送 uevent 事件给用户空间
udev/mdev; - udev 收到消息后自动执行类似
mknod /dev/xxx c 主号 次号,生成设备文件; - 在
/sys/class/my_class/下生成对应设备的属性目录,存放设备信息。
struct device *dev = device_create(cls, NULL, devno, NULL, "mydev");执行后两处产物:
/dev/mydev应用程序操作的设备节点/sys/class/my_class/mydev/设备 sysfs 目录
销毁配对
device_destroy(cls, devno);只调用 device_create 不先 class_create?
编译直接报错,缺少struct class参数,无法运行。
三、完整实现
char_demo.c
#include <linux/init.h> #include <linux/module.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/uaccess.h> // 1. 自定义参数 #define DEV_MAJOR 230 #define DEV_MINOR 0 #define DEV_COUNT 1 #define DEV_NAME "mychar" // 2. 全局字符设备结构体 static struct cdev char_dev; static struct class *dev_class; static char buf[128] = "char device test data"; // read:用户层read()触发,把内核数据拷贝到用户空间 static ssize_t char_read(struct file *file, char __user *ubuf, size_t size, loff_t *off) { int ret; // 如果偏移超过缓冲区总长度,返回0,cat读到0就停止循环 if (*off >= sizeof(buf)) return 0; // 计算本次能读多少:剩余字节 和 用户传入size 取小值 size_t read_len = min(size, sizeof(buf) - *off); ret = copy_to_user(ubuf, buf + *off, read_len); if (ret != 0) return -EFAULT; // 关键:更新文件偏移,光标向后移动 *off += read_len; return read_len; } // write:用户层write()触发,用户数据拷贝进内核 static ssize_t char_write(struct file *file, const char __user *ubuf, size_t size, loff_t *off) { int ret; ret = copy_from_user(buf, ubuf, size); if(ret != 0) return -EFAULT; return size; } static int char_open(struct inode *inode, struct file *file) { printk(KERN_INFO "char dev open\n"); return 0; } static int char_release(struct inode *inode, struct file *file) { printk(KERN_INFO "char dev close\n"); return 0; } // 绑定操作函数 static struct file_operations char_fops = { .owner = THIS_MODULE, .open = char_open, .read = char_read, .write = char_write, .release = char_release, }; // 模块加载入口 static int __init char_dev_init(void) { dev_t devno = MKDEV(DEV_MAJOR, DEV_MINOR); int ret; // 1. 注册设备号 ret = register_chrdev_region(devno, DEV_COUNT, DEV_NAME); if(ret < 0){ printk("register dev fail\n"); return ret; } // 2. 初始化cdev,绑定操作集 cdev_init(&char_dev, &char_fops); // 3. 添加cdev到内核 ret = cdev_add(&char_dev, devno, DEV_COUNT); if(ret < 0){ unregister_chrdev_region(devno, DEV_COUNT); return ret; } // 4. 创建/class 新版Linux6.x class_create 只传名字 dev_class = class_create("char_class"); if (IS_ERR(dev_class)) { ret = PTR_ERR(dev_class); cdev_del(&char_dev); unregister_chrdev_region(devno, DEV_COUNT); return ret; } // 5. 生成/dev设备节点 device_create(dev_class, NULL, devno, NULL, DEV_NAME); printk("char device init ok /dev/%s\n", DEV_NAME); return 0; } // 模块卸载入口 static void __exit char_dev_exit(void) { dev_t devno = MKDEV(DEV_MAJOR, DEV_MINOR); // 销毁设备节点、类 device_destroy(dev_class, devno); class_destroy(dev_class); // 删除cdev、注销设备号 cdev_del(&char_dev); unregister_chrdev_region(devno, DEV_COUNT); printk("char device exit\n"); } module_init(char_dev_init); module_exit(char_dev_exit); MODULE_LICENSE("GPL");Makefile
obj-m += char_demo.o KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) cleaninsmod char_demo.ko
root@1:/sys/class/char_class/mychar# ls
dev power subsystem uevent
root@1:/sys/class/char_class/mychar# cat uevent
MAJOR=230 主设备号
MINOR=0 次设备号
DEVNAME=mychar 设备名root@1:/dev# xxd mychar
00000000: 6368 6172 2064 6576 6963 6520 7465 7374 char device test
00000010: 2064 6174 6100 0000 0000 0000 0000 0000 data...........
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
root@1:/dev#