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

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. 并发安全 :多个进程可能同时操作设备,需通过锁(如 mutexspinlock)保证线程安全。

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 intbaseminor起始次设备号,通常设为 0
unsigned intcount需分配的连续次设备号数量。
const char*name设备名称(长度 ≤64 字节),出现在 /proc/devices 中(如 "mydev")。

返回值:成功返回 0;失败返回错误码(如 -EBUSY-EINVAL)。

实现原理:

  1. 动态分配主设备号: 内核从 chrdevs 数组尾部(主设备号 254)向前扫描,找到第一个空闲位置作为主设备号。若全部占用则返回 EBUSY
  2. 注册设备号范围: 调用 __register_chrdev_region(0, baseminor, count, name),其中 major=0 触发动态分配逻辑。
  3. 存储映射关系: 成功后在 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() 申请的设备号资源。

功能与定位

  1. 资源释放 释放字符设备驱动占用的设备号资源(主设备号 + 次设备号范围),避免系统资源泄漏。
  2. 生命周期管理 通常在驱动模块卸载函数(xxx_exit)中调用,与 cdev_del() 配合完成驱动的完整注销。
  3. 内核记录清理 从内核的 chrdevs 散列表中删除设备号记录,更新 /proc/devices 文件内容

参数&返回值

类型参数说明
dev_tfrom需释放的起始设备号(包含主设备号和起始次设备号)
unsigned intcount连续设备号数量(需与注册时一致)

错误处理:若参数无效(如 count=0 或设备号未注册),内核可能触发 WARN_ON 警告,但不会返回错误码

内部实现原理

  1. 散列表操作 根据 from 的主设备号定位内核 chrdevs 散列表的哈希桶,删除对应的 probe 节点(存储设备号范围与设备名)。
  2. 资源释放 释放设备号资源池中的连续设备号区间,允许其他驱动复用。
  3. 状态更新 更新 /proc/devices 文件,移除已注销的设备条目

新旧接口对比

特性unregister_chrdev_region(新式)unregister_chrdev(旧式)
适用注册函数alloc_chrdev_region / register_chrdev_regionregister_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定义设备操作函数(如 readwrite),用户空间系统调用最终触发此处函数驱动需实现 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)

字符设备驱动开发流程

  1. 申请设备号 静态(register_chrdev_region)或动态(alloc_chrdev_region)获取设备号。
  2. 定义文件操作集 实现 file_operations 中的关键函数(如 openreadwrite)。
  3. 初始化并注册 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

功能与定位

  1. 初始化 cdev 结构体 清除 cdev 内存并初始化其内部成员(如链表头 list 和内核对象 kobj)。
  2. 绑定文件操作集 将用户定义的 file_operations 结构体指针赋值给 cdev->ops,建立设备操作函数(如 openreadwrite)的映射关系。
  3. 为注册做准备cdev_add() 配合使用,完成字符设备向内核的注册,使设备可被应用层访问

类型参数说明
struct cdev*cdev待初始化的字符设备结构体指针(需已分配内存)
struct file_operations*fops设备操作函数集合(包含 openread 等函数指针)

内部实现原理

源码实现如下(简化版)

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;                      // 绑定文件操作集
}

关键步骤

  1. 内存清零:避免未初始化字段导致内核异常。
  2. 链表初始化:将设备加入内核管理的字符设备链表。
  3. 内核对象初始化:关联设备生命周期与内核对象机制(如引用计数)。
  4. 操作集绑定:使后续系统调用(如 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需手动设置 fopsowner
使用场景静态定义 cdev需动态创建设备(如模块卸载时自动释放)

18.cdev_add()

int cdev_add(struct cdev *p, dev_t dev, unsigned count);

cdev_add() 是 Linux 内核字符设备驱动开发中的核心注册函数,负责将字符设备对象(struct cdev)与设备号关联并加入内核管理系统,使设备可被用户空间访问。

功能与定位

  1. 设备注册:将初始化后的 cdev 结构体注册到内核的字符设备管理系统中,建立设备号与驱动的关联。
  2. 激活设备:调用成功后,设备立即“生效”(live),用户空间可通过文件操作(如 open()read())触发驱动函数。
  3. 设备号映射:支持连续次设备号范围(如 count=3 对应次设备号 0、1、2),适用于多实例设备(如 SCSI 磁带驱动)

参数&返回值

类型参数说明
struct cdev*p指向已初始化的 cdev 结构体(需通过 cdev_init 绑定 file_operations
dev_tdev起始设备号(由 alloc_chrdev_region 或 register_chrdev_region 分配)
unsigned intcount连续次设备号数量(通常为 1,多实例设备需扩展)

返回值:成功返回0;失败返回负错误码(如-EBUSY 设备号冲突)。


内部实现原理

  1. 设备号绑定devcount 赋值给 cdev->devcdev->count
  2. 哈希表注册 调用 kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p),将设备信息加入全局散列表 cdev_map
    • 以主设备号(major = MAJOR(dev))为索引计算哈希桶位置(i = major % 255)。
    • 将设备信息存入 struct probe 节点(含设备号范围、cdev 指针),链接至散列表。
  3. 设备可访问性 注册后,用户层调用 open() 时,内核通过 kobj_lookup() 根据设备号检索 cdev->ops,执行驱动函数(如 openread

常见错误与处理

错误场景原因解决方案
返回 -EBUSY设备号已被占用换用动态分配或调整设备号范围
返回 -EINVAL参数非法(如 count=0检查次设备号范围是否有效
用户层 open() 失败cdev_add 后未绑定 ops 函数检查 file_operations 是否完整实现
内核崩溃(NULL 指针)未初始化 cdev 或 ops 为 NULL确保 cdev_init 调用且 fops 函数已实现

资源释放与生命周期

  1. 注销设备 卸载驱动时调用 cdev_del(),从 cdev_map 删除设备记录:
    • 已打开的设备仍可操作(如 release 函数仍会被调用)。
    • 新打开的请求被拒绝(设备节点失效)。
  2. 释放设备号 必须调用 unregister_chrdev_region() 释放设备号资源。
  3. 内存释放 动态分配的 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),释放相关资源。


功能与定位

  1. 设备注销 将字符设备从内核的全局散列表 cdev_map 中移除,断开设备号与文件操作函数集(file_operations)的关联。
  2. 资源释放 触发 kobject_put() 释放 cdev 的内核对象(kobj),减少引用计数并可能回收内存(若为动态分配)。
  3. 生命周期终结 调用后设备进入“失效”状态:已打开的设备文件仍可操作(如 release() 函数会被调用),但新 open() 请求将失败
类型参数说明
struct cdev *p指向已注册的字符设备结构体(需非空且有效)

内部实现原理

  1. 散列表解绑 调用 cdev_unmap(p->dev, p->count),从全局散列表 cdev_map 中删除设备号范围对应的 probe 节点。
  2. 内核对象销毁 通过 kobject_put(&p->kobj) 释放 cdev 的嵌入对象,若引用计数归零则触发内存回收(动态分配时)。
  3. 设备状态更新 更新 /proc/devices 文件,移除设备条目

与相关函数的协作

  1. 初始化与注册cdev_init()cdev_add() 构成设备注册闭环。
  2. 资源释放链cdev_del()unregister_chrdev_region() 确保资源完全释放。
  3. 旧式接口对比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(如物理地址无效、权限不足或映射范围超出系统支持)。

核心功能

  1. 物理地址到虚拟地址的映射:内核无法直接访问物理地址(需通过页表转换),ioremap 会为指定的物理地址范围创建页表项,将其映射到内核虚拟地址空间的某个区域(通常是高端内存区域),使内核可通过虚拟地址读写设备的 I/O 内存。
  2. 处理硬件特性:底层会根据架构自动配置内存属性(如缓存、对齐等)。例如:
    • 对于需要 “无缓存” 访问的设备寄存器(避免 CPU 缓存导致数据不一致),底层实现会禁用该区域的缓存。
    • 确保映射地址满足 CPU 对齐要求(如某些架构要求访问 32 位寄存器时地址对齐到 4 字节)。
  3. 屏蔽架构差异:不同 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_nocacheioremap_wc 等)建立的物理地址到虚拟地址的映射,避免内核地址空间泄漏。它是设备驱动开发中管理 I/O 内存映射的必备函数。

参数

addr:由 ioremap 系列函数返回的内核虚拟地址(类型为 void __iomem *),即需要解除映射的地址。

返回值

无(void)。

核心功能

iounmap 的主要作用是撤销 ioremap 建立的页表映射关系,并释放相关资源:

  1. 从内核页表中移除与该虚拟地址对应的页表项,确保后续对该虚拟地址的访问会触发错误(避免野指针)。
  2. 释放映射过程中分配的辅助资源(如页表相关数据结构),具体依赖底层架构实现。
  3. 对于某些架构,可能会清理与该映射相关的缓存状态(如 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() 宏获取具体错误码。

核心作用

  1. 在 sysfs 中创建类目录调用 class_create 后,内核会在 /sys/class/ 下创建一个与 name 同名的目录(如 /sys/class/mydevcls),用于存放该类下所有设备的属性文件,实现设备的 sysfs 接口。
  2. 为设备节点创建提供基础单独的 class_create 仅创建类,还需通过 device_create 函数为该类添加具体设备,udev/mdev 会根据类和设备的信息自动在 /dev/ 目录下创建设备节点(如 /dev/myDev)。
  3. 设备分类管理同类设备(如所有 LED 设备、所有串口设备)可归到同一个类中,方便用户空间按类查找和操作设备(例如通过 /sys/class/ 快速定位设备)。

使用流程

在字符设备驱动中,class_create 通常与 device_create 配合使用,完整流程如下:

  1. 注册字符设备:通过 register_chrdev 或 alloc_chrdev_region 注册设备号和操作函数集。
  2. 创建设备类:调用 class_create 创建类。
  3. 创建设备节点:调用 device_create 为类添加设备,自动生成 /dev/ 下的设备节点。
  4. 驱动卸载时清理:通过 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");

注意事项

  1. 错误处理必须通过 IS_ERR() 宏判断 class_create 的返回值是否为错误指针,若失败需释放已分配的资源(如设备号),避免内存泄漏。
  2. 与 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 的主要作用是:

  1. 释放设备类(struct class)占用的内核资源,包括其内部管理的数据结构(如类属性、kobject 等)。
  2. 从内核的类系统中移除该设备类,确保内核不再对其进行管理。

需要注意的是,调用 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() 判断)。

功能说明

  1. 创建设备节点:在 /dev 目录下生成一个与驱动关联的设备文件(如 /dev/myled),用户空间程序可通过读写该文件与内核驱动交互。
  2. 关联设备类:将设备归属于 cls 参数指定的设备类,便于内核和用户空间对设备进行分类管理(如通过 /sys/class 目录查看类信息)。
  3. 注册设备:将设备信息注册到内核设备模型中,参与内核的设备生命周期管理。

使用示例

#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));// 错误处理(如销毁类、注销设备号等)
}

注意事项

  1. 配对使用 device_destroy:设备创建后,需在驱动退出时通过 device_destroy(cls, devt) 销毁设备节点,避免资源泄露。
  2. 设备号唯一性devt 必须是已通过 register_chrdev 或 alloc_chrdev_region 注册的有效设备号,否则会创建失败。
  3. 错误处理:必须检查返回值是否为错误指针(通过 IS_ERR() 判断),并通过 PTR_ERR() 获取具体错误码。
  4. 用户空间访问:设备节点创建后,用户空间可通过 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_createdevice_create 配合使用,完成设备节点的生命周期管理,确保驱动卸载时正确清理资源,避免内存泄漏或系统残留无效设备。

参数

  • class:指向设备所属的 struct class 结构体指针(由 class_create 创建)。
  • devt:设备的设备号(dev_t 类型,通常通过 MKDEV(major, minor) 生成)。

返回值:无(void)。

功能说明

device_destroy 的主要作用是:

  1. 从内核中移除由 device_create 创建的设备节点(即 /dev 目录下的对应文件)。
  2. 释放设备节点相关的内核资源(如设备结构体、引用计数等)。
  3. 通知系统设备已被销毁,触发相关的内核事件(如 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 允许用户空间程序向设备驱动发送控制命令,执行一些无法通过标准 readwrite 操作完成的特殊功能,例如:

  • 配置设备参数(如波特率、分辨率)
  • 获取设备状态信息(如缓冲区大小、连接状态)
  • 触发设备特定操作(如刷新缓冲区、重启设备)
  • 进行硬件控制(如控制 LED 灯、电机等)

ioctl(Input/Output Control)是一个用于设备输入输出控制的系统调用,主要用于与设备驱动程序进行交互,实现对设备的特殊控制操作。它在 Unix、Linux 等类 Unix 系统中广泛使用,是设备驱动程序提供给用户空间的重要接口之一。

参数说明:

  1. fd:打开设备的文件描述符(通过 open 系统调用获得)。
  2. request:控制命令,通常是一个宏定义,用于指定要执行的操作(由设备驱动定义)。
  3. ...:可选参数,根据 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;
}

注意事项

  1. ioctl 是设备相关的,不同设备支持的命令和参数格式可能完全不同,需参考具体设备的文档。
  2. 滥用 ioctl 可能导致系统不稳定,因此通常只在必要时使用(优先使用标准的 read/write 操作)。
  3. 在用户空间程序中使用 ioctl 时,需要确保对应的设备驱动已加载,且设备文件存在(通常位于 /dev 目录下)。
  4. 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 命令的格式,确保命令编号的唯一性,并包含命令的方向(数据传输方向)、大小和类型等信息。

各参数含义

  1. type:命令类型(8 位),通常是一个字符(如 'U''M' 等),用于区分不同设备或子系统的命令,避免冲突。
  2. nr:命令编号(8 位),在同一 type 下的唯一编号,用于区分不同的命令。
  3. size:数据结构的类型(仅 _IOR_IOW_IOWR 需要),宏会自动计算该类型的大小(sizeof(size)),表示 ioctl 传输的数据大小(14 位或 20 位,取决于架构)。
  4. 方向标识
    • _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是一个非常重要的数据结构,用于表示一个打开的文件。它不直接对应磁盘上的文件,而是内核为每个打开的文件(包括设备、管道等)创建的抽象表示存在于内核空间中。

关键成员说明:

  1. f_path 与 f_inode
    • f_path 包含文件的路径信息,其中 f_path.dentry 指向目录项(dentry)结构体,f_path.mnt 指向挂载点信息。
    • f_inode 指向文件对应的 inode 结构体,inode 包含文件的元数据(如权限、大小、磁盘位置等)。
  2. f_op
    • 指向 struct file_operations 结构体,该结构体包含了对文件进行操作的函数指针(如 readwriteopencloseioctl 等),是用户空间与内核 / 驱动交互的关键接口。
  3. f_count
    • 引用计数,记录文件被打开的次数。当 f_count 减为 0 时,内核会销毁该 struct file 结构体。使用 fget() 增加计数,fput() 减少计数。
  4. f_flags 与 f_mode
    • f_flags 存储文件打开时的标志(如 O_RDONLYO_NONBLOCKO_APPEND 等)。
    • f_mode 表示文件的访问权限模式(如读、写、执行权限)。
  5. f_pos
    • 当前文件指针的位置,记录下一次读写操作的偏移量。对于常规文件,f_pos 可以通过 lseek() 系统调用来修改。
  6. private_data
    • 驱动程序可以使用的私有数据指针,通常在 open 操作中初始化,用于保存与该文件实例相关的驱动特定信息。
  7. 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 对应磁盘上的静态文件信息,即使文件未被打开也存在于磁盘中(或内存缓存中)。

关键成员说明:

  1. 基本属性
    • i_mode:文件类型(普通文件、目录、设备等)和权限(读 / 写 / 执行权限)。
    • i_uid/i_gid:文件所有者的用户 ID 和组 ID,用于权限检查。
    • i_nlink:硬链接数量,当此值为 0 时,文件可被删除。
  2. 时间戳
    • i_atime:最后一次访问文件内容的时间(如read操作)。
    • i_mtime:最后一次修改文件内容的时间(如write操作)。
    • i_ctime:最后一次修改文件元数据的时间(如权限、所有者变更)。
    • i_crtime:文件创建时间(并非所有文件系统都支持)。
  3. 文件系统关联
    • i_sb:指向所属文件系统的超级块(struct super_block),超级块存储整个文件系统的元数据。
    • i_op:指向struct inode_operations结构体,包含针对 inode 的操作函数(如创建文件、删除文件、重命名等)。
  4. 存储相关
    • i_size:文件数据的总大小(字节)。
    • i_blocks:文件占用的磁盘块数量(以 512 字节为单位)。
    • i_blkbits:文件系统的块大小(如 12 表示 4096 字节,即 2^12)。
  5. 特殊文件类型
    • i_cdev:共用体,根据文件类型存储不同数据:
      • 管道文件:i_pipe指向管道信息
      • 块设备:i_bdev指向块设备结构体
      • 字符设备:i_cdev指向字符设备结构体
      • 符号链接:i_link存储链接目标路径
  6. 内核管理
    • i_count:引用计数,记录当前有多少struct file或其他结构引用该 inode。
    • i_state:inode 状态标志(如I_DIRTY表示需要写回磁盘,I_LOCK表示正在被操作)。
    • i_hash:用于将 inode 加入全局哈希表,加速查找。
    • i_dentry:链表头,连接所有引用该 inode 的目录项(dentry)。
  7. 私有数据
    • 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:参数类型(支持 intcharpbool 等多种类型)
  • 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/0y/nY/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:数组元素的数据类型(内核预定义的类型宏,如 intcharp 等)。
  • 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")。
  • numpNULL,则无法获取实际传递的元素数,仅能使用数组中已填充的值。

通过 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_parammodule_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:参数无效(如中断号非法、handlerNULL 等)。
    • ENOMEM:内存不足,无法完成注册。

35.4. 使用流程与示例

步骤1:定义中断处理函数

中断处理函数需快速执行(避免阻塞),仅处理紧急操作(如读取状态寄存器),耗时操作应交给“底半部”(如 taskletworkqueue)。

步骤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. 关键注意事项

  1. 中断处理函数的约束
    • 必须快速执行(通常几微秒内),不能调用可能睡眠的函数(如 kmalloc(..., GFP_KERNEL)mutex_lock 等),否则会导致系统死锁。
    • 若需处理耗时操作(如数据处理、用户空间通知),应使用“底半部”机制(tasklet_schedulequeue_work 等)延迟处理。
  2. 共享中断的条件
    • 所有共享设备必须在 flags 中设置 IRQF_SHARED
    • 每个设备的 dev 参数必须唯一(通常是设备结构体指针),用于 free_irq 时准确移除对应处理函数。
    • 中断处理函数必须能判断中断是否由自身设备触发(如检查硬件状态寄存器),并返回 IRQ_NONE 表示非自身中断。
  3. 中断号的获取
    • 对于SOC内置外设(如UART、SPI),中断号通常在设备树(Device Tree)中定义,通过 platform_get_irq(pdev, 0) 获取。
    • 对于GPIO中断,通过 gpio_to_irq(gpio_num) 转换获取。
  4. 调试与查看
    • 已注册的中断可通过 /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_irqvoid 类型函数,无返回值。但需注意:若参数无效(如 irq 未被申请、dev_id 不匹配),可能导致内核警告或错误(如 BUG)。

37.4. 使用流程与示例

free_irq 通常在驱动的退出函数(如 __exit 标记的函数)中调用,与 request_irq 形成对称操作:

  1. 驱动初始化时通过 request_irq 申请中断并注册处理程序;
  2. 驱动卸载时通过 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. 关键注意事项

  1. request_irq 参数严格一致

    free_irqirqdev_id 必须与 request_irq 中使用的参数完全相同,否则:

    • 非共享中断:可能导致内核错误(如尝试释放未申请的中断)。
    • 共享中断:若 dev_id 不匹配,会导致目标处理程序未被移除,或误删其他设备的处理程序。
  2. 避免在中断上下文调用

    free_irq 可能会等待中断处理程序执行完毕(若正在运行),因此不能在中断上下文(如中断处理函数内部)调用,否则会导致死锁。

  3. 共享中断的释放行为

    若中断线被多个设备共享(IRQF_SHARED),free_irq 仅移除当前 dev_id 对应的处理程序,其他共享设备的中断仍可正常工作;当最后一个共享设备释放中断后,中断线才会被完全释放。

  4. 确保只调用一次

    对同一 irqdev_id 多次调用 free_irq 会导致未定义行为(如内核崩溃),需在驱动退出逻辑中确保只调用一次。

  5. 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);

参数说明:

  • gpioGPIO引脚的内核编号(无符号整数)。这个编号是内核统一分配的,通常来自设备树(Device Tree)中的 gpio 属性,或通过平台特定方法(如 platform_get_gpio)获取。

38.3. 返回值

  • 成功:返回对应的中断号(IRQ number,正数),可直接用于 request_irq 等函数。
  • 失败:返回负数错误码,常见情况包括:
    • EINVALgpio 参数无效(如超出系统支持的GPIO范围)。
    • ENOSYS:当前GPIO控制器不支持中断功能,或该GPIO引脚无法配置为中断源。
    • ENOENT:该GPIO引脚未被映射到任何中断线。

38.4. 使用场景与示例

gpio_to_irq 通常在需要将GPIO引脚作为中断源的驱动中使用,流程如下:

  1. 申请并配置GPIO引脚(如设置为输入模式)。
  2. 调用 gpio_to_irq 将GPIO编号转换为IRQ号。
  3. 使用 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. 注意事项

  1. GPIO的中断能力

    并非所有GPIO引脚都支持中断功能,需查阅硬件手册确认该GPIO是否可配置为中断源。若GPIO不支持中断,gpio_to_irq 会返回 -ENOSYS-ENOENT

  2. 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;
}
  1. 资源申请顺序

    必须先通过 gpio_request 申请GPIO引脚(确保独占使用),再调用 gpio_to_irq,避免访问未分配的GPIO资源。

  2. 平台依赖性

    gpio_to_irq 的实现依赖于底层GPIO控制器驱动(如gpio_chip结构体中的to_irq回调),不同SoC(如ARM、RISC-V)的映射逻辑可能不同,但函数接口统一。

  3. 反向转换

    若需从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);

参数说明:

  • gpioGPIO引脚的内核编号(无符号整数),即内核统一分配的GPIO标识(与 gpio_requestgpio_to_irq 等函数使用的编号一致)。

39.3. 返回值

  • 成功:返回 01,分别表示引脚当前为逻辑低电平逻辑高电平
  • 未定义行为:若 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. 关键注意事项

  1. GPIO必须先申请并配置方向

    使用 gpio_get_value 前,必须通过 gpio_request 申请GPIO引脚(确保资源独占),并通过 gpio_direction_input 配置为输入模式(若用于读取外部信号)。若未配置方向,读取结果可能不准确。

  2. 输入与输出模式的区别

    • 输入模式:读取的是引脚的外部实际电平(受外部电路影响)。
    • 输出模式:读取的是内核最后一次设置的输出值(与外部电路无关,除非引脚被强制拉拽)。
  3. 避免在中断上下文滥用

    gpio_get_value 是原子操作(可在中断处理函数中调用),但频繁读取可能影响性能。中断处理中通常仅用于快速判断状态(如过滤虚假中断)。

  4. 硬件电平与逻辑电平的映射

    函数返回的 0/1逻辑电平,可能与硬件实际电压(如3.3V/0V)存在映射关系(由GPIO控制器驱动定义),但对用户透明(无需关心硬件细节)。

  5. 错误处理

    函数本身不返回错误码,因此需在调用前通过 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_oftimer 指针反向获取。

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),其工作流程如下:

  1. 当调用 mod_timer 激活定时器时,内核会根据 expirestimer_list 插入到对应的链表(按时间分组的链表,优化查询效率)。
  2. 系统时钟中断(每节拍触发一次)会检查当前 jiffies 是否达到链表中定时器的 expires
  3. 若达到到期时间,内核会将定时器从链表中移除,并在软中断上下文TIMER_SOFTIRQ)中调用其 function 回调函数。

40.5. 关键注意事项

  1. 回调函数的约束

    回调函数运行在软中断上下文,因此:

    • 不能调用可能睡眠的函数(如 kmalloc(..., GFP_KERNEL)mutex_lockschedule() 等);
    • 执行时间必须极短(微秒级),避免阻塞其他软中断。
  2. 一次性触发

    定时器默认是“一次性”的,回调函数执行后自动失效。若需周期性执行,需在回调函数内部再次调用 mod_timer 重新设置到期时间:

static void my_timer_func(struct timer_list *timer) {// 执行周期性任务...// 1秒后再次触发mod_timer(timer, jiffies + HZ);
}
  1. 时间精度

    定时器的最小精度是1个内核节拍(通常1ms),且受系统负载影响可能有延迟,不适合要求纳秒级精度的场景(需用 hrtimer 高精度定时器)。

  2. 多处理器安全

    mod_timerdel_timer_sync 等操作是原子的,可在多处理器环境中安全调用,内核通过自旋锁保证同步。

  3. 生命周期管理

    定时器结构体必须在回调函数执行期间保持有效(不能被释放),通常定义为全局变量或 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_listexpires 字段(定时器到期时间)就是基于 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)将毫秒转换为jiffiesmsecs_to_jiffies(500) = 500
usecs_to_jiffies(us)将微秒转换为jiffies(向上取整)usecs_to_jiffies(1500) = 2
jiffies_to_msecs(j)将jiffies转换为毫秒jiffies_to_msecs(2000) = 2000

41.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),不适合微秒级以下的高精度时间测量(需用 ktimehrtimer)。
  • 溢出处理: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->dataunsigned 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. 注意事项

  1. 初始化时机init_timer 必须在定时器激活(add_timermod_timer)之前调用,否则会导致内核链表操作错误(如野指针)。
  2. 重复初始化风险:若定时器已被激活(在链表中),需先调用 del_timer 删除,再重新初始化,否则会破坏内核定时器管理状态。
  3. 回调函数必须设置init_timer 不会检查 function 是否为空,若忘记手动设置 timer->function,定时器到期时会触发空指针异常(内核崩溃)。
  4. 逐步被淘汰:现代内核中,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 的使用需配合定时器的初始化和参数设置,典型流程如下:

  1. 定义 struct timer_list 变量;
  2. 初始化定时器(绑定回调函数);
  3. 设置 expires 到期时间(基于 jiffies);
  4. 调用 add_timer 激活定时器;
  5. (可选)需要时通过 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_timermod_timer 都可激活定时器,但核心区别在于:

  • add_timer:仅能激活未处于激活状态的定时器(未被添加或已过期/删除)。若对已激活的定时器调用 add_timer,会导致内核链表混乱(重复添加),触发 BUG 或崩溃。
  • mod_timer:可安全激活任何状态的定时器(未激活则添加,已激活则修改 expires 时间),是更通用、更推荐的接口。

43.6. 注意事项

  1. 激活前的初始化:调用 add_timer 前,必须确保定时器已通过 timer_setupinit_timer 初始化,且 expiresfunction 已正确设置,否则会导致未定义行为(如回调函数不执行或执行垃圾地址)。
  2. 不可重复激活:对已激活的定时器(已在链表中)调用 add_timer 是错误的,会破坏内核定时器链表,导致系统不稳定。若需修改时间,应使用 mod_timer
  3. 到期后自动失效:定时器触发回调后会自动从链表中移除(变为未激活状态),若需周期性执行,需在回调函数中重新设置 expires 并调用 add_timermod_timer
  4. 软中断上下文约束:回调函数运行在软中断上下文,不可调用可能睡眠的函数(如 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_timertimer_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 的典型使用流程:

  1. 初始化定时器(timer_setup);
  2. 调用 mod_timer 设置首次到期时间(等效于 add_timer);
  3. 在需要调整时间时(如事件触发),再次调用 mod_timer 修改到期时间;
  4. 驱动卸载时,通过 del_timerdel_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. 关键注意事项

  1. 回调函数的约束

    定时器回调函数运行在软中断上下文TIMER_SOFTIRQ),因此:

    • 不能调用可能睡眠的函数(如 kmalloc(..., GFP_KERNEL)mutex_lock 等);
    • 执行时间应尽可能短,避免阻塞其他软中断。
  2. 到期时间的计算

    expires 参数必须是基于 jiffies 的绝对时间,常用计算方式:

    • 延迟 n 秒:jiffies + n * HZHZ 是内核配置的每秒节拍数,通常为1000);
    • 延迟 n 毫秒:jiffies + msecs_to_jiffies(n)(需包含 <linux/jiffies.h>)。
  3. add_timer 的区别

    • add_timer 仅能注册未激活的定时器,若重复调用会导致错误;
    • mod_timer 可安全重复调用,自动处理“未激活→激活”或“激活→调整时间”的逻辑,更推荐使用。
  4. 多处理器安全

    mod_timer 是原子操作,可在多处理器环境中安全调用,内部通过自旋锁(timer->base->lock)保证同步。

  5. 周期性任务的实现

    若需周期性执行回调,需在回调函数内部再次调用 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 是内核中灵活控制定时器的核心函数,通过它可以动态调整延迟执行逻辑,广泛应用于超时处理、周期性任务、延迟响应等场景。使用时需注意软中断上下文的约束和时间计算的准确性。

 

http://www.dtcms.com/a/425303.html

相关文章:

  • 简述商务网站建设个人网站免费
  • 电子网站有哪些查询网官网
  • 男男床做第一次视频网站wordpress 一言
  • 手机网站用什么域名wordpress相册标签分类
  • 网站怎么制作成二维码自动点击关键词软件
  • 学习日报 20250929|数据库与缓存一致性策略的选择
  • 如何选择坪山网站建设微信公众号内容制作流程
  • 企业站模板明细桐乡市住房建设局网站公示
  • 清远网站开发广州微网站建设效果
  • 飞书轻松集成智能门锁,会议室预约开门密码自动下发
  • 内蒙古住房与建设官方网站建立什么指标体系和评价程序规范
  • 网站搜索优化官网诸城网站优化
  • 教育类网站首页设计模板swf格式网站链接怎样做
  • 整站seo包年费用广西住建厅考试培训中心
  • 旅游网站用dw怎么做中国建设银行企业网站
  • 网站基础优化网站用户体验评价方案
  • 网站的图片水印怎么做广州网站优化工具服务
  • 全网vip影视自助建站系统杭州微网站建设
  • 有没有个人做网站赚钱沈阳网站制作
  • 北京企业建站系统费用韶关哪里做网站
  • 成都网站建设专家优秀的html5网站
  • 论坛网站开发教程推广网站发布文章
  • 网站建设询价公告thinkphp网站开发技术
  • 网站建设源文件东海做网站公司
  • 苏州网站设计公司兴田德润好不好徐州html5响应式网站建设
  • 加强网站队伍建设互联网站账户e服务平台
  • 把做的网站发布打万维网上惠州城乡规划建设局网站
  • 昆明定制网站建设wordpress大前端5.0下载
  • ADC如何写入缓冲区及其处理
  • 网站建设的风险识别西宁网站制作哪家公司好