Day02_Linux内核模块
linxu内核模块
1.内核模块文件模板
#include <linux/init.h> #include <linux/module.h>static int __init mycdev_init(void) {return 0; } static void __exit mycdev_exit(void) {} module_init(mycdev_init); module_exit(mycdev_exit); MODULE_LICENSE("GPL");
2.日志打印
2.1查看日志级别
cat /proc/sys/kenel/printk
2.2修改日志级别
echo 4 4 1 7 > /proc/sys/kenel/printk
2.3查看日志指令
dmseg
3.驱动相关工具
3.1查看设备文件
ls -l /dev/myDev
3.2创建字符设备文件
mknod /dev/myDev c 242 0
3.3查看驱动设备
cat /proc/devices
3.4安装/卸载/查看驱动文件
`insmod demo.ko``rmmod demo``lsmod`
设备号
在Linux系统中,设备号(Device Number) 是用于唯一标识设备文件(字符设备或块设备)的数值,内核通过设备号来区分不同的设备,并将设备操作请求路由到对应的驱动程序。
设备号由两部分组成,共同构成一个32位整数(不同内核版本可能有细微差异):
设备号 = 主设备号(Major Number)(bit 0~11) + 次设备号(Minor Number)(bit 12~31)
4.主设备号(Major Number)
- 作用:标识设备所属的驱动程序。同一类设备(使用相同驱动)通常共享相同的主设备号。
- 范围:传统上为8位(0-255),现代内核扩展为12位(0-4095),支持更多设备类型。
- 示例:
- 硬盘(块设备)可能使用主设备号8
- 串口(字符设备)可能使用主设备号4
- 自定义字符设备可能使用动态分配的主设备号(如240)
5.次设备号(Minor Number)
- 作用:标识同一驱动程序管理的不同设备实例。驱动程序通过次设备号区分具体操作哪个设备。
- 范围:20位(0-1,048,575),支持单个驱动管理大量设备。
- 示例:
- 主设备号8(SCSI硬盘)下,次设备号0可能代表
sda
,1代表sda1
(第一个分区)- 主设备号13(输入设备)下,不同次设备号区分键盘、鼠标等不同输入设备
6.设备号的表示与操作
在Linux内核中,设备号通常用dev_t类型表示(定义在<linux/types.h>),可以通过以下宏进行分解和组合:
#include <linux/kdev_t.h>// 从dev_t中提取主设备号 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))// 从dev_t中提取次设备号 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))// 由主、次设备号组合成dev_t #define MKDEV(ma, mi) ((dev_t) (((ma) << MINORBITS) | (mi)))
其中:
MINORBITS
是次设备号的位数(通常为20)MINORMASK
是次设备号的掩码((1 << MINORBITS) - 1
)
7.设备号的分配方式
7.1静态分配:
- 手动指定主设备号(需确保不与已有设备冲突)
- 参考
/proc/devices
查看已分配的主设备号
7.2动态分配:
- 由内核自动分配未使用的主设备号(推荐方式)
- 使用
alloc_chrdev_region()
函数(字符设备)int alloc_chrdev_region(dev_t *dev, unsigned int first_minor, unsigned int count, const char *name);
8.查看设备号
8.1用户空间:通过ls -l /dev 查看设备文件的主/次设备号:
crw-rw-rw- 1 root root 1, 5 Apr 1 00:00 zero # 主设备号1,次设备号5 brw-rw---- 1 root disk 8, 0 Apr 1 00:00 sda # 主设备号8,次设备号0
8.2内核空间:通过/proc/devices 查看已注册的设备及其主设备号:
Character devices:1 mem4 /dev/vc/07 vcs Block devices:8 sd65 sd
9.总结
设备号是Linux设备管理的核心机制:
- 主设备号关联驱动程序,次设备号区分设备实例
- 内核通过设备号将操作请求分发到正确的驱动和设备
- 动态分配是现代驱动开发的推荐方式,可避免设备号冲突
理解设备号对于设备驱动开发、设备管理和系统调试都至关重要。
相关函数
10.打印-printk()
int printk(const char *fmt, ...);
printk
是 Linux 内核中最常用的调试和信息输出函数,类似于用户空间的printf
,但专为内核环境设计,定义在<linux/kernel.h>
头文件中。主要特点
- 支持格式化字符串输出,格式符与
printf
基本相同(% d, % s, % p 等)- 输出内容默认发送到内核日志缓冲区,可通过
dmesg
命令查看- 具有日志级别机制,用于控制消息的重要性和输出目的地
日志级别
printk
支持在格式字符串前添加日志级别标识符,格式为<n>
,其中n
是 0-7 的数字,代表不同级别:#define KERN_EMERG "<0>" // 系统紧急情况,必须立即处理 #define KERN_ALERT "<1>" // 需要立即采取动作 #define KERN_CRIT "<2>" // 临界条件 #define KERN_ERR "<3>" // 错误条件 #define KERN_WARNING "<4>" // 警告条件 #define KERN_NOTICE "<5>" // 正常但值得注意的情况 #define KERN_INFO "<6>" // 信息性消息 #define KERN_DEBUG "<7>" // 调试消息
如果不指定级别,会使用默认级别 DEFAULT_MESSAGE_LOGLEVEL(通常是 <4> 警告级别)。
11.驱动注册
struct file_operations{}-结构体
11.1结构体原型(简化版)
struct file_operations {struct module *owner; // 模块所有者,通常为 THIS_MODULEloff_t (*llseek) (struct file *, loff_t, int); // 定位文件指针ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); // 读操作ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 写操作ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); // 迭代读(新接口)ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); // 迭代写(新接口)int (*iopoll)(struct kiocb *kiocb, bool spin); // I/O 轮询int (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); // 无锁 IO 控制int (*compat_ioctl) (struct file *, unsigned int, unsigned long); // 兼容模式 IO 控制int (*mmap) (struct file *, struct vm_area_struct *); // 内存映射int (*open) (struct inode *, struct file *); // 打开文件int (*flush) (struct file *, fl_owner_t id); // 刷新操作int (*release) (struct inode *, struct file *); // 释放文件int (*fsync) (struct file *, loff_t, loff_t, int datasync); // 同步文件int (*aio_fsync) (struct kiocb *, int datasync); // 异步同步int (*fasync) (int, struct file *, int); // 异步通知int (*lock) (struct file *, int, struct file_lock *); // 文件锁ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); // 发送页面unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); // 获取未映射区域int (*check_flags)(int); // 检查文件标志int (*flock) (struct file *, int, struct file_lock *); // 文件锁ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); // 拼接写ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); // 拼接读int (*setlease)(struct file *, long, struct file_lock **, void **); // 设置租约long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); // 预分配空间void (*show_fdinfo)(struct seq_file *m, struct file *f); // 显示文件描述符信息 };
struct file_operations是 Linux 内核中最重要的结构体之一,定义在 <linux/fs.h> 头文件中,用于建立设备文件(或特殊文件)与内核驱动程序之间的操作映射关系。它包含了一系列函数指针,每个指针对应一种文件操作(如打开、读取、写入等),是字符设备、块设备和伪设备驱动的核心。
关键成员说明
1.
owner
:指向模块的指针,通常设为THIS_MODULE
,用于内核模块引用计数管理,确保模块在被使用时不被卸载。2.核心操作函数 :
read
/write
:最常用的读写操作,分别对应用户空间的read()
和write()
系统调用。
open
/release
:打开和关闭文件时触发,用于初始化或释放资源(如申请内存、释放锁等)。
unlocked_ioctl
:处理用户空间的ioctl()
系统调用,用于设备控制(如配置设备参数)。
mmap
:支持将设备内存映射到用户空间,允许用户直接访问设备内存。3. 其他重要函数 :
llseek
:用于移动文件指针(类似标准库的fseek
)。
fsync
:确保数据同步到物理设备(类似fsync
系统调用)。
fasync
:支持异步通知机制,当设备状态变化时主动通知用户空间。
使用示例(字符设备驱动)
#include <linux/fs.h> #include <linux/module.h>// 读操作实现 static ssize_t my_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {// 使用 copy_to_user 将内核数据复制到用户空间// ...return count; }// 写操作实现 static ssize_t my_dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {// 使用 copy_from_user 将用户数据复制到内核空间// ...return count; }// 打开操作实现 static int my_dev_open(struct inode *inode, struct file *filp) {// 初始化设备(如分配缓冲区、启动硬件等)return 0; }// 释放操作实现 static int my_dev_release(struct inode *inode, struct file *filp) {// 清理资源(如释放缓冲区、关闭硬件等)return 0; }// 定义 file_operations 结构体 static struct file_operations my_dev_fops = {.owner = THIS_MODULE,.open = my_dev_open,.release = my_dev_release,.read = my_dev_read,.write = my_dev_write,// 其他需要的操作... };// 模块初始化时注册字符设备 static int __init my_dev_init(void) {// 注册设备,关联 file_operations// ...return 0; }module_init(my_dev_init); MODULE_LICENSE("GPL");
注意事项:
1. 按需实现 :不需要的操作可以设为
NULL
,内核会使用默认行为(通常返回错误)。2. 权限检查 :用户空间的操作可能需要权限验证(如
CAP_SYS_ADMIN
),需在函数中显式处理。3. 并发安全 :多个进程可能同时操作设备,需通过锁(如
mutex
、spinlock
)保证线程安全。4. 错误处理 :操作失败时应返回标准错误码(如
-EIO
表示 I/O 错误,-ENOMEM
表示内存不足)。
struct file_operations 是内核驱动与用户空间交互的桥梁,理解并正确实现其中的函数是编写 Linux 设备驱动的核心技能。
12.register_chrdev()
#include <linux/fs.h>int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
register_chrdev()
是 Linux 内核中用于注册字符设备的传统函数,定义在<linux/fs.h>
头文件中,用于将字符设备驱动与设备号关联起来,使用户空间能够通过文件系统接口访问设备。参数说明
major
:主设备号。若为 0,内核会自动分配一个未使用的主设备号name
:设备名称,会显示在/proc/devices
中fops
:指向struct file_operations
的指针,定义了设备支持的操作集合返回值
- 成功:返回 0(当指定主设备号时)或分配的主设备号(当
major
为 0 时)- 失败:返回负数错误码(如
EBUSY
表示主设备号已被占用)
13.unregister_chrdev()
#include <linux/fs.h>void unregister_chrdev(unsigned int major, const char *name);
unregister_chrdev()
是 Linux 内核中用于注销字符设备的函数,与register_chrdev()
配套使用,用于在模块卸载时释放字符设备所占用的资源(主要是设备号)参数说明
major
:要注销的字符设备的主设备号(必须与注册时使用的主设备号一致)name
:设备名称(必须与register_chrdev()
中指定的名称一致)函数作用
- 从内核字符设备列表中移除指定的设备注册信息
- 释放主设备号,使其可被其他设备重新使用
- 解除设备与
file_operations
操作集的关联
14.alloc_chrdev_region()
#include <linux/fs.h>int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
alloc_chrdev_region
是 Linux 内核中用于动态分配字符设备号的函数,属于字符设备驱动注册的核心接口。函数功能
- 动态分配设备号:自动分配一个未使用的主设备号(范围 1~254),并基于此分配连续的次设备号范围。
- 避免冲突:适用于可加载模块场景,无需手动指定主设备号,减少设备号冲突风险。
- 内核记录:分配的设备号会注册到内核的
chrdevs
散列表,设备名称显示在/proc/devices
中。参数&返回值:
类型 参数 说明 dev_t*
dev
输出参数,存储分配到的起始设备号(主设备号 + 起始次设备号)。 unsigned int
baseminor
起始次设备号,通常设为 0
。unsigned int
count
需分配的连续次设备号数量。 const char*
name
设备名称(长度 ≤64 字节),出现在 /proc/devices
中(如"mydev"
)。返回值:成功返回
0
;失败返回错误码(如-EBUSY
、-EINVAL
)。实现原理:
- 动态分配主设备号: 内核从
chrdevs
数组尾部(主设备号 254)向前扫描,找到第一个空闲位置作为主设备号。若全部占用则返回EBUSY
。- 注册设备号范围: 调用
__register_chrdev_region(0, baseminor, count, name)
,其中major=0
触发动态分配逻辑。- 存储映射关系: 成功后在
chrdevs
数组中建立主设备号-次设备号范围-设备名
的映射。
15.unregister_chrdev_region()
#include <linux/fs.h>void unregister_chrdev_region(dev_t from, unsigned int count);
unregister_chrdev_region()
是 Linux 内核字符设备驱动开发中的核心资源释放函数,用于注销并释放先前通过alloc_chrdev_region()
或register_chrdev_region()
申请的设备号资源。功能与定位
- 资源释放 释放字符设备驱动占用的设备号资源(主设备号 + 次设备号范围),避免系统资源泄漏。
- 生命周期管理 通常在驱动模块卸载函数(
xxx_exit
)中调用,与cdev_del()
配合完成驱动的完整注销。- 内核记录清理 从内核的
chrdevs
散列表中删除设备号记录,更新/proc/devices
文件内容参数&返回值
类型 参数 说明 dev_t
from
需释放的起始设备号(包含主设备号和起始次设备号) unsigned int
count
连续设备号数量(需与注册时一致) 错误处理:若参数无效(如
count=0
或设备号未注册),内核可能触发WARN_ON
警告,但不会返回错误码内部实现原理
- 散列表操作 根据
from
的主设备号定位内核chrdevs
散列表的哈希桶,删除对应的probe
节点(存储设备号范围与设备名)。- 资源释放 释放设备号资源池中的连续设备号区间,允许其他驱动复用。
- 状态更新 更新
/proc/devices
文件,移除已注销的设备条目新旧接口对比
特性 unregister_chrdev_region
(新式)unregister_chrdev
(旧式)适用注册函数 alloc_chrdev_region
/register_chrdev_region
register_chrdev
资源粒度 精确控制次设备号范围(如仅释放 0~2 号) 强制释放整个主设备号(256 个次设备号) 灵活性 支持多设备实例共享主设备号 独占主设备号,浪费资源 现代驱动推荐 ✅ 是(≥Linux 2.6) ❌ 否(逐步淘汰
16.struct cdev{}-结构体
struct cdev {struct kobject kobj; // 内嵌的内核对象,用于设备模型管理[1]struct module *owner; // 指向所属模块(通常为 `THIS_MODULE`),防止模块卸载时设备被使用const struct file_operations *ops; // 关键!指向文件操作集,如 `open`、`read`、`write`struct list_head list; // 链表头,用于将已注册设备串联到内核全局链表dev_t dev; // 设备号(32位,高12位主设备号,低20位次设备号)unsigned int count; // 关联的次设备号数量(通常为1) };
struct cdev
是 Linux 内核中描述字符设备的核心数据结构,用于将设备号、文件操作接口(如读写函数)与内核模块绑定,实现用户空间对硬件的访问。
成员 作用 示例/说明 ops
定义设备操作函数(如 read
、write
),用户空间系统调用最终触发此处函数驱动需实现 struct file_operations
dev
唯一标识设备,主设备号区分驱动类型,次设备号区分实例 MKDEV(250, 0)
生成设备号count
支持多个次设备号共享同一驱动(如SCSI磁带机) 若为 n
,则次设备号范围[0, n-1]
owner
模块引用计数管理,避免模块卸载时设备正在使用 必须设为 THIS_MODULE
操作cdev 的核心函数
函数 作用 使用场景 cdev_init()
初始化 cdev
,绑定file_operations
驱动初始化阶段,需先清零结构体并链接操作集 cdev_add()
注册设备到内核,使设备“激活” 调用后设备可被操作,需确保驱动已准备就绪 cdev_del()
注销设备,移除内核中的 cdev
模块卸载时调用,之后不可再访问 cdev
cdev_alloc()
动态分配 cdev
内存(替代静态定义)需灵活管理设备对象时使用
设备号管理函数
- 静态申请:
register_chrdev_region(dev_t from, unsigned count, char *name)
已知设备号时使用(需避免冲突)。- 动态申请:
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, char *name)
系统自动分配空闲设备号。- 释放设备号:
unregister_chrdev_region(dev_t from, unsigned count)
。
字符设备驱动开发流程
- 申请设备号 静态(
register_chrdev_region
)或动态(alloc_chrdev_region
)获取设备号。- 定义文件操作集 实现
file_operations
中的关键函数(如open
、read
、write
)。- 初始化并注册
cdev
struct cdev my_cdev; cdev_init(&my_cdev, &my_fops); // 绑定操作集 cdev_add(&my_cdev, devno, 1); // 注册设备,count=1
4.创建设备节点 手动
mknod /dev/device c 250 0
或自动创建设备文件(如device_create
)。5.注销与清理 模块卸载时反向操作:
cdev_del(&my_cdev); unregister_chrdev_region(devno, 1);
关键注意点
ops
是核心:用户空间的open()
、read()
等系统调用通过ops
映射到驱动函数。cdev_add
的时机:调用后设备立即生效,需确保驱动完全就绪。- 老式驱动模型:
register_chrdev()
已过时,新驱动应使用cdev
接口。通过
struct cdev
,Linux 实现了字符设备的统一抽象,将硬件操作与用户接口解耦,是驱动开发的基石
17.cdev_init()
#include <linux/cdev.h>void cdev_init(struct cdev *cdev, const struct file_operations *fops);
cdev_init()
是 Linux 内核中字符设备驱动的核心初始化函数,用于关联字符设备结构体(struct cdev
)与文件操作函数集(struct file_operations
)功能与定位
- 初始化
cdev
结构体 清除cdev
内存并初始化其内部成员(如链表头list
和内核对象kobj
)。- 绑定文件操作集 将用户定义的
file_operations
结构体指针赋值给cdev->ops
,建立设备操作函数(如open
、read
、write
)的映射关系。- 为注册做准备 与
cdev_add()
配合使用,完成字符设备向内核的注册,使设备可被应用层访问
类型 参数 说明 struct cdev*
cdev
待初始化的字符设备结构体指针(需已分配内存) struct file_operations*
fops
设备操作函数集合(包含 open
、read
等函数指针)
内部实现原理
源码实现如下(简化版)
void cdev_init(struct cdev *cdev, const struct file_operations *fops) {memset(cdev, 0, sizeof(*cdev)); // 清零结构体INIT_LIST_HEAD(&cdev->list); // 初始化链表头kobject_init(&cdev->kobj, &ktype_cdev_default); // 初始化内核对象cdev->ops = fops; // 绑定文件操作集 }
关键步骤
- 内存清零:避免未初始化字段导致内核异常。
- 链表初始化:将设备加入内核管理的字符设备链表。
- 内核对象初始化:关联设备生命周期与内核对象机制(如引用计数)。
- 操作集绑定:使后续系统调用(如
open()
)能定位到驱动实现的函数
典型使用流程
字符设备驱动的注册需三步:申请设备号 → 初始化
cdev
→ 注册到内核#include <linux/cdev.h>static dev_t devno; // 设备号 static struct cdev my_cdev; // 字符设备结构体 static struct file_operations my_fops = { // 文件操作集.owner = THIS_MODULE,.open = my_open,.read = my_read, };// 模块加载函数 static int __init my_init(void) {// 1. 申请设备号(动态或静态)alloc_chrdev_region(&devno, 0, 1, "mydev");// 2. 初始化 cdevcdev_init(&my_cdev, &my_fops); // 关键步骤my_cdev.owner = THIS_MODULE;// 3. 注册到内核cdev_add(&my_cdev, devno, 1);return 0; }// 模块卸载函数 static void __exit my_exit(void) {cdev_del(&my_cdev); // 删除设备unregister_chrdev_region(devno, 1); // 释放设备号 }
cdev_init vs cdev_alloc
两者均用于初始化cdev,但适用场景不同
特性 cdev_init()
cdev_alloc()
内存管理 需提前分配 cdev
内存(栈或静态区)动态分配 cdev
内存(kzalloc
)初始化内容 需显式绑定 fops
需手动设置 fops
和owner
使用场景 静态定义 cdev
时需动态创建设备(如模块卸载时自动释放)
18.cdev_add()
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
cdev_add()
是 Linux 内核字符设备驱动开发中的核心注册函数,负责将字符设备对象(struct cdev
)与设备号关联并加入内核管理系统,使设备可被用户空间访问。功能与定位
- 设备注册:将初始化后的
cdev
结构体注册到内核的字符设备管理系统中,建立设备号与驱动的关联。- 激活设备:调用成功后,设备立即“生效”(live),用户空间可通过文件操作(如
open()
、read()
)触发驱动函数。- 设备号映射:支持连续次设备号范围(如
count=3
对应次设备号 0、1、2),适用于多实例设备(如 SCSI 磁带驱动)
参数&返回值
类型 参数 说明 struct cdev*
p
指向已初始化的 cdev
结构体(需通过cdev_init
绑定file_operations
)dev_t
dev
起始设备号(由 alloc_chrdev_region
或register_chrdev_region
分配)unsigned int
count
连续次设备号数量(通常为 1,多实例设备需扩展) 返回值:成功返回0;失败返回负错误码(如-EBUSY 设备号冲突)。
内部实现原理
- 设备号绑定 将
dev
和count
赋值给cdev->dev
和cdev->count
。- 哈希表注册 调用
kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p)
,将设备信息加入全局散列表cdev_map
:
- 以主设备号(
major = MAJOR(dev)
)为索引计算哈希桶位置(i = major % 255
)。- 将设备信息存入
struct probe
节点(含设备号范围、cdev
指针),链接至散列表。- 设备可访问性 注册后,用户层调用
open()
时,内核通过kobj_lookup()
根据设备号检索cdev->ops
,执行驱动函数(如open
、read
)
常见错误与处理
错误场景 原因 解决方案 返回 -EBUSY
设备号已被占用 换用动态分配或调整设备号范围 返回 -EINVAL
参数非法(如 count=0
)检查次设备号范围是否有效 用户层 open()
失败cdev_add
后未绑定ops
函数检查 file_operations
是否完整实现内核崩溃(NULL 指针) 未初始化 cdev
或ops
为NULL
确保 cdev_init
调用且fops
函数已实现资源释放与生命周期
- 注销设备 卸载驱动时调用
cdev_del()
,从cdev_map
删除设备记录:
- 已打开的设备仍可操作(如
release
函数仍会被调用)。- 新打开的请求被拒绝(设备节点失效)。
- 释放设备号 必须调用
unregister_chrdev_region()
释放设备号资源。- 内存释放 动态分配的
cdev
(如cdev_alloc()
分配)需额外kfree()
对比静态注册函数
特性 cdev_add
(动态注册)register_chrdev
(旧式静态注册)灵活性 支持多设备号、精细控制 固定主设备号,次设备号范围受限 资源占用 按需分配,节省内核空间 占用 256 个次设备号(浪费资源) 适用场景 现代驱动开发(≥Linux 2.6) 兼容旧驱动(已逐步淘汰) 复杂度 需手动管理 cdev
和设备号生命周期单函数调用,但功能受限
19.cdev_del()
void cdev_del(struct cdev *p);
cdev_del()是 Linux 字符设备驱动开发中的核心注销函数,用于从内核中移除已注册的字符设备对象(struct cdev),释放相关资源。
功能与定位
- 设备注销 将字符设备从内核的全局散列表
cdev_map
中移除,断开设备号与文件操作函数集(file_operations
)的关联。- 资源释放 触发
kobject_put()
释放cdev
的内核对象(kobj
),减少引用计数并可能回收内存(若为动态分配)。- 生命周期终结 调用后设备进入“失效”状态:已打开的设备文件仍可操作(如
release()
函数会被调用),但新open()
请求将失败
类型 参数 说明 struct cdev *
p
指向已注册的字符设备结构体(需非空且有效) 内部实现原理
- 散列表解绑 调用
cdev_unmap(p->dev, p->count)
,从全局散列表cdev_map
中删除设备号范围对应的probe
节点。- 内核对象销毁 通过
kobject_put(&p->kobj)
释放cdev
的嵌入对象,若引用计数归零则触发内存回收(动态分配时)。- 设备状态更新 更新
/proc/devices
文件,移除设备条目与相关函数的协作
- 初始化与注册
cdev_init()
→cdev_add()
构成设备注册闭环。- 资源释放链
cdev_del()
→unregister_chrdev_region()
确保资源完全释放。- 旧式接口对比
cdev_del
替代了过时的unregister_chrdev
,支持细粒度设备管理(如释放部分次设备号)
20.系统调用copy_to_user()
#include <linux/uaccess.h>unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
copy_to_user()是 Linux 内核中一个非常重要的函数,用于将数据从内核空间复制到用户空间
函数作用
- 将内核空间地址
from
处的n
个字节数据复制到用户空间地址to
处- 是内核与用户空间进行数据交互的安全方式
参数说明
to
:用户空间的目标地址(必须用__user
修饰,表明这是用户空间地址)from
:内核空间的源地址n
:要复制的字节数返回值
- 成功:返回 0
- 失败:返回未复制的字节数(非零值表示复制失败)
21.copy_from_user()
#include <linux/uaccess.h>unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
copy_from_user()
是 Linux 内核中与copy_to_user()
对应的函数,用于将数据从用户空间复制到内核空间函数作用
- 将用户空间地址
from
处的n
个字节数据复制到内核空间地址to
处- 提供内核从用户空间安全获取数据的机制
参数说明
to
:内核空间的目标地址from
:用户空间的源地址(必须用__user
修饰)n
:要复制的字节数返回值
- 成功:返回 0
- 失败:返回未复制的字节数(非零值表示复制失败)
22.内存映射ioremap()
#include <linux/io.h>void __iomem *ioremap(resource_size_t phys_addr, size_t size);
在 Linux 内核中,
<linux/io.h>
提供的ioremap
函数是内核访问设备 I/O 内存的核心接口,用于将硬件设备的物理地址空间映射到内核虚拟地址空间,以便内核通过虚拟地址便捷地读写设备寄存器或内存。它是体系结构无关的统一接口,屏蔽了底层硬件差异,是设备驱动开发中操作 I/O 内存的标准方式。参数:
phys_addr
:需要映射的物理起始地址(通常是设备的 I/O 内存地址,可通过platform_get_resource
等函数从设备树或资源中获取)。size
:需要映射的内存大小(字节数)。返回值:
- 成功:返回映射后的内核虚拟地址(类型为
void __iomem *
,__iomem
是内核用于标记 I/O 内存地址的属性,避免与普通内存混淆)。- 失败:返回
NULL
(如物理地址无效、权限不足或映射范围超出系统支持)。核心功能
- 物理地址到虚拟地址的映射:内核无法直接访问物理地址(需通过页表转换),
ioremap
会为指定的物理地址范围创建页表项,将其映射到内核虚拟地址空间的某个区域(通常是高端内存区域),使内核可通过虚拟地址读写设备的 I/O 内存。- 处理硬件特性:底层会根据架构自动配置内存属性(如缓存、对齐等)。例如:
- 对于需要 “无缓存” 访问的设备寄存器(避免 CPU 缓存导致数据不一致),底层实现会禁用该区域的缓存。
- 确保映射地址满足 CPU 对齐要求(如某些架构要求访问 32 位寄存器时地址对齐到 4 字节)。
- 屏蔽架构差异:不同 CPU 架构(如 x86、ARM、RISC-V)的物理地址映射机制不同,但
<linux/io.h>
中的ioremap
封装了这些差异,驱动开发者无需关心底层细节,只需调用统一接口即可。使用场景
ioremap
主要用于设备驱动中访问设备的 I/O 内存,例如:
- 读写网卡、显卡、传感器等外设的控制寄存器。
- 访问设备的缓冲区(如 DMA 缓冲区的物理地址映射)。
23.iounmap()
#include <linux/io.h>void iounmap(void __iomem *addr);
在 Linux 内核中,
<linux/io.h>
提供的iounmap
函数是ioremap
的配套函数,用于释放由ioremap
(或其变体,如ioremap_nocache
、ioremap_wc
等)建立的物理地址到虚拟地址的映射,避免内核地址空间泄漏。它是设备驱动开发中管理 I/O 内存映射的必备函数。参数:
addr
:由ioremap
系列函数返回的内核虚拟地址(类型为void __iomem *
),即需要解除映射的地址。返回值:
无(
void
)。核心功能
iounmap
的主要作用是撤销ioremap
建立的页表映射关系,并释放相关资源:
- 从内核页表中移除与该虚拟地址对应的页表项,确保后续对该虚拟地址的访问会触发错误(避免野指针)。
- 释放映射过程中分配的辅助资源(如页表相关数据结构),具体依赖底层架构实现。
- 对于某些架构,可能会清理与该映射相关的缓存状态(如 invalidate 缓存),确保硬件状态一致性。
24.设备节点class_create()
#include <linux/device.h>struct class *class_create(struct module *owner, const char *name);
在 Linux 内核驱动开发中,
class_create
是一个非常重要的函数,它用于在 sysfs 中创建一个设备类(class),为后续创建设备节点提供统一的分类和管理。设备类的主要作用是将功能相似的设备组织在一起,方便用户空间识别和操作,同时也是 udev/mdev 自动创建设备节点的基础。参数说明:
owner
:通常传入THIS_MODULE
,表示该类属于当前模块,用于模块引用计数管理。name
:类的名称,会在/sys/class/
目录下创建以该名称命名的子目录(如/sys/class/myled
)。返回值:
- 成功:返回指向
struct class
结构体的指针。- 失败:返回
ERR_PTR(error_code)
(错误指针),可通过IS_ERR()
宏判断是否出错,通过PTR_ERR()
宏获取具体错误码。核心作用
- 在 sysfs 中创建类目录调用
class_create
后,内核会在/sys/class/
下创建一个与name
同名的目录(如/sys/class/mydevcls
),用于存放该类下所有设备的属性文件,实现设备的 sysfs 接口。- 为设备节点创建提供基础单独的
class_create
仅创建类,还需通过device_create
函数为该类添加具体设备,udev/mdev
会根据类和设备的信息自动在/dev/
目录下创建设备节点(如/dev/myDev
)。- 设备分类管理同类设备(如所有 LED 设备、所有串口设备)可归到同一个类中,方便用户空间按类查找和操作设备(例如通过
/sys/class/
快速定位设备)。使用流程
在字符设备驱动中,
class_create
通常与device_create
配合使用,完整流程如下:
- 注册字符设备:通过
register_chrdev
或alloc_chrdev_region
注册设备号和操作函数集。- 创建设备类:调用
class_create
创建类。- 创建设备节点:调用
device_create
为类添加设备,自动生成/dev/
下的设备节点。- 驱动卸载时清理:通过
device_destroy
销毁设备,class_destroy
销毁类,最后注销字符设备。示例代码
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/device.h>#define DEV_NAME "mydev" #define CLASS_NAME "mydevcls"static int major; static struct class *myclass;// 设备操作函数集(简化) static struct file_operations fops = {.owner = THIS_MODULE,// 其他操作(open、read、write 等) };static int __init mydev_init(void) {// 1. 注册字符设备major = register_chrdev(0, DEV_NAME, &fops); // 动态分配主设备号if (major < 0) {printk("注册字符设备失败\n");return major;}// 2. 创建设备类myclass = class_create(THIS_MODULE, CLASS_NAME);if (IS_ERR(myclass)) { // 判断类创建是否失败printk("类创建失败\n");unregister_chrdev(major, DEV_NAME); // 回滚:注销设备return PTR_ERR(myclass);}// 3. 创建设备节点(/dev/mydev)device_create(myclass, NULL, MKDEV(major, 0), NULL, DEV_NAME);printk("驱动加载成功,主设备号:%d\n", major);return 0; }static void __exit mydev_exit(void) {// 4. 卸载时清理:先销毁设备,再销毁类,最后注销设备device_destroy(myclass, MKDEV(major, 0));class_destroy(myclass);unregister_chrdev(major, DEV_NAME);printk("驱动卸载成功\n"); }module_init(mydev_init); module_exit(mydev_exit); MODULE_LICENSE("GPL");
注意事项
- 错误处理必须通过
IS_ERR()
宏判断class_create
的返回值是否为错误指针,若失败需释放已分配的资源(如设备号),避免内存泄漏。- 与 device_create 配合
class_create
仅创建类框架,需通过device_create
创建设备实例,才能在/dev/
下生成可操作的设备节点。device_create
的原型如下:struct device *device_create(struct class *class, struct device *parent,dev_t devt,void *drvdata, const char *fmt, ...);
其中
devt
是设备号(由主设备号和次设备号组成),fmt
是设备节点名称。3.sysfs 与 udev 的关系
class_create
在 sysfs 中创建的类信息会被 udev/mdev 监控,udev 会根据这些信息自动在/dev/
下创建设备节点,无需手动执行mknod
命令。4.卸载时的顺序驱动卸载时,必须先通过
device_destroy
销毁所有设备,再通过class_destroy
销毁类,最后注销字符设备,否则会导致内核崩溃。
总结
class_create是 Linux 内核驱动中实现设备节点自动管理的核心函数之一,它通过在 sysfs 中创建设备类,为设备的分类、属性管理和自动节点创建提供了基础。在实际驱动开发中,需严格遵循「注册设备 → 创建类 → 创建设备 → 反向卸载」的流程,并做好错误处理,确保驱动的稳定性。
25.class_destroy()
#include <linux/device.h>void class_destroy(struct class *cls);
在 Linux 内核驱动开发中,
class_destroy
函数用于销毁通过class_create
函数创建的设备类(class),是设备类生命周期管理的重要函数,通常在驱动卸载时调用,以释放相关资源,避免内存泄漏。参数:
cls
是通过class_create
创建的设备类指针(struct class *
类型)。返回值:无返回值(
void
)。功能说明
class_destroy
的主要作用是:
- 释放设备类(
struct class
)占用的内核资源,包括其内部管理的数据结构(如类属性、kobject 等)。- 从内核的类系统中移除该设备类,确保内核不再对其进行管理。
需要注意的是,调用
class_destroy
前,必须确保该类下的所有设备已通过device_destroy
销毁,否则可能导致资源释放不彻底或内核错误。
26.device_create()
#include <linux/device.h>struct device *device_create(struct class *cls, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...);
在 Linux 内核驱动开发中,
device_create
是一个关键函数,用于在已创建的设备类(class)下创建设备节点(device node),从而使用户空间能够通过文件系统(通常是/dev
目录)访问内核驱动管理的硬件设备。参数说明:
cls
:指向已通过class_create
创建的设备类结构体(struct class
),表示设备所属的类。parent
:父设备指针,通常设为NULL
(表示无父设备)。devt
:设备号(dev_t
类型),由主设备号和次设备号组合而成(可通过MKDEV(major, minor)
生成)。drvdata
:驱动私有数据指针,可在设备操作中使用,通常设为NULL
。fmt
:设备节点名称的格式化字符串(如"myled"
,最终会在/dev
目录下生成/dev/myled
节点)。- 可变参数(
...
):用于填充fmt
中的格式化占位符。返回值:
- 成功:返回指向新创建的设备结构体(
struct device
)的指针。- 失败:返回
ERR_PTR(error_code)
(错误指针,可通过IS_ERR()
判断)。功能说明
- 创建设备节点:在
/dev
目录下生成一个与驱动关联的设备文件(如/dev/myled
),用户空间程序可通过读写该文件与内核驱动交互。- 关联设备类:将设备归属于
cls
参数指定的设备类,便于内核和用户空间对设备进行分类管理(如通过/sys/class
目录查看类信息)。- 注册设备:将设备信息注册到内核设备模型中,参与内核的设备生命周期管理。
使用示例
#include <linux/device.h>struct class *cls; // 设备类指针 dev_t dev_num; // 设备号// 假设已通过 class_create 创建 cls,通过 register_chrdev 注册设备号 dev_num// 创建设备节点 struct device *dev = device_create(cls, // 所属类NULL, // 无父设备dev_num, // 设备号NULL, // 无私有数据"mydev" // 设备节点名称(生成 /dev/mydev) );if (IS_ERR(dev)) {printk("设备创建失败: %ld\n", PTR_ERR(dev));// 错误处理(如销毁类、注销设备号等) }
注意事项
- 配对使用
device_destroy
:设备创建后,需在驱动退出时通过device_destroy(cls, devt)
销毁设备节点,避免资源泄露。- 设备号唯一性:
devt
必须是已通过register_chrdev
或alloc_chrdev_region
注册的有效设备号,否则会创建失败。- 错误处理:必须检查返回值是否为错误指针(通过
IS_ERR()
判断),并通过PTR_ERR()
获取具体错误码。- 用户空间访问:设备节点创建后,用户空间可通过
open
/read
/write
等系统调用操作该节点,间接调用驱动中定义的file_operations
方法。
与 class_create的关系
class_create
用于创建设备类(逻辑分类),device_create
用于在该类下创建设备实例(物理设备节点)。- 一个设备类可以包含多个设备(通过多次调用
device_create
生成多个节点,使用不同的次设备号)。通过
device_create
,内核驱动能够便捷地向用户空间暴露设备接口,是字符设备驱动开发中连接内核与用户空间的重要桥梁。
27.device_destroy()
#include <linux/device.h>void device_destroy(struct class *class, dev_t devt);
在 Linux 内核驱动开发中,
device_destroy
是一个用于销毁由device_create
创建的设备节点的函数。它通常与class_create
、device_create
配合使用,完成设备节点的生命周期管理,确保驱动卸载时正确清理资源,避免内存泄漏或系统残留无效设备。参数:
class
:指向设备所属的struct class
结构体指针(由class_create
创建)。devt
:设备的设备号(dev_t
类型,通常通过MKDEV(major, minor)
生成)。返回值:无(
void
)。功能说明
device_destroy
的主要作用是:
- 从内核中移除由
device_create
创建的设备节点(即/dev
目录下的对应文件)。- 释放设备节点相关的内核资源(如设备结构体、引用计数等)。
- 通知系统设备已被销毁,触发相关的内核事件(如
uevent
)。使用场景
device_destroy
通常在驱动的退出函数(module_exit
注册的函数)中调用,用于与驱动初始化时的device_create
操作对应,确保资源释放。
28.MKDEV()宏函数
#include <linux/kdev_t.h>#define MKDEV(major, minor) ((dev_t)(((major) << MINORBITS) | ((minor) & MINORMASK)))
在 Linux 内核编程中,
MKDEV
是一个非常重要的宏,用于将主设备号(major)和次设备号(minor)组合成一个完整的设备号(dev_t 类型)。设备号在 Linux 中是一个 32 位的值,其中高 12 位表示主设备号,低 20 位表示次设备号(不同架构可能有差异,但通常是这样分配的)。
其中:
major
是主设备号minor
是次设备号MINORBITS
是次设备号所占的位数(通常为 20)MINORMASK
是次设备号的掩码(通常为 0xfffff)
使用示例:
#include <linux/kdev_t.h>// 创建一个主设备号为 10,次设备号为 5 的设备号 dev_t dev = MKDEV(10, 5);
对应的,Linux 还提供了两个宏来从设备号中提取主设备号和次设备号:
MAJOR(dev)
:从设备号中提取主设备号MINOR(dev)
:从设备号中提取次设备号这些宏在设备驱动开发中非常常用,用于注册设备、创建设备节点等操作。
设备IO
29.ioctl()
#include <sys/ioctl.h>int ioctl(int fd, unsigned long request, ...);
ioctl
允许用户空间程序向设备驱动发送控制命令,执行一些无法通过标准read
、write
操作完成的特殊功能,例如:
- 配置设备参数(如波特率、分辨率)
- 获取设备状态信息(如缓冲区大小、连接状态)
- 触发设备特定操作(如刷新缓冲区、重启设备)
- 进行硬件控制(如控制 LED 灯、电机等)
ioctl
(Input/Output Control)是一个用于设备输入输出控制的系统调用,主要用于与设备驱动程序进行交互,实现对设备的特殊控制操作。它在 Unix、Linux 等类 Unix 系统中广泛使用,是设备驱动程序提供给用户空间的重要接口之一。参数说明:
fd
:打开设备的文件描述符(通过open
系统调用获得)。request
:控制命令,通常是一个宏定义,用于指定要执行的操作(由设备驱动定义)。...
:可选参数,根据request
的不同,可能是输入参数、输出参数或双向参数(如指针)。返回值:
- 成功时返回 0 或其他非负值(具体含义取决于设备驱动)。
- 失败时返回 -1,并设置
errno
表示错误原因(如EBADF
表示文件描述符无效,EINVAL
表示命令无效)。命令(request)的定义
request
参数是ioctl
的核心,它通常由设备驱动程序定义,遵循一定的格式规范(32 位系统中):
- 高 8 位:幻数(magic number),用于区分不同设备的命令(如
'T'
表示终端设备)。- 次 8 位:序列号,用于区分同一设备的不同命令。
- 次 14 位:参数大小(适用于有参数的命令)。
- 最低 2 位:方向(数据传输方向,如
_IOC_NONE
表示无数据,_IOC_READ
表示从设备读,_IOC_WRITE
表示向设备写)。可以通过系统提供的宏来定义命令,例如:
// 无参数的命令 #define MYDEV_IOCTL_RESET _IO('M', 0)// 读参数的命令(从设备读取数据到用户空间) #define MYDEV_IOCTL_GET_STATUS _IOR('M', 1, int)// 写参数的命令(从用户空间向设备写入数据) #define MYDEV_IOCTL_SET_CONFIG _IOW('M', 2, struct my_config)
文件 linux-5.10.10/Documentation/userspace-api/ioctl/ioctl-decoding.rst
============================== Decoding an IOCTL Magic Number ==============================To decode a hex IOCTL code:Most architectures use this generic format, but check include/ARCH/ioctl.h for specifics, e.g. powerpc uses 3 bits to encode read/write and 13 bits for size.====== ==================================bits meaning====== ==================================31-30 00 - no parameters: uses _IO macro10 - read: _IOR01 - write: _IOW11 - read/write: _IOWR29-16 size of arguments15-8 ascii character supposedlyunique to each driver7-0 function #====== ==================================So for example 0x82187201 is a read with arg length of 0x218, character 'r' function 1. Grepping the source reveals this is::#define VFAT_IOCTL_READDIR_BOTH _IOR('r', 1, struct dirent [2])
使用示例
以下是一个简单的示例,展示如何使用ioctl 控制一个虚构的字符设备:
#include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <errno.h>// 假设设备驱动定义了以下命令 #define MYDEV_MAGIC 'M' #define MYDEV_RESET _IO(MYDEV_MAGIC, 0) #define MYDEV_GET_VALUE _IOR(MYDEV_MAGIC, 1, int) #define MYDEV_SET_VALUE _IOW(MYDEV_MAGIC, 2, int)int main() {int fd;int value, ret;// 打开设备(假设设备文件为 /dev/mydev)fd = open("/dev/mydev", O_RDWR);if (fd == -1) {perror("open failed");return 1;}// 发送重置命令(无参数)ret = ioctl(fd, MYDEV_RESET);if (ret == -1) {perror("ioctl reset failed");close(fd);return 1;}// 设置值(向设备写入参数)value = 100;ret = ioctl(fd, MYDEV_SET_VALUE, &value);if (ret == -1) {perror("ioctl set failed");close(fd);return 1;}// 获取值(从设备读取参数)ret = ioctl(fd, MYDEV_GET_VALUE, &value);if (ret == -1) {perror("ioctl get failed");close(fd);return 1;}printf("Current value: %d\\n", value);// 关闭设备close(fd);return 0; }
注意事项
ioctl
是设备相关的,不同设备支持的命令和参数格式可能完全不同,需参考具体设备的文档。- 滥用
ioctl
可能导致系统不稳定,因此通常只在必要时使用(优先使用标准的read
/write
操作)。- 在用户空间程序中使用
ioctl
时,需要确保对应的设备驱动已加载,且设备文件存在(通常位于/dev
目录下)。- 64 位系统对
ioctl
命令的定义有一定扩展,以兼容 32 位应用程序,具体可参考 Linux 内核文档。总之,
ioctl
是设备控制的灵活接口,但使用时需谨慎,遵循设备驱动的规范。
30._IO&_IOR &_IOW & _IOWR-宏定义
// 无数据传输的命令 #define _IO(type, nr) _IOC(_IOC_NONE, (type), (nr), 0)// 从用户空间读取数据到内核空间(用户→内核) #define _IOR(type, nr, size) _IOC(_IOC_READ, (type), (nr), (sizeof(size)))// 从内核空间写入数据到用户空间(内核→用户) #define _IOW(type, nr, size) _IOC(_IOC_WRITE, (type), (nr), (sizeof(size)))// 双向数据传输(既有读也有写) #define _IOWR(type, nr, size) _IOC(_IOC_READ | _IOC_WRITE, (type), (nr), (sizeof(size)))
在 Linux 内核中,
_IO
、_IOR
、_IOW
和_IOWR
是一组用于构建ioctl
命令的宏,它们定义在<linux/ioctl.h>
头文件中。这些宏的作用是标准化ioctl
命令的格式,确保命令编号的唯一性,并包含命令的方向(数据传输方向)、大小和类型等信息。各参数含义
type
:命令类型(8 位),通常是一个字符(如'U'
、'M'
等),用于区分不同设备或子系统的命令,避免冲突。nr
:命令编号(8 位),在同一type
下的唯一编号,用于区分不同的命令。size
:数据结构的类型(仅_IOR
、_IOW
、_IOWR
需要),宏会自动计算该类型的大小(sizeof(size)
),表示ioctl
传输的数据大小(14 位或 20 位,取决于架构)。- 方向标识:
_IOC_NONE
:无数据传输(_IO
)。_IOC_READ
:用户空间从内核空间读取数据(_IOR
)。_IOC_WRITE
:用户空间向内核空间写入数据(_IOW
)。- 两者的组合:双向传输(
_IOWR
)。编码格式
_IOC
宏将上述参数编码为一个整数,以 32 位系统为例,格式如下:bit 31-30:方向(00=无,01=写,10=读,11=读写) bit 29-16:数据大小(共14位,最大支持16384字节) bit 15-8:命令类型(type,8位) bit 7-0:命令编号(nr,8位)
64 位系统的编码格式类似,但数据大小字段可能扩展到 20 位,支持更大的传输尺寸。
31.struct file{}-结构体
struct file {union {struct llist_node fu_llist; // 用于文件对象的链表管理struct rcu_head fu_rcuhead; // RCU(Read-Copy-Update)机制的头部} f_u;struct path f_path; // 文件对应的路径信息(包含dentry和vfsmount)struct inode *f_inode; // 指向文件对应的inode结构体const struct file_operations *f_op; // 指向文件操作函数集(如read/write/open等)spinlock_t f_lock; // 保护该结构体的自旋锁enum rw_hint f_write_hint; // 写操作的优化提示atomic_long_t f_count; // 引用计数(文件被打开的次数)unsigned int f_flags; // 文件打开时的标志(如O_RDONLY, O_WRONLY, O_RDWR等)fmode_t f_mode; // 文件的访问模式(如S_IRUSR, S_IWUSR等)loff_t f_pos; // 当前文件指针位置struct fown_struct f_owner; // 用于信号通知的属主信息const struct cred *f_cred; // 文件操作的 credentials(权限信息)struct file_ra_state f_ra; // 预读状态信息u64 f_version; // 版本号,用于检测文件是否被修改 #ifdef CONFIG_SECURITYvoid *f_security; // 安全模块相关数据 #endif/* 用于跟踪异步IO操作 */void *private_data; // 驱动程序可使用的私有数据 #ifdef CONFIG_EPOLLstruct list_head f_ep_links; // 用于epoll机制的链表struct list_head f_tfile_llink; // 用于信号驱动IO的链表 #endifstruct address_space *f_mapping; // 指向文件的地址空间结构体errseq_t f_wb_err; // 写回操作的错误序列struct list_head f_dentry_lru; // 用于dentry LRU链表 };
在 Linux 内核中,struct file是一个非常重要的数据结构,用于表示一个打开的文件。它不直接对应磁盘上的文件,而是内核为每个打开的文件(包括设备、管道等)创建的抽象表示存在于内核空间中。
关键成员说明:
- f_path 与 f_inode:
f_path
包含文件的路径信息,其中f_path.dentry
指向目录项(dentry)结构体,f_path.mnt
指向挂载点信息。f_inode
指向文件对应的 inode 结构体,inode 包含文件的元数据(如权限、大小、磁盘位置等)。- f_op:
- 指向
struct file_operations
结构体,该结构体包含了对文件进行操作的函数指针(如read
,write
,open
,close
,ioctl
等),是用户空间与内核 / 驱动交互的关键接口。- f_count:
- 引用计数,记录文件被打开的次数。当
f_count
减为 0 时,内核会销毁该struct file
结构体。使用fget()
增加计数,fput()
减少计数。- f_flags 与 f_mode:
f_flags
存储文件打开时的标志(如O_RDONLY
,O_NONBLOCK
,O_APPEND
等)。f_mode
表示文件的访问权限模式(如读、写、执行权限)。- f_pos:
- 当前文件指针的位置,记录下一次读写操作的偏移量。对于常规文件,
f_pos
可以通过lseek()
系统调用来修改。- private_data:
- 驱动程序可以使用的私有数据指针,通常在
open
操作中初始化,用于保存与该文件实例相关的驱动特定信息。- f_mapping:
- 指向文件的地址空间结构体(
struct address_space
),用于管理文件数据在内存中的缓存。
主要作用:
- 跟踪文件的打开状态和当前操作位置。
- 提供文件操作的入口点(通过
f_op
)。- 维护与文件相关的各种元数据和内核内部状态。
- 作为用户空间进程与内核文件系统 / 设备驱动之间的桥梁。
struct file
结构体由内核在open()
系统调用时创建,在最后一个close()
时销毁(当引用计数归零时)。理解该结构体对于开发内核模块、文件系统或设备驱动至关重要。
32.struct inode{} -结构体
struct inode {umode_t i_mode; // 文件类型和权限(如S_IFREG、S_IRUSR等)unsigned short i_opflags; // 操作标志(如IFS_*相关)kuid_t i_uid; // 所有者用户IDkgid_t i_gid; // 所有者组IDunsigned int i_flags; // 文件系统特定标志(如MS_SYNC等)const struct inode_operations *i_op; // inode操作函数集struct super_block *i_sb; // 指向所属的超级块(super block)struct address_space *i_mapping; // 地址空间(用于页缓存管理)/* 时间戳相关 */struct timespec64 i_atime; // 最后访问时间(access time)struct timespec64 i_mtime; // 最后修改时间(modify time)struct timespec64 i_ctime; // 最后状态改变时间(change time)struct timespec64 i_crtime; // 创建时间(部分文件系统支持)blkcnt_t i_blocks; // 文件占用的磁盘块数loff_t i_size; // 文件大小(字节数)struct inode *i_link; // 硬链接指向的inode(仅用于特殊文件)union {struct pipe_inode_info *i_pipe; // 管道文件的信息struct block_device *i_bdev; // 块设备文件的设备指针struct cdev *i_cdev; // 字符设备文件的设备指针char *i_link; // 符号链接的目标路径unsigned i_dir_seq; // 目录遍历序列(用于readdir)} i_cdev;__u32 i_generation; // 生成号(用于NFS等一致性检查)atomic_t i_count; // 引用计数unsigned int i_nlink; // 硬链接数量dev_t i_rdev; // 设备号(用于设备文件)unsigned int i_blkbits; // 每个块的位数(如12表示4096字节)u64 i_version; // 版本号(用于检测文件修改)blkcnt_t i_bytes; // 已分配的字节数(可能小于i_size)struct hlist_node i_hash; // 用于inode哈希表的节点struct list_head i_io_list; // 用于IO操作的链表struct list_head i_lru; // 用于LRU(最近最少使用)缓存管理struct list_head i_sb_list; // 超级块的inode链表成员struct list_head i_dentry; // 指向引用该inode的dentry链表unsigned long i_state; // inode状态(如I_DIRTY、I_LOCK等)struct rw_semaphore i_rwsem; // 用于保护inode的读写信号量spinlock_t i_lock; // 保护inode部分字段的自旋锁unsigned long i_mapping_seq; // 地址空间序列号/* 用于文件系统特定数据 */void *i_private; // 文件系统私有数据(如ext4_inode_info) };
在 Linux 内核中,struct inode是文件系统中的核心数据结构之一,用于表示磁盘上的一个文件或目录的元数据(metadata)。与 struct file(表示打开的文件实例)不同,inode 对应磁盘上的静态文件信息,即使文件未被打开也存在于磁盘中(或内存缓存中)。
关键成员说明:
- 基本属性
i_mode
:文件类型(普通文件、目录、设备等)和权限(读 / 写 / 执行权限)。i_uid
/i_gid
:文件所有者的用户 ID 和组 ID,用于权限检查。i_nlink
:硬链接数量,当此值为 0 时,文件可被删除。- 时间戳
i_atime
:最后一次访问文件内容的时间(如read
操作)。i_mtime
:最后一次修改文件内容的时间(如write
操作)。i_ctime
:最后一次修改文件元数据的时间(如权限、所有者变更)。i_crtime
:文件创建时间(并非所有文件系统都支持)。- 文件系统关联
i_sb
:指向所属文件系统的超级块(struct super_block
),超级块存储整个文件系统的元数据。i_op
:指向struct inode_operations
结构体,包含针对 inode 的操作函数(如创建文件、删除文件、重命名等)。- 存储相关
i_size
:文件数据的总大小(字节)。i_blocks
:文件占用的磁盘块数量(以 512 字节为单位)。i_blkbits
:文件系统的块大小(如 12 表示 4096 字节,即 2^12)。- 特殊文件类型
i_cdev
:共用体,根据文件类型存储不同数据:
- 管道文件:
i_pipe
指向管道信息- 块设备:
i_bdev
指向块设备结构体- 字符设备:
i_cdev
指向字符设备结构体- 符号链接:
i_link
存储链接目标路径- 内核管理
i_count
:引用计数,记录当前有多少struct file
或其他结构引用该 inode。i_state
:inode 状态标志(如I_DIRTY
表示需要写回磁盘,I_LOCK
表示正在被操作)。i_hash
:用于将 inode 加入全局哈希表,加速查找。i_dentry
:链表头,连接所有引用该 inode 的目录项(dentry
)。- 私有数据
i_private
:文件系统特定的扩展数据指针。例如,ext4 文件系统会将其指向struct ext4_inode_info
,存储 ext4 特有的 inode 信息(如块组、扩展属性等)。
主要作用:
- 存储文件的元数据(权限、时间戳、大小等)。
- 关联文件系统的操作方法(通过
i_op
)。- 连接文件与实际存储(通过地址空间
i_mapping
管理页缓存)。- 支持多种文件类型(普通文件、目录、设备文件、管道等)。
与
struct file
的区别:
struct inode
:表示磁盘上的文件元数据,是 "静态" 的,即使文件未被打开也存在。struct file
:表示进程打开的文件实例,是 "动态" 的,每个打开操作会创建一个新的struct file
,但共享同一个inode
。理解
struct inode
对于深入掌握 Linux 文件系统、内核模块开发以及设备驱动编程至关重要。
其他相关函数
32.内核传参 module_param()
module_param是 Linux 内核中用于将模块参数传递给内核模块的宏,它允许用户在加载模块时通过命令行指定参数值,从而灵活配置模块行为。
基本用法
module_param(name, type, perm);
- name:参数变量名(模块中定义的全局变量)
- type:参数类型(支持
int
、charp
、bool
等多种类型)- perm:参数在
/sys/module/<模块名>/parameters/
下的访问权限(如0644
)
示例代码
#include <linux/module.h> #include <linux/moduleparam.h>// 定义模块参数变量 static int num = 10; static char *str = "default"; static bool flag = false;// 注册模块参数 module_param(num, int, 0644); module_param(str, charp, 0644); module_param(flag, bool, 0444);// 参数描述(可选) MODULE_PARM_DESC(num, "An integer parameter (default: 10)"); MODULE_PARM_DESC(str, "A string parameter (default: 'default')"); MODULE_PARM_DESC(flag, "A boolean flag (default: false)");static int __init mymodule_init(void) {printk(KERN_INFO "num: %d\\n", num);printk(KERN_INFO "str: %s\\n", str);printk(KERN_INFO "flag: %d\\n", flag);return 0; }static void __exit mymodule_exit(void) {printk(KERN_INFO "Module exited\\n"); }module_init(mymodule_init); module_exit(mymodule_exit);MODULE_LICENSE("GPL");
使用方式
32.1编译模块后,加载时指定参数:
insmod mymodule.ko num=20 str="test" flag=1
32.2加载后可通过 sysfs 查看/修改参数(受权限限制):
cat /sys/module/mymodule/parameters/num echo 30 > /sys/module/mymodule/parameters/num # 需要相应权限
注意事项
- 权限
perm
需符合文件系统权限规则(如0644
表示所有者可读写,其他用户只读)- 布尔类型参数支持
1/0
、y/n
、Y/N
等多种输入形式- 模块参数必须是全局变量,且通常定义为
static
- 复杂类型(如数组)可使用
module_param_array
宏
module_param
机制为内核模块提供了灵活的配置方式,广泛用于驱动程序和内核组件中。
33.module_param_array()
在Linux内核模块开发中,module_param_array是一个宏,用于将模块中的数组变量声明为可从用户空间配置的参数。通过它,用户可以在加载模块时(使用insmod或modprobe)向模块传递数组类型的参数,增强模块的灵活性。
33.1. 功能与作用
module_param_array
的核心作用是:将模块内部定义的数组变量暴露为“模块参数”,允许用户在加载模块时指定数组的元素值。加载后,这些参数还可以通过/sys/module/<模块名>/parameters/
路径下的文件查看或修改(取决于权限设置)。
33.2. 函数原型与参数
module_param_array 宏定义在 <linux/moduleparam.h> 头文件中,原型如下:
各参数含义:
name
:模块中定义的数组变量名(必须是全局变量)。type
:数组元素的数据类型(内核预定义的类型宏,如int
、charp
等)。nump
:指针(通常是int *
类型),用于存储实际传递的元素个数(若为NULL
,则不记录)。perm
:参数的访问权限(文件系统权限位),用于控制/sys/module
下对应文件的读写权限(如S_IRUSR
表示用户可读)。
33.3支持的类型(type)
type
需使用内核预定义的类型宏,常见类型包括:
int
:整数类型。charp
:字符串指针(数组元素为字符串)。bool
:布尔值(1
表示真,0
表示假)。uint
:无符号整数。- 其他自定义类型(需通过
module_param_array_cb
注册解析函数)。33.4使用步骤与示例
步骤1:定义数组变量
首先在模块中定义需要作为参数的数组(全局变量)。
步骤2:用
module_param_array
声明参数通过宏声明数组为模块参数,并指定类型、元素计数指针和权限。
步骤3:(可选)添加参数描述
使用
MODULE_PARM_DESC
宏为参数添加描述,方便用户理解参数含义。示例代码:
#include <linux/init.h> #include <linux/module.h> #include <linux/moduleparam.h>// 1. 定义数组变量(全局) static int arr[5]; // 数组最大长度为5 static int num; // 用于存储实际传递的元素个数// 2. 声明数组为模块参数 module_param_array(arr, int, &num, S_IRUSR | S_IWUSR); // 3. 添加参数描述 MODULE_PARM_DESC(arr, "An integer array (max 5 elements)");static int __init mymodule_init(void) {int i;printk(KERN_INFO "Module loaded. arr has %d elements:\\n", num);for (i = 0; i < num; i++) {printk(KERN_INFO "arr[%d] = %d\\n", i, arr[i]);}return 0; }static void __exit mymodule_exit(void) {printk(KERN_INFO "Module unloaded\\n"); }module_init(mymodule_init); module_exit(mymodule_exit); MODULE_LICENSE("GPL");
33.5加载模块时传递数组参数
编译模块后,使用insmod或modprobe 加载时,通过逗号分隔的方式传递数组元素:
insmod mymodule.ko arr=10,20,30,40
- 上述命令向
arr
数组传递了4个元素(10,20,30,40
)。- 模块加载后,
num
会被自动设置为4
(实际传递的元素数)。- 若传递的元素数超过数组最大长度(示例中为5),多余元素会被截断,
num
等于数组长度。
33.6查看/修改参数
加载后,参数会在 `/sys/module/mymodule/parameters/arr` 路径下生成文件:- 查看参数:`cat /sys/module/mymodule/parameters/arr` - 修改参数(需权限允许,如 `S_IWUSR`):`echo "5,6,7" >/sys/module/mymodule/parameters/arr`
33.7注意事项
- 数组必须是全局变量,且在
module_param_array
声明前定义。perm
权限需合理设置:若只允许读取,可设为S_IRUSR
;若允许修改,需添加S_IWUSR
(但修改可能影响模块运行,需谨慎)。- 字符串数组(
charp
类型)的传递方式相同(如str_arr="a","b","c"
)。- 若
nump
为NULL
,则无法获取实际传递的元素数,仅能使用数组中已填充的值。通过
module_param_array
,内核模块可以灵活地接收用户空间传递的数组参数,适用于需要动态配置多个相关值的场景(如设备ID列表、阈值数组等)。
34.MODULE_PARM_DESC()-宏
在Linux内核模块开发中,MODULE_PARM_DESC是一个宏,用于为模块参数(包括通过
module_param声明的单个参数和module_param_array 声明的数组参数)添加描述信息。这些描述会被内核记录,方便用户了解参数的用途、取值范围或含义,提升模块的可维护性和易用性。
34.1. 功能与作用
MODULE_PARM_DESC
的核心作用是:为模块参数提供“文档说明”。当用户通过modinfo
命令查看模块信息,或在/sys/module/<模块名>/parameters/
路径下查看参数时,这些描述会被展示,帮助用户正确使用模块参数。34.2. 宏原型与参数
MODULE_PARM_DESC
宏定义在<linux/moduleparam.h>
头文件中,原型如下:#include <linux/moduleparam.h> MODULE_PARM_DESC(name, description);
参数含义:
name
:需要描述的模块参数名(必须与通过module_param
或module_param_array
声明的参数名完全一致)。description
:字符串常量,用于描述该参数的含义(如用途、取值范围、默认值等)。
34.3. 使用场景与示例
MODULE_PARM_DESC必须配合模块参数使用,通常在module_param或module_param_array 声明参数之后调用,为其添加说明。
示例1:为单个参数添加描述
#include <linux/init.h> #include <linux/module.h> #include <linux/moduleparam.h>// 定义单个参数 static int max_count = 10; // 默认值10// 声明为模块参数 module_param(max_count, int, S_IRUSR | S_IWUSR);// 为参数添加描述 MODULE_PARM_DESC(max_count, "Maximum number of operations (default: 10, range: 1-100)");// ... 模块初始化/退出函数省略 ... MODULE_LICENSE("GPL");
示例2:为数组参数添加描述
结合之前的module_param_array 示例:
#include <linux/init.h> #include <linux/module.h> #include <linux/moduleparam.h>// 定义数组参数 static int arr[5]; // 最大长度5 static int num; // 实际元素个数// 声明数组为模块参数 module_param_array(arr, int, &num, S_IRUSR | S_IWUSR);// 为数组参数添加描述 MODULE_PARM_DESC(arr, "Integer array (max 5 elements, e.g., arr=1,2,3)");// ... 模块初始化/退出函数省略 ... MODULE_LICENSE("GPL");
34.4. 查看描述信息的方式
添加描述后,用户可通过以下方式查看参数说明:
方式1:使用modinfo 命令
编译模块后,通过modinfo查看模块元数据,描述会显示在parm: 字段中:
modinfo mymodule.ko
输出示例:
filename: /path/to/mymodule.ko license: GPL parm: max_count:Maximum number of operations (default: 10, range: 1-100) (int) parm: arr:Integer array (max 5 elements, e.g., arr=1,2,3) (int)
方式2:查看/sys/module 下的文件
模块加载后,参数描述会被写入/sys/module/<模块名>/parameters/<参数名>/description
(部分内核版本支持):
cat /sys/module/mymodule/parameters/arr/description
输出示例:
Integer array (max 5 elements, e.g., arr=1,2,3)
34.5. 注意事项
- 参数名必须一致:
MODULE_PARM_DESC
中的name
必须与module_param
/module_param_array
声明的参数名完全相同,否则描述无法关联到参数。- 描述需简洁明确:应说明参数的用途、默认值、取值范围(如“1-100”)、格式(如数组用逗号分隔)等关键信息,帮助用户正确传递参数。
- 位置无严格限制:通常放在参数声明之后,但内核对其位置无强制要求,只要在模块加载前被编译即可。
- 不影响参数功能:
MODULE_PARM_DESC
仅用于添加描述信息,不影响参数的解析、传递或使用,是纯“文档性”的宏。通过
MODULE_PARM_DESC
为模块参数添加描述,是内核模块开发的最佳实践之一,尤其对于需要用户配置参数的模块,能显著提升易用性,减少用户误用的可能性。
中断相关函数
35.request_irq()
在Linux内核中,request_irq 是一个核心函数,用于申请中断线(IRQ line)并注册中断处理程序。当硬件设备产生中断时(如按键按下、数据到达等),内核会通过该函数注册的处理程序响应中断,是设备驱动中处理硬件中断的关键接口。
35.1. 功能与核心作用
request_irq
的主要作用是:
- 向内核申请使用特定的中断线(IRQ号);
- 注册一个中断处理函数(回调函数),当对应中断触发时,内核会调用该函数;
- 关联中断线与设备,确保中断能被正确识别和处理。
它是设备驱动与硬件中断交互的“桥梁”,让驱动能够响应硬件事件(如外设数据就绪、状态变化等)。
35.2. 函数原型与参数
request_irq
定义在<linux/interrupt.h>
头文件中,原型如下(内核版本不同可能略有差异,以较新内核为例):#include <linux/interrupt.h> int request_irq(unsigned int irq,irqreturn_t (*handler)(int, void *),unsigned long flags,const char *name,void *dev);
参数说明:
irq
:中断号(IRQ number),标识要申请的中断线。通常由硬件决定(如GPIO中断号、外设固定中断号),也可通过platform_get_irq()
等函数动态获取。handler
:中断处理函数(回调函数),中断触发时内核会调用此函数。原型为: 返回值为irqreturn_t
类型,通常是IRQ_HANDLED
(中断已处理)或IRQ_NONE
(中断未处理,可能属于其他设备)。typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
flags
:中断标志,控制中断的行为特性(可组合使用,通过位或|
操作)。常见标志:
IRQF_SHARED
:允许中断线被多个设备共享(需配合dev
参数区分设备)。IRQF_TRIGGER_RISING
:上升沿触发(适用于电平信号)。IRQF_TRIGGER_FALLING
:下降沿触发。IRQF_TRIGGER_HIGH
:高电平触发。IRQF_TRIGGER_LOW
:低电平触发。IRQF_DISABLED
:处理中断时关闭其他中断(慎用,可能导致中断延迟)。name
:设备名称,字符串,会显示在/proc/interrupts
中,用于调试(标识哪个设备占用该中断)。dev
:设备标识(私有数据),会传递给中断处理函数的dev_id
参数。若中断共享(IRQF_SHARED
),dev
必须唯一(通常是设备结构体指针),用于区分共享中断的不同设备。
35.3. 返回值
- 成功:返回
0
,表示中断线申请成功,处理函数已注册。- 失败:返回负数错误码,常见如:
EBUSY
:中断线已被占用,且不支持共享(未设置IRQF_SHARED
)。EINVAL
:参数无效(如中断号非法、handler
为NULL
等)。ENOMEM
:内存不足,无法完成注册。35.4. 使用流程与示例
步骤1:定义中断处理函数
中断处理函数需快速执行(避免阻塞),仅处理紧急操作(如读取状态寄存器),耗时操作应交给“底半部”(如
tasklet
、workqueue
)。步骤2:调用
request_irq
申请中断在驱动初始化时(如
probe
函数)调用,申请中断并注册处理函数。步骤3:卸载驱动时释放中断
通过
free_irq(irq, dev)
释放中断线,避免资源泄露。示例代码(简化的GPIO中断驱动):
#include <linux/module.h> #include <linux/interrupt.h> #include <linux/gpio.h>// 假设使用GPIO 10作为中断源,对应IRQ号为gpio_to_irq(10) #define GPIO_IRQ_PIN 10 static int irq_num; // 存储实际IRQ号// 设备私有数据(共享中断时用于区分设备) struct my_device {int id;// 其他设备信息... }; static struct my_device dev = { .id = 1 };// 步骤1:定义中断处理函数 static irqreturn_t my_irq_handler(int irq, void *dev_id) {struct my_device *dev = (struct my_device *)dev_id;// 检查是否是当前设备触发的中断(共享中断时必需)if (!gpio_get_value(GPIO_IRQ_PIN)) { // 假设低电平表示中断触发printk(KERN_INFO "Device %d: IRQ %d triggered\\n", dev->id, irq);return IRQ_HANDLED; // 已处理}return IRQ_NONE; // 未处理(可能属于其他设备) }// 驱动初始化函数 static int __init my_irq_init(void) {// 获取GPIO对应的IRQ号irq_num = gpio_to_irq(GPIO_IRQ_PIN);if (irq_num < 0) {printk(KERN_ERR "Failed to get IRQ for GPIO %d\\n", GPIO_IRQ_PIN);return irq_num;}// 步骤2:申请中断int ret = request_irq(irq_num,my_irq_handler,IRQF_TRIGGER_FALLING | IRQF_SHARED, // 下降沿触发,允许共享"my_device_irq", // 设备名称(显示在/proc/interrupts)&dev); // 传递设备私有数据if (ret != 0) {printk(KERN_ERR "Failed to request IRQ %d: %d\\n", irq_num, ret);return ret;}printk(KERN_INFO "Successfully requested IRQ %d\\n", irq_num);return 0; }// 驱动退出函数 static void __exit my_irq_exit(void) {// 步骤3:释放中断free_irq(irq_num, &dev);printk(KERN_INFO "IRQ %d released\\n", irq_num); }module_init(my_irq_init); module_exit(my_irq_exit); MODULE_LICENSE("GPL");
35.5. 关键注意事项
- 中断处理函数的约束:
- 必须快速执行(通常几微秒内),不能调用可能睡眠的函数(如
kmalloc(..., GFP_KERNEL)
、mutex_lock
等),否则会导致系统死锁。- 若需处理耗时操作(如数据处理、用户空间通知),应使用“底半部”机制(
tasklet_schedule
、queue_work
等)延迟处理。- 共享中断的条件:
- 所有共享设备必须在
flags
中设置IRQF_SHARED
。- 每个设备的
dev
参数必须唯一(通常是设备结构体指针),用于free_irq
时准确移除对应处理函数。- 中断处理函数必须能判断中断是否由自身设备触发(如检查硬件状态寄存器),并返回
IRQ_NONE
表示非自身中断。- 中断号的获取:
- 对于SOC内置外设(如UART、SPI),中断号通常在设备树(Device Tree)中定义,通过
platform_get_irq(pdev, 0)
获取。- 对于GPIO中断,通过
gpio_to_irq(gpio_num)
转换获取。- 调试与查看:
- 已注册的中断可通过
/proc/interrupts
查看(包含IRQ号、设备名称、触发次数等):cat /proc/interrupts
36.6. 替代函数
在现代内核中,
request_irq
仍是基础接口,但针对不同场景有更易用的封装:
devm_request_irq
:基于设备资源管理(devres
)的版本,无需手动调用free_irq
,设备释放时自动释放中断,减少资源泄露风险。request_threaded_irq
:用于注册“线程化中断处理函数”,将中断处理分为“顶半部”(快速响应)和“线程处理”(可睡眠,处理耗时操作)。
request_irq
是设备驱动响应硬件中断的核心接口,正确使用它需要理解中断的特性、处理函数的约束及共享机制,以保证系统的稳定性和响应速度。
37.free_irq()
在Linux内核中,free_irq是与request_irq配对使用的函数,用于释放已申请的中断线(IRQ line)并注销对应的中断处理程序。当设备驱动卸载或不再需要响应某个中断时,必须调用
free_irq 释放资源,避免中断线被长期占用导致资源泄露或系统异常。
37.1. 功能与核心作用
free_irq
的核心作用是:
- 从内核中断管理系统中移除通过
request_irq
注册的中断处理程序;- 释放中断线(IRQ)的占用状态(若该中断线无其他共享设备,则完全释放);
- 确保中断处理程序不会再被内核调用,避免驱动卸载后出现悬空调用。
它是中断资源管理的“收尾操作”,与
request_irq
形成“申请-释放”的完整生命周期。37.2. 函数原型与参数
free_irq
定义在<linux/interrupt.h>
头文件中,原型如下:#include <linux/interrupt.h> void free_irq(unsigned int irq, void *dev_id);
参数说明:
irq
:要释放的中断号(与request_irq
中申请的irq
一致)。dev_id
:设备标识(与request_irq
中传递的dev
参数完全一致)。 若中断是共享的(IRQF_SHARED
),dev_id
用于唯一标识要移除的中断处理程序(避免误删其他共享设备的处理程序);若为非共享中断,dev_id
通常为NULL
,但需与request_irq
保持一致。
37.3. 返回值
free_irq
是void
类型函数,无返回值。但需注意:若参数无效(如irq
未被申请、dev_id
不匹配),可能导致内核警告或错误(如BUG
)。37.4. 使用流程与示例
free_irq
通常在驱动的退出函数(如__exit
标记的函数)中调用,与request_irq
形成对称操作:
- 驱动初始化时通过
request_irq
申请中断并注册处理程序;- 驱动卸载时通过
free_irq
释放中断,确保资源回收。示例代码(基于之前的GPIO中断驱动):
#include <linux/module.h> #include <linux/interrupt.h> #include <linux/gpio.h>#define GPIO_IRQ_PIN 10 // 假设使用GPIO 10 static int irq_num;// 设备私有数据(共享中断时用于区分) struct my_device { int id; }; static struct my_device dev = { .id = 1 };// 中断处理函数 static irqreturn_t my_irq_handler(int irq, void *dev_id) {// 处理中断...return IRQ_HANDLED; }// 初始化函数:申请中断 static int __init my_irq_init(void) {irq_num = gpio_to_irq(GPIO_IRQ_PIN);if (irq_num < 0) return irq_num;// 申请中断(共享模式,dev_id为&dev)if (request_irq(irq_num, my_irq_handler,IRQF_TRIGGER_FALLING | IRQF_SHARED,"my_device", &dev)) {printk(KERN_ERR "Failed to request IRQ\\n");return -1;}return 0; }// 退出函数:释放中断 static void __exit my_irq_exit(void) {// 释放中断:参数需与request_irq完全一致free_irq(irq_num, &dev); // 共享中断必须传递正确的dev_idprintk(KERN_INFO "IRQ %d released\\n", irq_num); }module_init(my_irq_init); module_exit(my_irq_exit); MODULE_LICENSE("GPL");
37.5. 关键注意事项
与
request_irq
参数严格一致:
free_irq
的irq
和dev_id
必须与request_irq
中使用的参数完全相同,否则:
- 非共享中断:可能导致内核错误(如尝试释放未申请的中断)。
- 共享中断:若
dev_id
不匹配,会导致目标处理程序未被移除,或误删其他设备的处理程序。避免在中断上下文调用:
free_irq
可能会等待中断处理程序执行完毕(若正在运行),因此不能在中断上下文(如中断处理函数内部)调用,否则会导致死锁。共享中断的释放行为:
若中断线被多个设备共享(
IRQF_SHARED
),free_irq
仅移除当前dev_id
对应的处理程序,其他共享设备的中断仍可正常工作;当最后一个共享设备释放中断后,中断线才会被完全释放。确保只调用一次:
对同一
irq
和dev_id
多次调用free_irq
会导致未定义行为(如内核崩溃),需在驱动退出逻辑中确保只调用一次。与
devm_request_irq
的区别:若使用
devm_request_irq
(设备资源管理版本的中断申请),无需手动调用free_irq
,设备释放时内核会自动释放中断;而request_irq
必须手动调用free_irq
。37.6. 调试与验证
释放中断后,可通过以下方式验证:
- 查看
/proc/interrupts
,确认对应IRQ号的设备名称已移除(非共享情况下);- 检查内核日志(
dmesg
),无“invalid free_irq”等警告信息。
free_irq
是中断资源管理的关键函数,其正确使用直接影响系统稳定性。核心原则是:与request_irq
严格配对,参数完全一致,且仅在驱动退出时调用一次。
38.gpio_to_irq()
Linux内核中,gpio_to_irq 是一个用于将GPIO引脚编号转换为对应中断号(IRQ number) 的函数。它是GPIO中断处理的关键接口,用于建立GPIO引脚与硬件中断线之间的映射,使驱动程序能够通过中断方式响应GPIO引脚的状态变化(如电平跳变、按键触发等)。
38.1. 功能与核心作用
GPIO(通用输入输出)引脚通常可配置为中断源(如检测外部信号的上升沿、下降沿或电平变化)。
gpio_to_irq
的核心作用是:根据GPIO引脚的编号(内核统一编号),查询并返回该引脚对应的硬件中断号(IRQ)。得到IRQ号后,驱动程序可通过
request_irq
等函数注册中断处理程序,实现对GPIO引脚事件的中断响应。38.2. 函数原型与参数
gpio_to_irq
定义在<linux/gpio.h>
头文件中,原型如下:#include <linux/gpio.h> int gpio_to_irq(unsigned int gpio);
参数说明:
gpio
:GPIO引脚的内核编号(无符号整数)。这个编号是内核统一分配的,通常来自设备树(Device Tree)中的gpio
属性,或通过平台特定方法(如platform_get_gpio
)获取。38.3. 返回值
- 成功:返回对应的中断号(IRQ number,正数),可直接用于
request_irq
等函数。- 失败:返回负数错误码,常见情况包括:
EINVAL
:gpio
参数无效(如超出系统支持的GPIO范围)。ENOSYS
:当前GPIO控制器不支持中断功能,或该GPIO引脚无法配置为中断源。ENOENT
:该GPIO引脚未被映射到任何中断线。38.4. 使用场景与示例
gpio_to_irq
通常在需要将GPIO引脚作为中断源的驱动中使用,流程如下:
- 申请并配置GPIO引脚(如设置为输入模式)。
- 调用
gpio_to_irq
将GPIO编号转换为IRQ号。- 使用
request_irq
注册中断处理函数,响应GPIO引脚的中断事件。示例代码(按键中断驱动简化版):
#include <linux/module.h> #include <linux/gpio.h> #include <linux/interrupt.h>// 假设按键连接到GPIO 18(内核编号) #define KEY_GPIO 18 static int irq_num; // 存储转换后的IRQ号// 中断处理函数 static irqreturn_t key_irq_handler(int irq, void *dev_id) {printk(KERN_INFO "Key pressed! IRQ: %d\\n", irq);return IRQ_HANDLED; }static int __init key_irq_init(void) {int ret;// 步骤1:申请GPIO引脚(标记为输入)ret = gpio_request(KEY_GPIO, "key_gpio");if (ret != 0) {printk(KERN_ERR "Failed to request GPIO %d: %d\\n", KEY_GPIO, ret);return ret;}// 步骤2:配置GPIO为输入模式ret = gpio_direction_input(KEY_GPIO);if (ret != 0) {printk(KERN_ERR "Failed to set GPIO %d as input: %d\\n", KEY_GPIO, ret);gpio_free(KEY_GPIO); // 释放已申请的GPIOreturn ret;}// 步骤3:将GPIO转换为IRQ号irq_num = gpio_to_irq(KEY_GPIO);if (irq_num < 0) {printk(KERN_ERR "gpio_to_irq failed for GPIO %d: %d\\n", KEY_GPIO, irq_num);gpio_free(KEY_GPIO);return irq_num;}printk(KERN_INFO "GPIO %d mapped to IRQ %d\\n", KEY_GPIO, irq_num);// 步骤4:注册中断处理函数(下降沿触发)ret = request_irq(irq_num,key_irq_handler,IRQF_TRIGGER_FALLING, // 下降沿触发(按键按下时)"key_irq",NULL); // 非共享中断,dev_id可为NULLif (ret != 0) {printk(KERN_ERR "Failed to request IRQ %d: %d\\n", irq_num, ret);gpio_free(KEY_GPIO);return ret;}return 0; }static void __exit key_irq_exit(void) {// 释放中断和GPIO资源free_irq(irq_num, NULL);gpio_free(KEY_GPIO);printk(KERN_INFO "Key IRQ module unloaded\\n"); }module_init(key_irq_init); module_exit(key_irq_exit); MODULE_LICENSE("GPL");
38.5. 注意事项
GPIO的中断能力:
并非所有GPIO引脚都支持中断功能,需查阅硬件手册确认该GPIO是否可配置为中断源。若GPIO不支持中断,
gpio_to_irq
会返回-ENOSYS
或-ENOENT
。GPIO编号的有效性:
gpio
参数必须是内核认可的有效编号(通常通过设备树或平台代码定义)。使用前需通过gpio_is_valid(KEY_GPIO)
检查编号是否合法:if (!gpio_is_valid(KEY_GPIO)) {printk(KERN_ERR "Invalid GPIO number: %d\\n", KEY_GPIO);return -EINVAL; }
资源申请顺序:
必须先通过
gpio_request
申请GPIO引脚(确保独占使用),再调用gpio_to_irq
,避免访问未分配的GPIO资源。平台依赖性:
gpio_to_irq
的实现依赖于底层GPIO控制器驱动(如gpio_chip
结构体中的to_irq
回调),不同SoC(如ARM、RISC-V)的映射逻辑可能不同,但函数接口统一。反向转换:
若需从IRQ号反查对应的GPIO编号,可使用
irq_to_gpio
函数(但该函数并非在所有内核版本中都存在,且使用场景较少)。
gpio_to_irq
是连接GPIO引脚与中断系统的桥梁,在按键、传感器、外部触发信号等场景中广泛使用。正确使用它的前提是了解硬件GPIO的中断能力,并严格遵循GPIO资源的申请与释放流程。
39.gpio_get_value()
在Linux内核中,gpio_get_value 是一个用于读取GPIO引脚当前逻辑电平状态的函数。它是GPIO(通用输入输出)操作的基础接口之一,用于获取指定GPIO引脚的电平值(高电平或低电平),适用于需要检测外部信号状态的场景(如按键状态、传感器输出等)。
39.1功能与核心作用
GPIO引脚可配置为输入或输出模式。当配置为输入模式时,
gpio_get_value
用于读取该引脚的逻辑电平状态(与硬件实际电平可能存在映射关系,由GPIO控制器决定);当配置为输出模式时,该函数通常返回最后一次通过gpio_set_value
设置的输出电平(而非外部输入电平)。其核心作用是:为驱动程序提供一种简单的方式获取GPIO引脚的当前状态,是处理输入类GPIO设备(如按键、开关)的基础。39.2. 函数原型与参数
gpio_get_value
定义在<linux/gpio.h>
头文件中,原型如下:#include <linux/gpio.h> int gpio_get_value(unsigned int gpio);
参数说明:
gpio
:GPIO引脚的内核编号(无符号整数),即内核统一分配的GPIO标识(与gpio_request
、gpio_to_irq
等函数使用的编号一致)。39.3. 返回值
- 成功:返回
0
或1
,分别表示引脚当前为逻辑低电平或逻辑高电平。- 未定义行为:若
gpio
参数无效(如未通过gpio_request
申请、编号超出范围),函数行为未定义,可能返回随机值或导致内核错误。39.4. 使用场景与示例
gpio_get_value
通常在以下场景中使用:
- 读取输入模式GPIO的外部信号(如按键是否按下);
- 验证输出模式GPIO的当前设置(确认是否与预期一致);
- 中断处理函数中判断中断触发的有效性(如排除噪声导致的虚假中断)。
示例代码(读取按键GPIO状态):
#include <linux/module.h> #include <linux/gpio.h> #include <linux/delay.h>// 假设按键连接到GPIO 18(内核编号) #define KEY_GPIO 18static int __init gpio_value_init(void) {int ret;int value;// 步骤1:检查GPIO编号是否有效if (!gpio_is_valid(KEY_GPIO)) {printk(KERN_ERR "Invalid GPIO number: %d\\n", KEY_GPIO);return -EINVAL;}// 步骤2:申请GPIO引脚ret = gpio_request(KEY_GPIO, "key_gpio");if (ret != 0) {printk(KERN_ERR "Failed to request GPIO %d: %d\\n", KEY_GPIO, ret);return ret;}// 步骤3:配置GPIO为输入模式(必须先配置方向才能读取)ret = gpio_direction_input(KEY_GPIO);if (ret != 0) {printk(KERN_ERR "Failed to set GPIO %d as input: %d\\n", KEY_GPIO, ret);gpio_free(KEY_GPIO);return ret;}// 步骤4:读取GPIO电平状态value = gpio_get_value(KEY_GPIO);printk(KERN_INFO "GPIO %d current value: %d (0=low, 1=high)\\n", KEY_GPIO, value);// 模拟:等待1秒后再次读取(例如检测按键状态变化)mdelay(1000);value = gpio_get_value(KEY_GPIO);printk(KERN_INFO "GPIO %d value after 1s: %d\\n", KEY_GPIO, value);return 0; }static void __exit gpio_value_exit(void) {// 释放GPIO资源gpio_free(KEY_GPIO);printk(KERN_INFO "GPIO value module unloaded\\n"); }module_init(gpio_value_init); module_exit(gpio_value_exit); MODULE_LICENSE("GPL");
39.5. 关键注意事项
GPIO必须先申请并配置方向:
使用
gpio_get_value
前,必须通过gpio_request
申请GPIO引脚(确保资源独占),并通过gpio_direction_input
配置为输入模式(若用于读取外部信号)。若未配置方向,读取结果可能不准确。输入与输出模式的区别:
- 输入模式:读取的是引脚的外部实际电平(受外部电路影响)。
- 输出模式:读取的是内核最后一次设置的输出值(与外部电路无关,除非引脚被强制拉拽)。
避免在中断上下文滥用:
gpio_get_value
是原子操作(可在中断处理函数中调用),但频繁读取可能影响性能。中断处理中通常仅用于快速判断状态(如过滤虚假中断)。硬件电平与逻辑电平的映射:
函数返回的
0
/1
是逻辑电平,可能与硬件实际电压(如3.3V/0V)存在映射关系(由GPIO控制器驱动定义),但对用户透明(无需关心硬件细节)。错误处理:
函数本身不返回错误码,因此需在调用前通过
gpio_is_valid
检查编号有效性,并确保已成功申请GPIO(gpio_request
返回0),否则可能导致内核崩溃。39.6. 相关函数
gpio_set_value(unsigned int gpio, int value)
:设置输出模式GPIO的电平(value
为0或1)。gpio_direction_input(unsigned int gpio)
:配置GPIO为输入模式。gpio_direction_output(unsigned int gpio, int value)
:配置GPIO为输出模式,并设置初始电平。
gpio_get_value
是GPIO输入操作的核心函数,简单直观但需严格遵循GPIO资源管理流程(申请→配置→使用→释放)。在按键、传感器等输入设备驱动中,它是获取外部状态的基础接口。
定时器相关函数
40.struct timer_list{}-结构体
在Linux内核中,struct timer_list 是内核定时器的核心数据结构,用于表示一个“一次性延迟执行”的定时器。它能够在指定的未来时间点触发预设的回调函数,是实现延迟操作、超时处理、周期性任务等功能的基础。
40.1. 核心作用
struct timer_list
的主要作用是:
- 记录定时器的到期时间(
expires
);- 绑定定时器到期时要执行的回调函数(
function
);- 存储传递给回调函数的私有数据(
data
);- 通过内部链表项(
entry
)被内核定时器管理系统追踪和调度。40.2. 结构体定义(内核版本差异)
struct timer_list
的定义随内核版本略有变化,以下是两种典型版本的核心成员(简化后):(1)旧版本内核(如3.x-4.13)
#include <linux/timer.h>struct timer_list {struct list_head entry; // 链表项,用于加入内核定时器链表unsigned long expires; // 到期时间(单位:jiffies,内核节拍数)void (*function)(unsigned long data); // 回调函数(过期时执行)unsigned long data; // 传递给回调函数的私有数据struct tvec_base *base; // 指向定时器管理的内部数据结构(用户无需关心) };
(2)新版本内核(4.14+,引入timer_setup 后)
为了更安全地处理回调函数参数,新版本对回调函数原型进行了调整:
struct timer_list {struct list_head entry;unsigned long expires;// 回调函数参数改为struct timer_list*,可通过container_of获取私有数据void (*function)(struct timer_list *timer);u32 flags; // 标志位(如TIMER_IRQSAFE等)struct tvec_base *base; };
核心成员说明:
entry
:链表节点,内核通过它将定时器加入全局/局部定时器链表,进行统一管理和调度。expires
:定时器的到期时间(绝对时间),单位是jiffies
(内核启动后的总节拍数,1节拍 ≈ 1/HZ 秒,HZ通常为1000,即1ms/节拍)。function
:定时器到期时触发的回调函数。旧版本参数为unsigned long data
,新版本为struct timer_list *timer
(更灵活,可通过container_of
关联私有数据)。data
(旧版本)/flags
(新版本):旧版本用于传递用户私有数据;新版本通过flags
控制定时器行为(如是否在中断上下文安全执行),私有数据需通过container_of
从timer
指针反向获取。40.3. 初始化与使用流程
使用
struct timer_list
需遵循“定义→初始化→设置到期时间→激活”的流程,关键步骤如下:步骤1:定义定时器结构体
static struct timer_list my_timer; // 全局或静态变量(需保证生命周期)
步骤2:初始化定时器(绑定回调函数)
根据内核版本选择初始化方式:
(1)旧版本:setup_timer(已不推荐)
// 旧版回调函数(参数为unsigned long) static void my_timer_func(unsigned long data) {printk(KERN_INFO "Timer expired! data: %lu\\n", data); }// 初始化:绑定回调函数和私有数据 setup_timer(&my_timer, my_timer_func, (unsigned long)0x12345); // data=0x12345
(2)新版本:timer_setup(推荐)
// 新版回调函数(参数为struct timer_list*) static void my_timer_func(struct timer_list *timer) {// 通过container_of获取关联的私有数据(假设有一个包含timer的结构体)struct my_device *dev = container_of(timer, struct my_device, timer);printk(KERN_INFO "Timer expired! dev id: %d\\n", dev->id); }// 初始化:绑定回调函数(flags=0表示默认行为) timer_setup(&my_timer, my_timer_func, 0);
步骤3:设置到期时间并激活定时器
通过mod_timer或add_timer激活定时器(推荐mod_timer,更灵活):
// 设置定时器在3秒后到期(HZ=1000时,3*HZ=3000节拍) mod_timer(&my_timer, jiffies + 3 * HZ); // jiffies是当前节拍数
步骤4:(可选)修改或删除定时器
调整到期时间:再次调用mod_timer(自动覆盖旧时间):
mod_timer(&my_timer, jiffies + 5 * HZ); // 改为5秒后到期
删除定时器(驱动卸载时必须执行):
del_timer_sync(&my_timer); // 同步删除(等待回调执行完毕)
40.4. 工作原理
内核维护着全局的定时器管理系统(基于
tvec_base
),其工作流程如下:
- 当调用
mod_timer
激活定时器时,内核会根据expires
将timer_list
插入到对应的链表(按时间分组的链表,优化查询效率)。- 系统时钟中断(每节拍触发一次)会检查当前
jiffies
是否达到链表中定时器的expires
。- 若达到到期时间,内核会将定时器从链表中移除,并在软中断上下文(
TIMER_SOFTIRQ
)中调用其function
回调函数。40.5. 关键注意事项
回调函数的约束:
回调函数运行在软中断上下文,因此:
- 不能调用可能睡眠的函数(如
kmalloc(..., GFP_KERNEL)
、mutex_lock
、schedule()
等);- 执行时间必须极短(微秒级),避免阻塞其他软中断。
一次性触发:
定时器默认是“一次性”的,回调函数执行后自动失效。若需周期性执行,需在回调函数内部再次调用
mod_timer
重新设置到期时间:static void my_timer_func(struct timer_list *timer) {// 执行周期性任务...// 1秒后再次触发mod_timer(timer, jiffies + HZ); }
时间精度:
定时器的最小精度是1个内核节拍(通常1ms),且受系统负载影响可能有延迟,不适合要求纳秒级精度的场景(需用
hrtimer
高精度定时器)。多处理器安全:
mod_timer
、del_timer_sync
等操作是原子的,可在多处理器环境中安全调用,内核通过自旋锁保证同步。生命周期管理:
定时器结构体必须在回调函数执行期间保持有效(不能被释放),通常定义为全局变量或
kmalloc
分配的动态内存(需确保释放前已删除定时器)。40.6. 与高精度定时器(hrtimer)的区别
struct timer_list
是“低精度定时器”,适用于对时间精度要求不高(毫秒级)的场景;若需要微秒/纳秒级精度,应使用struct hrtimer
(高精度定时器),但后者实现更复杂,开销也更高。
struct timer_list
是Linux内核中最基础、最常用的定时器机制,广泛应用于设备驱动(如超时重试、 watchdog)、内核子系统(如网络超时、内存回收)等场景。掌握其初始化和使用规则,是实现内核延迟逻辑的基础。
41.jiffies-全局变量
在Linux内核中,jiffies 是一个全局变量,用于记录系统从启动开始经过的内核节拍数(ticks),是内核时间管理的基础组件之一。它本质上是一个计数器,每经过一个“节拍”(由内核配置的固定时间间隔),其值就会加1。
41.1. 核心概念:节拍(Tick)
“节拍”是内核时间管理的基本单位,由内核编译时的配置项
HZ
决定:
HZ
表示每秒的节拍数(赫兹),常见取值为100、250、1000(不同系统可能不同,现代Linux多为1000)。- 1个节拍的时长 =
1/HZ
秒。例如:
- 若
HZ=1000
,则1个节拍 = 1毫秒(ms);- 若
HZ=250
,则1个节拍 = 4毫秒。41.2.
jiffies
的定义与类型
jiffies
定义在<linux/jiffies.h>
中,类型为unsigned long
(32位或64位,取决于内核架构):#include <linux/jiffies.h> extern unsigned long volatile jiffies; // volatile确保每次访问都从内存读取(避免编译器优化)
- 32位系统中,
jiffies
是32位变量,若HZ=1000
,约49.7天后会溢出(2^32 / 1000 / 86400 ≈ 49.7
天)。- 64位系统中,
jiffies
是64位变量,溢出周期极长(约几百年),可忽略溢出问题。41.3. 主要用途
jiffies
是内核中表示时间的“通用货币”,主要用于:
- 定时器和延迟计算:如
struct timer_list
的expires
字段(定时器到期时间)就是基于jiffies
设定的(例如jiffies + 10*HZ
表示10秒后)。- 进程调度:记录进程的运行时间、睡眠时间等。
- 超时判断:例如设备驱动中判断操作是否超时(如“等待100ms后若未响应则报错”)。
- 性能统计:计算函数执行时间、系统负载等。
41.4. 常用操作与宏
由于
jiffies
可能溢出(32位系统),内核提供了安全的时间比较宏(避免直接用>
/<
比较):
宏 功能 示例(判断 a
是否在b
之后)time_after(a, b)
若 a
表示的时间在b
之后,返回真if (time_after(jiffies, timeout)) { ... }
time_before(a, b)
若 a
表示的时间在b
之前,返回真if (time_before(jiffies, start)) { ... }
time_after_eq(a, b)
若 a >= b
,返回真time_before_eq(a, b)
若 a <= b
,返回真41.5. 时间单位转换
内核提供了多个宏用于
jiffies
与实际时间(秒、毫秒等)的转换:
宏 功能 示例(HZ=1000时) HZ
每秒的节拍数 1*HZ
= 1000 节拍 = 1秒msecs_to_jiffies(ms)
将毫秒转换为jiffies msecs_to_jiffies(500)
= 500usecs_to_jiffies(us)
将微秒转换为jiffies(向上取整) usecs_to_jiffies(1500)
= 2jiffies_to_msecs(j)
将jiffies转换为毫秒 jiffies_to_msecs(2000)
= 200041.6. 示例:使用jiffies 计算延迟
#include <linux/jiffies.h> #include <linux/delay.h>// 记录开始时间 unsigned long start = jiffies;// 执行某些操作(假设耗时不确定) do_something();// 计算操作耗时(转换为毫秒) unsigned long duration_ms = jiffies_to_msecs(jiffies - start); printk(KERN_INFO "Operation took %lu ms\\n", duration_ms);// 超时判断示例:等待最多1秒(1000ms) unsigned long timeout = jiffies + msecs_to_jiffies(1000); while (condition_not_met()) {if (time_after(jiffies, timeout)) {printk(KERN_ERR "Timeout!\\n");return -ETIMEDOUT;}msleep(10); // 短暂休眠,避免CPU空转 }
41.7. 注意事项
- 精度限制:
jiffies
的精度由HZ
决定(最低1ms),不适合微秒级以下的高精度时间测量(需用ktime
或hrtimer
)。- 溢出处理:32位系统中必须使用
time_after
等宏比较时间,直接比较(如jiffies > timeout
)可能因溢出导致错误。- 用户空间不可见:
jiffies
是内核空间变量,用户空间程序无法直接访问(需通过系统调用间接获取时间)。
jiffies
是内核时间管理的“基石”,贯穿于定时器、调度、超时处理等多个核心功能中。理解它的工作原理,是掌握Linux内核时间机制的基础。
42.init_timer()
在Linux内核中,init_timer是一个用于初始化定时器结构体struct timer_list的旧版函数,主要作用是为定时器的基础成员赋值,使其能够被内核定时器系统正确管理。它在早期内核版本(如2.6.x到4.13)中广泛使用,目前已逐渐被更安全的timer_setup 函数替代,但理解其功能仍有助于掌握内核定时器的历史实现。
42.1. 功能与核心作用
init_timer
的核心功能是初始化struct timer_list
结构体的基础成员,使其处于“可配置”状态。具体包括:
- 初始化定时器的链表项
entry
(用于将定时器加入内核的定时器管理链表);- 设置
base
指针(指向当前CPU的定时器管理结构tvec_base
,用于内核内部调度);- 清除定时器的内部标志位(确保初始状态干净)。
注意:
init_timer
仅初始化基础结构,不会设置定时器的回调函数(function
)和私有数据(data
),这两个关键成员需要在调用init_timer
之后手动赋值。42.2. 函数原型与参数
init_timer
定义在<linux/timer.h>
头文件中,原型如下:void init_timer(struct timer_list *timer);
参数说明:
timer
:指向需要初始化的struct timer_list
结构体的指针(必须是已分配的内存,如全局变量或动态分配的变量)。42.3. 使用流程与示例
使用
init_timer
的典型流程是:定义定时器 → 初始化基础结构 → 手动设置回调和数据 → 设置到期时间 → 激活定时器。示例代码(旧版内核风格):
#include <linux/module.h> #include <linux/timer.h> #include <linux/jiffies.h>// 1. 定义定时器结构体 static struct timer_list my_timer;// 2. 定义定时器回调函数(旧版原型:参数为unsigned long) static void my_timer_func(unsigned long data) {printk(KERN_INFO "Timer triggered! Private data: %lu\\n", data); }static int __init init_timer_demo_init(void) {// 3. 初始化定时器基础结构init_timer(&my_timer);// 4. 手动设置回调函数和私有数据(init_timer不处理这两项)my_timer.function = my_timer_func; // 绑定回调函数my_timer.data = 0x12345; // 设置私有数据(传递给回调)// 5. 设置到期时间(3秒后,HZ=1000时为3000节拍)my_timer.expires = jiffies + 3 * HZ;// 6. 激活定时器(加入内核管理链表)add_timer(&my_timer);printk(KERN_INFO "Timer initialized and activated\\n");return 0; }static void __exit init_timer_demo_exit(void) {// 7. 卸载时删除定时器del_timer_sync(&my_timer);printk(KERN_INFO "Timer deleted\\n"); }module_init(init_timer_demo_init); module_exit(init_timer_demo_exit); MODULE_LICENSE("GPL");
42.4. 与新版函数timer_setup 的区别
现代内核(4.14+)引入了
timer_setup
函数,逐步替代init_timer
,两者的核心区别如下:
特性 init_timer
(旧版)timer_setup
(新版)回调函数绑定 需手动赋值 timer->function
初始化时直接传入回调函数(参数为 struct timer_list*
)私有数据传递 需手动赋值 timer->data
(unsigned long
)通过 container_of
从定时器指针反向获取(更灵活)安全性 不检查回调函数是否为空,易导致空指针错误 强制要求传入回调函数,避免未初始化的定时器被激活 推荐使用场景 仅兼容旧版内核代码 所有新代码均推荐使用 新版timer_setup 示例:
// 新版回调函数(参数为struct timer_list*) static void my_timer_func(struct timer_list *t) {// 通过container_of获取私有数据(假设有包含定时器的结构体)struct my_dev *dev = container_of(t, struct my_dev, timer);printk(KERN_INFO "Timer triggered! dev id: %d\\n", dev->id); }// 初始化定时器(直接绑定回调函数) timer_setup(&my_timer, my_timer_func, 0); // 0为标志位(无特殊配置)
42.5. 注意事项
- 初始化时机:
init_timer
必须在定时器激活(add_timer
或mod_timer
)之前调用,否则会导致内核链表操作错误(如野指针)。- 重复初始化风险:若定时器已被激活(在链表中),需先调用
del_timer
删除,再重新初始化,否则会破坏内核定时器管理状态。- 回调函数必须设置:
init_timer
不会检查function
是否为空,若忘记手动设置timer->function
,定时器到期时会触发空指针异常(内核崩溃)。- 逐步被淘汰:现代内核中,
init_timer
已被标记为“不推荐使用”(__deprecated
),新代码应优先使用timer_setup
,以提高安全性和兼容性。42.6总结
init_timer
是早期内核中初始化定时器结构体的基础函数,负责设置struct timer_list
的基础成员,但需要手动绑定回调函数和私有数据。随着内核演进,它已被更安全的timer_setup
替代,后者通过强制绑定回调函数减少了错误风险。理解init_timer
的功能,有助于掌握内核定时器机制的发展和兼容性处理。
43.add_timer()
在Linux内核中,add_timer是一个用于激活定时器(struct timer_list) 的函数,它将初始化好的定时器添加到内核的定时器管理链表中,使其开始被内核追踪,当到达预设的expires
时间时,自动触发回调函数。
43.1. 功能与核心作用
add_timer
的核心作用是:将一个已初始化的struct timer_list
结构体注册到内核定时器系统,使其从“未激活”状态变为“激活”状态。激活后,内核会在expires
时间(基于jiffies
的绝对时间)到达时,在软中断上下文执行定时器的回调函数(function
)。它是定时器生命周期中的“激活”步骤,与
del_timer
(删除)、mod_timer
(修改)共同构成定时器的管理接口。43.2. 函数原型与参数
add_timer定义在<linux/timer.h> 头文件中,原型如下:
void add_timer(struct timer_list *timer);
参数说明:
timer
:指向已初始化的struct timer_list
结构体的指针。该结构体必须已通过timer_setup
(新版)或init_timer
(旧版)初始化,并设置了expires
(到期时间)和function
(回调函数)。43.3. 使用流程与示例
add_timer
的使用需配合定时器的初始化和参数设置,典型流程如下:
- 定义
struct timer_list
变量;- 初始化定时器(绑定回调函数);
- 设置
expires
到期时间(基于jiffies
);- 调用
add_timer
激活定时器;- (可选)需要时通过
mod_timer
修改时间,或通过del_timer
删除。示例代码:
#include <linux/module.h> #include <linux/timer.h> #include <linux/jiffies.h>// 定义定时器 static struct timer_list my_timer;// 定时器回调函数(软中断上下文,不可睡眠) static void timer_callback(struct timer_list *t) {printk(KERN_INFO "Timer expired at jiffies: %lu\\n", jiffies);// 若需重复触发,可在此处重新设置并激活(更推荐用mod_timer)// my_timer.expires = jiffies + HZ; // 1秒后再次触发// add_timer(&my_timer); }static int __init add_timer_demo_init(void) {// 步骤1:初始化定时器(绑定回调函数)timer_setup(&my_timer, timer_callback, 0);// 步骤2:设置到期时间(2秒后,HZ=1000时为2000节拍)my_timer.expires = jiffies + 2 * HZ;// 步骤3:激活定时器add_timer(&my_timer);printk(KERN_INFO "Timer added. Will expire after 2 seconds\\n");return 0; }static void __exit add_timer_demo_exit(void) {// 步骤4:退出时删除定时器(避免回调函数在模块卸载后执行)del_timer_sync(&my_timer);printk(KERN_INFO "Timer deleted\\n"); }module_init(add_timer_demo_init); module_exit(add_timer_demo_exit); MODULE_LICENSE("GPL");
43.4. 工作原理
add_timer
内部会将定时器加入内核的定时器管理链表(由tvec_base
结构体管理,按expires
时间分组),并确保:
- 定时器被标记为“激活”状态(
timer->base
被设置为当前 CPU 的tvec_base
指针);- 内核在时钟中断(每节拍触发一次)中检查链表,当
jiffies
达到timer->expires
时,将定时器从链表中移除,并触发回调函数function
。43.5. 与
mod_timer
的区别
add_timer
和mod_timer
都可激活定时器,但核心区别在于:
add_timer
:仅能激活未处于激活状态的定时器(未被添加或已过期/删除)。若对已激活的定时器调用add_timer
,会导致内核链表混乱(重复添加),触发BUG
或崩溃。mod_timer
:可安全激活任何状态的定时器(未激活则添加,已激活则修改expires
时间),是更通用、更推荐的接口。43.6. 注意事项
- 激活前的初始化:调用
add_timer
前,必须确保定时器已通过timer_setup
或init_timer
初始化,且expires
和function
已正确设置,否则会导致未定义行为(如回调函数不执行或执行垃圾地址)。- 不可重复激活:对已激活的定时器(已在链表中)调用
add_timer
是错误的,会破坏内核定时器链表,导致系统不稳定。若需修改时间,应使用mod_timer
。- 到期后自动失效:定时器触发回调后会自动从链表中移除(变为未激活状态),若需周期性执行,需在回调函数中重新设置
expires
并调用add_timer
或mod_timer
。- 软中断上下文约束:回调函数运行在软中断上下文,不可调用可能睡眠的函数(如
kmalloc(..., GFP_KERNEL)
、mutex_lock
等)。43.7总结
add_timer
是激活内核定时器的基础接口,用于将初始化好的定时器添加到内核管理系统。但由于其无法处理已激活定时器的限制,现代内核更推荐使用mod_timer
(兼具激活和修改功能)。在使用add_timer
时,需严格确保定时器处于未激活状态,避免重复添加导致的错误。
44.mod_timer()
在Linux内核中,mod_timer 是一个用于修改已注册定时器的到期时间的函数,是内核定时器机制的核心接口之一。它允许动态调整定时器的触发时间(无论是提前、推迟还是重新激活已过期的定时器),广泛用于需要灵活控制延迟执行逻辑的场景(如超时处理、周期性任务等)。
44.1. 功能与核心作用
内核定时器(
struct timer_list
)用于在指定时间点执行回调函数(一次性触发)。mod_timer
的核心作用是:
- 调整已通过
add_timer
注册的定时器的到期时间(expires
);- 若定时器尚未注册(未调用
add_timer
),则自动完成注册(等效于add_timer
);- 若定时器已过期(回调函数已执行),则重新激活它,使其在新的时间点触发。
相比先调用
del_timer
删除定时器再用add_timer
重新添加的方式,mod_timer
更高效(内部优化了状态判断和操作),是修改定时器的首选方法。44.2. 函数原型与参数
mod_timer
定义在<linux/timer.h>
头文件中,原型如下:int mod_timer(struct timer_list *timer, unsigned long expires);
参数说明:
timer
:指向定时器结构体(struct timer_list
)的指针,需提前通过setup_timer
或timer_setup
初始化(设置回调函数和私有数据)。expires
:定时器的新到期时间,单位是内核节拍数(jiffies
),表示从当前时刻起,经过expires - jiffies
个节拍后触发回调。 (jiffies
是内核全局变量,记录系统启动后的总节拍数,1个节拍的时长由内核配置决定,通常为1ms~10ms)。44.3. 返回值
- 返回
0
:表示定时器在修改前未处于活动状态(未注册或已过期)。- 返回
1
:表示定时器在修改前处于活动状态(已注册且未过期)。44.4. 定时器结构体与初始化
使用
mod_timer
前,需先定义并初始化struct timer_list
结构体,包含回调函数(function
)和私有数据(data
):#include <linux/timer.h>// 定时器结构体 static struct timer_list my_timer;// 定时器回调函数(软中断上下文,不能睡眠) static void my_timer_callback(struct timer_list *t) {// 回调逻辑(如打印信息、触发其他操作等)printk(KERN_INFO "Timer triggered!\\n");// 若需周期性执行,可在此处再次调用mod_timer重新设置到期时间mod_timer(&my_timer, jiffies + HZ); // 1秒后再次触发(HZ为每秒节拍数) }// 初始化定时器(通常在驱动初始化函数中) static void init_my_timer(void) {// 初始化定时器:绑定回调函数和私有数据timer_setup(&my_timer, my_timer_callback, 0);// 也可使用旧版接口setup_timer(新版内核推荐timer_setup)// setup_timer(&my_timer, my_timer_callback, (unsigned long)&some_data); }
44.5. 使用流程与示例
mod_timer
的典型使用流程:
- 初始化定时器(
timer_setup
);- 调用
mod_timer
设置首次到期时间(等效于add_timer
);- 在需要调整时间时(如事件触发),再次调用
mod_timer
修改到期时间;- 驱动卸载时,通过
del_timer
或del_timer_sync
删除定时器。示例代码(动态调整定时器):
#include <linux/module.h> #include <linux/timer.h> #include <linux/jiffies.h>static struct timer_list my_timer; static unsigned int delay = 2; // 默认延迟2秒// 定时器回调函数 static void timer_func(struct timer_list *t) {printk(KERN_INFO "Timer: %d seconds passed\\n", delay);// 若需继续执行,重新设置定时器(延迟时间可动态修改)mod_timer(&my_timer, jiffies + delay * HZ); }// 模块初始化 static int __init mod_timer_demo_init(void) {// 初始化定时器timer_setup(&my_timer, timer_func, 0);printk(KERN_INFO "Module loaded. Initial delay: %d seconds\\n", delay);// 首次设置定时器:delay秒后触发(等效于add_timer)mod_timer(&my_timer, jiffies + delay * HZ);return 0; }// 模块退出 static void __exit mod_timer_demo_exit(void) {// 删除定时器(确保回调不再执行)del_timer_sync(&my_timer);printk(KERN_INFO "Module unloaded\\n"); }module_init(mod_timer_demo_init); module_exit(mod_timer_demo_exit); MODULE_LICENSE("GPL");
示例说明:
- 初始化时通过
timer_setup
绑定回调函数timer_func
;- 首次调用
mod_timer
设置定时器在delay*HZ
(2秒)后触发;- 回调函数中再次调用
mod_timer
,使定时器每隔delay
秒触发一次(实现周期性任务);- 退出时用
del_timer_sync
同步删除定时器(确保回调执行完毕后再返回)。44.6. 关键注意事项
回调函数的约束:
定时器回调函数运行在软中断上下文(
TIMER_SOFTIRQ
),因此:
- 不能调用可能睡眠的函数(如
kmalloc(..., GFP_KERNEL)
、mutex_lock
等);- 执行时间应尽可能短,避免阻塞其他软中断。
到期时间的计算:
expires
参数必须是基于jiffies
的绝对时间,常用计算方式:
- 延迟
n
秒:jiffies + n * HZ
(HZ
是内核配置的每秒节拍数,通常为1000);- 延迟
n
毫秒:jiffies + msecs_to_jiffies(n)
(需包含<linux/jiffies.h>
)。与
add_timer
的区别:
add_timer
仅能注册未激活的定时器,若重复调用会导致错误;mod_timer
可安全重复调用,自动处理“未激活→激活”或“激活→调整时间”的逻辑,更推荐使用。多处理器安全:
mod_timer
是原子操作,可在多处理器环境中安全调用,内部通过自旋锁(timer->base->lock
)保证同步。周期性任务的实现:
若需周期性执行回调,需在回调函数内部再次调用
mod_timer
(如示例中所示),注意每次需重新计算expires
(基于当前jiffies
)。44.7. 相关函数
timer_setup(struct timer_list *timer, void (*func)(struct timer_list *), unsigned int flags)
:初始化定时器(新版内核推荐)。add_timer(struct timer_list *timer)
:注册定时器(首次激活,已被mod_timer
替代)。del_timer(struct timer_list *timer)
:删除定时器(非同步,可能在回调执行中返回)。del_timer_sync(struct timer_list *timer)
:同步删除定时器(等待回调执行完毕后返回,更安全)。
mod_timer
是内核中灵活控制定时器的核心函数,通过它可以动态调整延迟执行逻辑,广泛应用于超时处理、周期性任务、延迟响应等场景。使用时需注意软中断上下文的约束和时间计算的准确性。