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

linux驱动-字符设备

目录

一、概念

核心特点

典型应用场景

与块设备的区别

二、代码实现

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系统中一种以字符为单位进行数据传输的设备类型,与块设备(以固定大小的数据块为单位)相对。字符设备通常用于需要逐字节或非结构化数据流传输的场景,例如键盘、鼠标、串口、终端等。

核心特点

  1. 按字节访问:数据以字符流形式传输,不支持随机访问(如直接跳转到指定位置)。
  2. 无缓存机制:数据通常直接传输,不经过系统缓冲区(少数例外可通过设置实现)。
  3. 实时性高:适用于对延迟敏感的设备,如传感器或交互式输入设备。

典型应用场景

  1. 输入设备:键盘、鼠标、触摸屏。
  2. 输出设备:串口终端、打印机。
  3. 虚拟设备/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_t

1.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

使用步骤

  1. 自己选一个未被占用的主设备号(查看/proc/devices
  2. MKDEV(指定主号, 起始次号)生成 dev_t
  3. 调用 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、创建设备容器

函数作用

  1. /sys/class/下创建一个分类文件夹,用来归类同一类型的设备;
  2. 生成struct class结构体,是device_create必须依赖的父容器;
  3. 向内核设备模型注册设备分类,为后续自动生成/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 指针才能调用。

函数作用

  1. 在上面创建好的 class 分类下,新建一个具体设备;
  2. 内核发送 uevent 事件给用户空间udev/mdev
  3. udev 收到消息后自动执行类似mknod /dev/xxx c 主号 次号,生成设备文件;
  4. /sys/class/my_class/下生成对应设备的属性目录,存放设备信息。
struct device *dev = device_create(cls, NULL, devno, NULL, "mydev");

执行后两处产物:

  1. /dev/mydev应用程序操作的设备节点
  2. /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) clean

insmod 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#

http://www.jsqmd.com/news/1082481/

相关文章:

  • 中文主题建模开源工具链实战:从清洗到可汇报报告
  • 山西酒店快装包工包料
  • HS-PEG-Silane 合成副产物产生机理与实操规避方案
  • AI音乐作品怎么发行
  • 技术解析:如何通过秒传脚本实现百度网盘文件的永久分享
  • ETS2LA终极指南:如何在欧洲卡车模拟2中实现智能自动驾驶
  • Deep3D终极指南:如何用AI将普通2D视频变成立体3D大片?
  • 第 39 篇:数据存储——MongoDB 数据库
  • 5分钟掌握URLFinder:终极网页链接提取与敏感信息检测完整指南
  • 没有公网IP如何连接PostgreSQL?CentOS部署与远程访问指南
  • MinIO集群安全漏洞CVE-2023-28432深度剖析:从信息泄露到JWT认证修复
  • 智能家居联动控制管理系统
  • CLP-SNN:基于脉冲神经网络的持续学习算法与Loihi 2实现
  • 番茄小说下载器:用Rust构建的智能电子书获取工具
  • 任意金额支付漏洞深度剖析:从原理到修复的完整攻防指南
  • Visual C++ Redistributable AIO:一键解决Windows运行库问题的终极方案
  • MetaboAnalystR 4.0终极指南:构建高效代谢组学分析工作流
  • idea安装完插件要是一半都是被禁用看看是不是刚安装完右下角有个排序什么什么的问题。
  • 如何通过DLSS Swapper轻松管理游戏DLSS版本:新手完整指南
  • Adobe破解工具终极指南:三步免费解锁专业设计软件
  • 知识产权贯标是什么?有什么好处?
  • PDF 转 Markdown 这件事,MinerU 做到了 69K Stars 的水平
  • 树莓派安全加固实战:从系统更新到入侵防御的完整指南
  • 图p-能量:从谱理论到3-能量下界证明的非线性推广
  • 计算机毕业设计之果蔬仓库管理系统
  • 【信息科学与工程学】计算机科学与自动化——第二十篇 计算机体系架构 系列三 计算机体系结构01 ISA设计、流水线、超标量、缓存一致性、SIMD/GPU、乱序执行、CPU 设计、GPU设计、性能优化
  • 网盘直链下载助手:一键获取真实下载地址,告别限速烦恼
  • 5步掌握B站大会员视频下载神器:bilibili-downloader完全指南
  • 插板阀真空度稳定控制技术:阀门与真空泵的协同工作
  • linux常用快捷键