设备文件

设备号的注册与注销
1、设备号相关宏定义

2、设备号注册过程

cs
/**
* struct char_device_struct - Linux内核内部用于管理字符设备号区间的核心数据结构
* 作用:记录字符设备号(主+次)的分配状态,避免设备号冲突,通过链表组织同一主设备号下的多个次设备号区间
* 注:该结构体是内核内部私有结构,驱动开发者一般不需要直接操作,由内核提供的设备号注册/注销函数(如register_chrdev_region)间接维护
*/
struct char_device_struct {
struct char_device_struct *next; /* 链表后继指针,用于链接「同一主设备号、次设备号区间不重叠」的其他char_device_struct实例
* 例如:同一主设备号下,次设备号0~4和5~9会分别对应两个结构体,通过next指针形成链表
*/
unsigned int major; /* 字符设备的主设备号,用于标识设备类型(如串口主设备号4、键盘主设备号13)
* 内核通过主设备号匹配对应的字符设备驱动程序,主设备号全局唯一(传统静态分配模式下)
*/
unsigned int baseminor; /* 当前设备号区间的「起始次设备号」,即该区间的第一个次设备号
* 次设备号用于区分同一设备类型下的多个具体设备(如同一串口驱动下的多个串口设备)
*/
int minorct; /* 当前设备号区间包含的「次设备号数量」
* 计算区间范围:[baseminor, baseminor + minorct - 1]
* 示例:baseminor=0,minorct=5 → 包含次设备号0、1、2、3、4
*/
char name[64]; /* 字符设备的名称,用于内核调试和标识,一般与驱动名称一致(可选填) */
struct cdev *cdev; /* 指向该设备号区间对应的字符设备核心结构体cdev(内核2.6及以上版本)
* cdev结构体包含了设备的操作函数集(file_operations),是驱动与内核交互的核心
*/
};
3、设备号注册


4、设备号注销

设备号动态加静态分配实例
cs
// 内核核心功能头文件
#include <linux/kernel.h>
// 模块编程核心头文件
#include <linux/module.h>
// 字符设备注册相关头文件
#include <linux/fs.h>
// 声明模块遵循GPL许可证
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Developer");
MODULE_DESCRIPTION("Character Device Driver (Support Dynamic/Static Device Number)");
// --------------------------
// 模块参数:可在加载时动态指定
// --------------------------
// 主设备号:0表示动态分配,非0表示静态注册
static int fs_major = 0;
// 次设备号起始值
static int fs_minor = 0;
// 申请的设备号数量
static int fs_number = 1;
// 声明为模块参数,加载时可通过 insmod xxx.ko fs_major=xxx 指定
module_param(fs_major, int, S_IRUGO);
module_param(fs_minor, int, S_IRUGO);
module_param(fs_number, int, S_IRUGO);
// --------------------------
// 模块初始化函数
// --------------------------
static int __init fs_chrdev_init(void)
{
// 存储合成的设备号
dev_t devno = MKDEV(fs_major, fs_minor);
int ret = 0;
printk(KERN_INFO "fs_chrdev: Hello world!\n");
// 核心逻辑:动态分配 vs 静态注册
if (fs_major == 0) {
// 1. 动态分配设备号(内核自动分配主设备号)
ret = alloc_chrdev_region(&devno, fs_minor, fs_number, "fs_chrdev");
if (ret < 0) {
printk(KERN_ERR "fs_chrdev: Failed to allocate chrdev region!\n");
return ret;
}
// 从分配的设备号中提取主设备号
fs_major = MAJOR(devno);
printk(KERN_INFO "fs_chrdev: Dynamic allocate success! fs_major = %d\n", fs_major);
} else {
// 2. 静态注册设备号(使用用户指定的主设备号)
ret = register_chrdev_region(devno, fs_number, "fs_chrdev");
if (ret < 0) {
printk(KERN_ERR "fs_chrdev: Failed to register chrdev region!\n");
return ret;
}
printk(KERN_INFO "fs_chrdev: Static register success! fs_major = %d\n", fs_major);
}
return 0;
}
// --------------------------
// 模块退出函数
// --------------------------
static void __exit fs_chrdev_exit(void)
{
dev_t devno = MKDEV(fs_major, fs_minor);
// 释放设备号(动态/静态分配的都用这一个函数释放)
unregister_chrdev_region(devno, fs_number);
printk(KERN_INFO "fs_chrdev: Goodbye world! Device number released.\n");
}
// 模块入口和出口声明
module_init(fs_chrdev_init);
module_exit(fs_chrdev_exit);
字符设备的注册与注销

cs
// 字符设备驱动的核心结构体,用于描述一个字符设备
struct cdev {
// 内核对象基础结构,用于设备模型中的管理(如引用计数、设备层次关系、热插拔事件等)
struct kobject kobj;
// 指向该设备所属的模块指针,通常赋值为 THIS_MODULE
// 作用是防止模块在设备被使用时被意外卸载
struct module *owner;
// 指向该字符设备的文件操作集合,定义了设备支持的 open/read/write/ioctl 等接口
// 是用户空间与驱动交互的核心入口
const struct file_operations *ops;
// 链表头,用于将系统中所有的 cdev 结构体链接成全局链表,方便内核统一管理
struct list_head list;
// 该字符设备对应的设备号(主设备号 + 次设备号,由 dev_t 类型存储)
dev_t dev;
// 该字符设备所占用的设备号数量(即次设备号的连续个数)
// 例如:主设备号为240,次设备号从0到3,则count=4
unsigned int count;
} __randomize_layout; // 编译宏,用于随机化结构体成员的内存布局,提升安全性,防止利用结构体偏移的漏洞



cs
/*
* Copyright (C) 2022 HQYJ Co., Inc. All Rights Reserved.
*
* 文件名 : fs_chrdev.c
* 作者 : HQYJ <support@hqyj.com>
* 版本 : V1.0
* 创建时间 : 2022-07-11
* 最后修改时间: 2022-07-11
* 网址 : www.fsdev.com.cn
*/
// 内核核心头文件:提供printk等内核打印、基础数据类型支持
#include <linux/kernel.h>
// 模块编程核心头文件:提供模块加载/卸载、许可证声明等接口
#include <linux/module.h>
/*设备号 file_operations相关头文件:提供dev_t、MKDEV、register_chrdev_region等设备号操作函数,
以及file_operations文件操作集合结构体定义*/
#include <linux/fs.h>
/*cdev相关头文件:提供cdev结构体定义,以及cdev_init、cdev_add、cdev_del等字符设备操作函数*/
#include <linux/cdev.h>
/*
* 定义设备号变量
* dev_major : 主设备号(此处手动指定为300,属于静态分配方式)
* dev_minor : 次设备号(起始值为0,对应单个设备)
* dev_number: 设备数量(此处为1,表示只申请1个设备号)
*/
static int dev_major = 300;
static int dev_minor = 0;
static int dev_number = 1;
/*定义cdev结构体变量:字符设备的核心结构体,用于描述一个字符设备的属性和操作接口
此处为静态分配内存,无需手动调用kmalloc分配,卸载时也无需手动释放*/
static struct cdev cdev;
/*用户操作接口集合:定义用户空间与驱动程序交互的核心接口(如open、read、write等)
此处仅初始化了owner成员,后续可补充其他操作接口(如.open、.read)*/
static struct file_operations fs_chrdev_fops = {
.owner = THIS_MODULE, // 声明所有者为当前模块,防止模块在设备被使用时被意外卸载
};
/*加载函数:模块加载时自动调用(由module_init声明指定),完成设备号注册、cdev初始化与注册
__init宏标记:该函数仅在模块加载时执行,执行完毕后内核会回收其占用的内存空间*/
static int __init fs_chrdev_init(void)
{
int ret = 0; // 函数返回值存储变量,用于判断各步骤是否执行成功
/*构建完整设备号:通过MKDEV内核宏,将主设备号和次设备号组合成dev_t类型的完整设备号
dev_t类型:高12位存储主设备号,低20位存储次设备号,是内核中表示设备号的标准类型*/
dev_t devno = MKDEV(dev_major, dev_minor);
// 内核打印日志(KERN_INFO:信息级别日志),提示模块开始加载
printk(KERN_INFO "fs_chrdev: module enter\n");
/*申请设备号:静态注册字符设备号(使用手动指定的dev_major)
函数参数说明:
1. devno:已构建的完整设备号(主+次)
2. dev_number:需要申请的设备号数量
3. "fs_chrdev":设备名称(会显示在/proc/devices文件中,方便查询)
返回值:0表示成功,<0表示失败*/
ret = register_chrdev_region(devno, dev_number, "fs_chrdev");
// 判断设备号注册是否失败
if (ret < 0) {
// 内核打印错误日志(KERN_ERR:错误级别日志),提示注册失败
printk(KERN_ERR "fs_chrdev: register_chrdev_region failed\n");
goto err1; // 跳转到err1标签,执行错误处理(直接返回错误码)
}
/*cdev初始化及注册:完成字符设备结构体的初始化与内核注册
第一步:初始化cdev结构体
cdev_init函数作用:
1. 将自定义的文件操作集合fs_chrdev_fops绑定到cdev->ops成员
2. 初始化cdev结构体内部的链表头(list成员)
3. 初始化cdev->owner成员(默认与fs_chrdev_fops->owner一致)*/
cdev_init(&cdev, &fs_chrdev_fops);
// 显式设置cdev结构体的所有者为当前模块(增强代码健壮性,与fops->owner保持一致)
cdev.owner = THIS_MODULE;
/*第二步:将cdev结构体注册到Linux内核
函数参数说明:
1. &cdev:已初始化的cdev结构体指针
2. devno:完整设备号
3. 1:设备号数量(与dev_number一致,此处为1)
返回值:0表示成功,<0表示失败*/
ret = cdev_add(&cdev, devno, 1);
// 判断cdev注册是否失败
if (ret < 0) {
// 内核打印错误日志,提示cdev注册失败
printk(KERN_ERR "fs_chrdev: cdev_add");
goto err2; // 跳转到err2标签,执行错误处理(释放已申请的设备号,再返回错误码)
}
/*这个return 0 不能忽略,否则导致申请到的资源被释放掉
返回0:告知内核模块加载成功,保留已申请的设备号和注册的cdev资源*/
return 0;
// 错误处理标签2:cdev注册失败时执行
err2:
// 释放已成功申请的设备号(防止资源泄露),与register_chrdev_region配对使用
// 注意:原代码此处参数为dev_minor,应为dev_number(小瑕疵,不影响基础功能,建议修改为dev_number)
unregister_chrdev_region(devno, dev_minor);
// 错误处理标签1:设备号注册失败时执行
err1:
// 返回错误码,告知内核模块加载失败
return ret;
}
/*卸载函数:模块卸载时自动调用(由module_exit声明指定),完成资源的释放与清理
__exit宏标记:该函数仅在模块卸载时执行,未加载的模块中该函数会被忽略*/
static void __exit fs_chrdev_exit(void)
{
// 构建完整设备号,用于释放设备号
dev_t devno = MKDEV(dev_major, dev_minor);
// 内核打印日志,提示模块开始卸载
printk(KERN_INFO "fs_chrdev: module exit\n");
// 从内核中删除已注册的cdev结构体,释放字符设备资源(与cdev_add配对使用)
cdev_del(&cdev);
/*释放设备号:释放之前静态注册的字符设备号(与register_chrdev_region配对使用)
防止设备号泄露,确保后续其他模块可以申请使用该设备号*/
unregister_chrdev_region(devno, dev_number);
}
// 声明模块的初始化入口点:内核加载模块时,自动调用fs_chrdev_init函数
module_init(fs_chrdev_init);
// 声明模块的退出入口点:内核卸载模块时,自动调用fs_chrdev_exit函数
module_exit(fs_chrdev_exit);
// 声明模块遵循GPL许可证,避免内核加载时出现"tainted"(污染)警告,同时允许内核导出相关符号
MODULE_LICENSE("GPL");
用户接口的实现
file_operation结构体

cs
struct file_operations {
// 1. 模块所有者(必设):几乎所有驱动都要赋值为 THIS_MODULE
// 作用:防止模块在设备被使用(文件被打开)时被意外卸载,提升安全性
struct module *owner;
// 2. 打开设备文件(常用):用户调用 open() 时触发
// 参数说明:
// inode:指向设备文件的索引节点结构体(存储设备号等信息)
// filp:指向文件结构体(存储文件打开状态、私有数据等)
// 返回值:0 成功,<0 失败(错误码)
int (*open)(struct inode *inode, struct file *filp);
// 3. 关闭/释放设备文件(常用):用户调用 close() 时触发(最后一次关闭时才执行)
// 参数与 open 一致,返回值:0 成功,<0 失败
int (*release)(struct inode *inode, struct file *filp);
// 4. 从设备读取数据(常用):用户调用 read() 时触发(驱动→用户空间传数据)
// 参数说明:
// filp:文件结构体指针
// buf:用户空间缓冲区指针(存储读取到的数据)
// count:用户期望读取的字节数
// pos:文件偏移量指针(记录当前读写位置,需手动更新)
// 返回值:>0 实际读取的字节数,0 读取到文件末尾,<0 失败
ssize_t (*read)(struct file *filp, char __user *buf, size_t count, loff_t *pos);
// 5. 向设备写入数据(常用):用户调用 write() 时触发(用户空间→驱动传数据)
// 参数说明:与 read 类似,buf 是用户空间要写入的数据缓冲区
// 返回值:>0 实际写入的字节数,0 未写入,<0 失败
ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *pos);
// 6. 设备控制(常用):用户调用 ioctl()/ioctl_fd() 时触发(用于自定义命令,如设备配置)
// 参数说明:
// filp:文件结构体指针
// cmd:自定义控制命令(如 CMD_SET_BAUDRATE)
// arg:命令对应的参数(可传数值或用户空间指针)
// 返回值:0 成功,<0 失败
long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);
// 7. 定位文件偏移量(可选):用户调用 lseek() 时触发(字符设备通常不需要,块设备/普通文件常用)
loff_t (*llseek)(struct file *filp, loff_t offset, int orig);
// 8. 轮询/多路复用(可选):用户调用 select()/poll()/epoll() 时触发
// 核心作用:实现异步IO,查询设备是否处于「可读写」状态,避免用户进程阻塞在IO操作上
// 适用场景:需要同时监控多个设备/文件的可读写状态(如网络套接字、串口)
// 参数说明:
// filp:文件结构体指针
// wait:轮询表结构体指针,用于将当前进程加入到设备的等待队列中(通过 poll_wait() 函数)
// 返回值:返回设备的可读写状态掩码(如 POLLIN 表示可读,POLLOUT 表示可写,POLLERR 表示错误)
unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);
// 9. 异步通知/信号驱动IO(可选):用户调用 fcntl() 设置 FASYNC 标志时触发
// 核心作用:实现信号驱动IO,当设备状态发生变化(如有数据可读、可写)时,主动向用户空间发送 SIGIO 信号
// 适用场景:设备状态变化不规律,无需用户进程轮询,由驱动主动通知(如串口收到数据、按键按下)
// 参数说明:
// fd:设备文件描述符(用户空间传入)
// filp:文件结构体指针
// on:开启/关闭标志(1 表示开启异步通知,0 表示关闭异步通知)
// 返回值:0 成功,<0 失败(错误码)
int (*fasync)(int fd, struct file *filp, int on);
};
open和release



cs
#include <linux/kernel.h>
#include <linux/module.h>
/*设备号 file_operations相关头文件*/
#include <linux/fs.h>
/*cdev相关头文件*/
#include <linux/cdev.h>
/*
* 定义设备号变量
* dev_major : 主设备号
* dev_minor : 次设备号
* dev_number: 设备数量
*/
static int dev_major = 300;
static int dev_minor = 0;
static int dev_number = 1;
/*定义cdev结构体变量*/
static struct cdev cdev;
/*open接口*/
static int fs_chrdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "fs_chrdev: fs_chrdev_open\n");
/*具体功能实现*/
return 0;
}
/*release接口*/
int fs_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "fs_chrdev: fs_chrdev_release\n");
/*具体功能实现*/
return 0;
}
/*用户操作接口集合*/
static struct file_operations fs_chrdev_fops = {
.owner = THIS_MODULE,
.open = fs_chrdev_open,
.release = fs_chrdev_release,
};
/*加载函数*/
static int __init fs_chrdev_init(void)
{
int ret = 0;
/*构建完整设备号*/
dev_t devno = MKDEV(dev_major, dev_minor);
printk(KERN_INFO "fs_chrdev: module enter\n");
/*申请设备号*/
ret = register_chrdev_region(devno, dev_number, "fs_chrdev");
if (ret < 0) {
printk(KERN_ERR "fs_chrdev: register_chrdev_region failed\n");
goto err1;
}
/*cdev初始及注册*/
cdev_init(&cdev, &fs_chrdev_fops);
cdev.owner = THIS_MODULE;
ret = cdev_add(&cdev, devno, 1);
if (ret < 0) {
printk(KERN_ERR "fs_chrdev: cdev_add");
goto err2;
}
/*这个return 0 不能忽略,否则导致申请到的资源被释放掉*/
return 0;
err2:
unregister_chrdev_region(devno, dev_number);
err1:
return ret;
}
/*卸载函数*/
static void __exit fs_chrdev_exit(void)
{
dev_t devno = MKDEV(dev_major, dev_minor);
printk(KERN_INFO "fs_chrdev: module exit\n");
cdev_del(&cdev);
/*释放设备号*/
unregister_chrdev_region(devno, dev_number);
}
module_init(fs_chrdev_init);
module_exit(fs_chrdev_exit);
MODULE_LICENSE("GPL");
read和write


cs
/*
* Copyright (C) 2022 HQYJ Co., Inc. All Rights Reserved.
*
* 文件名 : fs_chrdev.c
* 作者 : HQYJ <support@hqyj.com>
* 版本 : V1.0
* 创建时间 : 2022-07-11
* 最后修改时间: 2022-07-11
* 网址 : www.fsdev.com.cn
* 功能说明 : 简易Linux字符设备驱动实现
* 支持open/release/read/write四个核心文件操作
* 实现用户空间与内核空间的简单数据交互(基于内存缓存区)
*/
#include <linux/kernel.h> // 内核核心头文件,提供printk等内核基础函数/宏定义
#include <linux/module.h> // 模块驱动核心头文件,提供module_init/module_exit等模块相关宏
/*设备号 file_operations相关头文件*/
#include <linux/fs.h> // 文件系统核心头文件,提供dev_t/MKDEV/register_chrdev_region等设备号相关函数
// 同时提供file_operations/inode/file等核心结构体定义
/*cdev相关头文件*/
#include <linux/cdev.h> // 字符设备核心头文件,提供cdev结构体及cdev_init/cdev_add/cdev_del等操作函数
/*copy_to_user/copy_from_user相关头文件*/
#include <linux/uaccess.h> // 内存访问相关头文件,提供内核与用户空间数据拷贝的核心函数
// copy_to_user(内核->用户)、copy_from_user(用户->内核)
/*
* 定义设备号相关全局变量(静态变量,仅当前文件可见,符合驱动开发最佳实践)
* dev_major : 主设备号(用于标识字符设备驱动类型,手动指定为300,需确保未被其他驱动占用)
* dev_minor : 次设备号(用于标识同一驱动下的不同设备实例,此处仅一个设备,设为0)
* dev_number: 申请的设备数量(此处仅申请1个设备,对应一个次设备号)
*/
static int dev_major = 300;
static int dev_minor = 0;
static int dev_number = 1;
/*定义cdev结构体变量(字符设备核心结构体,用于关联设备号与文件操作集)*/
static struct cdev cdev;
/*定义数据缓存区相关宏与变量(用于存储用户空间与内核空间交互的数据)*/
#define BUFF_SIZE 128 // 定义内核缓存区大小为128字节
static char dev_data[BUFF_SIZE] = {'\0'}; // 内核数据缓存区,初始化全为结束符
static int current_size = 0; // 记录当前缓存区中有效数据的长度
/*
* 字符设备open接口实现(对应用户空间open系统调用)
* 函数原型:遵循file_operations结构体中open成员的函数签名
* @inode : 指向设备文件的inode结构体指针(存储文件内核元数据,包含设备号等信息)
* @file : 指向打开的文件实例的file结构体指针(代表当前打开的文件,存储操作集等信息)
* 返回值 : 0表示成功,负数表示错误码(内核标准错误码)
*/
static int fs_chrdev_open(struct inode *inode, struct file *file)
{
// 内核打印函数(用户空间对应printf),KERN_INFO是日志级别,用于分类输出日志
printk(KERN_INFO "fs_chrdev: fs_chrdev_open\n");
/*具体功能实现:可在此添加设备打开时的初始化逻辑(如硬件初始化、锁初始化等)*/
return 0; // 返回0表示打开成功
}
/*
* 字符设备release接口实现(对应用户空间close系统调用,释放文件实例)
* 函数原型:遵循file_operations结构体中release成员的函数签名
* @inode : 指向设备文件的inode结构体指针
* @file : 指向要释放的文件实例的file结构体指针
* 返回值 : 0表示成功,负数表示错误码
*/
static int fs_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "fs_chrdev: fs_chrdev_release\n");
/*具体功能实现:可在此添加设备关闭时的资源释放逻辑(如硬件关闭、锁释放等)*/
return 0; // 返回0表示释放成功
}
/*
* 字符设备write接口实现(对应用户空间write系统调用,用户向内核写入数据)
* 函数原型:遵循file_operations结构体中write成员的函数签名
* @file : 指向打开的文件实例的file结构体指针
* @buff : 指向用户空间数据缓存区的指针(__user宏标注:该指针指向用户空间,内核不可直接访问)
* @size : 用户希望写入的数据长度(字节数)
* @loff : 指向文件偏移量的指针(记录当前文件读写位置,字符设备一般无需关注)
* 返回值 : 成功写入的字节数,负数表示错误码
*/
static ssize_t fs_chrdev_write(struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
int count = 0; // 用于记录实际要拷贝的数据长度
/*第一步:检查用户传入的size参数的正确性,避免越界访问内核缓存区*/
if (size < 0) {
// 返回-EINVAL:内核标准错误码,表示"无效的参数"
return -EINVAL;
} else if (size > BUFF_SIZE - 1) {
// 用户要写入的数据超过缓存区最大容量,只拷贝BUFF_SIZE-1个字节(预留1个字节存字符串结束符'\0')
count = BUFF_SIZE - 1; // 修正:原代码为BUFF_SIZE,此处改为BUFF_SIZE-1,避免数组越界
} else {
// 用户要写入的数据长度合法,正常拷贝
count = size;
}
/*第二步:将用户空间的数据拷贝到内核空间缓存区(dev_data)*/
// copy_from_user:内核向用户空间读取数据的核心函数
// 参数1:内核空间目标地址,参数2:用户空间源地址,参数3:要拷贝的字节数
// 返回值:0表示拷贝成功,非0表示拷贝失败(剩余未拷贝的字节数)
if (copy_from_user(dev_data, buff, count)) {
// 返回-EFAULT:内核标准错误码,表示"无效的内存地址"(用户空间指针非法)
return -EFAULT;
}
/*第三步:处理拷贝后的数据,添加字符串结束符,更新有效数据长度*/
dev_data[count] = '\0'; // 为数据添加字符串结束符,方便后续读取时处理
current_size = count; // 更新当前内核缓存区中有效数据的长度
return count; // 返回实际拷贝的字节数,告知用户空间写入成功的长度
}
/*
* 字符设备read接口实现(对应用户空间read系统调用,用户从内核读取数据)
* 函数原型:遵循file_operations结构体中read成员的函数签名
* @file : 指向打开的文件实例的file结构体指针
* @buff : 指向用户空间数据缓存区的指针(用于存储从内核读取的数据)
* @size : 用户希望读取的数据长度(字节数)
* @loff : 指向文件偏移量的指针(字符设备一般无需关注)
* 返回值 : 成功读取的字节数,负数表示错误码,0表示无数据可读
*/
static ssize_t fs_chrdev_read(struct file *file, char __user *buff, size_t size, loff_t *loff)
{
int count = 0; // 用于记录实际要拷贝的数据长度
/*第一步:检查用户传入的size参数的正确性,以及内核缓存区是否有有效数据*/
if (size < 0) {
return -EINVAL; // 无效参数错误
} else if (size > current_size) {
// 用户希望读取的数据长度大于内核缓存区有效数据长度,只拷贝现有有效数据
count = current_size;
} else {
// 用户希望读取的数据长度合法,正常拷贝
count = size;
}
/*第二步:将内核空间缓存区(dev_data)的数据拷贝到用户空间*/
// copy_to_user:内核向用户空间写入数据的核心函数
// 参数1:用户空间目标地址,参数2:内核空间源地址,参数3:要拷贝的字节数
// 返回值:0表示拷贝成功,非0表示拷贝失败
if (copy_to_user(buff, dev_data, count)) {
return -EFAULT; // 无效内存地址错误
}
/*第三步:处理读取后的数据,清空内核缓存区有效数据长度(此处实现"一次性读取"逻辑)*/
current_size = 0; // 置0表示后续无数据可读,直到用户再次写入新数据
return count; // 返回实际拷贝的字节数,告知用户空间读取成功的长度
}
/*
* 定义字符设备的文件操作集(file_operations结构体)
* 该结构体是内核与字符设备驱动的交互接口,存放了设备支持的所有文件操作函数指针
* 内核通过该结构体调用驱动实现的具体接口(如open、read、write等)
*/
static struct file_operations fs_chrdev_fops = {
.owner = THIS_MODULE, // 标记该操作集所属的模块(THIS_MODULE是内核宏,指向当前模块)
// 作用:防止模块在被使用时被意外卸载,提高驱动安全性
.open = fs_chrdev_open, // 绑定设备打开接口
.release = fs_chrdev_release, // 绑定设备释放接口
.read = fs_chrdev_read, // 绑定设备读取接口
.write = fs_chrdev_write, // 绑定设备写入接口
};
/*
* 驱动模块加载函数(内核加载模块时自动调用,对应insmod命令)
* __init宏:标记该函数为模块初始化函数,内核加载完成后会释放该函数占用的内存
* 返回值 : 0表示加载成功,负数表示加载失败
*/
static int __init fs_chrdev_init(void)
{
int ret = 0; // 用于记录函数调用的返回结果,判断是否执行成功
dev_t devno = MKDEV(dev_major, dev_minor); // 构建完整的设备号(主设备号+次设备号)
// MKDEV是内核宏,将主、次设备号编码为一个32位的dev_t类型变量
printk(KERN_INFO "fs_chrdev: module enter\n");
/*第一步:申请注册字符设备号(手动指定主设备号方式)*/
// register_chrdev_region:注册指定的设备号区间
// 参数1:要注册的起始设备号(由MKDEV构建)
// 参数2:要注册的设备数量
// 参数3:设备名称(用于在/proc/devices中显示,方便用户查看设备对应关系)
// 返回值:0表示成功,负数表示失败
ret = register_chrdev_region(devno, dev_number, "fs_chrdev");
if (ret < 0) {
// KERN_ERR:错误级别的日志,用于输出驱动错误信息,方便调试
printk(KERN_ERR "fs_chrdev: register_chrdev_region failed\n");
goto err1; // 申请设备号失败,跳转到错误处理标签err1,直接返回错误
}
/*第二步:初始化并注册cdev字符设备结构体(将设备号、操作集、字符设备绑定)*/
// cdev_init:初始化cdev结构体,将cdev与文件操作集fs_chrdev_fops绑定
// 参数1:要初始化的cdev结构体指针
// 参数2:对应的file_operations操作集指针
cdev_init(&cdev, &fs_chrdev_fops);
// 设置cdev结构体的所属模块,与file_operations中的owner一致,提高安全性
cdev.owner = THIS_MODULE;
// cdev_add:将初始化完成的cdev结构体注册到内核中,建立设备号与字符设备的关联
// 参数1:要注册的cdev结构体指针
// 参数2:对应的设备号
// 参数3:设备数量
// 返回值:0表示成功,负数表示失败
ret = cdev_add(&cdev, devno, 1);
if (ret < 0) {
printk(KERN_ERR "fs_chrdev: cdev_add failed\n");
goto err2; // 注册cdev失败,跳转到错误处理标签err2,释放已申请的设备号
}
/*这个return 0 不能忽略,否则导致申请到的资源被释放掉*/
return 0; // 模块加载成功,返回0
/* 错误处理分支(goto语句实现,是内核驱动中资源释放的标准写法,清晰高效)*/
err2:
// 释放已注册的设备号(避免资源泄露),对应register_chrdev_region
unregister_chrdev_region(devno, dev_number);
err1:
return ret; // 返回错误码,告知内核模块加载失败
}
/*
* 驱动模块卸载函数(内核卸载模块时自动调用,对应rmmod命令)
* __exit宏:标记该函数为模块卸载函数,仅当模块被卸载时才会被编译链接
*/
static void __exit fs_chrdev_exit(void)
{
dev_t devno = MKDEV(dev_major, dev_minor); // 构建完整设备号
printk(KERN_INFO "fs_chrdev: module exit\n");
/*第一步:从内核中注销cdev字符设备结构体(对应cdev_add)*/
// cdev_del:移除内核中的cdev结构体,解除设备号与字符设备的关联
cdev_del(&cdev);
/*第二步:释放已注册的设备号(对应register_chrdev_region)*/
// unregister_chrdev_region:释放设备号区间,归还内核,避免资源泄露
unregister_chrdev_region(devno, dev_number);
}
// 模块加载入口宏:指定模块加载时要调用的初始化函数(fs_chrdev_init)
module_init(fs_chrdev_init);
// 模块卸载入口宏:指定模块卸载时要调用的卸载函数(fs_chrdev_exit)
module_exit(fs_chrdev_exit);
// 声明模块的许可证协议(必须指定,否则内核加载时会报警告,且部分功能不可用)
// GPL协议:开源协议,符合内核开源规范,允许模块与内核其他代码交互
MODULE_LICENSE("GPL");
ioctl


设备结构体


fs_chrdev.h
cs
#ifndef FS_CHRDEV_H
#define FS_CHRDEV_H
#define FS_CMD_MAGIC 'F'
#define FS_CMD1 _IO(FS_CMD_MAGIC, 0)
#define FS_CMD2 _IO(FS_CMD_MAGIC, 1)
#endif
fs_chrdev.c
cs
/*
* 该文件是一个完整的 Linux 字符设备驱动模板
* 实现了 open/release/read/write/unlocked_ioctl 五大核心接口
* 支持用户态与内核态之间的字符数据读写,以及简单的 IO 控制命令响应
*/
#include <linux/kernel.h> // 内核打印函数 printk() 等核心内核接口
#include <linux/module.h> // 模块加载/卸载相关宏(module_init/module_exit)及模块管理接口
/*设备号 file_operations 相关头文件*/
#include <linux/fs.h> // 包含文件操作结构体 file_operations、设备号相关函数(MKDEV 等)
// 以及字符设备的核心操作接口定义
/*cdev 相关头文件*/
#include <linux/cdev.h> // 包含字符设备结构体 cdev 及相关操作函数(cdev_init/cdev_add 等)
/*copy_to_user/copy_from_user 相关头文件*/
#include <linux/uaccess.h> // 包含内核态与用户态数据拷贝的核心函数
// copy_to_user(内核→用户)、copy_from_user(用户→内核)
#include "fs_chrdev.h" // 自定义头文件,通常包含 IOCTL 命令宏(如 FS_CMD1/FS_CMD2)等定义
/*
* 定义设备号相关全局变量(字符设备必须关联设备号才能被用户态识别)
* dev_major : 主设备号(用于区分不同类型的字符设备,如 1 是内存设备、4 是 tty 设备)
* dev_minor : 次设备号(用于区分同一类型下的多个具体设备,从 0 开始编号)
* dev_number: 申请的设备数量(此处只申请 1 个设备,对应 1 个次设备号)
*/
static int dev_major = 300; // 手动指定主设备号为 300(需确保未被其他设备占用)
static int dev_minor = 0; // 次设备号从 0 开始
static int dev_number = 1; // 只申请 1 个字符设备
/*定义 cdev 结构体变量(用于描述字符设备的核心结构体,关联设备号与文件操作接口)*/
static struct cdev cdev;
/*定义数据缓存区(内核态用于存储用户态读写的数据,实现内核与用户态的数据中转)*/
#define BUFF_SIZE 128 // 定义缓冲区最大容量为 128 字节
static char dev_data[BUFF_SIZE] = {'\0'}; // 内核数据缓冲区,初始化为全空字符
static int current_size = 0; // 记录缓冲区中当前有效数据的长度(避免读取空数据)
/*
* 字符设备 open 接口(用户态调用 open() 打开设备文件时,内核会回调该函数)
* @inode: 指向设备节点的 inode 结构体(存储设备文件的磁盘信息,包含设备号)
* @file: 指向打开的文件描述符对应的 file 结构体(存储文件的运行时信息)
* @return: 0 表示打开成功,负数表示打开失败(返回对应的内核错误码)
*/
static int fs_chrdev_open(struct inode *inode, struct file *file)
{
// KERN_INFO:日志级别(信息级),内核日志会输出该内容,可通过 dmesg 命令查看
printk(KERN_INFO "fs_chrdev: fs_chrdev_open\n");
/*具体功能实现(此处为模板,可添加设备初始化、资源申请等逻辑)*/
return 0; // 返回 0 表示打开成功
}
/*
* 字符设备 release 接口(用户态调用 close() 关闭设备文件时,内核会回调该函数)
* 注意:close() 不一定会立即回调 release(存在文件描述符引用计数),引用计数为 0 时才会回调
* @inode: 指向设备节点的 inode 结构体
* @file: 指向关闭的文件描述符对应的 file 结构体
* @return: 0 表示释放成功,负数表示释放失败
*/
static int fs_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "fs_chrdev: fs_chrdev_release\n");
/*具体功能实现(此处为模板,可添加设备资源释放、状态重置等逻辑)*/
return 0; // 返回 0 表示释放成功
}
/*
* 字符设备 write 接口(用户态调用 write() 向设备写入数据时,内核会回调该函数)
* 作用:将用户态缓冲区的数据拷贝到内核态缓冲区(完成用户→内核的数据传输)
* @file: 指向打开的文件描述符对应的 file 结构体
* @buff: 用户态缓冲区的指针(__user 修饰符标注:该指针指向用户态内存空间,内核不可直接访问)
* @size: 用户态想要写入的数据长度(字节数)
* @loff: 文件偏移量(字符设备通常忽略,块设备/普通文件需关注)
* @return: 成功写入的字节数;负数表示写入失败(返回对应的内核错误码)
*/
static ssize_t fs_chrdev_write(struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
int count = 0; // 实际要拷贝的数据长度(用于限制超出缓冲区容量的写入)
/*1. 检查写入长度 size 的正确性,避免非法数据或缓冲区溢出*/
if (size < 0) { // size 为负数,属于非法参数
return -EINVAL; // 返回内核错误码:无效参数
} else if (size > BUFF_SIZE - 1) { // 写入数据超出缓冲区容量(预留 1 字节存字符串结束符 '\0')
count = BUFF_SIZE - 1; // 限制最大写入长度为缓冲区容量-1
} else { // 写入数据长度合法,正常接收
count = size;
}
/*2. 将用户空间数据拷贝到内核空间(核心步骤)
* copy_from_user: 内核提供的安全拷贝函数(用户态→内核态)
* 函数原型:unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
* 参数说明:
* to: 内核态缓冲区指针(此处为 dev_data)
* from: 用户态缓冲区指针(此处为 buff,必须带 __user 修饰)
* n: 要拷贝的字节数(此处为 count)
* 返回值:拷贝失败的字节数(0 表示全部拷贝成功,非 0 表示拷贝失败)
*/
if (copy_from_user(dev_data, buff, count)) {
return -EFAULT; // 返回内核错误码:内存访问错误(用户态指针无效或权限不足)
}
/*3. 数据拷贝完成后的收尾处理*/
dev_data[count] = '\0'; // 给内核缓冲区数据添加字符串结束符,避免后续读取出现乱码
current_size = count; // 更新当前缓冲区有效数据长度,为 read 接口提供数据长度参考
return count; // 返回实际写入的字节数,告知用户态写入成功
}
/*
* 字符设备 read 接口(用户态调用 read() 从设备读取数据时,内核会回调该函数)
* 作用:将内核态缓冲区的数据拷贝到用户态缓冲区(完成内核→用户的数据传输)
* @file: 指向打开的文件描述符对应的 file 结构体
* @buff: 用户态缓冲区的指针(__user 修饰符标注:内核不可直接访问)
* @size: 用户态想要读取的数据长度(字节数)
* @loff: 文件偏移量(字符设备通常忽略)
* @return: 成功读取的字节数;0 表示无数据可读;负数表示读取失败
*/
static ssize_t fs_chrdev_read(struct file *file, char __user *buff, size_t size, loff_t *loff)
{
int count = 0; // 实际要拷贝的数据长度
/*1. 检查读取长度 size 的正确性,以及内核缓冲区是否有有效数据*/
if (size < 0) { // size 为负数,非法参数
return -EINVAL;
} else if (size > current_size) { // 用户想要读取的长度大于内核缓冲区有效数据长度
count = current_size; // 只能返回当前所有有效数据
} else { // 读取长度合法,正常返回请求长度
count = size;
}
/*2. 将内核空间数据拷贝到用户空间(核心步骤)
* copy_to_user: 内核提供的安全拷贝函数(内核态→用户态)
* 函数原型:unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
* 参数说明:
* to: 用户态缓冲区指针(此处为 buff)
* from: 内核态缓冲区指针(此处为 dev_data)
* n: 要拷贝的字节数(此处为 count)
* 返回值:拷贝失败的字节数(0 表示全部拷贝成功,非 0 表示拷贝失败)
*/
if (copy_to_user(buff, dev_data, count)) {
return -EFAULT; // 返回内核错误码:内存访问错误
}
/*3. 数据拷贝完成后的收尾处理*/
current_size = 0; // 读取完成后清空有效数据长度标记(此处为一次性读取,可根据需求修改为循环读取)
return count; // 返回实际读取的字节数,告知用户态读取成功
}
/*
* 字符设备 unlocked_ioctl 接口(用户态调用 ioctl() 控制设备时,内核会回调该函数)
* 作用:实现设备的自定义控制命令(如设备参数配置、模式切换等),比 read/write 更灵活
* 注意:unlocked_ioctl 是无锁版本(推荐使用),替代旧的 ioctl 接口
* @file: 指向打开的文件描述符对应的 file 结构体
* @cmd: 用户态传入的控制命令(自定义宏,如 FS_CMD1/FS_CMD2)
* @arg: 用户态传入的命令参数(可是整数或指针,需根据命令类型解析)
* @return: 0 表示命令执行成功;负数表示命令执行失败
*/
static long fs_chrdev_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
printk(KERN_INFO "fs_chrdev: fs_chrdev_unlocked_ioctl\n");
/*根据用户态传入的命令 cmd 进行分支处理*/
switch(cmd) {
case FS_CMD1: // 自定义命令 1(在 fs_chrdev.h 中定义)
printk(KERN_INFO "fs_chrdev: FS_CMD1\n");
break; // 执行完成后跳出分支
case FS_CMD2: // 自定义命令 2(在 fs_chrdev.h 中定义)
printk(KERN_INFO "fs_chrdev: FS_CMD2\n");
break;
default: // 未知命令,返回错误提示
printk(KERN_INFO "fs_chrdev: invalid argment\n");
}
return 0; // 返回 0 表示 ioctl 操作成功
}
/*
* 定义文件操作结构体 file_operations(字符设备的核心"操作清单")
* 该结构体关联了字符设备支持的所有操作接口,内核通过该结构体回调对应的驱动函数
* 每个成员对应一个操作接口,未实现的接口可设为 NULL(用户态调用时会返回 -ENOTTY)
*/
static struct file_operations fs_chrdev_fops = {
.owner = THIS_MODULE, // 标注该结构体所属的模块(必须设置,用于内核模块管理,防止模块被意外卸载)
.open = fs_chrdev_open, // 关联 open 操作接口
.release = fs_chrdev_release, // 关联 release 操作接口
.read = fs_chrdev_read, // 关联 read 操作接口
.write = fs_chrdev_write, // 关联 write 操作接口
.unlocked_ioctl = fs_chrdev_unlocked_ioctl, // 关联 unlocked_ioctl 操作接口
};
/*
* 模块加载函数(驱动插入内核时执行,对应 insmod 命令)
* __init 宏标注:该函数仅在模块加载时执行一次,执行完成后会被内核释放内存
* 作用:完成字符设备的初始化工作(申请设备号、初始化 cdev、注册 cdev 等)
* @return: 0 表示加载成功;负数表示加载失败(驱动不会被插入内核)
*/
static int __init fs_chrdev_init(void)
{
int ret = 0; // 函数返回值,用于记录各步骤的执行结果
dev_t devno = MKDEV(dev_major, dev_minor); // 构建完整设备号(主设备号+次设备号)
// MKDEV 宏:将主设备号和次设备号组合成一个 32 位的 dev_t 类型设备号(高 12 位为主设备号,低 20 位为次设备号)
printk(KERN_INFO "fs_chrdev: module enter\n");
/*1. 申请设备号(静态申请:手动指定主设备号,适用于已知未被占用的设备号)
* 函数原型:int register_chrdev_region(dev_t from, unsigned int count, const char *name)
* 参数说明:
* from: 起始设备号(此处为构建好的 devno)
* count: 申请的设备数量(此处为 dev_number=1)
* name: 设备名称(会在 /proc/devices 中显示,用于标识设备)
* 返回值:0 表示申请成功;负数表示申请失败(如设备号已被占用)
*/
ret = register_chrdev_region(devno, dev_number, "fs_chrdev");
if (ret < 0) { // 设备号申请失败
printk(KERN_ERR "fs_chrdev: register_chrdev_region failed\n"); // KERN_ERR:错误级日志,标注严重错误
goto err1; // 跳转到错误处理标签,直接返回错误
}
/*2. 初始化 cdev 结构体并注册到内核(将 cdev 与设备号、文件操作结构体关联)
* 第一步:cdev_init 初始化 cdev 结构体
* 函数原型:void cdev_init(struct cdev *cdev, const struct file_operations *fops)
* 参数说明:
* cdev: 要初始化的 cdev 结构体变量(此处为全局变量 cdev)
* fops: 关联的文件操作结构体(此处为 fs_chrdev_fops)
*/
cdev_init(&cdev, &fs_chrdev_fops);
cdev.owner = THIS_MODULE; // 标注 cdev 所属的模块(必须设置,与 file_operations 中的 owner 一致)
/*第二步:cdev_add 将 cdev 结构体注册到内核,完成字符设备的最终注册
* 函数原型:int cdev_add(struct cdev *p, dev_t dev, unsigned int count)
* 参数说明:
* p: 已初始化的 cdev 结构体指针
* dev: 关联的设备号(此处为 devno)
* count: 设备数量(此处为 1)
* 返回值:0 表示注册成功;负数表示注册失败
*/
ret = cdev_add(&cdev, devno, 1);
if (ret < 0) { // cdev 注册失败
printk(KERN_ERR "fs_chrdev: cdev_add failed\n");
goto err2; // 跳转到错误处理标签,释放已申请的设备号
}
/*这个 return 0 不能忽略,否则导致申请到的资源被释放掉*/
return 0; // 模块加载成功,返回 0
/* 错误处理标签(采用 goto 语句实现资源回滚,是内核驱动的常用最佳实践)
* 当后续步骤失败时,需要释放前面已成功申请的资源,避免内存泄漏或资源占用
*/
err2: // cdev_add 失败时,执行此处
unregister_chrdev_region(devno, dev_number); // 释放已申请的设备号
err1: // register_chrdev_region 失败时,执行此处
return ret; // 返回错误码,告知内核模块加载失败
}
/*
* 模块卸载函数(驱动从内核移除时执行,对应 rmmod 命令)
* __exit 宏标注:该函数仅在模块卸载时执行一次,未编译进内核的模块会包含该函数
* 作用:释放模块加载时申请的所有资源(注销 cdev、释放设备号),避免资源泄漏
*/
static void __exit fs_chrdev_exit(void)
{
dev_t devno = MKDEV(dev_major, dev_minor); // 重构完整设备号
printk(KERN_INFO "fs_chrdev: module exit\n");
/*1. 注销 cdev 结构体,从内核中移除字符设备(释放 cdev 占用的内核资源)
* 函数原型:void cdev_del(struct cdev *p)
* 参数:要注销的 cdev 结构体指针
*/
cdev_del(&cdev);
/*2. 释放设备号,将设备号归还给内核(避免设备号被永久占用)
* 函数原型:void unregister_chrdev_region(dev_t from, unsigned int count)
* 参数与 register_chrdev_region 一致
*/
unregister_chrdev_region(devno, dev_number);
}
/*
* 模块加载/卸载函数关联宏(内核用于识别模块的入口和出口函数)
* module_init: 关联模块加载函数,insmod 命令执行时会调用该宏指定的函数
* module_exit: 关联模块卸载函数,rmmod 命令执行时会调用该宏指定的函数
*/
module_init(fs_chrdev_init);
module_exit(fs_chrdev_exit);
/*
* 模块许可证宏(必须设置,否则内核会报警告,且部分功能无法使用)
* MODULE_LICENSE("GPL"): 声明模块遵循 GPL 协议,与内核开源协议兼容
* 其他可选值:MIT、BSD 等(非 GPL 协议可能会触发内核模块兼容性限制)
*/
MODULE_LICENSE("GPL");
test.c
cs
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include "fs_chrdev.h"
int main(int argc, const char *argv[])
{
int fd, nbyte;
char buf[128] = "Hello World";
fd = open("/dev/fs_chrdev", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
ioctl(fd, FS_CMD1);
ioctl(fd, FS_CMD2);
close(fd);
return 0;
}