一、驱动概述
globalmem 是一款经典的字符设备驱动示例,核心功能是在内核空间模拟一块固定大小的全局内存区域,提供用户空间与内核空间的数据交互接口(读、写、定位、IO控制)。本驱动基于 Linux 6.6 内核编写,适配内核最新接口规范,修正旧版本(如Linux 3.x/4.x)中过时的函数与宏定义,遵循内核编码规范,支持单设备实例,可轻松扩展为多设备实例。
Linux 6.6 内核适配要点:
移除过时的 __devexit、__devinit 宏(内核5.10+已废弃),统一使用 __init、__exit。
IO控制接口统一使用 unlocked_ioctl(ioctl 已废弃,内核强制要求)。
内存分配优先使用 kzalloc(自带清0,避免野指针),释放对应使用 kfree,遵循内核内存管理规范。
打印信息规范:使用 pr_info 替代 printk(KERN_INFO),pr_notice 替代 printk(KERN_NOTICE),提升代码可读性与兼容性。
模块参数、设备号申请/释放、cdev 操作接口保持兼容,但需严格遵循内核错误处理流程(避免资源泄漏)。
二、完整驱动代码(globalmem.c)
/* * Linux 6.6 内核 globalmem 字符设备驱动 * 功能:模拟全局内存,支持读写、seek、IO控制(内存清空)、文件私有数据机制 * 遵循 GPLv2 协议,适配内核6.6接口规范 */#include<linux/module.h>#include<linux/fs.h>#include<linux/init.h>#include<linux/cdev.h>#include<linux/slab.h>#include<linux/uaccess.h>#include<linux/ioctl.h>// 设备相关宏定义(Linux 6.6 推荐规范:使用幻数避免IO命令冲突)#define GLOBALMEM_MAGIC 'g'// 幻数(0~0xff,避免与内核现有幻数冲突)#define GLOBALMEM_SIZE 0x1000 // 全局内存大小(4KB)#define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0) // IO控制命令:清空内存(无数据传输)#define GLOBALMEM_MAJOR 230 // 预设主设备号(可通过模块参数修改)#define DEVICE_NAME "globalmem"// 设备名称// 全局变量声明staticint globalmem_major = GLOBALMEM_MAJOR; // 主设备号module_param(globalmem_major, int, S_IRUGO); // 模块参数:允许用户空间修改主设备号MODULE_PARM_DESC(globalmem_major, "Major number of globalmem device (default: 230)");// 设备结构体(封装cdev和内存区域,体现内核面向对象封装思想)structglobalmem_dev {structcdevcdev;// 字符设备核心结构体unsignedchar mem[GLOBALMEM_SIZE]; // 模拟的全局内存区域};staticstructglobalmem_dev *globalmem_devp;// 设备结构体实例指针// 函数声明(驱动核心接口)staticintglobalmem_open(struct inode *inode, struct file *filp);staticintglobalmem_release(struct inode *inode, struct file *filp);staticssize_tglobalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);staticssize_tglobalmem_write(struct file *filp, constchar __user *buf, size_t size, loff_t *ppos);staticloff_tglobalmem_llseek(struct file *filp, loff_t offset, int orig);staticlongglobalmem_ioctl(struct file *filp, unsignedint cmd, unsignedlong arg);// 文件操作结构体(关联驱动核心接口,Linux 6.6 无接口变更)staticconststructfile_operationsglobalmem_fops = { .owner = THIS_MODULE, // 所属模块,避免模块被意外卸载 .llseek = globalmem_llseek, // 文件定位接口 .read = globalmem_read, // 读接口(用户->内核) .write = globalmem_write, // 写接口(内核->用户) .unlocked_ioctl = globalmem_ioctl, // IO控制接口(无大内核锁,内核推荐) .open = globalmem_open, // 设备打开接口 .release = globalmem_release, // 设备释放接口};/** * globalmem_open - 设备打开函数 * @inode:inode节点指针(关联设备文件的索引信息) * @filp:文件结构体指针(关联打开的设备文件) * 返回值:0-成功,负数-失败 * 功能:将设备结构体指针赋值给文件私有数据,供后续接口使用 */staticintglobalmem_open(struct inode *inode, struct file *filp){// 将文件私有数据指向设备结构体实例,后续read/write/ioctl可直接获取 filp->private_data = globalmem_devp;return0; // 打开成功(无额外初始化操作)}/** * globalmem_release - 设备释放函数 * @inode:inode节点指针 * @filp:文件结构体指针 * 返回值:0-成功 * 功能:释放设备资源(本驱动无额外资源,仅返回成功) */staticintglobalmem_release(struct inode *inode, struct file *filp){return0; // 释放成功}/** * globalmem_read - 设备读函数 * @filp:文件结构体指针 * @buf:用户空间缓冲区指针(存储读取的数据) * @size:用户请求读取的字节数 * @ppos:文件当前读写偏移量指针 * 返回值:成功读取的字节数,0-到达文件末尾,负数-失败 * 功能:将内核空间mem数组的数据拷贝到用户空间缓冲区 */staticssize_tglobalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos){unsignedlong p = *ppos; // 当前偏移量unsignedint count = size; // 请求读取的字节数int ret = 0; // 返回值structglobalmem_dev *dev = filp->private_data;// 获取设备结构体实例// 边界检查:偏移量超出内存大小,返回0(表示到达文件末尾)if (p >= GLOBALMEM_SIZE)return0;// 调整读取字节数:避免超出内存剩余空间if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;// 将内核空间数据拷贝到用户空间(copy_to_user:内核->用户,失败返回未拷贝字节数)if (copy_to_user(buf, dev->mem + p, count)) { ret = -EFAULT; // 拷贝失败,返回错误码(坏地址) } else { *ppos += count; // 更新读写偏移量 ret = count; // 返回成功读取的字节数 pr_info("read %u bytes from offset %lu\n", count, p); // 打印日志(内核日志) }return ret;}/** * globalmem_write - 设备写函数 * @filp:文件结构体指针 * @buf:用户空间缓冲区指针(存储要写入的数据) * @size:用户请求写入的字节数 * @ppos:文件当前读写偏移量指针 * 返回值:成功写入的字节数,负数-失败 * 功能:将用户空间缓冲区的数据拷贝到内核空间mem数组 */staticssize_tglobalmem_write(struct file *filp, constchar __user *buf, size_t size, loff_t *ppos){unsignedlong p = *ppos; // 当前偏移量unsignedint count = size; // 请求写入的字节数int ret = 0; // 返回值structglobalmem_dev *dev = filp->private_data;// 获取设备结构体实例// 边界检查:偏移量超出内存大小,返回0(无法写入)if (p >= GLOBALMEM_SIZE)return0;// 调整写入字节数:避免超出内存剩余空间if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;// 将用户空间数据拷贝到内核空间(copy_from_user:用户->内核,失败返回未拷贝字节数)if (copy_from_user(dev->mem + p, buf, count)) { ret = -EFAULT; // 拷贝失败,返回错误码(坏地址) } else { *ppos += count; // 更新读写偏移量 ret = count; // 返回成功写入的字节数 pr_info("written %u bytes to offset %lu\n", count, p); // 打印日志 }return ret;}/** * globalmem_llseek - 文件定位函数 * @filp:文件结构体指针 * @offset:用户请求的偏移量 * @orig:定位基准(0-文件开头,1-当前位置,2-文件末尾) * 返回值:新的偏移量,负数-失败 * 功能:修改文件当前读写偏移量,支持从开头、当前位置定位(Linux 6.6 支持文件末尾定位,本驱动暂不启用) */staticloff_tglobalmem_llseek(struct file *filp, loff_t offset, int orig){loff_t ret = 0; // 新的偏移量switch (orig) {case0: // 从文件开头定位(SEEK_SET)// 偏移量为负,非法请求if (offset < 0) { ret = -EINVAL;break; }// 偏移量超出内存大小,非法请求if ((unsignedint)offset > GLOBALMEM_SIZE) { ret = -EINVAL;break; } filp->f_pos = (unsignedint)offset; // 更新文件偏移量 ret = filp->f_pos;break;case1: // 从当前位置定位(SEEK_CUR)// 偏移后超出内存大小,非法请求if ((filp->f_pos + offset) > GLOBALMEM_SIZE) { ret = -EINVAL;break; }// 偏移后为负,非法请求if ((filp->f_pos + offset) < 0) { ret = -EINVAL;break; } filp->f_pos += offset; // 更新文件偏移量 ret = filp->f_pos;break;default: // 不支持的定位基准,返回错误 ret = -EINVAL;break; }return ret;}/** * globalmem_ioctl - IO控制函数 * @filp:文件结构体指针 * @cmd:用户请求的IO控制命令 * @arg:用户传递的参数(本驱动无参数,置0) * 返回值:0-成功,负数-失败 * 功能:处理用户空间发送的IO控制命令(本驱动仅支持MEM_CLEAR:清空内存) */staticlongglobalmem_ioctl(struct file *filp, unsignedint cmd, unsignedlong arg){structglobalmem_dev *dev = filp->private_data;// 获取设备结构体实例// 命令合法性检查:检查幻数和命令序号(避免命令冲突)if (_IOC_TYPE(cmd) != GLOBALMEM_MAGIC)return -EINVAL;if (_IOC_NR(cmd) > 0)return -EINVAL;// 处理具体命令switch (cmd) {case MEM_CLEAR: // 清空全局内存memset(dev->mem, 0, GLOBALMEM_SIZE); // 将mem数组清0 pr_info("globalmem: memory cleared successfully\n");break;default: // 不支持的命令,返回错误return -EINVAL; }return0; // 命令执行成功}/** * globalmem_setup_cdev - 初始化cdev结构体并添加到内核 * @dev:设备结构体实例指针 * @index:次设备号索引(单设备为0) * 功能:初始化cdev、关联文件操作结构体、将cdev添加到内核字符设备链表 */staticvoidglobalmem_setup_cdev(struct globalmem_dev *dev, int index){dev_t devno = MKDEV(globalmem_major, index); // 生成设备号(主设备号+次设备号)// 初始化cdev:关联设备结构体和文件操作结构体 cdev_init(&dev->cdev, &globalmem_fops); dev->cdev.owner = THIS_MODULE; // 关联所属模块// 将cdev添加到内核(注册字符设备),次设备号范围:index ~ index+0(单设备)int err = cdev_add(&dev->cdev, devno, 1);if (err) { pr_notice("globalmem: error %d adding cdev%d\n", err, index); }}/** * globalmem_init - 驱动初始化函数(模块加载时执行) * 返回值:0-成功,负数-失败 * 功能:申请设备号、分配设备结构体内存、初始化cdev、注册设备 */staticint __init globalmem_init(void){int ret;dev_t devno = MKDEV(globalmem_major, 0); // 生成设备号(次设备号为0)// 申请设备号:两种方式(预设主设备号/动态分配)if (globalmem_major) {// 预设主设备号,申请指定设备号(次设备号0~0,单设备) ret = register_chrdev_region(devno, 1, DEVICE_NAME); } else {// 动态分配设备号(内核自动分配主设备号) ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME); globalmem_major = MAJOR(devno); // 获取动态分配的主设备号 }// 设备号申请失败,返回错误if (ret < 0)return ret;// 分配设备结构体内存(kzalloc:分配+清0,GFP_KERNEL:内核态常规内存分配) globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);if (!globalmem_devp) { // 内存分配失败 ret = -ENOMEM;goto fail_malloc; // 跳转至错误处理,释放已申请的设备号 }// 初始化cdev并添加到内核(单设备,次设备号为0) globalmem_setup_cdev(globalmem_devp, 0); pr_info("globalmem: driver initialized successfully\n"); pr_info("globalmem: major number = %d\n", globalmem_major);return0; // 初始化成功// 错误处理:内存分配失败,释放设备号fail_malloc: unregister_chrdev_region(devno, 1); pr_err("globalmem: failed to allocate memory, driver init failed\n");return ret;}/** * globalmem_exit - 驱动退出函数(模块卸载时执行) * 功能:从内核移除cdev、释放设备结构体内存、释放设备号,避免资源泄漏 */staticvoid __exit globalmem_exit(void){// 从内核移除cdev(注销字符设备) cdev_del(&globalmem_devp->cdev);// 释放设备结构体内存 kfree(globalmem_devp);// 释放设备号 unregister_chrdev_region(MKDEV(globalmem_major, 0), 1); pr_info("globalmem: driver exited successfully\n");}// 模块加载/卸载入口(Linux 6.6 无接口变更)module_init(globalmem_init);module_exit(globalmem_exit);// 模块信息声明(遵循GPL协议,否则内核拒绝加载)MODULE_AUTHOR("Linux Driver Developer");MODULE_LICENSE("GPL v2");MODULE_DESCRIPTION("Global Memory Character Device Driver for Linux 6.6 Kernel");MODULE_VERSION("1.0");
三、Makefile(Linux 6.6 内核编译适配)
编写适配 Linux 6.6 内核的 Makefile,确保驱动能够正确编译生成 .ko 模块(需指定内核源码路径)。
# Linux 6.6 内核 globalmem 驱动 Makefile# 说明:需修改 KERNELDIR 为本地 Linux 6.6 内核源码路径# 内核源码路径(请根据实际情况修改)KERNELDIR ?= /usr/src/linux-6.6.0# 当前驱动源码目录PWD := $(shell pwd)# 模块名称MODULE_NAME := globalmem# 编译规则:默认编译模块obj-m := $(MODULE_NAME).o# 编译命令:进入内核源码目录,执行模块编译all:$(MAKE) -C $(KERNELDIR) M=$(PWD) modules# 清理编译生成的文件clean:$(MAKE) -C $(KERNELDIR) M=$(PWD) clean rm -f *.ko.cmd *.mod.cmd *.o.cmd *.symvers
四、代码解析(Linux 6.6 适配重点)
1. 内核接口适配
Linux 6.6 内核对字符设备驱动的核心接口无重大变更,但废弃了部分旧接口,本驱动主要做了以下适配:
打印函数:使用 pr_info、pr_notice、pr_err 替代 printk 带日志级别,这些函数是内核推荐的打印接口,自动关联模块信息,日志输出更规范。
IO控制命令:使用内核推荐的 _IO 宏生成命令码,通过幻数(GLOBALMEM_MAGIC)避免不同设备驱动的命令冲突,替代了旧版本中直接定义命令为 0x1 的不规范方式。
废弃宏移除:移除了 __devexit、__devinit 等内核 5.10+ 已废弃的宏,统一使用 __init(模块加载初始化)、__exit(模块卸载清理)。
2. 核心功能模块
设备结构体封装:将 cdev(字符设备核心)和 mem(全局内存)封装在 struct globalmem_dev 中,体现内核面向对象的封装思想,便于后续扩展(如添加多设备实例、中断处理等)。
文件私有数据:在 globalmem_open 中,将设备结构体指针赋值给 filp->private_data,后续 read、write、ioctl 等接口可直接通过该指针获取设备实例,无需全局变量遍历,提升效率且便于多设备扩展。
设备号管理:支持两种设备号申请方式(预设主设备号/动态分配),动态分配可避免主设备号冲突,适配多驱动共存场景;模块卸载时,严格释放设备号和内存,避免资源泄漏。
数据交互安全:使用 copy_to_user(内核->用户)和 copy_from_user(用户->内核)进行数据拷贝,这两个函数会检查用户空间地址合法性,避免内核崩溃,是内核用户空间与内核空间数据交互的标准接口。
3. 错误处理规范
Linux 6.6 内核对驱动错误处理要求更严格,本驱动遵循以下规范:
设备号申请失败时,直接返回错误码,不进行后续操作。
内存分配失败时,跳转至 fail_malloc 标签,释放已申请的设备号,避免资源泄漏。
IO控制命令、文件定位等操作,先进行合法性检查(如偏移量、命令幻数),非法请求返回对应错误码(-EINVAL、-EFAULT),符合内核错误处理规范。
五、驱动编译与测试(Linux 6.6 环境)
1. 编译前提
安装 Linux 6.6 内核源码(需与当前系统内核版本一致)。
安装内核编译依赖工具:sudo apt install gcc make linux-headers-6.6.0(Ubuntu/Debian 系统)。
修改 Makefile 中的 KERNELDIR,指向本地 Linux 6.6 内核源码路径。
2. 编译驱动
# 进入驱动源码目录cd /path/to/globalmem-driver# 执行编译make# 编译成功后,生成 globalmem.ko 模块文件
3. 加载与测试驱动
# 加载驱动模块(需root权限)sudo insmod globalmem.ko# 查看模块是否加载成功lsmod | grep globalmem# 查看设备号(确认主设备号为230或动态分配的号码)cat /proc/devices | grep globalmem# 创建设备节点(主设备号230,次设备号0)sudo mknod /dev/globalmem c 230 0# 修改设备节点权限(允许普通用户读写)sudo chmod 666 /dev/globalmem# 测试驱动功能# 1. 写数据到设备echo"hello linux 6.6 globalmem" > /dev/globalmem# 2. 读设备数据(验证写功能)cat /dev/globalmem# 3. 清空设备内存(IO控制命令)sudo ioctl /dev/globalmem $(echo -n -e "\x01\x67\x00\x00\x00\x00\x00\x00")# 4. 再次读设备数据(验证清空功能,应无输出)cat /dev/globalmem# 卸载驱动模块sudo rmmod globalmem# 删除设备节点sudo rm /dev/globalmem
六、多设备实例扩展(可选)
基于本驱动,可轻松扩展为支持多个 globalmem 设备实例(修改以下部分即可,适配 Linux 6.6 内核):
/* * Linux 6.6 内核 globalmem 字符设备驱动(多设备实例版) * 功能:支持5个独立设备实例,每个实例拥有独立全局内存,支持读写、seek、IO控制 * 遵循 GPLv2 协议,适配内核6.6接口规范,可直接编译使用 */#include<linux/module.h>#include<linux/fs.h>#include<linux/init.h>#include<linux/cdev.h>#include<linux/slab.h>#include<linux/uaccess.h>#include<linux/ioctl.h>#include<linux/container_of.h> // 多设备必备,用于通过cdev获取设备实例// 设备相关宏定义(多设备适配)#define GLOBALMEM_MAGIC 'g'// 幻数(避免IO命令冲突)#define GLOBALMEM_SIZE 0x1000 // 单个设备全局内存大小(4KB)#define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0) // IO控制命令:清空内存#define GLOBALMEM_MAJOR 230 // 预设主设备号(可通过模块参数修改)#define DEVICE_NAME "globalmem"// 设备名称前缀#define DEVICE_NUM 5 // 多设备实例数量(5个,次设备号0~4)// 全局变量声明staticint globalmem_major = GLOBALMEM_MAJOR; // 主设备号module_param(globalmem_major, int, S_IRUGO); // 模块参数:允许修改主设备号MODULE_PARM_DESC(globalmem_major, "Major number of globalmem device (default: 230)");// 设备结构体(多设备适配,每个实例独立)structglobalmem_dev {structcdevcdev;// 字符设备核心结构体unsignedchar mem[GLOBALMEM_SIZE]; // 单个设备的独立全局内存int dev_idx; // 设备索引(0~4,区分不同设备实例)};staticstructglobalmem_dev *globalmem_devp;// 设备结构体数组指针(存储多个实例)// 函数声明(驱动核心接口,多设备通用)staticintglobalmem_open(struct inode *inode, struct file *filp);staticintglobalmem_release(struct inode *inode, struct file *filp);staticssize_tglobalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);staticssize_tglobalmem_write(struct file *filp, constchar __user *buf, size_t size, loff_t *ppos);staticloff_tglobalmem_llseek(struct file *filp, loff_t offset, int orig);staticlongglobalmem_ioctl(struct file *filp, unsignedint cmd, unsignedlong arg);// 文件操作结构体(多设备通用,无修改)staticconststructfile_operationsglobalmem_fops = { .owner = THIS_MODULE, .llseek = globalmem_llseek, .read = globalmem_read, .write = globalmem_write, .unlocked_ioctl = globalmem_ioctl, .open = globalmem_open, .release = globalmem_release,};/** * globalmem_open - 设备打开函数(多设备适配) * 核心:通过container_of获取当前打开的设备实例,避免全局变量遍历 */staticintglobalmem_open(struct inode *inode, struct file *filp){// 通过inode的i_cdev成员,反向获取对应的设备结构体实例(多设备关键)structglobalmem_dev *dev = container_of(inode->i_cdev, structglobalmem_dev, cdev); filp->private_data = dev; // 绑定设备实例到文件私有数据 pr_info("globalmem: device %d opened successfully\n", dev->dev_idx);return0;}/** * globalmem_release - 设备释放函数(多设备通用) */staticintglobalmem_release(struct inode *inode, struct file *filp){structglobalmem_dev *dev = filp->private_data; pr_info("globalmem: device %d released successfully\n", dev->dev_idx);return0;}/** * globalmem_read - 设备读函数(多设备通用,操作当前设备实例的内存) */staticssize_tglobalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos){unsignedlong p = *ppos;unsignedint count = size;int ret = 0;structglobalmem_dev *dev = filp->private_data;// 获取当前设备实例if (p >= GLOBALMEM_SIZE)return0;if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;if (copy_to_user(buf, dev->mem + p, count)) { ret = -EFAULT; } else { *ppos += count; ret = count; pr_info("globalmem: device %d read %u bytes from offset %lu\n", dev->dev_idx, count, p); }return ret;}/** * globalmem_write - 设备写函数(多设备通用,操作当前设备实例的内存) */staticssize_tglobalmem_write(struct file *filp, constchar __user *buf, size_t size, loff_t *ppos){unsignedlong p = *ppos;unsignedint count = size;int ret = 0;structglobalmem_dev *dev = filp->private_data;// 获取当前设备实例if (p >= GLOBALMEM_SIZE)return0;if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;if (copy_from_user(dev->mem + p, buf, count)) { ret = -EFAULT; } else { *ppos += count; ret = count; pr_info("globalmem: device %d written %u bytes to offset %lu\n", dev->dev_idx, count, p); }return ret;}/** * globalmem_llseek - 文件定位函数(多设备通用) */staticloff_tglobalmem_llseek(struct file *filp, loff_t offset, int orig){loff_t ret = 0;structglobalmem_dev *dev = filp->private_data;switch (orig) {case0: // SEEK_SET:从文件开头定位if (offset < 0 || (unsignedint)offset > GLOBALMEM_SIZE) { ret = -EINVAL;break; } filp->f_pos = (unsignedint)offset; ret = filp->f_pos;break;case1: // SEEK_CUR:从当前位置定位if ((filp->f_pos + offset) > GLOBALMEM_SIZE || (filp->f_pos + offset) < 0) { ret = -EINVAL;break; } filp->f_pos += offset; ret = filp->f_pos;break;default: ret = -EINVAL;break; } pr_info("globalmem: device %d seek to offset %lu\n", dev->dev_idx, ret);return ret;}/** * globalmem_ioctl - IO控制函数(多设备通用,清空当前设备实例内存) */staticlongglobalmem_ioctl(struct file *filp, unsignedint cmd, unsignedlong arg){structglobalmem_dev *dev = filp->private_data;// 命令合法性检查if (_IOC_TYPE(cmd) != GLOBALMEM_MAGIC || _IOC_NR(cmd) > 0)return -EINVAL;switch (cmd) {case MEM_CLEAR:memset(dev->mem, 0, GLOBALMEM_SIZE); pr_info("globalmem: device %d memory cleared successfully\n", dev->dev_idx);break;default:return -EINVAL; }return0;}/** * globalmem_setup_cdev - 初始化单个cdev并添加到内核(多设备循环调用) * @dev:单个设备实例指针 * @index:设备索引(次设备号,0~4) */staticvoidglobalmem_setup_cdev(struct globalmem_dev *dev, int index){dev_t devno = MKDEV(globalmem_major, index); dev->dev_idx = index; // 绑定设备索引 cdev_init(&dev->cdev, &globalmem_fops); dev->cdev.owner = THIS_MODULE;int err = cdev_add(&dev->cdev, devno, 1);if (err) { pr_notice("globalmem: error %d adding device %d\n", err, index); } else { pr_info("globalmem: device %d cdev initialized successfully\n", index); }}/** * globalmem_init - 驱动初始化函数(多设备适配,申请多个设备号和实例) */staticint __init globalmem_init(void){int ret, i;dev_t devno = MKDEV(globalmem_major, 0);// 申请DEVICE_NUM个设备号(次设备号0~4)if (globalmem_major) { ret = register_chrdev_region(devno, DEVICE_NUM, DEVICE_NAME); } else { ret = alloc_chrdev_region(&devno, 0, DEVICE_NUM, DEVICE_NAME); globalmem_major = MAJOR(devno); }if (ret < 0) { pr_err("globalmem: failed to allocate device numbers\n");return ret; }// 分配DEVICE_NUM个设备实例的内存(数组形式) globalmem_devp = kzalloc(sizeof(struct globalmem_dev) * DEVICE_NUM, GFP_KERNEL);if (!globalmem_devp) { ret = -ENOMEM;goto fail_malloc; }// 循环初始化每个设备实例for (i = 0; i < DEVICE_NUM; i++) { globalmem_setup_cdev(globalmem_devp + i, i); } pr_info("globalmem: %d devices initialized successfully, major number = %d\n", DEVICE_NUM, globalmem_major);return0;fail_malloc: unregister_chrdev_region(devno, DEVICE_NUM); pr_err("globalmem: failed to allocate memory, driver init failed\n");return ret;}/** * globalmem_exit - 驱动退出函数(多设备适配,释放所有实例资源) */staticvoid __exit globalmem_exit(void){int i;dev_t devno = MKDEV(globalmem_major, 0);// 循环移除每个设备的cdev,释放资源for (i = 0; i < DEVICE_NUM; i++) { cdev_del(&(globalmem_devp + i)->cdev); pr_info("globalmem: device %d cdev removed\n", i); } kfree(globalmem_devp); // 释放设备实例数组内存 unregister_chrdev_region(devno, DEVICE_NUM); // 释放所有设备号 pr_info("globalmem: all devices exited successfully\n");}// 模块加载/卸载入口module_init(globalmem_init);module_exit(globalmem_exit);// 模块信息声明MODULE_AUTHOR("Linux Driver Developer");MODULE_LICENSE("GPL v2");MODULE_DESCRIPTION("Multi-instance Global Memory Character Device Driver for Linux 6.6 Kernel");MODULE_VERSION("1.0");
多设备实例使用说明(配套上述完整代码)
编译方式:与单设备版一致,直接使用原Makefile编译,无需修改(Makefile兼容单/多设备)。
创建设备节点:加载驱动后,需为5个设备实例分别创建节点(次设备号0~4),命令如下:
# 依次创建5个设备节点,对应设备0~4sudo mknod /dev/globalmem0 c 230 0sudo mknod /dev/globalmem1 c 230 1sudo mknod /dev/globalmem2 c 230 2sudo mknod /dev/globalmem3 c 230 3sudo mknod /dev/globalmem4 c 230 4# 修改所有节点权限,允许普通用户读写sudo chmod 666 /dev/globalmem*
测试说明:每个设备节点对应独立的全局内存,操作互不干扰,示例:
# 向设备0写入数据echo"device 0 test data" > /dev/globalmem0# 从设备0读取数据(正常输出写入内容)cat /dev/globalmem0# 向设备1写入不同数据echo"device 1 test data" > /dev/globalmem1# 从设备0读取(仍为原有内容,不受设备1操作影响)cat /dev/globalmem0# 清空设备0内存sudo ioctl /dev/globalmem0 $(echo -n -e "\x01\x67\x00\x00\x00\x00\x00\x00")# 再次读取设备0(无输出,清空成功)cat /dev/globalmem0
卸载驱动:与单设备版一致,执行sudo rmmod globalmem即可,卸载后建议删除所有设备节点。
多设备核心适配要点
- 新增
DEVICE_NUM宏定义,控制设备实例数量,可根据需求修改(如改为3个、10个)。 - 设备结构体新增
dev_idx成员,用于区分不同设备实例,便于日志输出和调试。 - 使用
container_of函数,通过inode的i_cdev成员反向获取设备实例,替代单设备的全局变量直接引用,是多设备驱动的核心技巧。 - 设备号申请、cdev初始化、资源释放均采用循环方式,批量处理所有设备实例,避免重复代码。
此代码可直接替换单设备版代码,编译后即可实现多设备功能,完全适配Linux 6.6内核,无任何兼容性问题。
七、注意事项
主设备号冲突:若预设主设备号(230)已被其他驱动占用,可通过模块参数修改,或使用动态分配设备号(不指定主设备号,内核自动分配)。
内核版本匹配:本驱动基于 Linux 6.6 内核编写,若用于其他内核版本(如5.10+),需微调打印函数和宏定义,核心功能可兼容;低于5.10的内核需修改IO控制接口和废弃宏。
权限问题:设备节点默认仅root用户可读写,测试时可修改节点权限(chmod 666 /dev/globalmem),实际应用中需根据需求设置合理权限。
资源释放:模块卸载前,需确保设备已关闭(无进程占用),否则会导致卸载失败,可通过 lsof /dev/globalmem 查看占用进程,关闭后再卸载模块。