Linux中断处理
IMX6ULL嵌入式Linux驱动开发学习
以下内容是我在学习正点原子IMX6ULL开发板alpha中记录的笔记,部分摘录自正点原子IMX6ULL开发手册。
一、Linux内核中断处理
1.1 裸机中断
- 使能中断,初始化相应的寄存器。
- 注册中断服务函数,也就是向
irqTable数组(裸机例程)的指定标号处写入中断服务函数。 - 中断发生后进入
IRQ中断服务函数,在IRQ中断服务函数中,根据中断号在irqTable里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
1.2 Linux中断
-
先知道要使用的中断对应的中断号。
-
根据终端号申请
request_irq,request_irq函数可能会导致睡眠,此函数同时会激活中断。/* @param irq 中断号 * @param handler 中断服务函数 * @param flags 中断标志、中断触发方式 * @param name 中断名字 * @param dev 使用共享中断时,唯一用来区分的标志。当多个设备共享一个中断线,共享的所有中断都必须指定此标志。 * @return 0 中断申请成功,其他负值 中断申请失败,如果返回 -EBUSY 的话表示中断已经被申请了。 */ int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); -
不再使用中断时,需要释放中断
free_irq。/* @param irq 中断号 * @param dev 如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。 * 共享中断只有在释放最后中断处理函数的时候才会被禁止掉。 */ void free_irq(unsigned int irq, void *dev); -
在使用
request_irq函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示irqreturn_t (*irq_handler_t) (int, void *)第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向
void的指针,也就是个通用指针,需要与request_irq函数的dev参数保持一致。用于区分共享中断的不同设备,dev也可以指向设备数据结构。中断处理函数的返回值为irqreturn_t类型,irqreturn_t类型定义如下所示:enum irqreturn { IRQ_NONE = (0 << 0), IRQ_HANDLED = (1 << 0), IRQ_WAKE_THREAD = (1 << 1), }; typedef enum irqreturn irqreturn_t;irqreturn_t是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:return IRQ_RETVAL(IRQ_HANDLED) -
中断使能和禁止
enable_irq和disable_irq用于使能和禁止指定的中断,irq就是要禁止的中断号。void enable_irq(unsigned int irq); void disable_irq(unsigned int irq);disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数:void disable_irq_nosync(unsigned int irq)disable_irq_nosync函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就是在学习
STM32的时候常说的关闭全局中断,这个时候可以使用如下两个函数:local_irq_enable(); local_irq_disable();local_irq_enable用于使能当前处理器中断系统,local_irq_disable用于禁止当前处理器中断系统。
但是在任务中使用这两个函数会出现问题。比如假如
A任务调用local_irq_disable关闭全局中断10秒,当关闭了2秒的时候B任务开始运行,B任务也调用local_irq_disable关闭全局中断3秒, 3秒以后B任务调用local_irq_enable函数将全局中断打开了。此时才过去 2+3=5 秒的时间,然后全局中断就被打开了,此时A任务要关闭10秒全局中断的愿望就破灭了,然后A任务就“生气了”,结果很严重,可能系统都要被A任务整崩溃。为了解决这个问题,B任务不能直接简单粗暴的通过local_irq_enable函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数:local_irq_save(flags); local_irq_restore(flags);这两个函数是一对,
local_irq_save函数用于禁止中断,并且将中断状态保存在flags中。
local_irq_restore用于恢复中断,将中断恢复到flags状态。
1.3 上半部和下半部
-
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
-
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
-
关于代码属于上半部或下半部的参考
- 如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
- 如果要处理的任务对时间敏感,可以放到上半部。
- 如果要处理的任务与硬件有关,可以放到上半部。
- 除了上述三点以外的其他任务,优先考虑放到下半部。
Linux对下半部的处理方式。
1.3.1 软中断
软中断必须在编译的时候静态注册(写入到内核中)!软中断不要去用。
Linux 内核使用结构体softirq_action 表示软中断,softirq_action结构体定义在文件 include/linux/interrupt.h 中,内容如下:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
在 kernel/softirq.c 文件中一共定义了 10 个软中断,如下所示:
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:
enum
{
HI_SOFTIRQ = 0, /* 高优先级软中断 */
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
};
softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。
-
要使用软中断要先注册。
/* @brief 注册软中断服务函数 * @param nr 要开启的软中断 是上面枚举中的一个。 * @param action 软中断对应的处理函数 */ void open_softirq(int nr, void (*action)(struct softirq_action *))软中断必须在编译的时候静态注册!
-
触发软中断
注册好软中断以后需要通过
raise_softirq函数触发,raise_softirq函数原型如下:/* @brief 触发软中断 * @param nr 要触发的软中断 */ void raise_softirq(unsigned int nr)
1.3.2 tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,建议使用 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,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函数原型如下:
/* @brief 初始化 tasklet
* @param t 要初始化的 tasklet
* @param func tasklet 的处理函数。
* @param data 要传递给 func 函数的参数
*/
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
也 可 以 使 用 宏 DECLARE_TASKLET 来 一 次 性 完 成 tasklet 的 定 义 和 初 始 化 ,DECLARE_TASKLET 定义在 include/linux/interrupt.h 文件中,定义如下:
/* name 为要定义的 tasklet 名字,
* 这个名字就是一个 tasklet_struct 类型的结构变量,
* func 就是 tasklet 的处理函数,data 是传递给 func 函数的参数。
*/
DECLARE_TASKLET(name, func, data);
在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运
行, tasklet_schedule 函数原型如下:
/* @param t 要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。*/
void tasklet_schedule(struct tasklet_struct *t);
tasklet 使用顺序:
- 定义一个
tasklet。 - 初始化
tasklet,重点是设置对应的处理函数。 - 在上半部中调用
tasklet_schedule函数,使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);
......
}
1.3.3 工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
Linux 内核使用 work_struct 结构体表示一个工作,内容如下(省略掉条件编译):
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,内容如下(省略掉条件编译):
struct workqueue_struct {
struct list_head pwqs;
struct list_head list;
struct mutex mutex;
int work_color;
int flush_color;
atomic_t nr_pwqs_to_flush;
struct wq_flusher *first_flusher;
struct list_head flusher_queue;
struct list_head flusher_overflow;
struct list_head maydays;
struct worker *rescuer;
int nr_drainers;
int saved_max_active;
struct workqueue_attrs *unbound_attrs;
struct pool_workqueue *dfl_pwq;
char name[WQ_NAME_LEN];
struct rcu_head rcu;
unsigned int flags ____cacheline_aligned;
struct pool_workqueue __percpu *cpu_pwqs;
struct pool_workqueue __rcu *numa_pwq_tbl[];
};
Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作, Linux 内核使用worker 结构体表示工作者线程, worker 结构体内容如下:
struct worker {
union {
struct list_head entry;
struct hlist_node hentry;
};
struct work_struct *current_work;
work_func_t current_func;
struct pool_workqueue *current_pwq;
bool desc_valid;
struct list_head scheduled;
struct task_struct *task;
struct worker_pool *pool;
struct list_head node;
unsigned long last_active;
unsigned int flags;
int id;
char desc[WORKER_DESC_LEN];
struct workqueue_struct *rescue_wq;
};
每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,只需要定义工作(work_struct)即可,关于工作队列和工作者线程基本不用去管。
-
创建工作直接定一个
work_struct结构体,然后使用INIT_WORK宏来初始化工作即可,INIT_WORK宏定义如下:/* _work 表示要初始化的工作, _func 是工作对应的处理函数。*/ #define INIT_WORK(_work, _func)也可以使用
DECLARE_WORK宏一次性完成工作的创建和初始化,宏定义如下:/* n 表示定义的工作(work_struct), f 表示工作对应的处理函数。*/ #define DECLARE_WORK(n, f) -
同
tasklet一样,工作也是需要调度才能运行的,工作的调度函数为schedule_work,函数原型如下所示:/* @brief 调度工作 * @param work 要调度的工作 * @return 结果 0 成功,其他值 失败 */ bool schedule_work(struct work_struct *work)
工作队列使用顺序:
- 定义一个
work。 - 初始化
work,重点同样是是设置对应的处理函数。 - 在上半部中调用
schedule_work函数,使work在合适的时间运行。
/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{
/* 根据成员变量反推出结构体的首地址 */
//struct demo_struct *p = container_of(work, struct demo_struct, 成员变量名);
/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 work */
schedule_work(&testwork);
.

本文详细介绍了Linux内核中断处理机制,包括裸机中断、Linux中断的注册与释放,以及上半部和下半部(软中断、tasklet和工作队列)的应用。特别关注了如何使用tasklet编写按键中断驱动,涵盖了设备树中断节点信息和测试APP的交互。
最低0.47元/天 解锁文章
7794

被折叠的 条评论
为什么被折叠?



