Linux 中断处理

本文详细解析了Linux中断机制,包括中断的基本概念、中断处理程序的注册与释放、中断上下文、上半部与下半部的区别,以及中断绑定到CPU的方法。还介绍了软中断、tasklet和工作队列等下半部执行机制的使用和实现。
摘要由CSDN通过智能技术生成

一、基本概念

1、中断及中断上下文

        中断是一种由硬件设备产生的信号,不同设备产生的中断通过中断号来区分。CPU在接收到中断信号后,根据中断号执行对应的中断处理程序(Interrupt Service Routine) 

内核对异常和中断的处理类似,差别只在于中断是由硬件引起的

异常举例:软中断实现系统调用,缺页异常,除0异常

中断上下文:执行一个中断处理程序时,内核处于中断上下文中

注:在中断上下文中不允许睡眠,这是因为中断上下文没有后备进程,即无法被调度唤醒

2、上半部与下半部

        一般将中断处理分为上半部和下半部

上半部:在接收到中断后只完成有严格时限要求的工作,如对中断应答或者复位硬件,此时中断是被禁止的

下半部:指中断处理流程中推后执行的那一部分,在合适的时机执行,此时允许相应所有的中断

3、下半部和推后执行的工作

         除了对时间敏感且保证不被其他中断打断的部分,剩余的任务考虑在下半部执行,下半部的实现机制有以下几种

下半部机制功能状态
BH(bottom half)一个静态创建、由32个bottom halves组成的链表,上半部通过32整数中的一位来标识出哪个bottom half可以执行,虽然分属不同处理器,也不允许任何两个bottom half同时执行。(不够灵活,简单有性能瓶颈)Linux 2.5移除
任务队列引入任务队列机制来实现工作的推后执行,替代BH机制。驱动程序会将下半部注册到相应的等待队列,等待调用执行。(不够灵活,不能满足性能要求较高的子系统)Linux 2.5移除
软中断(Softirq)一组静态定义的下半部接口,有32个,可以在所有处理器上同时运行,同类型的接口也可以同时执行。tasklet是需要在编译阶段进行静态注册。(针对性能要求较高的子系统)Linux 2.3引入
tasklet一组基于软中断实现的灵活性强、动态创建的下半部实现机制, 不同类型的tasklet可以在不同的处理器上执行, 但类型相同tasklet,不能同时执行。tasklet可以通过代码进行动态注册。(大部分的场景)Linux 2.3引入
工作队列(Work queues)工作队列取代了任务队列Linux 2.3引入

二、中断处理程序

1、注册中断处理程序

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name,void *dev);
irq:要分配的中断号
handler:指向这个中断的处理程序
flag:可以为0,也可以是一个或多个标志的掩码
    IRQF_DISABLED --- 内核在处理中断处理程序期间,禁止所有的其他中断
    IRQF_SAMPLE_RANDOM --- 该设备产生的中断对内核熵池有贡献,内核熵池负责从各个随机事件导出真正的随机数
    IRQF_TIMER --- 为系统定时器的中断处理而准备
    IRQF_SHARED --- 多个中断处理程序之间共享中断线,同一个中断线的所有的中断处理程序都要指定该标志
name:设备ASCLL文本,会被proc/interrupts文件使用,以便与用户通信
dev:主要用于共享中断线,dev提供唯一标志信息(cookie),用来区分共享一个中断的多个设备,当一个中断处理程序需要释放时,以便从共享中断线的诸多处理程序中删除指定的一个,如果没有共享中断,设置为NULL即可

注:此函数会睡眠,所以不要在中断上下文中调用

2、释放中断处理程序

void free_irq(unsigned int irq, void *dev)

        卸载驱动处理程序时,需要注销相应的中断处理程序,并释放中断线

        如果指定的中断线不是共享的,那么,函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则删除dev所对应的处理程序,共享中断线只有在删除了最后一个中断处理程序时才会被禁用。

3、中断处理主程序

static irqreturn_t intr_handler(int irq, void *dev);
irq:处理程序要响应中断的中断号
dev:与传递给request_irq()的dev保持一致,dev将提供唯一的标志信息(cookie),用来区分共享同一个中断处理程序的多个设备

        Linux中断处理程序是不允许嵌套其他中断处理程序,中断上下文的代码应当迅速简洁,中断处理程序没有独立的栈,与所中断进程共享内核栈

4、中断控制

函数说明
local_irq_disable()禁止本地中断传递
local_irq_enable()激活本地中断传递
local_irq_save(unsigned long flags)保存本地中断传递的当前状态,然后禁止本地中断传递
local_irq_restore(unsigned long flags)恢复本地中断传递到给定的状态
disable_irq(unsigned int irq)禁止给定中断线,并确保该函数返回之前在该中断线上没有处理程序在运行
disable_irq_nosync(unsigned int irq)禁止给定中断线,不会等待当前中断处理程序执行完毕
enable_irq(unsigned int irq)激活给定中断线
synchronize_irq(unsigned int irq)等待一个特定的中断处理程序退出,才会返回
irqs_disabled()如果本地处理器上的中断系统被禁止,则返回非0;否则返回0
in_interrupt()如果在中断上下文中(包括执行中断处理程序和正在执行下半部处理程序), 则返回非0, 如果在进程上下文中,则返回0
in_irq()如果当前正在执行中断处理程序, 则返回非0;否则返回0

三、中断绑定

1、查看中断号

cat proc/interrupts
           CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7       
 11:      17280      12860       7073       8441       7254      10057      11085      10031     GICv3  27 Level     arch_timer
 13:          0          0          0          0          0          0          0          0     GICv3  38 Level     arch_mem_timer
 15:          0          0          0          0          0          0          0          0     GICv3  23 Level     arm-pmu
 16:          0          0          0          0          0          0          0          0     GICv3 960 Edge      gh_msgq_tx
 17:          9          0          0          0          0          0          0          0     GICv3 961 Edge      gh_msgq_rx
 18:      13799       1329          0          0          0          0          0          0     GICv3 261 Level     ipcc_0
 19:         62          0          0          0          0          0          0          0     GICv3  94 Level     qcom_cpucp

1、irq逻辑中断号
2、中断在各CPU发生的次数
3、中断所属设备类名称
4、硬件中断号
5、中断触发方式
6、中断处理函数

2、将中断绑定到CPU 

smp_affinity:通过 bitmask 算法绑定CPU  
        echo 0xf > /proc/irq/45/smp_affinity

smp_affinity_list:通过数字指定CPU编号  
        echo 0-3 > /proc/irq/45/smp_affinity_list //作用是将中断号45的设备中断处理(包括软中断和硬中断)均摊绑定到0、1、2、3各个CPU

四、下半部机制

1、软中断

        软中断是在编译期间静态分配的,软中断由softirq_action结构表示。软中断保留给系统中对时间要求最严格以及最重要的下半部使用

struct softirq_action
{
    void (*action)(struct softirq_action *);
};

        注册的软中断,都在这个数组里面,每个被注册的软中断都占据该数组的一项

static struct softirq_action softirq_vec[NR_SOFTIRQS]
(1)软中断处理程序

        同一个处理器,一个软中断不会抢占另外一个软中断,但中断处理程序可以抢占软中断。其他软中断(包括相同类型的软中断)可以在其他处理器上同时执行 

void softirq_handler(struct softirq_action *);
(2)软中断执行程序

        每个处理器都有一个ksoftirqd/n线程,他们会通过softirq_pending()发现是否有待处理的软中断,如果发现,就会调用do_softirq() 循环遍历,调用相应的处理程序

(3)软中断类型

        下面是按照优先级排列的 

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */

enum
{
	HI_SOFTIRQ=0,     /*优先级较高的tasklet*/
	TIMER_SOFTIRQ,    /*定时器的下半部*/
	NET_TX_SOFTIRQ,   /*发送网络数据包*/
	NET_RX_SOFTIRQ,   /*接收网络数据包*/
	BLOCK_SOFTIRQ,    /*block装置*/
	IRQ_POLL_SOFTIRQ, 
	TASKLET_SOFTIRQ,  /*正常优先权的tasklet*/
	SCHED_SOFTIRQ,    
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS
};
(4)使用软中断
a.注册软中断
open_softirq(NET_TX_SOFTIRQ,net_tx_action);
open_softirq(NET_RX_SOFTIRQ,net_rx_action);

b.触发软中断
//将软中断设置成挂起状态,下次调用do_softirq()函数时投入运行。
raise_softirq(NET_TX_SOFTIRQ);
//中断已经被禁止,调用下面指令
raise_softirq_irqoff(NET_TX_SOFIRQ);

2、tasklet

        tasklet是一种利用软中断实现的一种下半部机制, 但是,它的接口更简单,锁保护也要求较低

struct tasklet_struct
{
    struct tasklet_struct *next;//链表,指向下一个tasklet
    unsigned long state;//tasklet状态, 0/准备执行/正在运行
    atomic_t count;//引用计数,为0才被激活
    void (*func)(unsigned long);//tasklet处理函数
    unsigned long data;//给tasklet处理函数的参数
};
(1)调度tasklet

        已经通过tasklet_schedule()完成调度,等待执行的tasklet会被存放在两个链表tasklet_vec和tasklet_hi_vec,将软中断TASKLET_SOFTIRQ或HI_SOFTIRQ设置成挂起状态,等待下一次调用do_softirq()就会执行

tasklet_vec:存放普通的tasklet,由tasklet_schedule()进行调度,使用的是TASKLET_SOFTIRQ软中断。
tasklet_hi_vec:存放高优先级的tasklet,由tasklet_hi_schedule()进行调度,使用的是HI_SOFTIRQ软中断

//将软中断TASKLET_SOFTIRQ设置成挂起状态,等待do_softirq()调用
    raise_softirq_irqoff(softirq_nr);
(2)使用tasklet

静态创建一个tasklet结构

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

通过传入一个利用tasklet_init()函数动态创建tasklet结构

void tasklet_init(struct tasklet_struct *t,
          void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->data = data;
}
EXPORT_SYMBOL(tasklet_init);

调度你创建的tasklet

tasklet_schedule(&my_tasklet)

3、工作队列

        在中断处理中,经常用到工作队列,这样便能缩短中断处理时的时间。工作队列可以睡眠,比如需要获取大量内存或者需要获取信号量

         Linux中的Workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内核线程,方便了用户的编程

(1)常用函数
a.INIT_WORK()

        通过此函数将work_struct与函数建立联系

#ifdef CONFIG_LOCKDEP
#define __INIT_WORK_KEY(_work, _func, _onstack, _key)			\
	do {								\
		__init_work((_work), _onstack);				\
		(_work)->data = (atomic_long_t) WORK_DATA_INIT();	\
		lockdep_init_map(&(_work)->lockdep_map, "(work_completion)"#_work, (_key), 0); \
		INIT_LIST_HEAD(&(_work)->entry);			\
		(_work)->func = (_func);				\
	} while (0)
#else
#define __INIT_WORK_KEY(_work, _func, _onstack, _key)			\
	do {								\
		__init_work((_work), _onstack);				\
		(_work)->data = (atomic_long_t) WORK_DATA_INIT();	\
		INIT_LIST_HEAD(&(_work)->entry);			\
		(_work)->func = (_func);				\
	} while (0)
#endif

#define __INIT_WORK(_work, _func, _onstack)				\
	do {								\
		static __maybe_unused struct lock_class_key __key;	\
									\
		__INIT_WORK_KEY(_work, _func, _onstack, &__key);	\
	} while (0)

#define INIT_WORK(_work, _func)						\
	__INIT_WORK((_work), (_func), 0)

work:work_struct结构体
func:函数名
struct work_struct {
         atomic_long_t data;             /*工作处理函数func的参数*/                         
         struct list_head entry;         /*连接工作的指针*/            
         work_func_t func;               /*函数指针,指向func函数*/
#ifdef CONFIG_LOCKDEP
         struct lockdep_map lockdep_map;
#endif
};
b.schedule_work(work)
static inline bool schedule_work(struct work_struct *work)
{
	return queue_work(system_wq, work);
}

        当中断来了,立马调用schedule_work(work),然后退出,此函数一般在中断上半部。

        中断结束后,内核线程会自动调用work结构体对应的func函数

static inline bool schedule_delayed_work(struct delayed_work *dwork,
					 unsigned long delay)
{
	return queue_delayed_work(system_wq, dwork, delay);
}

         有时并不希望work立即执行,而是希望过一段delay时间后再执行。工作队列是没有优先级的,基本按照FIFO的方式进行处理。

c.cancel_work_sync(work);
/*取消work结构体对应的func函数,但会等待其执行完,一般在exit中使用*/
bool cancel_work_sync(struct work_struct *work)
{
	return __cancel_work_timer(work, false);
}
EXPORT_SYMBOL_GPL(cancel_work_sync);
d.queue_work(workqueue, work)
/*调度执行一个指定workqueue中的任务*/
static inline bool queue_work(struct workqueue_struct *wq,
			      struct work_struct *work)
{
	return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}        
e.flush_work(work)
/*等待工作队列完成执行工作*/
bool flush_work(struct work_struct *work)
{
	return __flush_work(work, false);
}
EXPORT_SYMBOL_GPL(flush_work);

【参考博客】

[1] Linux 内核设计与实现

[2] Linux内核 | 中断机制 - 世至其美

[3] INIT_WORK()工作队列使用-CSDN博客

[4] https://www.cnblogs.com/oceanding/p/7595738.html

  • 37
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值