关于linux中断的杂记
ARM contex-A系列的内核不支持中断嵌套(不支持中断嵌套的cpu在处理一个中断时无法相应其他中断)。在内核中断函数中,如果中断处理的时间过长,产生中断嵌套,重者系统崩溃,
轻者也会影响其他事件的处理,这就是中断中不能使用延时函数的原因。
但是有些高实时性设备(比如网卡),就是需要处理大量业务,为了满足中断处理时间尽量短的原则,我们将一些简单的处理放在中断中实现,这个阶段叫做中断的上
半部,剩下的复杂、耗时间的操作丢给内核线程,让内核来调度其执行,这就是中断的下半部。
linux中断的处理流程:
中断事件----->跳转中断处理程序入口------->执行中断处理程序上半部------->将中断处理程序下半部交给内核调度----->结束中断处理
linux中断的处理方式:
1.softirq:处理比较快,但是它是内核级别的机制,需要修改整个内核源码,不推荐也不常用,会并发地执行在不同的cpu上
2.tasklet:内部实际调用了softirq,会并发地执行在不同的cpu上,在软中断中划分一部分线程专门用于处理tasklet数据结构中待处理的接口。大部分中断的下半部分使用
tasklet即可,只有对像网络这样对性能要求非常高的情况,才需要使用软终端,2个相同的软中断有可能同时执行。两个不同类型的tasklet可以在不同的处理器上运行,但类型相同的tasklet不能同时执行
3.workqueue:工作队列,对资源的消耗更加少切只会线性地串行执行而不会并发执行。对于一些性能要求较高的子系统,如网络部分,不能胜任
tasklet:启动下半部实际上就是tasklet结构体描述的对象丢给内核线程的操作,基本上就是定义好处理函数后初始化一个tasklet对象实例,然后将结构体
交给内核,并等待内核调度软中断线程来执行。
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);//下半部的处理逻辑
unsigned long data;//传递给func
}
1、初始化对象
struct tasklet_struct mytasklet;
tasklet_init(struct tasklet_struct *t,void(*func(unsigned long),unsigned long data)
或者
DECLARE_TASKLET(name,function,data)
2、构造”下半部“的实现逻辑
void key_tasklet_half_irq(unsigned long data)
{
}
3”上半部“启动”下半部“
tasklet_schedule(&key_dev->mytasklet)
4、在模块卸载时注销内核线程中的对象
tasklet_kill(&key_dev->mytasklet);
workqueue:
工作队列的使用情况分为两种:一是使用内核定义的工作队列,二是自行定义工作队列。常规是使用内核定义的工作队列,除非由特别多的工作会加入到工作队列中,
否则不需要自己定义工作队列。
struct work_struct
{
atomic_long_t data;
struct list_head entry;
work_func_t func;
}
初始化对象:
struct work_struct mywork;
定义服务接口函数:
void xxx_func(struct work_struct *work)
绑定处理函数接口
INIT_WORK(struct work_struct *work,work_func_t func);
构造”下半部分“实现逻辑
void work_irq_half(struct work_struct *work_struct)
{
}
将工作计入到队列中等待调度
schedule_work(struct work_struct *work);
确保没有工作队列入口在系统中任何地方运行,会等待所有在处理的队列完成
void flush_schedule_work(void)
延时调度工作
int schedule_delayed_work(struct delayed_struct *work, unsigned long delay);
取消加入工作队列的工作
int cancel_delayed_work(struct delay_struct *work);
取消加入工作队列的工作的接口函数cancel_delayed_work只用于取消延时的工作队列,因为非延时的工作队列通常在执行调度请求后很快就会执行,此时取消
用处不大,当然如果发起调度和取消调度都在中断上下文是可以的。
中断
1.硬件中断------异步中断
硬件中断本质上是一种电信号,有硬件设备发出,用于通知处理器特定事件,不同的设备对应不同的中断,每个中断通过唯一的数字标示,称为中断请求(IRQ)。
处理器内部对其进行编号后,也称为中断号。
2.异常----同步中断
异常不同于中断,产生时必须考虑与处理器时钟同步。异常常常称为同步中断,是由于处理器执行时由于编程失误而导致的错误指令或执行期间出现特殊情况(
如缺页),必须靠内核来处理,处理器就会产生一个异常。
我们通常所说的中断,指的是硬件产生的异步中断,由于许多处理器体系结构处理异常与处理中断的方式类似,因此内核对他们的处理方式也类似。
软中断:
工作方式类似与异步中断,区别是它是通过软件引起的中断,异步中断是硬件引发的。
中断处理程序:
相应一个特定中断时,内核会执行一个函数,称为中断处理程序或者中断服务程序。设备产生的每一个中断都会与特定的处理函数绑定,一个设备可能会产生多个不同
中断,那么该设备就可以对应多个处理程序,相应地,设备驱动程序就需要准备多个这样的函数。
linux中断处理程序特点:
linux中,中断处理程序看起来想普通c函数,按特定类型声明,以便内核能以标准的方式传递处理程序的信息。
中断处理程序与其他内核函数的区别:中断处理程序是内核调用来相应中断的,而他们被运行于被称为中断上下文的特殊上下文中,由于中断随时可能发生,因此
中断处理程序必须随时、尽快执行,这样才能尽快、及时地相应中断,同时恢复中断代码的执行。
上下半部的对比:
1.上半部
中断处理程序是上半部,接收到中断以后,就立即开始执行,但只能做严格限时的工作,如对接收中断进行应答或者复位硬件,这些工作都是在所有中断被禁止的
情况下完成的。
2.下半部
中断处理程序中,能被允许稍后完成的工作会推迟到下半部,linux提供下半部的各种机制。
注册中断处理程序
如果设备使用中断,那么相应的驱动程序就需要注册一个中断处理程序
驱动程序注册并激活一个中断处理程序:
request_irq:分配一个指定的中断号,可能会休眠,因此不能在中断上下文或者其他不被允许阻塞的代码中调用
int request_irq(
unsigned int irq, /*要分配的中断号*/
irqreturn_t (*handler)(int,void*,struct pt_regs *),/*指向实际的中断处理程序,收到中断时内核调用*/
unsigned long irqflags,/*标志位*/
const char *devname,/*中断相关的设备ASCLL文本表示法*/
void *dev_id /*用于共享中断线*/
)
参数说明:
irq:表示要分配的中断号。对应某些设备,该值通常为预先固定的,对于大多数设备,该值可以探测或者通过编程动态确定
handler:函数指针,指向该中断的实际中断处理程序,系统接收到中断,该函数就会被调用。
irqflags:可以为0,也可以为下面一个或者多个标志的位掩码:
SA_INTERRUPT:表明给定的中断处理程序是一个快速中断处理程序,默认不带该标志。
在本地处理器上,快速中断处理程序是在禁止所有中断的情况下运行的,以便于不受其他中断影响,快速执行,适用于时钟中断。
SA_SAMPLE_RANDOM:表明该设备产生的中断对内核熵池有贡献,内核熵池负责提供从各种随机事件导出真正的随机数,如果制定该标志,表明该设备
的中断事件间隔就会作为熵值填充到熵池,如果你的设备以预知的速率产生中断(如系统定时器),或者可能受到外部攻击,就不要设置该标志。
SA_SHIRQ:表明可以在多个中断处理程序之间共享中短线。在同一个给定的中断线上注册的每个处理程序必须指定这个标志,否则在每条中断线上只能有一个处理
程序。
devname:与中断相关的设备的ASCII文本表示法,例如,PC机上键盘中断对应的这个值为”keyboard“,这些名字会被/proc/irq和/proc/interrupt文件使用,
以便与用户通信。可以理解为设备名字
dev_id:用于共享中断线,当一个中断处理程序需要释放时,dev_id将提供唯一的标志信息(cookie),以便从共享中断线的诸多中断处理程序中删除指定的
那一个,如果没有该参数,那么内核不可能知道在给定的中断线上到底要删除哪个处理程序,如果无需共享中断线,那么设置该参数为NULL即可。若需要共享,则
必须吧dev_id这个指针传递给内核,因为不同的处理程序函数可能位于不同的驱动中,它们共享一条中断线,内核必须准确为他们创造执行环境,此时通过该指针
将有用的环境信息传递给他们。实践中,经常通过它传递驱动程序的设备结构,这个指针是唯一的,而且有可能在中断处理程序内及设备模式中被用到。
request_irq为什么会睡眠:
因为在注册过程中,内核需要在/proc/irq文件中创建一个与中断对应的项,则会调用proc_mkdir()来创建这个新的procfs项,proc_mkdir()调用proc_create()
对这个新的procfs项进行设置,proc_create申请内存时调用的是kmalloc来请求内核分配内存,kmalloc()是可以睡眠的,因此request_irq会睡眠。
释放中断处理程序:
void free_irq(unsigned int irq,void *dev_id);
如果指定的中断线不是共享的,那么该函数将删除处理程序并且禁用中断线;如果是共享,则仅删除dev_id对应的处理程序,该中断线只有在所有的处理程序都被删除后才会被禁用。
这也是dev_id必须唯一的原因:共享中断线中,用来区分不同的处理程序。必须从进程上下文中调用free_irq();
编写中断处理程序:
典型的中断处理程序声明:
static irqreturn_t intr_handler(int irq,void *dev_id,struct pt_regs *regs);
参数说明:
irq:要相应的中断号
dev_id:与中断处理程序注册时传递给request_irq()的参数dev_id必须一致。
regs:指向结构体的指针,包含了处理中断前处理器的寄存器和状态,除了调试,其他时候很少使用。
irqreturn_t:实际上是int类型,为了与早起内核兼容,2.6以前版本是void类型
linux中中断处理程序无需重入,当给定的中断处理程序正在执行使,相应的终端线在所有的处理器上都会被屏蔽,以防止同一个中断线上接收另一个新的中断,
也就是说,同一个中断线,同一个中断处理程序,在执行完毕前不可能同时在两个处理器上执行,加上中断程序不能休眠,因此无需考虑可重入性。
注意:
1、如果中断程序可以被中断(不是同一个中断线的中断),称为嵌套中断或者中断嵌套。
2、旧版本linux允许中断嵌套,但是2010以后提交的版本以及禁止中断嵌套。
中断上下文:
当执行一个中断处理程序(中断上半部)或中断下半部时,内核处于中断上下文中。
进程上下文是内核所处的模式。在进程上下文中,可通过“current”宏关联当前进程,此外,因为进程是以进程上下文的形式连接到内核中,因此,在进程上下文
中可以睡眠,也可以调度程序。
中断上下文的特性:
1、中断上下文和进程没什么关系,跟“current”宏也不相干(尽管它会指向被中断的进程),因为没有进程的背景,所以中断上下文不能睡眠,否则没办法对其重新调度。
毕竟调度器调度的是进程。
2、严格时间限制,尽可能迅速、简短
中断上下文具有严格的时间限制,因为它打断了其他代码
3、尽量把耗时间的工作放在下半部执行
因为中断处理程序要求迅速、简短,因此需要将耗时间的部分抽离出来,放到下半部分执行
4、中断程序栈有限,谨慎使用
中断处理程序共享所中断进程的内核栈,大小是两页,32位体系结构上是8kB,64位体系是16kB
中断处理上半部与下半部的工作划分虽然没有严格的标准限制,但可参照如下标准:
1、如果一个任务对时间非常敏感,将其放入上半部
2、如果一个任务与硬件相关,将其放入上半部
3、如果一个任务要保证不被其他中断打断,将其放入上半部执行
4、其他任务可以考虑放入下半部
为什么要使用下半部:
洗完减少中断处理程序中需要完成的工作量,因为它运行时,当前中断线在所有的处理器上都会被屏蔽,如果是SA_INTERRUPT类型的中断处理程序,执行时会禁止所有的
本地中断,因此缩短中断屏蔽时间,将一些工作放到以后去做,对系统的响应能力和性能都至关重要。
具体放到什么时候做:
没有明确时间,只要在中断恢复后执行即可,通常下半部在处理程序一返回就会马上执行。关键在于,这些工作运行时,系统可以响应所有中断。
上下文:可以看做是运行所需的参数以及环境变量
中断上下文:硬件传过来的参数以及内核所需保存和设置的环境变量