当前位置:首页>Linux>Linux 字符设备驱动基础开发详解(1)

Linux 字符设备驱动基础开发详解(1)

  • 2026-09-02 20:03:36
Linux 字符设备驱动基础开发详解(1)

Linux 字符设备驱动基础开发详解(1)

“

书接上回 Linux 应用程序如何操作底层硬件。 在上一小节中我们分析了 Linux 操作底层硬件的原理,以及什么是字符设备。本节将具体讲解字符设备驱动的开发流程与关键步骤之-如何在内核空间中加载/卸载设备驱动?。


01 学习重点

在明确字符设备概念及其运行空间(内核空间)的基础上,接下来我们需要焦两个核心问题:

  1. 如何在内核空间中加载/卸载设备驱动?
  2. 如何初始化外设? —— 与裸机开发类似,Linux 驱动同样需要初始化相应外设寄存器(这个问题将在下一节中详细学习)。

02 驱动的加载与卸载

两种加载方式

方式
说明
特点
编译进内核
将驱动程序直接编进 Linux 内核
内核启动时自动加载,无需手动操作
编译成模块
编译为 .ko 模块文件
内核启动后用 insmod / rmmod 动态加载/卸载,修改驱动只需重新编译模块,无需重编整个内核

⚠️ 模块依赖与正确用法

内核中可能存在多个驱动,且模块之间会有依赖(互相调用,类似 include、import)。

  • 仅用 insmod:无法自动处理依赖,依赖未加载时可能失败。
  • � 推荐*:使用 modprobe 加载模块。它会解析依赖,按顺序加载当前模块及其依赖。
  • modprobe 默认在 /lib/modules/<kernel-version> 下查找模块(如内核 4.1.15 则查找 /lib/modules/4.1.15)。
  • 卸载建议:用 rmmod 卸载当前模块;慎用 modprobe -r,它会连同依赖一起卸载,可能影响其他模块。

入口与出口:init 与 release

驱动需显式指定加载时入口和卸载时出口,对应「初始化」和「释放」逻辑:

  • module_init(xxx_init):指定模块加载函数。执行 insmod 时,内核会调用 xxx_init()。
  • module_exit(xxx_exit):指定模块卸载函数。执行 rmmod 时,内核会调用 xxx_exit()。
// 驱动入口函数:加载时执行
staticint __init xxx_init(void)
{
// 入口函数具体内容(如注册设备、申请资源)
return0;
}

// 驱动出口函数:卸载时执行
staticvoid __exit xxx_exit(void)
{
// 出口函数具体内容(如注销设备、释放资源)
}

// 将上面两个函数指定为驱动的入口和出口
module_init(xxx_init);
module_exit(xxx_exit);

03 设备与设备号

一切皆文件

Linux 遵循 「一切皆文件」:硬件设备在系统中以设备节点的形式呈现(设备树等机制在后续说明)。
应用层通过操作设备文件来访问设备,背后由 file_operations 与驱动对接。

设备号:主设备号 + 次设备号

为便于管理,每个设备都有设备号,由两部分组成:

部分
含义
主设备号
对应某一类/某一个驱动
次设备号
同一驱动下的不同设备实例
  • 内核用 dev_t(32 位)表示设备号:高 12 位为主设备号,低 20 位为次设备号。
  • 因此主设备号范围为 0~4095,且应不与其他驱动冲突。
  • 查看已占用设备号:**cat /proc/devices**(静态分配时选号前建议先查)。

静态分配 vs 动态分配

  • 静态分配:自己指定主设备号(如 200),需确保未被占用。
  • 动态分配:由内核分配主设备号,避免冲突,推荐在可插拔或通用驱动中使用。
/*
 * 动态申请设备号
 * @param dev      : 输出参数,保存申请到的设备号
 * @param baseminor: 次设备号起始值(常为 0)
 * @param count    : 申请的设备数量(一段连续号,主设备号相同,次设备号从 baseminor 递增)
 * @param name     : 设备名(出现在 /proc/devices)
 */

intalloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, constchar *name);

/*
 * 注销字符设备后释放设备号
 * @param from : 要释放的起始设备号
 * @param count: 从 from 起要释放的设备号数量
 */

voidunregister_chrdev_region(dev_t from, unsigned count);

为了让快速上手,我们本篇先使用设备号静态分配, 以及 最简单的旧版静态注册接口 register_chrdev。


04 file_operations:应用与驱动的桥梁

file_operations 是连接用户空间系统调用与内核驱动实现的桥梁。
用户态调用 open、read、write、close 等,最终会落到该结构体中对应的函数指针。

因此在模块加载函数中需要注册字符设备(绑定设备号与 file_operations),在模块卸载函数中需要注销设备。

注册 / 注销接口(旧接口示例)

/**
 * 注册字符设备(指定主设备号)
 * @param major : 主设备号
 * @param name  : 设备名
 * @param fops  : 指向 file_operations 的指针
 */

staticinlineintregister_chrdev(unsignedint major, constchar *name,
const struct file_operations *fops)
;

/**
 * 注销字符设备
 * @param major : 要注销的设备的主设备号
 * @param name  : 要注销的设备名
 */

staticinlinevoidunregister_chrdev(unsignedint major, constchar *name);

05 实现操作函数与完整流程示例

驱动需要实现 open、read、write、release 等,并填入 **file_operations**,再在 init 中注册、在 exit 中注销。

操作函数与 file_operations 定义

// 打开设备
staticintchrtest_open(struct inode *inode, struct file *filp){ return0; }

// 从设备读取
staticssize_tchrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return0;
}

// 向设备写入
staticssize_tchrtest_write(struct file *filp, constchar __user *buf, size_t cnt, loff_t *offt)
{
return0;
}

// 关闭/释放设备
staticintchrtest_release(struct inode *inode, struct file *filp){ return0; }

// 连接用户空间与内核驱动的核心结构
staticstructfile_operationstest_fops = {
    .owner   = THIS_MODULE,
    .open    = chrtest_open,
    .read    = chrtest_read,
    .write   = chrtest_write,
    .release = chrtest_release,
};

在 init/exit 中注册与注销

#define CHRDEVBASE_MAJOR 200        // 主设备号(需保证未占用)
#define CHRDEVBASE_NAME "chrdevbase"// 设备名

staticint __init xxx_init(void)
{
int retvalue = 0;

    retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &test_fops);
if (retvalue < 0) {
        printk("driver register failed\n");
return retvalue;
    }
return0;
}

staticvoid __exit xxx_exit(void)
{
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase_exit()\n");
}

module_init(xxx_init);
module_exit(xxx_exit);

模块信息(必选与可选)

// LICENSE 必须声明,否则编译会报错
MODULE_LICENSE("GPL");
// 作者信息可选
MODULE_AUTHOR("xxxxx");

06 小结

步骤
内容
1
实现 入口xxx_init、出口xxx_exit,并用 module_init / module_exit 注册
2
实现 open / read / write / release 等,并填入 file_operations
3
在 init 中 注册字符设备(register_chrdev 或新接口 + 设备号),在 exit 中 注销(unregister_chrdev 等)
4
合理选择设备号(静态时用 cat /proc/devices 避免冲突,或使用 动态分配)
5
添加 **MODULE_LICENSE("GPL")**,必要时加 MODULE_AUTHOR

按上述步骤即可完成一个最简字符设备驱动的加载、卸载与读写框架,为后续具体硬件操作打下基础。

最新文章

随机文章