Linux中断

一.中断简介

什么是中断

Interrupt Request,简称IRQ,中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的 CPU 暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。

软件对硬件进行配置后,软件期望等待硬件的某种状态(比如,收到了数据,GPIO状态变化),这里有两种方式,一种是轮询(polling): CPU 不断的去读硬件状态。另一种是当硬件完成某种事件后,给 CPU 一个中断,让 CPU 停下手上的事情,去处理这个中断。很显然,中断的交互方式提高了系统的吞吐。

当 CPU 收到一个中断 (IRQ)的时候,会去执行该中断对应的处理函数(ISR)。普通情况下,会有一个中断向量表(IDT),向量表中定义了 CPU 对应的每一个外设资源的中断处理程序的入口,当发生对应的中断的时候, CPU 直接跳转到这个入口执行程序。也就是中断上下文。(注意:中断上下文中,不可阻塞睡眠)。

二.内核中断初始化流程

2.1 中断相关的数据结构

结构名称

作用

中断描述符:irq_desc

IRQ 的软件层面上的资源描述

响应函数:irqaction

IRQ 的通用操作,例如函数入口

中断数据:irq_data

中断描述符中应该会包括底层irq chip相关的数据结构

操作合集:irq_chip

对应每个芯片的具体实现

2.1.1 中断描述符 irq_desc
struct irq_desc { 
    struct irq_data        irq_data; 
    unsigned int __percpu    *kstat_irqs;------IRQ的统计信息 
    irq_flow_handler_t    handle_irq;--------流控函数 
    struct irqaction    *action; -----------处理函数
    unsigned int        status_use_accessors;-----中断描述符的状态,参考IRQ_xxxx 
    unsigned int        core_internal_state__do_not_mess_with_it;
    unsigned int        depth;----------描述嵌套深度的信息
    unsigned int        wake_depth;--------电源管理中的wake up source相关
    unsigned int        irq_count; 
    unsigned long        last_unhandled;   
    unsigned int        irqs_unhandled; 
    raw_spinlock_t        lock; 
    struct cpumask        *percpu_enabled;
#ifdef CONFIG_SMP 
    const struct cpumask    *affinity_hint;----和irq affinity相关,后续单独文档描述 
    struct irq_affinity_notify *affinity_notify; 
#ifdef CONFIG_GENERIC_PENDING_IRQ 
    cpumask_var_t        pending_mask; 
#endif 
#endif 
    unsigned long        threads_oneshot; 
    atomic_t        threads_active; 
    wait_queue_head_t       wait_for_threads; 
#ifdef CONFIG_PROC_FS 
    struct proc_dir_entry    *dir;--------该IRQ对应的proc接口 
#endif 
    int            parent_irq; 
    struct module        *owner; 
    const char        *name; 
} ____cacheline_internodealigned_in_smp
2.1.2 响应函数 irqaction

主要是用来存用户注册的中断处理函数,一个中断可以有多个处理函数 ,当一个中断有多个处理函数,说明这个是共享中断,所谓共享中断就是一个中断的来源有很多,这些来源共享同一个引脚。

所以在irq_desc结构体中的action成员是个链表,以action为表头,若是一个以上的链表就是共享中断

struct irqaction {
         irq_handler_t handler;    //等于用户注册的中断处理函数,中断发生时就会运行这个中断处理函数
         unsigned long flags;      //中断标志,注册时设置,比如上升沿中断,下降沿中断等
         cpumask_t mask;           //中断掩码
         const char *name;         //中断名称,产生中断的硬件的名字
         void *dev_id;             //设备id
         struct irqaction *next;    //指向下一个成员
         int irq;                    //中断号,
         struct proc_dir_entry *dir;    //指向IRQn相关的/proc/irq/
};
2.1.3 中断数据 irq_data

中断描述符中会包括底层irq chip相关的数据结构,linux kernel中把这些数据组织在一起,形成struct irq_data,具体代码如下:

struct irq_data { 
    u32            mask;----------TODO 
    unsigned int   irq;--------IRQ number 
    unsigned long  hwirq;-------HW interrupt ID 
    unsigned int   node;-------NUMA node index 
    unsigned int  state_use_accessors;--------底层状态,参考IRQD_xxxx 
    struct irq_chip    *chip;----------该中断描述符对应的irq chip数据结构 
    struct irq_domain  *domain;--------该中断描述符对应的irq domain数据结构 
    void            *handler_data;--------和外设specific handler相关的私有数据 
    void            *chip_data;---------和中断控制器相关的私有数据 
    struct msi_desc *msi_desc; 
    cpumask_var_t        affinity;-------和irq affinity相关 
};
2.1.4 操作合集 irq_chip

irq_chip 是一串和芯片相关的函数指针,这里定义的非常的全面,基本上和 IRQ 相关的可能出现的操作都全部定义进去了,具体根据不同的芯片,需要在不同的芯片的地方去初始化这个结构,然后这个结构会嵌入到通用的 IRQ 处理软件中去使用,使得软件处理逻辑和芯片逻辑完全的分开。

struct irq_chip {
    const char    *name;
    unsigned int    (*irq_startup)(struct irq_data *data);-------------初始化中断
    void        (*irq_shutdown)(struct irq_data *data);----------------结束中断
    void        (*irq_enable)(struct irq_data *data);------------------使能中断
    void        (*irq_disable)(struct irq_data *data);-----------------关闭中断
 
    void        (*irq_ack)(struct irq_data *data);---------------------应答中断
    void        (*irq_mask)(struct irq_data *data);--------------------屏蔽中断
    void        (*irq_mask_ack)(struct irq_data *data);----------------应答并屏蔽中断
    void        (*irq_unmask)(struct irq_data *data);------------------解除中断屏蔽
    void        (*irq_eoi)(struct irq_data *data);---------------------发送EOI信号,表示硬件中断处理已经完成。
 
    int        (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);--------绑定中断到某个CPU
    int        (*irq_retrigger)(struct irq_data *data);----------------重新发送中断到CPU
    int        (*irq_set_type)(struct irq_data *data, unsigned int flow_type);----------------------------设置触发类型
    int        (*irq_set_wake)(struct irq_data *data, unsigned int on);-----------------------------------使能/关闭中断在电源管理中的唤醒功能。
 
    void        (*irq_bus_lock)(struct irq_data *data);
    void        (*irq_bus_sync_unlock)(struct irq_data *data);
 
    void        (*irq_cpu_online)(struct irq_data *data);
    void        (*irq_cpu_offline)(struct irq_data *data);
 
    void        (*irq_suspend)(struct irq_data *data);
    void        (*irq_resume)(struct irq_data *data);
    void        (*irq_pm_shutdown)(struct irq_data *data);
...
    unsigned long    flags;
}

对应关系:

参考文献:https://stephenzhou.blog.csdn.net/article/details/90648475?spm=1001.2014.3001.5502

2.2 中断初始化流程

内核启用中断以前,必须把IDT表的初始地址装到idtr寄存器,并初始化表中的每一项。这个动作是在初始化系统时完成的。

start_kernel 开始,在这里面,调用了和 machine 相关的 IRQ 的初始化 init_IRQ()

asmlinkage __visible void __init start_kernel(void)
{
	char *command_line;
	char *after_dashes;
.....
    early_irq_init();
    init_IRQ();
.....
}

在 init_IRQ 中,调用了 machine_desc->init_irq()

void __init init_IRQ(void)
{
	int ret;
 
	if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)
		irqchip_init();
	else
		machine_desc->init_irq();
 
	if (IS_ENABLED(CONFIG_OF) && IS_ENABLED(CONFIG_CACHE_L2X0) &&
	    (machine_desc->l2c_aux_mask || machine_desc->l2c_aux_val)) {
		if (!outer_cache.write_sec)
			outer_cache.write_sec = machine_desc->l2c_write_sec;
		ret = l2x0_of_init(machine_desc->l2c_aux_val,
				   machine_desc->l2c_aux_mask);
		if (ret && ret != -ENODEV)
			pr_err("L2C: failed to init: %d\n", ret);
	}
 
	uniphier_cache_init();
}

machine_desc->init_irq() 完成对中断控制器的初始化,为每个irq_desc结构安装合适的流控handler,为每个irq_desc结构安装irq_chip指针,使他指向正确的中断控制器所对应的irq_chip结构的实例,同时,如果该平台中的中断线有多路复用(多个中断公用一个irq中断线)的情况,还应该初始化irq_desc中相应的字段和标志,以便实现中断控制器的级联。这里初始化的时候回调用到具体的芯片相关的中断初始化的地方。

这些函数定义在 include/linux/irq.h 文件。是对芯片初始化的时候可见的 APIs,用于指定中断“流控”中的 :

irq_flow_handler_t handle

也就是中断来的时候,最后那个函数调用。

中断流控函数,分几种,电平触发的中断,边沿触发的,等:

/*
 * Built-in IRQ handlers for various IRQ types,
 * callable via desc->handle_irq()
 */
extern void handle_level_irq(struct irq_desc *desc);
extern void handle_fasteoi_irq(struct irq_desc *desc);
extern void handle_edge_irq(struct irq_desc *desc);
extern void handle_edge_eoi_irq(struct irq_desc *desc);
extern void handle_simple_irq(struct irq_desc *desc);
extern void handle_untracked_irq(struct irq_desc *desc);
extern void handle_percpu_irq(struct irq_desc *desc);
extern void handle_percpu_devid_irq(struct irq_desc *desc);
extern void handle_bad_irq(struct irq_desc *desc);
extern void handle_nested_irq(unsigned int irq);

而在这些处理函数里,会去调用到 : handle_irq_event

/**
 *	handle_level_irq - Level type irq handler
 *	@desc:	the interrupt description structure for this irq
 *
 *	Level type interrupts are active as long as the hardware line has
 *	the active level. This may require to mask the interrupt and unmask
 *	it after the associated handler has acknowledged the device, so the
 *	interrupt line is back to inactive.
 */
void handle_level_irq(struct irq_desc *desc)
{
	raw_spin_lock(&desc->lock);
	mask_ack_irq(desc);
 
	if (!irq_may_run(desc))
		goto out_unlock;
 
	desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
 
	/*
	 * If its disabled or no action available
	 * keep it masked and get out of here
	 */
	if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
		desc->istate |= IRQS_PENDING;
		goto out_unlock;
	}
 
	kstat_incr_irqs_this_cpu(desc);
	handle_irq_event(desc);
 
	cond_unmask_irq(desc);
 
out_unlock:
	raw_spin_unlock(&desc->lock);
}

而这个 handle_irq_event 则是调用了处理,handle_irq_event_percpu

irqreturn_t handle_irq_event(struct irq_desc *desc)
{
	irqreturn_t ret;
 
	desc->istate &= ~IRQS_PENDING;
	irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
	raw_spin_unlock(&desc->lock);
 
	ret = handle_irq_event_percpu(desc);
 
	raw_spin_lock(&desc->lock);
	irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
	return ret;
}

handle_irq_event_percpu->__handle_irq_event_percpu-> 【action->handler()

这里终于看到了调用 的地方了,就是咱们通过 request_irq 注册进去的函数;

三.中断的处理流程

3.1 发生中断

CPU执行异常向量vector_irq的代码, 即异常向量表中的中断异常的代码,它是一个跳转指令,跳去执行真正的中断处理程序,在vector_irq里面,最终会调用中断处理的总入口函数。

C 语言的入口为 : asm_do_IRQ(unsigned int irq, struct pt_regs *regs)

asmlinkage void __exception_irq_entry
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
	handle_IRQ(irq, regs);
}

该函数的入参 irq 为中断号。

asm_do_IRQ -> handle_IRQ

void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
	__handle_domain_irq(NULL, irq, false, regs);
}

handle_IRQ -> __handle_domain_irq

int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
			bool lookup, struct pt_regs *regs)
{
	struct pt_regs *old_regs = set_irq_regs(regs);
	unsigned int irq = hwirq;
	int ret = 0;
 
	irq_enter();
 
#ifdef CONFIG_IRQ_DOMAIN
	if (lookup)
		irq = irq_find_mapping(domain, hwirq);
#endif
 
	/*
	 * Some hardware gives randomly wrong interrupts.  Rather
	 * than crashing, do something sensible.
	 */
	if (unlikely(!irq || irq >= nr_irqs)) {
		ack_bad_irq(irq);
		ret = -EINVAL;
	} else {
		generic_handle_irq(irq);
	}
 
	irq_exit();
	set_irq_regs(old_regs);
	return ret;
}

这里请注意:

先调用了 irq_enter 标记进入了硬件中断:

irq_enter是更新一些系统的统计信息,同时在__irq_enter宏中禁止了进程的抢占。虽然在产生IRQ时,ARM会自动把CPSR中的I位置位,禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。那为何还要禁止抢占?这是因为要考虑中断嵌套的问题,一旦流控层或驱动程序主动通过local_irq_enable打开了IRQ,而此时该中断还没处理完成,新的irq请求到达,这时代码会再次进入irq_enter,在本次嵌套中断返回时,内核不希望进行抢占调度,而是要等到最外层的中断处理完成后才做出调度动作,所以才有了禁止抢占这一处理

再调用 generic_handle_irq

最后调用 irq_exit 删除进入硬件中断的标记

__handle_domain_irq -> generic_handle_irq

int generic_handle_irq(unsigned int irq)
{
	struct irq_desc *desc = irq_to_desc(irq);
 
	if (!desc)
		return -EINVAL;
	generic_handle_irq_desc(desc);
	return 0;
}
EXPORT_SYMBOL_GPL(generic_handle_irq);

首先在函数 irq_to_desc 中根据发生中断的中断号,去取出它的 irq_desc 中断描述结构,然后调用 generic_handle_irq_desc

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
	desc->handle_irq(desc);
}

这里调用了 handle_irq 函数。

总结:

vector_irq()->vector_irq()->__irq_svc()
  ->svc_entry()-----------------------------------------------保护中断现场
  ->irq_handler()->gic_handle_irq()---------具体到GIC中断控制器对应的就是gic_handle_irq(),此处从架构相关进入了GIC相关处理。
    ->GIC_CPU_INTACK---------------------------读取IAR寄存器,响应中断。
    ->handle_domain_irq()
      ->irq_enter()------------------------------------进入硬中断上下文
      ->generic_handle_irq()
        ->generic_handle_irq_desc()->handle_fasteoi_irq()--根据中断号分辨不同类型的中断,对应不同处理函数,这里中断号取大于等于32。
          ->handle_irq_event()->handle_irq_event_percpu()
            ->action->handler()---------------------对应到特定中断的处理函数,即上半部。
              ->__irq_wake_thread()--如果中断函数处理返回IRQ_WAKE_THREAD,则唤醒中断下半部,但不是立即执行中断线程。
      ->irq_exit()------------------------------------退出硬中断上下文。视情况处理软中断。
        ->invoke_softirq()---------------处理软中断,超出一定条件任务就会交给软中断线程处理。
    ->GIC_CPU_EOI-----------------------写EOI寄存器,表示结束中断。至此GIC才会接收新的硬件中断,此前一直是屏蔽硬件中断的。
  ->svc_exit---------------------------------------------------------恢复中断现场
 

参考文献:https://stephenzhou.blog.csdn.net/article/details/90707175?spm=1001.2014.3001.5502

3.2. 注册中断

涉及文件:interrupt.h

调用接口request_irq()request_threaded_irq()向系统申请注册一个中断。

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
       irq_handler_t thread_fn,
       unsigned long flags, const char *name, void *dev);

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
     const char *name, void *dev)
{
 return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

参数含义:

irq

表了该中断的中断号,一般 CPU 的中断号都会事先定义好。

handler

中断发生后的 ISR(中断服务程序)

flags

中断标志

name

中断相关的设备 ASCII 文本,例如 "keyboard",这些名字会在 /proc/irq 和 /proc/interrupts 文件使用

dev

用于共享中断线,传递驱动程序的设备结构。非共享类型的中断,直接设置成为 NULL

中断标志: flag 的含义:

#define IRQF_TRIGGER_NONE    0x00000000
#define IRQF_TRIGGER_RISING    0x00000001-----------------------上升沿触发
#define IRQF_TRIGGER_FALLING    0x00000002----------------------下降沿触发
#define IRQF_TRIGGER_HIGH    0x00000004-------------------------高电平触发
#define IRQF_TRIGGER_LOW    0x00000008--------------------------地电平触发
#define IRQF_TRIGGER_MASK    (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
                 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)--------四种触发类型
#define IRQF_TRIGGER_PROBE    0x00000010

#define IRQF_SHARED        0x00000080---------------------------多个设备共享一个中断号
#define IRQF_PROBE_SHARED    0x00000100-------------------------中断处理程序允许sharing mismatch发生
#define __IRQF_TIMER        0x00000200--------------------------标记一个时钟中断
#define IRQF_PERCPU        0x00000400---------------------------属于某个特定CPU的中断
#define IRQF_NOBALANCING    0x00000800--------------------------禁止在多CPU之间做中断均衡
#define IRQF_IRQPOLL        0x00001000--------------------------中断被用作轮询
#define IRQF_ONESHOT        0x00002000--------------------------一次性触发中断,不允许嵌套。
#define IRQF_NO_SUSPEND        0x00004000-----------------------在系统睡眠过程中不要关闭该中断
#define IRQF_FORCE_RESUME    0x00008000-------------------------在系统唤醒过程中必须抢孩子打开该中断
#define IRQF_NO_THREAD        0x00010000------------------------表示该中断不会给线程化
#define IRQF_EARLY_RESUME    0x00020000
#define IRQF_COND_SUSPEND    0x00040000

#define IRQF_TIMER        (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)

调用 request _irq 成功执行返回 0。常见错误是 -EBUSY,表示给定的中断线已经在使用(或者没有指定 IRQF_SHARED

注意:request_irq 函数可能引起睡眠,所以不允许在中断上下文或者不允许睡眠的代码中调用

3.3. 释放中断

调用接口free_irq()向系统释放注册的中断处理程序。

const void *free_irq(unsigned int irq, void *dev_id)

注意:Linux 中的中断处理程序是无须重入的。当给定的中断处理程序正在执行的时候,其中断线在所有的处理器上都会被屏蔽掉,以防在同一个中断线上又接收到另一个新的中断。通常情况下,除了该中断的其他中断都是打开的,也就是说其他的中断线上的重点都能够被处理,但是当前的中断线总是被禁止的,故,同一个中断处理程序是绝对不会被自己嵌套的。

3.4. 中断上下文

与进程上下文不一样,内核执行中断服务程序的时候,处于中断上下文。中断处理程序并没有自己的独立的栈,而是使用了内核栈,其大小一般是有限制的(32bit 机器 8KB)。所以其必须短小精悍。同时中断服务程序是打断了正常的程序流程,这一点上也必须保证快速的执行。同时中断上下文中是不允许睡眠,阻塞的。

中断上下文不能睡眠的原因是:

  1. 中断处理的时候,不会发生进程切换,因为在中断context中,唯一能打断当前中断handler的只有更高优先级的中断,它不会被进程打断,如果在 中断context中休眠,则没有办法唤醒它,因为所有wake_up_xxx都是针对某个进程而言的,而在中断context中,没有进程的概念,没有一个task_struct(这点对于softirq和tasklet一样),因此真的休眠了,比如调用了会导致block的例程,内核几乎肯定会死。
  2. schedule()在切换进程时,保存当前的进程上下文(CPU寄存器的值、进程的状态以及堆栈中的内容),以便以后恢复此进程运行。中断发生后,内核会先保存当前被中断的进程上下文(在调用中断处理程序后恢复);但在中断处理程序里,CPU寄存器的值肯定已经变化了吧(最重要的程序计数器PC、堆栈SP等),如果此时因为睡眠或阻塞操作调用了schedule(),则保存的进程上下文就不是当前的进程context了.所以不可以在中断处理程序中调用schedule()。
  3. 内核中schedule()函数本身在进来的时候判断是否处于中断上下文:

if(unlikely(in_interrupt())) BUG();

因此,强行调用schedule()的结果就是内核BUG异常重启。

  1. 中断handler会使用被中断的进程内核堆栈,但不会对它有任何影响,因为handler使用完后会完全清除它使用的那部分堆栈,恢复被中断前的原貌。
  2. 处于中断context时候,内核是不可抢占的。因此,如果休眠,则内核一定挂起

3.5. 启用和禁用中断

有时设备驱动程序必须在一个时间段内阻塞中断的发出。通常来说,我们必须在拥有自旋锁的时候阻塞中断,以免死锁系统。理论上我们应该少使用这种接口。

3.5.1 禁用单个中断

当驱动程序需要禁用某个特定的中断线的中断产生时,可以使用内核提供的三个接口。

涉及文件:manage.c

void disable_irq(int irq); //不但会禁止给定的中断,同时也会等待当前正在执行的中断处理例程完成。
void disable_irq_nosync(int irq); //该操作禁止中断后会立即返回,但是会让驱动程序处于竞态状态。
void enable_irq(int irq);  //启用指定的中断

调用这些函数中任何一个都会更新可编程中断控制器(PIC)中指定中断的掩码,因而可以在所有处理器上禁用或启用IRQ,且这些函数调用可以嵌套,既调用两次次disable_irq()就需要在执行两次enable_irq()恢复。

3.5.2 禁用所有中断

内核提供了两个函数接口禁用所有中断。

#define local_irq_enable()	do { raw_local_irq_enable(); } while (0)
#define local_irq_disable()	do { raw_local_irq_disable(); } while (0)

#define local_irq_save(flags)	do { raw_local_irq_save(flags); } while (0)
#define local_irq_restore(flags) do { raw_local_irq_restore(flags); } while (0)

local_irq_save()的调用将当前中断状态保存到flags中,然后禁用当前处理器上的中断发送。

local_irq_disable()不保存状态而关闭本地处理器上的中断发送;只有我们知道中断并未在其他地方被禁用的情况下,才能使用这个版本。


四. 中断顶半部和底半部

在linux里,中断处理分为顶半(top half),底半(bottomhalf),在顶半里处理优先级比较高的事情,要求占用中断时间尽量的短,在处理完成后,就激活底半,有底半处理其余任务。

玩过 MCU 的人都知道,中断服务程序的设计最好是快速完成任务并退出,因为此刻系统处于被中断中。但是在 ISR 中又有一些必须完成的事情,比如:清中断标志,读/写数据,寄存器操作等。

在 Linux 中,同样也是这个要求,希望尽快的完成 ISR。但事与愿违,有些 ISR 中任务繁重,会消耗很多时间,导致响应速度变差。

Linux 中针对这种情况,将中断分为了两部分:

顶半部(top half):收到一个中断,立即执行,有严格的时间限制,只做一些必要的工作,比如:应答,复位等。这些工作都是在所有中断被禁止的情况下完成的。

底半部(bottom half):能够被推迟到后面完成的任务会在底半部进行。在适合的时机,下半部会被开中断执行。

4.1 顶/底的分配
    • 如果一个任务对时间非常敏感,将其放在顶半部中执行;
    • 如果一个任务和硬件有关,将其放在顶半部中执行;
    • 如果一个任务要保证不被其他中断打断,将其放在顶半部中执行;
    • 其他所有任务,考虑放置在底半部执行。
4.2 底半部实现机制分类

中断处理程序包括上半部硬件中断处理程序,下半部处理机制,包括:软中断tasklet、工作列队workqueue、中断线程化。

4.2.1 tasklet

tasklet执行上下文是软中断,执行时机通常是顶半部返回的时候。使用时只需要定义tasklet及其处理函数,并将两者关联即可,例如:

void my_tasklet_func(unsigned long); /*定义一个处理函数*/
/*定义一个tasklet结构 my_tasklet 与my_tasklet_func(data)函数相关联 */
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data); 

代码DECLARE_TASKLET(my_tasklet,my_tasklet_func,data)实现了定义名称为my_tasklet的 tasklet,并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data。

在需要调度tasklet的时候引用一个tasklet_schedule() 函数就能使系统在适当的时候进行调度运行:

tasklet_schedule(&my_tasklet);

tasklet使用模板:

/* 定义tasklet和底半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);

/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long)
{
	...
}

/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
	...
	tasklet_schedule(&xxx_tasklet);
	...
}

/* 设备驱动模块加载函数 */
int __init xxx_init(void)
{
	...
	/* 申请中断 */
	result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
	...
	return IRQ_HANDLED;
}

/* 设备驱动模块卸载函数 */
void __exit xxx_exit(void)
{
	...
	/* 释放中断 */
	free_irq(xxx_irq, xxx_interrupt);
	...
}

参考文献:https://stephenzhou.blog.csdn.net/article/details/86529841?spm=1001.2014.3001.5502

4.2.2 工作队列 workqueue

工作队列的使用方法和tasklet非常相似,但是工作队列的执行上下文是内核线程,因此可以调度和睡 眠。下面的代码用于定义一个工作队列和一个底半部执行函数:

struct work_struct my_wq; /* 定义一个工作队列 */
void my_wq_func(struct work_struct *work); /* 定义一个处理函数 */

通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定:

INIT_WORK(&my_wq, my_wq_func);
/* 初始化工作队列并将其与处理函数绑定 */

与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如:

schedule_work(&my_wq); /* 调度工作队列执行 */

工作队列使用模板 :

/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);

/* 中断处理底半部 */
void xxx_do_work(struct work_struct *work)
{
	...
}

/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
	...
	schedule_work(&xxx_wq);
	...
	return IRQ_HANDLED;
}

/* 设备驱动模块加载函数 */
int xxx_init(void)
{
	...
	/* 申请中断 */
	result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx" , NULL);
	...
	/* 初始化工作队列 */
	INIT_WORK(&xxx_wq, xxx_do_work);
	...
}

/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
	...
	/* 释放中断 */
	free_irq(xxx_irq, xxx_interrupt);
	...
}
4.2.3 线程化IRQ request_threaded_irq

2009年2.6.xx开始,可以使用request_threaded_irq申请线程化中断,handler不是在中断上下文里执行,而是在新创建的线程里执行,这样,该handler非常像执行workqueue,拥有所有workqueue的特性,但是省掉了创建,初始化,调度workqueue的繁多步骤。处理起来非常简单。让我们看看这个接口。

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
                     irq_handler_t thread_fn,
                     unsigned long flags, const char *name, void *dev);
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
                          irq_handler_t handler, irq_handler_t thread_fn,
                          unsigned long irqflags, const char *devname,
                          void *dev_id);

和request_irq非常类似,irq是中断号, handler是在发生中断时,首先要执行的code,非常类似于顶半,
该函数最后会return IRQ_WAKE_THREAD来唤醒中断线程,thread_fn,是要在线程里执行的handler,
非常类似于底半。 后三个参数基本和request_irq相同。irqsflags新增加了一个标志,IRQF_ONESHOT,
用来标明是在中断线程执行完后在打开该中断,该标志非常有用,否则中断有可能一直在顶半执行,
而不能处理中断线程。例如对于gpio level中断,如果不设置该位,在顶半执行完成后,会打开中断,
此时由于电平没有变化,马上有执行中断,永远没有机会处理线程。

由此可见,它们比request_irq()、devm_request_irq()多了一个参数thread_fn。用这两个API申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号,如果其他中断也通过request_threaded_irq()申请,自然会得到新的内核线程。

参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数则执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。

线程化IRQ示例:

//中断上半部执行
static irqreturn_t ipcl_request_irq_handler(int irq, void *private)
{
    struct driver_data *data = private;

    ipcl_update_stats(data, IPCL_STATS_VIP_SRQ);

    disable_irq_nosync(data->irq);
    ipcl_disable_j6_srq(data);

    return IRQ_WAKE_THREAD;
}
//中断下半部执行
static struct sched_param param = { .sched_priority = MAX_RT_PRIO - 9 };
static irqreturn_t ipcl_request_irq_thread(int irq, void *private)
{
    struct driver_data *data = private;

    if (current->rt_priority != param.sched_priority)
        sched_setscheduler(current, SCHED_FIFO, &param);

    ipcl_sm(data);
    enable_irq(data->irq);

    /* reqs is not empty, trigger VIP to start transmit immediately */
    if (!ipcl_reqs_is_empty(data)) {
        ipcl_enable_j6_srq(data);
    }

    return IRQ_HANDLED;
}
//初始化中断线程
static int ipcl_irq_setup(struct spi_device *spi)
{
    int ret;
    const char *of_name = "spi,vip-srq";
    struct device_node *of_node = spi->dev.of_node;
    struct driver_data *drv_data = spi_get_drvdata(spi);

    drv_data->vip_srq = of_get_named_gpio(of_node, of_name, 0);
    if (!gpio_is_valid(drv_data->vip_srq)) {
        ipcl_err(IPCL_DBG_CORE_FLOW, "Bad %s property value %d\n",
                 of_name, drv_data->vip_srq);
        return -EINVAL;
    }

    gpio_request_one(drv_data->vip_srq, GPIOF_IN, "srq-irq");
    drv_data->irq = gpio_to_irq(drv_data->vip_srq);
    if (drv_data->irq < 0) {
        ipcl_err(IPCL_DBG_CORE_FLOW, "failed to map GPIO to IRQ: %d\n",
                 drv_data->vip_srq);
        gpio_free(drv_data->vip_srq);
        return -EINVAL;
    }

    ret = request_threaded_irq(drv_data->irq, ipcl_request_irq_handler,
                               ipcl_request_irq_thread, IRQF_TRIGGER_FALLING, "ipcl", drv_data);
    if (ret) {
        ipcl_err(IPCL_DBG_CORE_FLOW, "Unable to request ipcl IRQ, ret=%d\n", ret);
        gpio_free(drv_data->vip_srq);
    }

    return 0;
}
4.2.5 中断共享

多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,Linux支持这种中断共享。下面是中断共享的使用方法。

a. 共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断

的所有设备也都以IRQF_SHARED标志申请该中断。

b. 尽管内核模块可访问的全局地址都可以作为request_irq(…,void*dev_id)的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。

c. 在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回IRQ_HANDLED。在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的dev_id参数迅速地判

断是否为本设备的中断,若不是,应迅速返回IRQ_NONE,如图10.5所示。

4.2.6 共享中断编程模板:
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
	...
	int status = read_int_status();      /* 获知中断源 */
	if(!is_myint(dev_id,status))         /* 判断是否为本设备中断 */
			return IRQ_NONE;            /* 不是本设备中断,立即返回 */

	/* 是本设备中断,进行处理 */
	...
	return IRQ_HANDLED;                /* 返回IRQ_HANDLED表明中断已被处理 */
}

/* 设备驱动模块加载函数 */
int xxx_init(void)
{
	...
	/* 申请共享中断 */
	result = request_irq(sh_irq, xxx_interrupt, IRQF_SHARED, "xxx", xxx_dev);
	...
}

/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
	...
	/* 释放中断 */
	free_irq(xxx_irq, xxx_interrupt);
	...
}

五.内核定时器

软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。

Linux内核所提供的用于操作定时器的数据结构和函数如下。

1.数据结构 timer_list

timer_list在Linux内核中,timer_list结构体的一个实例对应一个定时器,如代码所示。

struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
	struct list_head entry;
	unsigned long expires;
	struct tvec_base *base;

	void (*function)(unsigned long);
	unsigned long data;

	int slack;

	#ifdef CONFIG_TIMER_STATS
	int start_pid;
	void *start_site;
	char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

当定时器期满后,其中第10行的function()成员将被执行,而第11行的data成员则是传入其中的参数,第7行的expires则是定时器到期的时间(jiffies)。

如下代码定义一个名为my_timer的定时器:

struct timer_list my_timer;

2.初始化定时器

init_timer是一个宏,它的原型等价于:

void init_timer(struct timer_list * timer);

上述init_timer()函数初始化timer_list的entry的next为NULL,并给base指针赋值。TIMER_INITIALIZER(_function,_expires,_data)宏用于赋值定时器结构体的function、expires、data和base成员,这个宏等价于:

#define TIMER_INITIALIZER(_function, _expires, _data) { \
	.entry = { .prev = TIMER_ENTRY_STATIC }, \
	.function = (_function), \
	.expires = (_expires), \
	.data = (_data), \
	.base = &boot_tvec_bases, \
}

DEFINE_TIMER(_name,_function,_expires,_data)宏是定义并初始化定时器成员的“快捷方式”,这个宏定义为:

#define DEFINE_TIMER(_name, _function, _expires, _data)\
struct timer_list _name =\
TIMER_INITIALIZER(_function, _expires, _data)

此外,setup_timer()也可用于初始化定时器并赋值其成员,其源代码为:

#define __setup_timer(_timer, _fn, _data, _flags) \
do { \
	__init_timer((_timer), (_flags)); \
	(_timer)->function = (_fn); \
	(_timer)->data = (_data); \
} while (0)

3.增加定时器

void add_timer(struct timer_list * timer);

上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中。

4.删除定时器

int del_timer(struct timer_list * timer);

上述函数用于删除定时器。del_timer_sync()是del_timer()的同步版,在删除一个定时器时需等待其被处理完,因此该函数的调用不能发生在中断上下文中。

5.修改定时器的expire

int mod_timer(struct timer_list *timer, unsigned long expires);

上述函数用于修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数。

6.定时器中断使用模板

/* xxx设备结构体 */
struct xxx_dev {
	struct cdev cdev;
	...
	timer_list xxx_timer; /* 设备要使用的定时器 */
};

/* xxx驱动中的某函数 */
xxx_func1(…)
{
	struct xxx_dev *dev = filp->private_data;
	...
	/* 初始化定时器 */
	init_timer(&dev->xxx_timer);
	dev->xxx_timer.function = &xxx_do_timer;
	dev->xxx_timer.data = (unsigned long)dev;
	/* 设备结构体指针作为定时器处理函数参数 */
	dev->xxx_timer.expires = jiffies + delay;
	/* 添加(注册)定时器 */
	add_timer(&dev->xxx_timer);
	...
}

/* xxx驱动中的某函数 */
xxx_func2(…)
{
	...
	/* 删除定时器 */
	del_timer (&dev->xxx_timer);
	...
}
/* 定时器处理函数 */
static void xxx_do_timer(unsigned long arg)
{
	struct xxx_device *dev = (struct xxx_device *)(arg);
	...
	/* 调度定时器再执行 */
	dev->xxx_timer.expires = jiffies + delay;
	add_timer(&dev->xxx_timer);
	...
}

从代码第18、39行可以看出,定时器的到期时间往往是在目前jiffies的基础上添加一个时延,若为Hz,则表示延迟1s。

在定时器处理函数中,在完成相应的工作后,往往会延后expires并将定时器再次添加到内核定时器链

表中,以便定时器能再次被触发。

6.内核中延迟的工作delayed_work

对于周期性的任务,除了定时器以外,在Linux内核中还可以利用一套封装得很好的快捷机制,其本质是利用工作队列和定时器实现,这套快捷机制就是delayed_workdelayed_work结构体的定义如代码所示。

struct delayed_work {
	struct work_struct work;
	struct timer_list timer;

/* target workqueue and CPU ->timer uses to queue ->work */
	struct workqueue_struct *wq;
	int cpu;
};

我们可以通过如下函数调度一个delayed_work在指定的延时后执行:

int schedule_delayed_work(struct delayed_work *work, unsigned long delay);

当指定的delay到来时,delayed_work结构体中的work成员work_func_t类型成员func()会被执行。work_func_t类型定义为:

typedef void (*work_func_t)(struct work_struct *work);

其中,delay参数的单位是jiffies,因此一种常见的用法如下:

schedule_delayed_work(&work, msecs_to_jiffies(poll_interval));

msecs_to_jiffies()用于将毫秒转化为jiffies。如果要周期性地执行任务,通常会在delayed_work的工作函数中再次调用schedule_delayed_work(),周而复始。如下函数用来取消delayed_work:

int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);

六. 查看中断统计

/proc/interrupts中的字段依次是逻辑中断号、中断在各CPU上发生的次数,中断所属父设备名称(中断控制器的名字)、硬件中断号、中断触发方式(电平或边沿)、中断名称

中断绑核:

BusyBox v1.20.2 (2024-01-17 02:46:04 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
/*查看usb的中断统计*/
# cat /proc/interrupts |grep usb
cat /proc/interrupts |grep usb
274:    3071522          0          0          0          0          0  MT_SYSIRQ 122 Level     11271000.usb
313:          0          0          0          0          0          0  MT_SYSIRQ 123 Level     xhci-hcd:usb1
314:    1535437          0          0          0          0          0  MT_SYSIRQ 249 Level     xhci-hcd:usb3

/*调整usb中断到CPU1处理*/
# echo 2 >/proc/irq/274/smp_affinity
echo 2 >/proc/irq/274/smp_affinity

/*再次查看中断统计*/
# cat /proc/interrupts |grep usb
cat /proc/interrupts |grep usb
274:    3081637        971          0          0          0          0  MT_SYSIRQ 122 Level     11271000.usb
313:          0          0          0          0          0          0  MT_SYSIRQ 123 Level     xhci-hcd:usb1
314:    1537705          0          0          0          0          0  MT_SYSIRQ 249 Level     xhci-hcd:usb3
#

七.总结:

1.Linux的中断处理分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时操作。tasklet

和工作队列都是调度中断底半部的良好机制,tasklet基于软中断实现,内核定时器也依靠软中断实现。

2.内核中的延时可以采用忙等待或睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对延迟时间的要求并不是很精确的情况下,睡眠等待通常是值得推荐的,而ndelay()、udelay()忙等待机制在驱动中通常是为了配合硬件上的短时延迟要求。

3.中断函数可能引起睡眠,所以不允许在中断上下文或者不允许睡眠的代码中调用,例如,在中断中使用kmalloc(1024, GFP_KERNEL)会在内存紧张时进入睡眠,可以使用GFP_ATOMIC代替。

4.在所有关于 CPU 的底层编程中,无一例外的,都要求,中断处理尽可能的快,一方面是处理完毕后,能够更快的响应其他中断的请求,另一方面,能够快速的回到进程上下文,处理相关业务。

5.中断线程化是实时Linux项目开发的一个新特性,目的是降低中断处理对系统实时延迟的影响。

6.在LInux内核里,中断具有最高优先级,只要有中断发生,内核会暂停手头的工作转向中断处理,等到所有挂起等待的中断和软终端处理完毕后才会执行进程调度,因此这个过程会造成实时任务得不到及时处理。

7.中断上下文总是抢占进程上下文,中断上下文不仅是中断处理程序,还包括softirq、tasklet等,中断上下文成了优化Linux实时性的最大挑战之一。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值