美术馆网站建设,ckplayer 视频网站,网站制造,文档共享网站建设前言 本文介绍中断机制#xff0c;中断作为需要频繁使用的功能#xff0c;本文将详细介绍linux内核中的中断机制。 嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程#xff0c;未来预计四个月将高强度更新本专栏#xff0c;喜欢的可以关注本博主并订阅本专栏#xff…前言 本文介绍中断机制中断作为需要频繁使用的功能本文将详细介绍linux内核中的中断机制。 嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程未来预计四个月将高强度更新本专栏喜欢的可以关注本博主并订阅本专栏一起讨论一起学习。现在关注就是老粉啦 行文目录 前言1. 中断机制介绍1.1 中断概述1.2 中断的作用1.3 中断的产生 2. 中断实现原理2.1 中断处理流程2.2 中断向量表 3. 中断来源3.1 软件中断3.1.1 CPU异常3.1.2 指令中断 3.2 硬件中断3.2.1 外设中断3.2.2 处理器间中断 4. 上半部与下半部4.1 软中断4.2 tasklet4.3 工作队列 5. 中断的API参考资料 1. 中断机制介绍
1.1 中断概述 当你在刷手机的时候此时突然npy打电话来了于是你退出刷手机状态接听npy电话此过程即为中断。 简单来说中断会让CPU停止正在执行的程序转而让CPU执行中断处理函数执行完再返回原程序。 另外整个操作系统就是一个中断驱动的死循环即裸机开发中常写的while(true) {}。其他所有的事情都是由操作系统提前注册的中断机制和其对应的中断处理函数完成。
1.2 中断的作用 中断主要有4个用途外设异步通知CPUCPU间发送消息处理CPU异常实现系统调用
1.3 中断的产生 中断信号的产生有以下4个来源 1. 外设 外设产生的中断信号是异步的一般也叫硬件中断硬件中断按照是否可以屏蔽分为可屏蔽中断和不可屏蔽中断例如网卡、磁盘、定时器都可以产生硬件中断。 2. CPU一个CPU向另一个CPU发送中断叫做IPI处理器间中断是一种特殊的硬件中断也是异步的。 3. CPU异常CPU在执行指令的过程中发现异常会向自己发送中断信号这种中断是同步的一般也叫做软件中断。 4. 中断指令直接用CPU指令来产生中断信号这种中断和CPU异常一样是同步的也可以叫做软件中断。例如中断指令int 0x80可以用来实现系统调用。
2. 中断实现原理
2.1 中断处理流程 在单片机或裸机开发中中断的处理方法是 ①、使能中断初始化相应的寄存器 ②、注册中断服务函数也就是向 irqTable 数组的指定标号处写入中断服务函数 ③、中断发生以后进入 IRQ 中断服务函数在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数找到以后执行相应的中断处理函数。 中断的执行时间不可以过长否则会影响对新的中断信号的响应性所以要尽量缩短中断执行场景的时间为此对异步中断的处理方法有两种 1、立即完全处理 对于简单好处理的异步中断可以立即进行完全处理。 2、立即预处理上半部稍后完全处理下半部 对于处理起来耗时的可以采取立即预处理加稍后完全处理的方式来实现中断。 立即预处理只能用直接处理来实现而稍后完全处理的方法分两类直接中断后处理有 softirq(软中断)、tasklet(微任务)、线程化中断后处理有workqueue(工作队列)、threaded_irq(中断线程)。 此处有一个概念硬件中断、软件中断、硬中断、软中断是不同的概念前两个是中断来源后两个是中断处理方式。
2.2 中断向量表 我们每个人都有各自的身份证代表每个人的唯一id这样通过身份证就可以指定唯一的人。中断也是这样的不同的中断信号有不同的处理方式那么系统如何区分呢即通过中断向量号。中断向量号是一个整数CPU收到一个中断信号会根据这个信号的中断的向量号去查询中断向量表根据中断向量表调用相应的处理函数。 中断向量表是一个表表里面存放的是中断向量。中断服务程序的入口地址或存放中端服务程序的首地址成为中断向量因此中断向量表是一系列中断服务程序入口地址组成的表。
3. 中断来源
3.1 软件中断 软件中断主要是两类CPU异常和指令中断。
3.1.1 CPU异常 CPU在执行过程中遇到异常就会给自己发送异常信号但是异常信号不一都是错误。
3.1.2 指令中断 指令中断是因为执行指令而产生了中断指令中断是执行特定指令而发生的中断设计这些指令的目的就是为了产生中断。其中INT n可以产生任意中断Linux用int ix80来作为系统调用的指令。
3.2 硬件中断 硬件中断分为外设中断和处理器间中断(IPI)。
3.2.1 外设中断 外设中断和软件中断有一个很大的不同软件中断是CPU自己给自己发送中断而外设中断是需要外设发送中断给CPU。显然不可能将所有外设都直接连到CPU上因此需要一个中间设备替CPU连接到所有外设接受中断信号这个设备叫中断控制器 不同的架构有不同的中断控制器比如STM32这种Cortex-M内核的单片机叫NVICCortex-A中叫GICx86上Intel开发的叫APIC。
3.2.2 处理器间中断
4. 上半部与下半部 上半部希望执行时间快不会占用很长时间的处理下半部多用于处理耗时的代码保证中断函数的快进快出。 哪部分属于上半部哪部分属于下半部没有明确规定可以有一下一些原则 ①、要处理的内容不希望被其他中断打断可以放入上半部 ②、如果要处理的任务对时间敏感可以放上半部 ③、如果要处理的任务与硬件有关可以放上半部 上半部的实现直接编写中断处理函数下半部有多种实现机制具体下文介绍。
4.1 软中断 Linux内核中使用结构体softirq_action表示软中断。
/** description : 注册软中断处理函数* param-nr : 要开启的软中断* param-action: 软中断对应的处理函数* return : 无*/
void open_softirq(int nr, void (*action)(struct softirq_action *))软中断类型枚举如下
enum
{HI_SOFTIRQ0, /* 高优先级软中断 */TIMER_SOFTIRQ, /* 定时器软中断 */NET_TX_SOFTIRQ, /* 网络数据发送软中断 */NET_RX_SOFTIRQ, /* 网络数据接收软中断 */BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, /* tasklet 软中断 */SCHED_SOFTIRQ, /* 调度软中断 */HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */RCU_SOFTIRQ, /* RCU 软中断 */NR_SOFTIRQS
};注册好软中断后需要通过raise_softirq() 函数触发
/** description: 出发软中断* param-nr : 要触发的中断* return : 无*/
void raise_softirq(unsigned int nr)4.2 tasklet tasklet使用方法简单、灵活自带有锁机制可以防止多个CPU同时运行是中断处理下半部分最常用的一种方法通过执行中断处理程序来快速完成上半部分的工作接着通过调用tasklet使得下半部分的工作得以完成。tasklet执行过程中是可以被硬件中断所中止的这样不会影响系统实时性。是一种将任务推后执行的一种机制。 软中断和tasklet之间建议使用tasklet用tasklet_struct结构体表示tasklet
struct tasklet_struct
{struct tasklet_struct *next; /* 下一个 tasklet */unsigned long state; /* tasklet 状态 */atomic_t count; /* 计数器记录对 tasklet 的引用数 */void (*func)(unsigned long); /* tasklet 执行的函数 */unsigned long data; /* 函数 func 的参数 */
};初始化tasklet使用tasklet_init函数
/** description: 初始化tasklet* param-t : 要初始化的tasklet* param-func : tasklet的处理函数* param-data : 要传递给func的参数* return : 无*/
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);也可以使用宏定义来初始化
#include linux/interrupt.h
/** description: 初始化tasklet* param-name : 要初始化的tasklet的名字* param-func : tasklet的处理函数* param-data : 要传递给func的参数*/
DECLARE_TASKLET(name, func, data)如果中途不想使用tasklet则可以调用该函数释放它不能在tasklet回调函数调用。和初始化函数作用相反。这个函数首先等待该tasklet执行完毕然后再将它释放。
/** description: 释放tasklet* param-t : 要释放的tasklet* return : 无*/
void tasklet_kill(struct tasklet_struct *t);在上半部中使用tasklet_schedule函数使tasklet在合适的时间运行
/** description: 上半部中调用使tasklet运行* param-t : 要调度的tasklet* return : 无*/
void tasklet_schedule(struct tasklet_struct *t)tasklet的使用模板
/* 定义 taselet */
struct tasklet_struct testtasklet;/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{/* tasklet 具体处理内容 */
}/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{....../* 调度 tasklet */tasklet_schedule(testtasklet);......
}/* 驱动入口函数 */
static int __init xxxx_init(void)
{....../* 初始化 tasklet */tasklet_init(testtasklet, testtasklet_func, data);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, xxx, xxx_dev);......
}总结tasklet被调用之后其绑定的处理函数不会被马上运行需要在合适的时机去运行在tasklet被调度以后只要有机会它就会尽可能早的运行在它还没有得到运行机会之前如果一个相同的tasklet又被调度了那么它仍然只会运行一次。本质上tasklet链表不能存在相同的tasklet对象。
4.3 工作队列 工作队列是另外一种下半部执行方式工作队列在进程上下文执行工作队列将要推后的工作交给一个内核线程去执行因为工作队列工作在进程上下文因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列否则的话就只能选择软中断或 tasklet。 tasklet机制是在中断上下文执行所以在tasklet中不可以执行休眠动作。 关于workqueue与tasklet这两个机制的选择看具体的工作过程有没有休眠动作 Linux内核使用work_struct 结构体表示一个工作
struct work_struct {atomic_long_t data; struct list_head entry;work_func_t func; /* 工作队列处理函数 */
};在实际的驱动开发中我们只需要定义工作(work_struct)即可关于工作队列和工作者线程基本不用管。我们需要做的是定义一个work_struct 结构体然后使用宏定义来初始化工作
// _work表示要初始化的工作_func是工作对应的处理函数
#define INIT_WORK(_work, _func)也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化
// n表示定义的工作(work_struct), f表示要处理的函数
#define DECLARE_WORK(n, f)和 tasklet 一样工作也是需要调度才能运行的工作的调度函数为 schedule_work
/** description: 在上半部中对工作的调度* param-work : 要调度的工作* return : 0成功其他值失败*/
bool schedule_work(struct work_struct *work)工作队列的使用模板
/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */void testwork_func_t(struct work_struct *work);
{/* work 具体处理内容 */
}/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{....../* 调度 work */schedule_work(testwork);......
}/* 驱动入口函数 */
static int __init xxxx_init(void)
{....../* 初始化 work */INIT_WORK(testwork, testwork_func_t);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, xxx, xxx_dev);......
}总结当调用了schedule_work后很快会执行工作函数工作函数执行完毕后work对象便自动从工作队列中移除所以就不需要用户开发中在驱动卸载函数中手动移除work对象了
5. 中断的API 在Linux内核中想使用某个中断需要申请可以用request_irq函数其注册的中断服务函数属于中断处理的上半部只要中断触发就会立即执行。
/** description : 申请中断向量* param-irq : 要申请中断的中断号* param-handler: 中断处理函数* param-flags : 中断标志* param-name : 中断名字* param-dev : 如果flags是IRQF_SHARED的话dev用来区分不同的中断一般情况下将dev设置为设备结构体dev会传递给中断处理函数的第二个参数* return : 0中断申请成功其他负值表示中断申请失败如果返回-EBUSY的话表示中断已经被申请了。*/
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);中断标志有如下所示几个
标志描述IRQF_SHARED多个设备共享一个中断线共享的所有中断都必须指定此标志。request_irq 函数的 dev 参数就是唯一区分他们的标志IRQF_ONESHOT单词触发中断执行一次就结束IRQF_TRIGGER_NONE无触发IRQF_TRIGGER_RISING上升沿触发IRQF_TRIGGER_FALLING下降沿触发IRQF_TRIGGER_HIGH高电平触发IRQF_TRIGGER_LOW低电平触发 表中的标志可以通过 | 来实现各种组合。
/** description : 释放相应的中断* param-irq : 要释放中断的中断号* param-dev : 如果flags是IRQF_SHARED的话dev用来区分不同的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。* return : 无*/
void free_irq(unsigned int irq, void *dev);/** description : 中断处理函数* param-first : 中断处理函数要响应的中断号* param-second: 一个void指针需要与request_irq函数的dev参数保持一致用于区分共享中断的不同设备* return : 返回irq_handler_t是一个枚举类型*/
irqreturn_t(*irq_handler_t)(int, void*);enable_irq()和disable_irq()用于使能和禁止中断其中disable_irq()要等当前正在执行的中断函数处理函数执行完才返回因此使用者必须保证不会产生新的中断并且确保所有已经开始执行的中断处理函数已经全部退出。
/** description: 中断使能与禁止* param-irq : 要禁止的中断号* return : 无*/
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)当要立即返回时可以使用以下函数
/** description: 调用后立即返回不会等待当前中断处理程序执行完毕* param-irq : 要禁止的中断号* return : 无*/
void disable_irq_nosync(unsigned int irq)local_irq_enable() 用于使能当前处理器中断系统local_irq_disable() 用于禁止当前处理器中断系统。
/** description: 打开和关闭全局中断* param : 无* return : 无*/
local_irq_enable()
local_irq_disable()对于关闭全局中断但是途中又要打开一会执行完成后又要保持关闭状态的话可以使用如下函数执行完会将中断状态恢复到以前的状态。 local_irq_save 函数用于禁止中断并且将中断状态保存在 flags 中。local_irq_restore 用于恢复中断将中断到 flags 状态。
/** description: 打开和关闭全局中断* param-flags: 保存中断状态的变量* return : 无*/
local_irq_save(flags)
local_irq_restore(flags)参考资料
[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第五十一章、第十七章
[2] 【操作系统】浅谈 Linux 中的中断机制
[3] Linux内核5. 中断和中断处理
[4] Linux_中断下半部