Linux内核基本知识
Linux 驱动的分类
-
字符设备驱动 — 鼠标/键盘,在对应 /dev 目录下有设备文件
-
网络设备驱动 — 网卡,使用 ifconfig 进行配置
-
块设备驱动 — eMMC、SD卡
最基本框架
#include <linux/init.h>
#include <linux/module.h>
// 驱动入口函数
static int hello_init(void)
{
return 0;
}
// 驱动出口函数
static void hello_exit(void)
{
return;
}
module_init(hello_init); // 告诉Linux,这个驱动入口函数是那个装载入口函数
module_exit(hello_exit); // 告诉Linux,这个驱动出口函数是那个装载出口函数
source insight === 只是代码阅读工具
驱动代码从下向上读
调试硬件
之前学硬件开发(功能开发 功能和你业务代码是杂糅在一起 比如你要开灯 串口通信 + led的控制 + 自定义协议 + 解析协议实现对应的 led的控制)单片机
裸机 ==== 寄存器操作
hal库操作 ==== cubmx hal库函数
freeRTOS实时操作系统
linux驱动开发
1、只给应用层提供对应操作硬件设备的接口,具体怎么实现什么功能怎么使用应用层管理 美颜相机(只给你提供相机接口(打开 读取 关闭))
Linux一切皆文件 /对应设备文件,/dev 去操作对应的硬件设备
2、Linux可以是开发 多任务去访问设备
Samba远程链接虚拟机
| 依赖 | 依赖虚拟机软件(VMware Tools) | 依赖 Samba 服务 | 依赖 SSH 服务 |
| 传输方式 | 虚拟机专用通道 | 网络协议(SMB) | 网络协议(SSH) |
| 主要用途 | 临时传文件 | 文件共享(类 NAS) | 代码开发、调试 |
| 权限控制 | 较弱 | 精细 | 基于 SSH 用户权限 |
| 访问设备 | 仅限宿主机 | 局域网任何设备 | 仅限发起 SSH 连接的设备 |
| 实时性 | 需要手动刷新 | 需要手动刷新 | 实时同步,即时生效 |
| 开发体验 | 差(需手动拷贝) | 一般(需手动拷贝) | 极佳(本地化体验) |
内核编译
由于驱动的代码是在内核层运行的;但是gcc是在应用层运行的;所以gcc不能编译驱动代码
所以要用linux源码的编译系统Makefile编译
内核编译思想: Linux 内核源码的编译系统可以编译我们编写的模块代码。
两种方式:
第一种(产品发布阶段)
把自己写的驱动源码拷贝到Linux内核源码树下,进行对应的配置编译,编译进内核。
通过编写Makefile和Kconfig文件,并执行make menuconfig配置界面来选择是否将驱动编译进内核。
第二种(产品研发阶段)
自己编译Makefile,然后利用Linux内核的编译系统,编译自己的模块代码。
Makefile告诉内核编译系统怎么编译你的.c源码,内核编译系统根据内核源码树里的规则和配置,最终生成一个可以在ARM开发板上加载的.ko驱动模块文件。
模块的内核编译makefile文件讲解
在linux编译系统(Makefile)里面KERNELRELEASE第一次调用的时候不赋值;第二次调用的时候才赋值;
ifeq ($(KERNELRELEASE),) 用于判断当前是否处于内核构建系统的第二次调用:如果 KERNELRELEASE 为空,说明是第一次被用户手动调用,此时执行主机端配置(如指定交叉编译工具链和目标平台);若不为空,则说明已被内核构建系统调用,此时执行模块编译(obj-m)。
KERNELRELEASE当前正在编译的内核版本号字符串(如 5.15.0-91-generic),用于标识模块编译目标的内核源码树版本。
我们写的makefile会被两次调用;
第一次我们在终端执行make的时候makefile被第一次调用;
第二次调用是linux编译系统调用我们写的makefile;
第二次调用是由 make -C $(BUILD_SYSTEM) M=$(MODULE_PATH) modules 这条命令触发的,它会进入内核源码树的 Makefile,而内核的 Makefile 会递归调用当前目录的 Makefile 来编译具体的模块目标(obj-m),所以第二次是内核构建系统主动、自动地重新读取并执行你的 Makefile。
所以通过KERNELRELEASE来判断是第一次还是第二次调用
自己编译的Makefile如下:
ifeq ($(KERNELRELEASE),)
ARM_BUILD_SYSTEM=/home/hqyj/fs4412/linux.3.14/linux-3.14
X86_BUILD_SYSTEM=/lib/modules/$(shell uname -r)/build
MODULE_PATH=$(shell pwd)
arm_module:
make -C $(ARM_BUILD_SYSTEM) M=$(MODULE_PATH) ARCH=arm CROSS_COMPILE=arm-linux- modules
x86_module:
make -C $(X86_BUILD_SYSTEM) M=$(MODULE_PATH) modules
clean:
make -C $(ARM_BUILD_SYSTEM) M=$(MODULE_PATH) clean
make -C $(X86_BUILD_SYSTEM) M=$(MODULE_PATH) clean
install:
cp ./Hello-driver.ko ~/fs4412/rootfs/
else
obj-m = Hello-driver.o //新的驱动只需要改这里
endif
虚拟机(也是linux系统)的内核源码的编译系统在 /lib/modules/$(uname -r)/build 目录下,它是一个指向内核源码树的符号链接。
uname -r 来获取当前内核版本号
modules 是 Makefile 里的一个编译目标,执行它告诉编译系统只生成内核模块(.ko文件),而不是编译整个内核。
驱动模块操作指令
sudo insmod hello-driver.ko安装驱动文件
dmesg:查看驱动;用于查看内核日志,可以查看驱动运行过程中通过 printk 打印的信息,适用于调试
sudo dmesg -c:清除内核日志
sudo rmmod hello-driver卸载驱动文件
lsmod:列出当前有哪些驱动;用于列出当前已加载的驱动模块,查看驱动是否成功加载;
modinfo hello-driver.ko 查看驱动的信息;用于查看驱动文件(.ko)本身携带的元信息(如作者、许可证、设备别名等),无需加载即可查询。
编译内核操作系统
make ARCH=arm exynos_defconfig //导入配置;会清除菜单里面的配置
make menuconfig
make ARCH=arm CROSS_COMPILE=arm-linux- uImage
驱动传参
作用
1.设置驱动相关参数;比如设置缓冲区大小
2.设置完全校验;防止驱动被盗用
方法
1.传递普通的参数
module_param(name, type, perm);
name: 传递的进去的名称
type: 类型
perm: 参数读写的权限 (S_IRUSR) (可以进去看一下 /sys/module/驱动名/parameters/)
static int a = 0;
static char *my_char = "a"; //对于字符来说只能传字符指针
static int count = 0;
static int number[10];
module_param(a, int, 0644);
module_param(my_char, charp, 0644);
insmod hello.ko a=1
2.传递数组
module_param_array(name, type, nump, perm);
name: 传递的进去的名称
type: 类型
nump: 实际传入进去的参数个数
perm: 参数读写的权限 (S_IRUSR) (可以进去看一下 /sys/module/驱动名/parameters/)
module_param_array(number, int, &count, 0644);
字符设备驱动框架
字符设备和块设备会生成设备文件
应用层操作设备文件从而操作硬件设备;通过linux提供的框架将open和硬件接口联系
对于应用层和接口
函数接口通过函数指针来规定返回值和参数规定死;上层代码只认这个,底层通过注册回调函数来填充实现,调用时通过指针间接执行,从而实现接口标准化、逻辑解耦和硬件抽象。
// 1. 规定接口(函数指针类型)
typedef int (*key_operation_t)(int key_code, int value);
// 2. 上层框架只认这个接口
struct key_driver {
key_operation_t report_key; // 函数指针作为接口
};
// 3. 底层驱动实现具体功能(回调函数)
int my_key_report(int key_code, int value) {
printk("Key %d = %d\\n", key_code, value);
return 0;
}
// 4. 注册:把实现绑定到接口
struct key_driver drv;
drv.report_key = my_key_report; // 函数指针指向具体实现
// 5. 调用:硬件触发时通过函数指针调用
drv.report_key(1, 1); // 实际执行的是 my_key_report

对硬件来说只写操作设备的接口
由于对于上层来说只有设备文件;所以要设备文件和接口关联;通过linux设备框架来关联
函数接口通过结构体file_operations规定;里面存放了很多函数;
高类聚低耦合:把相关的紧密放一起(高内聚),不相关的别扯上关系(低耦合)
内核open设备文件的驱动查找流程
应用层范围,设备号就藏在设备文件中
创建设备文件
mknod 设备文件名 设备文件类型 主设备号 次设备号
mknod /dev/led c 77 0
内核用inode和file这两个结构体来记录信息为了方便查找,提高效率。
inode 这个结构体,第一次打开这个设备时,对应的 i_cdev 是没有值的,发现对应的指针为空,它就会通过设备号去查找,去找对应的底下的 cdev,找到之后会把对应的 cdev 的地址放到 i_cdev 中,第二次 open 打开这个设备文件的时候,对应的 i_cdev 已经记录了,就可以通过 i_cdev 找到。
我们对应打开文件之后,一般都是读写,那对应的 read 和 write 第一个参数都是文件描述符,你通过文件描述符找到的就是 file 结构体,从而找到 file_operations,如果没有file 这个结构体,需要重新找一遍,大大提高了对应的效率。
当驱动被加载时,它会向内核注册自己的主设备号和一组操作函数(file_operations)。当用户程序调用 open 打开 /dev 下的对应设备文件时,内核通过文件中的主设备号找到并绑定该驱动,之后用户程序对该文件描述符的读写操作,就会自动被内核转发到驱动注册的对应函数,从而实现对硬件的控制。

cdev
把写好的接口函数写入cdev里面去
cdev结构体:描述通用的字符设备结构体;不然一个设备一个结构体太占内存了
几个设备提供几个函数接口;然后把函数接口放到cdev结构体中
struct cdev {
struct kobject kobj; /* 内嵌内核对象,用于设备模型管理(sysfs、引用计数) */
struct module *owner; /* 指向拥有此设备的模块(通常为 THIS_MODULE),防止模块被卸载 */
const struct file_operations *ops; /* 文件操作函数集(open/read/write/ioctl 等),驱动核心接口 */
struct list_head list; /* 链表节点,用于将设备挂入内核全局链表或哈希表 */
dev_t dev; /* 设备号(主设备号 + 次设备号),唯一标识设备 */
unsigned int count; /* 次设备号数量,表示该设备占用的连续次设备号范围大小 */
};
//主要填充dev和ops
常见的内存分配函数
malloc 分配的空间"不连续";虚拟地址连续,物理地址可能不连续
kmalloc 分配的空间"连续";虚拟地址连续,物理地址也连续
┌─────────────────────────────────────┐ 高地址
│ 内核空间 │ ← 内核代码、数据、kmalloc分配在这里
│ (1GB on 32-bit Linux) │
├─────────────────────────────────────┤
│ 栈区 (Stack) │ ← 局部变量、函数调用
├─────────────────────────────────────┤
│ | | │
│ | | (内存映射区) │ ← mmap、共享库
│ | | │
├─────────────────────────────────────┤
│ 堆区 (Heap) │ ← malloc分配在这里(用户空间)
├─────────────────────────────────────┤
│ BSS段 │ ← 未初始化全局/静态变量
├─────────────────────────────────────┤
│ 数据段 (Data) │ ← 已初始化全局/静态变量
├─────────────────────────────────────┤
│ 代码段 (Text) │ ← 程序指令、只读
└─────────────────────────────────────┘ 低地址
devm_kzalloc 分配在内核空间的 slab 堆区,属于内核态内存,物理连续且自动管理生命周期。
devm_kzalloc 为 key 结构体分配并清零内存,且内存生命周期绑定到 pdev->dev,设备注销时自动释放。
设备号
设备号是内核查找cdev的唯一索引,主设备号标识设备类型,次设备号区分同类型的不同设备。
驱动的标识:设备号
-
12bit(主设备号)+ 20bit(次设备号)= 32bit
-
主设备号:标识一类设备
-
次设备号:为了区分同类设备的不同设备
| 查看对象 | /dev 目录下的具体设备文件(节点) | 内核中已注册的设备驱动程序(由主设备号标识) |
| 展示内容 | 主设备号 和 次设备号 | 仅主设备号,以及对应的驱动名称 |
| 信息维度 | 个体信息:每个设备文件是一个独立的个体,拥有自己完整的设备号 (major, minor) | 类别信息:展示有哪些驱动类别(主设备号)已被内核识别,不关心每个类别下的具体个体(次设备号) |
| 适用场景 | 当你想知道某一个特定设备文件(如 /dev/sda1)的设备号时 | 当你想知道系统支持哪些类型的设备,或验证一个驱动程序是否已成功加载时 |
驱动核心思想
向应用层的程序提供设备的操作函数接口;如何让应用层的程序找到底层驱动提供的接口
linux应用程序和驱动程序相互关系
底层写驱动的对应的本质:为上层提供接口,函数接口,硬件的函数接口。
向上层应用工程师提供函数接口的方法,函数怎么写?怎么规定写法
在c语言中函数指针来限制函数的写法
在c++有语法可以对子类函数接口来进行限制,多态的场合写的纯虚函数,父类定义纯虚函数,相当于规定“必须有这个功能”
总结:
我们写驱动就是给别人提供一种函数接口,给上层的工程师调用,但是上层工程师不知道我们今天给提供了什么接口函数,不知道函数名,调用不了,但是他必须要调用我们函数,这个时候能想到的就是函数指针。底层驱动把具体的函数地址赋值给这个指针,这样就能调用函数
内核发展
字符驱动框架
2.4 linux kernel 字符驱动框架
LED驱动缺点:
可移植性差,原因:驱动中包含了特定平台的硬件信息(华清板子 + ccynos4412芯片),如果是其他平台,硬件信息会有差异,所以驱动无法直接使用!
总线、设备、驱动框架
2.6 linux kernel 总线、设备、驱动框架
总线、设备、驱动 => 增加驱动的可扩展性和可移植性
将设备的信息从驱动中分离出来,我们需要在操作系统中,添加设备和驱动两部分。
设备中包含设备的信息(资源),驱动中包含的是操作设备函数接口。
为了让驱动最终能操作我们的硬件设备,我们在驱动中必须获取设备的信息(资源)。
设备驱动=设备+驱动;驱动中不再直接包含设备的信息
设备和驱动是分离的,它们都会向内核总线(如 platform 总线)注册。当任何一方(无论是设备先来,还是驱动先来)注册时,内核都会触发一次总线上的匹配扫描,寻找名称或ID匹配的对方。一旦匹配成功,操作系统就会自动调用驱动中实现的 probe 函数。
当设备和驱动匹配成功后,内核会将这个设备的详细信息(包括设备树解析出的资源、名称、ID等)封装成 platform_device 结构体,并通过 probe 函数的参数传递给驱动。
在 probe 函数里,开发者只需要使用操作系统提供的通用API(如 platform_get_resource、devm_clk_get 等),就能从匹配到的设备信息中获取硬件资源(地址、中断、GPIO等),而无需关心这些信息是如何被记录的。
设备与驱动匹配成功后,二者通过指针相互绑定(如 pdev->driver 指向驱动,drv->devices 链接设备),形成 “你中有我,我中有你” 的关联。
在Linux内核中,总线本质上是两个用于管理和匹配的链表,分别挂载着该总线上的设备和驱动;当设备或驱动注册时,内核通过总线遍历这两个链表进行匹配,匹配成功则调用驱动的probe函数。
设备树
cd /home/hqyj/fs4412/linux.3.14/linux-3.14/
vim arch/arm/boot/dts/exynos4412-fs4412.dts
驱动文件(.ko)从 NFS 根文件系统读取,所以更新后立即生效;设备树(.dtb)在 U-Boot 阶段从 eMMC/SD 卡分区读取,所以即使 NFS 目录中的 .dtb 文件更新了也不会生效。改用 TFTP 加载设备树即可解决
make dtbs 必须在 Linux 内核源码的根目录下执行; exynos4412-fs4412.dts 文件正好在这个源码树中。
早期没有设备树只有镜像文件;包含我们上面写的配置信息
设备的信息是针对于特定平台的,如果我们在Linux内核中包含太多设备信息,则Linux内核移植性就会变差。引入设备树之后,设备的信息的描述不在是以代码的形式存在于Linux内核源代码中,这种做法实际上是将设备的信息,从Linux内核中独立出来,单独描述
Linux内核在启动的时候,要求把设备树文件传递给它。它拿到设备树之后,会解析设备树文件,从而识别设备信息。
//@ 后面通常跟设备基地址
fs4412-led@11000c40{
compatible = "fs4412-led";
reg = <0x11000c40 8>;
};
Linux 内核platform总线上设备与驱动的匹配规则
<1>设备树中的compatible属性与驱动中指定的of_match_table中的compatible进行匹配
如果没有匹配成功:
<2>如果驱动中有id_table,则拿id_table中记录的名字与设备的名字匹配
如果驱动中没有id_table,则拿驱动的名字与设备的名字(platform_device 结构体中 name 字段)匹配
of 是 "Open Firmware"(开放固件)设备树
设备树后缀三个设备文件
.dtb:二进制文件;给操作系统用的可执行文件
.dts:.c文件;设备信息源文件;给工程师看的
.dtsi:.h文件;设备树的头文件
写设备树步骤
写设备树需要看驱动
1.拷贝设备树
通过当前设备树的头文件找出所有设备树文件;然后拷贝出来到电脑的共享文件夹中;
因为在内核(linux)里面查会有很多设备树文件,不方便看
2.找厂家的GPIO控制器节点
每个GPIO控制器必包含gpio-controller属性
# 在设备树目录下递归搜索 gpio-controller 属性
cd 源码/arch/arm/boot/dts/
grep "gpio-controller" * -nR
| grep | Linux 下的文本搜索工具(Global Regular Expression Print) |
| "gpio-controller" | 设备树中声明某个硬件节点为 GPIO 控制器的标签,告诉内核“我能管理 GPIO 引脚,其他设备可以通过我申请使用具体引脚”。 |
| * | 表示当前目录下的所有文件(作为搜索范围) |
| -n | 显示行号(在匹配结果中显示该行在文件中的行号) |
| -R | 递归搜索(Recursive,进入所有子目录继续搜索) |
就可以找到GPIO控制器节点,并规定了使用该控制器时,需要提供2个参数
//gpx2:gpx2前面的是标签名:方便其他节点引用;后面的是节点名
gp2: gp2 {
gpio-controller;
//#xxx-cells = <数字>
//含义:xxx 这个信息,需要使用多少个 32-bit 数字(u32)来表示。
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
3.确认参数含义
实在不知道怎么写在源码/Documentation/devicetree/bindings:有各个设备树的解释
grep "gpio-cells" * -nR | grep "厂家名字"
查找到的信息如下

还不知道的话在arch/arm/boot/dts/里面查看这个节点中别人是怎么写的
# 进入设备树目录
cd arch/arm/boot/dts/
# 搜索某个节点或属性的用法(以 GPIO 为例)
grep "gpios =" * -nR | grep "&gpx"
# 或者搜索特定控制器的引用
grep "&gpx2" * -nR
4.根据原理图来确定管脚
知道哪个是GPIO控制节点然后根据自己的设备电路图找到对应的节点
5.引用该节点
在自己的设备树中(linux-3.14/arch/arm/boot/dts)引用该节点

我们创建了多个设备文件;那么驱动怎么操作设备文件获取不同的设备信息
全局的pled只能一次记录一个设备的信息;其他的就丢了;此时用链表

驱动通过led_open函数的inode参数获取次设备号,并用该次设备号在设备信息链表/数组中定位到对应的私有数据结构,从而区分并操作不同的LED设备。
中断(Interrupt)
注意
1.中断打断其他程序的执行,所以中断处理的时候需要尽可能的快,不能在中断处理过程中做耗时很长的事情。
2.中断打断的当前的程序执行,所以在中断处理的时候,需要先保存现场(CPU的状态和CPU内部寄存器的值:压栈保存)在中断处理结束的时候,则恢复现场。
概念
1.中断源:产生中断的源头
2.中断号:是SOC芯片厂家对SOC芯片内部中断源的编号
3.中断处理函数:中断产生之后,需要调用执行的函数
4.中断控制器:控制中断的优先级、中断是否被允许产生
5.内部中断和外部中断:
内部中断:SOC芯片内部控制器产生的中断
外部中断:SOC芯片外部管脚通过电平触发产生的中断
高电平触发、低电平触发、上升沿触发、下降沿触发、双边沿触发
Linux 中断分为三类:
软件中断(内核代码主动触发,用于异步任务处理)、
私有中断(PPI)(仅绑定的特定 CPU 核心接收,如 CPU 本地定时器)
共享中断(SPI)(中断信号可以发给多个 CPU 核心,由系统决定由哪个核心处理。可被多个外设共用,但同一中断号上多个设备需共享一个 ISR,由驱动自己判断谁触发了中断)。

当 GPIO、定时器(Timer)、ADC 等外设模块产生中断信号后,这些信号会先经过各自内部的子中断控制器(如 Combiner)进行初步合并或管理,随后统一汇总到中断控制器(GIC,Generic Interrupt Controller)。GIC 作为中断枢纽,负责对每个中断源进行编号、使能、屏蔽和优先级仲裁,并根据配置将中断请求分类为普通中断(IRQ)或快速中断(FIQ),最终分发给ARM 核心(ARM Core)。ARM 核接收到中断信号后,会立即保存当前程序的执行上下文(现场),然后跳转到异常处理向量表,执行对应的中断服务例程(ISR),处理完后再恢复现场并返回被打断的程序继续执行。
查找设备树
先在自己设备树目录下查找看别人怎么写的
grep "interrupt-parent" * -nR
然后在linux-share/learn-driver/dts自己的设备树下查找;看看芯片厂家怎么写的
grep "interrupt-controller" * -nR


combiner要看芯片手册支不支持

就近原则:谁直接控制我们中断就写谁;我们选图三
实在不知道怎么找在芯片的参考文档linux-3.14/Documentation/devicetree/bindings中找
grep "#interrupt-cells" * -nR | grep exynos
先找板子再找厂家;从范围小的找起


| 第一个参数(中断号) | 1 | 硬件原理图:按键连接在 GPX1_1 引脚,对应中断编号 1 |
| 第二个参数(触发方式) | 8 | 芯片手册/文档:低电平触发 |
所以写出来的设备树是

写程序
1.先调用platform总线设备结构体;然后注册和注销设备
注册设备还可以用以下代码
/*
* 功能:将 platform_driver 结构体注册到内核平台总线
*module_platform_driver() 是一个宏,它会在预处理阶段自动展开成一段完整的代码
*同时包含了注册(module_init)和注销(module_exit)两部分。
* 等价于在 module_init 中调用 platform_driver_register()
* 并在 module_exit 中调用 platform_driver_unregister()
*/
module_platform_driver(key_driver);
/*
* MODULE_AUTHOR:声明驱动作者信息
* 可通过 modinfo 命令查看
*/
MODULE_AUTHOR("HQYJ");
/*
* MODULE_LICENSE:声明模块许可证(内核强制要求)
*/
MODULE_LICENSE("GPTL");
2.设备树匹配表
/* 设备树匹配表:用于定义该驱动支持哪些硬件设备 */
static const struct of_device_id key_of_match[] = {
{ .compatible = "fs4412-key" },
{ /* Sentinel */ } // 空哨兵,表示表结束
};
/* 将设备树匹配表导出为模块设备表,供内核在加载时识别 */
MODULE_DEVICE_TABLE(of, key_of_match);
在设备树中,你只能看到 compatible = "snps,dw-apb-uart" 这个节点属性;而 MODULE_DEVICE_TABLE() 的产物在设备树中看不到,只能在模块文件(.ko)的元数据中看到
//modinfo 命令用于查看 Linux 内核模块(.ko 文件)的详细信息
modinfo key_driver.ko
3.驱动匹配和分离函数
其中驱动匹配函数需要获取资源
//第一种:获取中断资源
res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
if(!res) {
printk("Fail to platform_get_resource:IRQ\\n");
return -ENODEV;
}
printk("IRQ number:%d\\n",res->start);
printk("interrupt-name:%s\\n",res->name);
printk("interrupt flags:%#x\\n",(unsigned int)res->flags);
//第二种:自己解析设备树节点获取中断号
irq_number = irq_of_parse_and_map(np,0);
if(!irq_number) {
printk("Fail to irq_of_parse_and_map\\n");
return -ENODEV;
}
printk("irq number:%d\\n",irq_number);
共享中断
多个驱动程序申请的是同一个中断号(多个设备请求的中断是同一个中断的时候)
此时中断产生的时候,操作系统是不区分那个设备产生了中断,操作系统做法是将这个中断号关联
所有中断处理函数全部调用一次,所以此时中断处理函数必须能判别是否是自己的设备产生的中断,如果不是,立即返回,如果是,则做中断处理。
<1>注册中断的时候,需要IRQF_SHARED标志
<2>给中断处理函数传递的参数必须是唯一的,不能是NULL

全局的irq_desc数组按中断号(0到NR_IRQS-1)索引,每个irq_desc结构体记录对应中断号、指向特定中断控制器操作函数集(irq_chip,如VIC0/VIC1)的指针,以及一个由irqaction结构体组成的链表。irqaction用于描述具体的中断处理动作,包含处理函数handler、传递给处理函数的参数、中断标志、设备名称,并通过next指针支持多个设备共享同一中断号,当发生中断时,内核会通过该中断号找到对应的irq_desc,进而调用其irq_chip中的控制函数(如enable/disable)并遍历执行action链表上的所有处理函数。
中断上半部和下半部
将中断处理函数中需要做的事情,分成两部分,在不同的函数中完成。
上半部是中断处理函数,在中断上下文中执行,屏蔽中断、不可被打断,负责紧急任务;
下半部在中断使能状态下执行,可被打断,并在中断上下文或进程上下文中运行,负责耗时的剩余工作。
上半部在中断产生时立即执行,在中断上下文中运行且屏蔽中断,只负责紧急、快速的任务(如清中断标志、拷贝硬件数据);
下半部则在合适的时机(通常上半部结束后触发)执行,不屏蔽中断、可被打断,负责耗时较长或可能需要休眠的剩余工作(如数据处理、协议解析)。
以网卡为例,中断触发后,上半部快速拷贝数据包并触发下半部,下半部随后解析以太网头、IP头、TCP/UDP头及用户数据。这样设计既保证了中断响应的实时性,又避免长时间屏蔽中断导致系统性能下降。
下半部的实现机制
软中断

分析到这里,要告诉大家一个不幸的消息,Linux 内核并没有导出open_softirq、raise_softirq函数的符号,也就说我们无法以动态加载模块的方式使用软中断。如果大家需要使用软中断,可以修改内核代码,导出这两个函数的符号,然后重新编译内核。
查看是否导出
EXPORT_SYMBOL
tasklet
tasklet 是基于软中断实现的。Linux 内核支持的 10 种软中断中,HI_SOFTIRQ 和 TASKLET_SOFTIRQ 就是用来实现 tasklet 的。
软中断不能直接使用,Linux 内核为了方便开发者,在软中断之上封装了 tasklet 接口,使用更简单。所以,除非对性能要求特别高,否则都应使用 tasklet。
work queue
工作队列(work queue)是另一种将工作推后执行的形式,它把工作交由一个内核线程去执行,在进程上下文运行,但不能访问用户空间。其最大特点是允许重新调度甚至睡眠。
选择工作队列还是软中断/tasklet,可遵循以下规则:
推后执行的任务需要睡眠 → 只能选工作队列;
任务需要延时指定时间再触发 → 选工作队列(可利用timer延时);
任务需要在一个tick内处理 → 选软中断或tasklet(可抢占普通进程和内核线程);
任务对延迟时间无要求(无关紧要的任务) → 选工作队列。
另外,如果需要用一个可以重新调度的实体来执行下半部,应使用工作队列。它是唯一能在进程上下文运行的下半部机制,也只有它可以睡眠。这在需要获取大量内存、获取信号量、执行阻塞式I/O操作时非常有用。
| 软中断 | 中断 | 高 (需要自己确保软中断的执行顺序及锁机制) |
好 (全部自己实现,便于调优) |
没有 |
| tasklet | 中断 | 中 (提供了简单的接口来使用软中断) |
中 | 同类型不能同时执行 |
| 工作队列 | 进程 | 低 (在进程上下文中运行,与写用户程序差不多) |
差 | 没有 (和进程上下文一样被调度) |
Input子系统
Input 子系统用于管理各类输入设备(如按键、触摸屏、鼠标等),负责对外部事件的感知与上报。它将硬件层产生的原始输入事件,经过内核驱动和核心层处理后,通过统一的接口(如 /dev/input/eventX)提供给用户空间应用程序使用。

事件处理 handler 驱动模块
evdev : 通用输入事件接口, 将不同设备的输入 ( 如键盘、 鼠标、 触摸屏 ) 转换为统一的事件格式。 用户 可以通过设备文件 /dev/eventX 或 /dev/input/eventX 读取事件。 evdev 次设设备号范围: 64-95 和 256-1024 , 如果设备数量不超过 32 , 使用静态保留区范围 64-95 , 如 果超过 32 个, 则在 256-1024 之间动态分配。次设备号不一定,但主设备号是一定的
mousedev : 一个早期的鼠标事件接口, 支持 PS/2 接口的鼠标, 现在鼠标基本上都是 USB 或蓝牙接口 的, 他们使用了 HID 接口, HID 后文会单独介绍。
joydev : 专为游戏手柄、 摇杆设计, 处理多轴、 按键事件, 生成 /dev/input/jsX 设备节点。 提供标准游 戏设备接口, 支持校准和力反馈功能, 方便游戏或输入工具直接访问。
input_leds : 管理输入设备的 LED 状态 ( 如键盘的 Caps Lock 、 Num Lock )通过 sysfs ( 如 /sys/class/leds ) 或 ioctl 控制 LED 开关, 是硬件驱动与用户空间控制的中继。
Input 子系统框架
input_dev :硬件驱动层, 代表一个 input 设备。
input_handler : 事件处理层, 代表一个事件处理器。
input_handle : 核心层, 代表一个配对的 input 设备与 input 事件处理器。

设备匹配流程
Linux 内核中维护了一个名为 input_handler_list 的链表, 每个 input_handler 都会被注册到这个链表 上, 而链表是通过类型为 list_head 的成员 node 串联起来的。

匹配成功后


输入设备结构体
struct input_dev {
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)]; // 设备属性
unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 事件类型表
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 按键事件码表
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 相对坐标事件码表
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; // 绝对坐标事件码表
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; // 混杂事件码表
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; // led灯事件码表
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; // 声音事件码表
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; // 力反馈事件码表
unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; // 开关事件码表
}
在注册 input_dev前需要初始化一些必要的数据包括 input_dev的evbit 、keybit 、absbit 和relbit等几个重要成员,evbit代表当前设备支持哪些事件类型, 其他几个表示当前设备支持那些事件码。 比如一个按键驱动需要初始化 evbit 和 keybit ,evbit 的 EV_KEY 位要置位,keybit 的键值位要置位。
使用 set_bit 这个函数进行操作:
void set_bit(unsigned int nr, volatile unsigned long *p);
//示例:
set_bit(EV_KEY, input_dev->evbit); // 支持按键事件
set_bit(KEY_1, input_dev->keybit); // 支持数字1键
set_bit(KEY_2, input_dev->keybit); // 支持数字2键
input能支持哪些类型事件
struct input_event {
__kernel_ulong_t __sec; // 上报时间秒
__kernel_ulong_t __usec; // 上报时间微秒
__u16 type; // 上报事件类型
__u16 code; // 上报事件编码
__s32 value; // 上报事件值
}
Input 子系统常用接口
/* 手动分配 input_dev 结构体 */
struct input_dev *input_allocate_device(void);
/* 释放 input_dev 结构体 */
void input_free_device(struct input_dev *dev);
/* 自动管理 input_dev 结构体,当 dev 被释放时 input_dev 结构体自动释放 */
struct input_dev *devm_input_allocate_device(struct device *dev);
/* 注册 input_dev 到系统中的 input 核心层 */
int input_register_device(struct input_dev *dev);
/* 在 input 核心层注销 input_dev */
void input_unregister_device(struct input_dev *dev);
具体的事件还需要下面的函数
/* 上报一个按键事件 */
void input_report_key(struct input_dev *dev, unsigned int code, int value);
/* 上报一个相对坐标事件 */
void input_report_rel(struct input_dev *dev, unsigned int code, int value);
/* 上报一个绝对坐标事件 */
void input_report_abs(struct input_dev *dev, unsigned int code, int value);
/* 标记一组输入事件的结束 */
void input_sync(struct input_dev *dev);
/* 上报触摸点信息 */
void input_mt_slot(struct input_dev *dev, int slot);
/* 添加新触摸点 */
bool input_mt_report_slot_state(struct input_dev *dev, unsigned int tool_type, bool active);
/* 同步触摸事件 */
void input_mt_sync(struct input_dev *dev);
在中断章节里我们已经讲解了按键驱动的编写,这里只需要在其基础上增加输入设备相关代码即可,调整部分包括:
1、定义input_dev结构体
2、probe函数中的修改
通过input_allocate_device为input_dev分配内存并初始化;
设置input_dev的evbit成员和keybit的成员;
通过input_register_device将input_dev注册到系统中;
3、remove函数的修改
通过input_unregister_device将input_dev从系统中注销;
通过input_free_device将input_dev释放掉;
4、中断处理函数的修改
判断具体按键,通过input_report_key将按键事件提交给上层,一次按键需要同步两个事件包括按下和抬起;
调用input_sync同步事件;
引入 input 子系统,让你的按键从"内核调试工具"升级为"系统标准输入设备"!
-
之前:按键只有内核开发者能看见(看 dmesg)
-
之后:按键所有人都能用(应用程序 + 系统服务 + GUI)
加载驱动 (insmod)
↓
module_init → key_driver_init()
↓
platform_driver_register(&key_driver) // 向平台总线注册驱动
↓
平台总线根据 of_match_table 匹配设备树节点
↓
匹配成功 → key_probe() 被调用
↓
1. 分配 key_data 结构体(存储 irq 和 input 指针)
2. 获取中断资源(中断号、触发方式)
3. 分配 input 设备(为应用层提供接口)
4. 设置 input 设备(设备名、支持事件类型、支持按键)
5. 注册 input 设备(生成 /dev/input/eventX)
6. 保存 key_data 到 platform(方便 remove 时使用)
7. 注册中断(绑定中断号和中断处理函数)
↓
驱动加载完成,等待按键中断
↓
按下 K2/K3 → 触发硬件中断
↓
CPU 跳转到 key_interrupt() 执行(中断上下文)
↓
1. input_report_key(…, KEY_2, 1) → 上报按下事件
2. input_sync() → 同步事件到应用层
3. msleep(20) → 防抖延时
4. input_report_key(…, KEY_2, 0) → 上报释放事件
5. input_sync() → 再次同步
6. printk 打印日志
↓
返回 IRQ_HANDLED(告诉内核中断已处理)
↓
应用层通过 read("/dev/input/eventX") 收到按键事件
↓
卸载驱动 (rmmod)
↓
module_exit → key_driver_exit()
↓
platform_driver_unregister()
↓
key_remove() 被调用
↓
1. 获取 key_data 结构体
2. input_unregister_device() → 注销 input 设备
↓
驱动卸载完成






