中断的上下半部处理机制
上半部
是不能中断的,对于实时要求高的,必须放在上半部。
上半部的功能是响应中断。当中断发生时,它就把设备驱动程序中中断处理例程的下半部挂到设备的下半部执行队列中去,然后继续等待新的中断到来。这样一来, 上半部的执行速度就会很快,它就可以接受更多它负责的设备所产生的中断了。上半部之所以快,是因为它是完全屏蔽中断的,如果它没有执行完,其他中断就不能 及时地处理,只能等到这个中断处理程序执行完毕以后。所以要尽可能多的对设备产生的中断进行服务和处理,中断处理程序就一定要快。
下半部
是可以中断的
下半部的实现主要是通过软中断 ,tasklet,和工作队列来实现的.
下半部的功能是处理比较复杂的过程。下半部和上半部最大的区别是可中断,而上半部却不可中断。下半部几乎完成了中断处理程序所有的事情,因为上半部只是将 下半部排到了它们所负责的设备中断的处理队列中去,然后就不做其它的处理了。下半部所负责的工作一般是查看设备以获得产生中断的事件信息,并根据这些信息 (一般通过读设备上的寄存器得来)进行相应的处理。下半部是可中断的。
中断申请
request_irq()以及free_irq()
request_threaded_irq
使用request_threaded_irq申请的中断,handler不是在中断上下文里执行,而是在新创建的线程里执行,这样,该handler非常像执行workqueue,拥有所有workqueue的特性,但是省掉了创建,初始化,调度workqueue的繁多步骤。处理起来非常简单。
request_threaded_irq()是Linux kernel 2.6.30 之后新加的irq handler API
request_threaded_irq 是在将上半部的硬件中断处理缩短为只确定硬体中断来
自我们要处理的装置,唤醒kernel thread 执行后续中断任务。
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id)
IRQF_SHARED 共享中断时,dev_id不能为空,因为释放irq时要区分哪个共享中断
irq:中断号
handler:发生中断时首先要执行的硬中断处理函数,这个函数可以通过返回
IRQ_WAKE_THREADED唤醒中断线程,也可
返回IRQ_HANDLE不执行中断线程
thread_fn : 中断线程,类似于中断下半部
后三个参数与request_irq中的一致
关于IRQF_ONESHOT, 直到线程函数执行完毕才会开启该中断
中断上下文
进程上下文:
通过系统调用,用户空间的应用程序就会进入内核空间,由内核代表该进程运行于内核空间,这就涉及到上下文的切换.
用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行.
所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
系统调用时的当前进程要进行模式的切换,要从用户态切换到内核态,那么用户态的一些寄存 器信息,就要进行压栈的操作,压入内核栈,以免下次进行现场恢复(恢复到用户空间)的时候,直接从内核栈弹出寄存器的信息
当我们进程进行切换的时候,用户态的寄存器信息,是保存在哪里的呢?用户态的信息是保存在task_struct里面的成员变量thread里面;
中断上下文:
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。
中断上下文和特定进程无关。
一个中断上下文,通常都会始终占有CPU(当然中断可以嵌套,但我们一般不这样做),不可以被打断。正因为如此,运行在中断上下文的代码就要受一些限制,不能做下面的事情:
中断上下文不能做的事情
1、睡眠或者放弃CPU。
这样做的后果是灾难性的,因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉
2、尝试获得信号量
如果获得不到信号量,代码就会睡眠,会产生和上面相同的情况
3、执行耗时的任务
中断处理应该尽可能快,因为内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。
4、访问用户空间的虚拟地址
因为中断上下文是和特定进程无关的,它是内核代表硬件运行在内核空间,所以在终端上下文无法访问用户空间的虚拟地址
中断流程
1.进入中断处理程序--->
2.保存关键上下文---->
3.开中断(sti指令)--->
4.进入中断处理程序的 handler--->
5.关中断(cli指令)---->
6.写EOI寄存器(表示中断处理完成)---->
7.开中断。
异常和中断的区别和联系
1、针对SoC来说,发生复位、软中断、中断、快速中断、取指令异常、数据异常等,我们都统一叫异常。所以说:中断其实是异常的一种。
2、异常的定义就是突发事件,打断了CPU的正常常规业务,CPU不得不跳转到异常向量表中去执行异常处理程序;中断是异常的一种,一般特指SoC内的内部外设产生的打断SoC常规业务,或者外部中断(SoC的GPIO引脚传回来的中断)。
SoC对中断的实现机制:异常向量表
(1)异常向量表是CPU中某些特定地址的特定定义。当中断发生的时候,中断要想办法通知CPU去处理中断,怎么做到?这就要靠异常向量表。
(2)在CPU设计时,就事先定义了CPU中一些特定地址作为特定异常的入口地址(譬如定义0x00000000地址为复位异常向量地址,则发生复位异常时CPU会自动跳转到0x00000000地址去执行指令。
又譬如外部中断对应的异常向量地址为0x30000008,则发生外部中断后,CPU会硬件自动跳转到0x30000008地址去执行指令。)
(3)以上讲的是CPU硬件设计时对异常向量表的支持,下来就需要软件支持了。硬件已经决定了发生什么异常CPU自动跳转PC到哪个地址去执行,软件需要做的就是把处理这个异常的代码的首地址填入这个异常向量地址。
(4)异常向量表中各个向量的相对位置是固定的,但是他们的起始地址是不固定的,各种SoC可以不一样,而且复杂ARM中还可以让用户来软件设置这个异常向量表的基地址。
(5)扩展到所有架构的CPU中:所有架构(譬如51单片机、PIC单片机)的CPU实现中断都是通过异常向量表实现的,这个机制是不变的;但是不同CPU异常向量表的构造和位置是不同的。
下半部的处理机制:工作队列
workqueue与tasklet类似,都是允许内核代码请求某个函数在将来的时间被调用(延迟),
每个workqueue就是一个内核进程。
workqueue与tasklet的区别:
1.tasklet是通过软中断实现的,在软中断上下文中运行,tasklet代码必须是原子的
workqueue是通过内核进程实现的,就没有上述限制的,最爽的是,工作队列函数可以休眠
2.tasklet始终运行在被初始提交的同一处理器上,workqueue不一定
3.tasklet不能设定延时时间(即使很短),workqueue可以设定延迟时间
我们把推后执行的任务叫做工作(work)
,描述它的数据结构为work_struct:
struct work_struct {
atomic_long_t data; /*工作处理函数func的参数*/
#define WORK_STRUCT_PENDING 0 /* T if work item pending execution */
#define WORK_STRUCT_STATIC 1 /* static initializer (debugobjects) */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry; /*连接工作的指针*/
work_func_t func; /*工作处理函数*/
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
首先,创建一个workqueue,实际上就是建立一个内核进程
内核进程worker_thread做的事情很简单,死循环而已,不停的执行workqueue上的work_list
接口函数 | 说明 |
---|---|
create_workqueue | 用于创建一个workqueue队列,为系统中的每个CPU都创建一个内核线程。输入参数: @name:workqueue的名称 |
create_singlethread_workqueue | 用于创建workqueue,只创建一个内核线程。输入参数 |
destroy_workqueue | 释放workqueue队列 |
schedule_work | 调度执行一个具体的任务,执行的任务将会被挂入Linux系统提供的workqueue——keventd_wq |
schedule_delayed_work | 延迟一定时间去执行一个具体的任务,功能与schedule_work类似,多了一个延迟时间 |
queue_work | 调度执行一个指定workqueue中的任务 |
queue_delayed_work | 延迟调度执行一个指定workqueue中的任务 |
共享队列( 特殊的工作队列唯一的,内核自己的)
其实内核有自己的一个workqueue,叫keventd_wq,这个工作队列也叫做“共享队列”。
do_basic_setup --> init_workqueues --> create_workqueue("events");
默认的工作者线程叫做events/n,这里n是处理器的编号.
若驱动模块使用的workqueue功能很简单的话,可以使用“共享队列”,不用自己再建一个队列
使用共享队列,有这样一套API
int schedule_work(struct work_struct *work)
int schedule_delayed_work(struct delayed_work *dwork,unsigned long delay)
void flush_scheduled_work(void)
驱动例子 goodix_ts
1.probe
INIT_WORK(&ts->work, goodix_ts_work_func);//struct work_struct work,ts是client私有数据结构体
2. 工作任务
static void goodix_ts_work_func(struct work_struct *work)
struct goodix_ts_data *ts = container_of(work, struct goodix_ts_data, work);