Linux中断操作

一、内核的中断处理框架

1)内核处理一个外部设备中断的流程

a. 比如当外设0触发一个中断电信号时,中断控制器PIC先通过与相连的一条IRQ line收到这个信号,如果它没有被屏蔽,那么PIC应该在INT引脚上产生一个中断信号告诉CPU,当CPU接收到该信号后就开始跳转到中断向量表的通用外部设备中断处理函数入口去执行。

b. 由于所有的外部设备中断都是在这个通用中断处理函数中处理的,因此需要一个软件中断号irq让通用中断处理函数来区分中断来自哪个设备。在这个通用中断处理函数中首先从PIC得到这个irq,然后通过调用do_IRQ(unsigned int irq, struct pt_regs *regs)函数去执行这个外设0驱动中通过request_irq()所注册的ISR回调。

/*
1)这里需要注意,处理器在接收到PIC发来的中断信号时,处理器的硬件逻辑会自动屏蔽掉处理器
响应外部中断的能力,因此如果内核实现的中断处理框架不主动打开中断的话,整个中断处理流程是在
关闭硬中断的情况下进行的。因为各个外设的中断处理函数一定是由驱动程序实现的,内核无法保证这些中断
处理函数执行时间的长短,如果某一中断处理函数执行时间过长,则将会导致系统可能长时间无法接收中断,
这可能会使某些外部设备丢失数据或者操作系统响应时间变长等。为了解决这个问题,内核为驱动程序提供
的中断处理机制分为二部分:HARDIRQ(中断处理上半部分的称谓)和SOFTIRQ(中断处理下半部分的称谓)。
前者在关闭硬中断的情况下执行,用来完成中断发生后最关键的操作,它的执行时间应该尽可能短;后者是在
硬中断开启的情况下进行,此时外部设备仍可以继续硬中断处理器,外设的驱动程序可以将一些比较耗时的
工作延迟到这部分执行。
2)do_iRQ函数处理的过程中,计算机处于可屏蔽的中断上下文(硬中断上下文和软中断上下文),其边界是
irq_enter()和irq_exit()。
3)do_IRQ的前半部处于硬中断上下文,也就是中断的上半部,其边界是loacl_irq_disable()
和local_irq_enable();后半部分为软中断上下文,也就是中断的下半部,其边界是
local_bh_disable()和local_bh_enable()
4)参数irq是通用中断处理函数提供的软件中断号virq, regs是保存下来的被中断任务的执行现场,不同
处理器有不同的执行现场,也就是有不同的寄存器。
*/
asmlinkage __irq_entry int do_IRQ(unsigned int irq, struct pt_regs *regs)
{
    struct pt_regs *old_regs = set_irq_regs(regs);
    /*对irq_enter的调用可以认为是HARDIRQ部分的开始*/
    irq_enter();

    irq = irq_demux(irq_lookup(irq));

    if (irq != NO_IRQ_IGNORE) {
        handle_one_irq(irq);
        irq_finish(irq);
    }
    /*SOFTIRQ在irq_exit中完成*/
    irq_exit();
    /*恢复现场*/
    set_irq_regs(old_regs);

    return IRQ_HANDLED;
}

c. 上述当其执行完成后,通用中断处理函数的剩余代码将完成中断现场的恢复工作,这时才标志着外设0发生的中断处理流程的结束。

Linux中断操作_第1张图片

2)几个重要的数据结构

struct irq_desc {
    struct irq_common_data    irq_common_data;
    struct irq_data        irq_data;
    /*用于系统的中断统计计数*/
    unsigned int __percpu    *kstat_irqs;
    /*在handle_irq指向的函数内部,才会调用设备驱动提供的ISR。它与irq是一一对应的,代表了
    对一条IRQ line上的处理动作。*/
    irq_flow_handler_t    handle_irq;
    /*1)针对某一具体设备的中断处理的抽象。设备驱动程序会通过request_irq()来向其中挂载设备特定
    的中断处理函数,其中的handler成员便为设备的中断服务例程ISR。
      2)通过action成员,可以在一条IRQ line上挂载多个设备(共享中断),换句话说多个设备可以通过
    同一条IRQ line来共享同一个软件中断号irq,形成所谓的中断链,所以可以推想到action中必然有
    构成链表的成员*/
    struct irqaction    *action;
    ......
    /*操作irq_desc数组时用作互斥保护的成员,因为irq_desc在多个处理器之间共享,即便是
    单处理器系统,也有并发操作该数组的可能*/
    raw_spinlock_t        lock;
    ......
    /*handle_irq对应的名字,最终会出现在/proc/interrupts文件中*/
    const char        *name;
};

struct irq_data {
    u32            mask;
    /*软件中断号*/
    unsigned int        irq;   
    unsigned long        hwirq;
    struct irq_common_data    *common;
    /*代表当前中断来自的PIC,是在软件层面对PIC的一个抽象。通过chip来屏蔽各种不同硬件平台上
    的PIC的差异,给上层软件提供同一的对PIC操作的接口,如果系统中只有一个PIC,那么irq_desc
    数组的每一项中的chip都应该指向该PIC对象。PIC对象用来实现对PIC的配置,配置工作主要有设定
    外部设备的中断触发信号的类型,屏蔽或者启用某一设备的中断信号,向发出中断请求的设备发送中断
    响应信号等。*/
    struct irq_chip        *chip;  
    struct irq_domain    *domain;
#ifdef    CONFIG_IRQ_DOMAIN_HIERARCHY
    struct irq_data        *parent_data;
#endif
    void            *chip_data;
};

struct irqaction {
    irq_handler_t        handler;
    /*调用handler时传给它的参数,在多个设备共享一个软件irq的情况下特别重要,这种链式的action
    中,设备驱动程序通过dev_id来标识自己*/
    void            *dev_id;
    void __percpu        *percpu_dev_id;
    /*指向下一个action对象,用于多个设备共享同一个软件irq的情形,此时action构成一个链表*/
    struct irqaction    *next;
    /*当驱动程序调用request_threaded_irq函数来安装ISR时,用来实现irq_thread机制*/
    irq_handler_t        thread_fn;
    /*当驱动程序调用request_threaded_irq函数来安装ISR时,用来实现irq_thread机制*/
    struct task_struct    *thread;
    struct irqaction    *secondary;
    unsigned int        irq;
    unsigned int        flags;
    /*当驱动程序调用request_threaded_irq函数来安装ISR时,用来实现irq_thread机制*/
    unsigned long        thread_flags;
    unsigned long        thread_mask;
    const char        *name;
    /*中断处理函数中用来创建在proc文件系统中的目录项*/
    struct proc_dir_entry    *dir;
};

3)中断处理的下半部(软中断):irq_exit()

void irq_exit(void)
    |--__irq_exit_rcu();
        /*invoke_softirq为真正处理softirq的函数。它被调用的前提是:
        1)当前不在interrupt上下文。不在interrupt上下文保证了如果代码正在SOFTIRQ部分
        执行时(此时处理器可以处理外部中断),如果发生了一个外部中断,那么在中断处理函数结束
        HARDIRQ时,将不会处理softirq,而是直接返回,这样此前被中断的SOFTIRQ部分将继续
        执行。
        2)__softirq_pending(内核用这个无符号整型变量来表示当前正在被等待处理的
        softirq,每一种softirq在__softirq_pending中占据一位,每个CPU都拥有自己的
        __softirq_pending变量)中有等待的softirq。Linux-6.1内核中定义了如下几种软中断,
        每个对应__softirq_pending变量中的一位:
            enum
            {
                HI_SOFTIRQ=0,   //用于实现tasklet,在softirq_init()中初始化
                TIMER_SOFTIRQ,   //用于定时器,在init_timers()中初始化
                NET_TX_SOFTIRQ,  //用于网络设备的发生和接收操作
                NET_RX_SOFTIRQ,  //用于网络设备的发生和接收操作
                BLOCK_SOFTIRQ,   //用于块设备操作
                IRQ_POLL_SOFTIRQ,
                TASKLET_SOFTIRQ,  //用于实现tasklet,在softirq_init()中初始化
                SCHED_SOFTIRQ,   //用于调度器,在sched_init()中初始化
                HRTIMER_SOFTIRQ,  //用于定时器,在hrtimers_init()中初始化
                RCU_SOFTIRQ, 
                NR_SOFTIRQS
            };
        */
        |--if (!in_interrupt() && local_softirq_pending())
                invoke_softirq();

这里需要说明的是内核定义了数组softirq_vec,用来存放softirq对应的处理函数。所以invoke_softirq的核心思想是,从CPU本地的__softirq_pending最低为开始,一次往高位扫描,如果发现某位为1,说明对应改位有个等待的softirq需要处理,那么久调用sotfirq_vec数组中的action函数。这个过程会一直持续下去,直到__softirq_pending为0

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

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

二、在驱动程序中的应用

1)驱动中安装和释放中断API

request_irq()和devm_request_irq()

驱动程序如何与上述的Linux的中断处理框架交互?通过如下API向irq_desc[irq]::action中安装设备的中断处理例程。

/*
0)返回值IRQ_NONE表示中断例程发现正在处理一个不是自己的设备触发的中断,此时它唯一要做的就是返回
该值;IRQ_HANDLED表示中断例程成功处理了自己设备的中断;IRQ_WAKE_THREAD表示中断处理例程被用
来唤醒一个等待在它的irq上的一个进程,此时它返回该值。
1)参数irq是当前要安装的中断处理例程ISR对应的软件中断号。
2)参数handler是ISR。
3)参数flags是标志变量,可影响内核在ISR时的一些行为模式。比如如果flags中的IRQF_SHARD位被置1,
表明正在安装一个共享的中断,这种情况下驱动必须提供最后一个参数dev。再比如flags参数如果设置了
IRQF_TRIGGER_MASK标志位,表明驱动程序需要利用request_irq()对irq的触发类型进行进程配置。
4)参数name是当前安装中断ISR的设备名称,内核会在proc文件系统中生成name的一个入口点。
5)参数dev是传递到ISR的指针,在中断共享的情形下,将在free_irq时被用到,以区分当前要释放的是哪一个
struct irqaction对象;因此要保证dev参数在内核整个中断处理框架中的唯一性,通常是将设备驱动所
管理的与设备相关的某一数据结构对象的指针作为dev参数;另外由于内核中断处理框架在调用设备驱动的
ISR时,会将该dev参数一并传入,因此也可以借助它在被中断的进程与中断处理例程中传递传递数据之用。
*/
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
        const char *name, void *dev)
{
    /*thread_fn参数传为NULL,这个参数跟内核中一个用于中断处理的线程irq_thread有关,
    即中断处理的irq_thread机制,如果传为NULL就不会涉及irq_thread部分。*/
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
         unsigned long irqflags, const char *devname, void *dev_id)
{
    /*thread_fn参数传为NULL*/
    return devm_request_threaded_irq(dev, irq, handler, NULL, irqflags,
                     devname, dev_id);
}

request_threaded_irq()和devm_request_threaded_irq()

在驱动程序中安装一个中断除了使用上述二个API外,如果要想用到内核的irq_thread机制可以直接使用以下二个函数,使用时需要实现其thread_fn, 对应struct irqaction对象的thread_fn成员。

用这两个API申请中断的时候, 在API内部会调用__setup_irq()为相应的中断号irq生成一个名为irq_thread的独立线程(注意这个线程只针对当前这个中断号, 如果其他中断也使用该API申请的话, 自然会得到新的内核线程)。

irq_thread线程被创建出来时以TASK_INTERRUPTIBLE的状态等待中断的发生,当中断发生时action->handler只负责唤醒睡眠的irq_thread,后者将调用action->thread_fn进行实际的中断处理工作。因为irq_thread本质是系统的一个独立进程,所以采用这种机制将使实质的中断处理发生在进程空间,而不是中断上下文。

/*
1)参数handler对应的函数执行于中断上下文, thread_fn参数对应的函数则执行于内核线程。 
如果handler结束的时候, 返回值是IRQ_WAKE_THREAD, 内核会调度对应线程执行thread_fn
对应的函数。
2)另外这二个函数支持在irqflags中设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上
下文中屏蔽对应的中断号, 而在内核调度thread_fn执行后, 重新使能该中断号。 对于我们无法在
上半部清除中断的情况, IRQF_ONESHOT特别有用, 避免了中断服务程序一退出, 中断就洪泛的情况。
*/
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)

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)

handler参数可以设置为NULL, 这种情况下, 内核会用默认的irq_default_primary_handler()代替handler, 并会使用IRQF_ONESHOT标记。

/*
 * Default primary interrupt handler for threaded interrupts. Is
 * assigned as primary handler when request_threaded_irq is called
 * with handler == NULL. Useful for oneshot interrupts.
 */
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
    return IRQ_WAKE_THREAD;
}

free_irq()和devm_free_irq()

//在驱动中释放中断
void *free_irq(unsigned int irq, void *dev_id);
void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id);

2)tasklet

内核中使用软中断的API

/*用来注册对应的软中断处理函数
参数nr就是要开启的软中断,是以上提到的内核定义的软中断类型之一。
参数action为软中断对应的处理回调函数,供内核中断处理框架调用。
*/
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

/*raise_softirq()函数在触发一个软中断之前要先禁止中断,触发之后再恢复原来的状态;
如果中断本来就已经被禁止了,那么可以调用raise_softirq_irqoff()函数,将会带来一些优化效果。*/
inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);
    /*
     * If we're in an interrupt or softirq, we're done
     * (this also catches softirq-disabled code). We will
     * actually run the softirq once we return from
     * the irq or softirq.
     * Otherwise we wake up ksoftirqd to make sure we
     * schedule the softirq soon.
     */
    if (!in_interrupt() && should_wake_ksoftirqd())
        wakeup_softirqd();
}

void raise_softirq(unsigned int nr)
{
    unsigned long flags;

    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

tasklet

是内核提供的几种softirq之一,所以tasklet在中断上下文中执行(所以虽然可以延迟,但是有的延迟操作也不能用tasklet中完成)。设备驱动程序的中断处理例程常常使用tasklet,至于其他的类型,一般情况下驱动模块使用的不多(内核的一些核心模块会用到)。内核将tasklet分成TASKLET_SOFTIRQ和HI_SOFTIRQ二种,其中后者的执行顺序优于前者。

struct tasklet_struct
{
    /*用来将系统中的tasklet对象构建成链表*/
    struct tasklet_struct *next;
    /*记录每个tasklet在系统中的状态,其值为TASKLET_STATE_SCHED和TASKLET_STATE_RUN二者
    之一。前者表示tasklet已经被提交,后者只能用在SMP系统中,表示当前tasklet正在执行*/
    unsigned long state;
    /*用来实现tasklet的disable和enable操作,count.counter=0表示当前tasklet是enabled的,
    可以被调度执行,否则是个disabled的,不可以被执行*/
    atomic_t count;
    bool use_callback;
    /*该tasklet上的执行函数或者延迟函数,当tasklet在SOFTIRQ部分被调度执行时,该函数指针指向
    的函数被调用,用来完成驱动程序中实际的延迟操作任务*/
    union {
        void (*func)(unsigned long data);
        void (*callback)(struct tasklet_struct *t);
    };
    /*func所指向的函数被调用时,data作为参数传递给func。驱动程序可以使用data向tasklet上
    指向的函数传递特定的参数*/
    unsigned long data;
};

tasklet机制的初始化在内核启动过程中:

/*在Linux系统初始化期间调用softirq_init为TASKLET_SOFTIRQ和HI_SOFTIRQ安装执行函数*/
void __init softirq_init(void)
{
    int cpu;
    /*该循环用来初始化管理tasklet链表的遍历tasKlet_vec和tasklet_hi_vec*/
    for_each_possible_cpu(cpu) {
        per_cpu(tasklet_vec, cpu).tail =
            &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =
            &per_cpu(tasklet_hi_vec, cpu).head;
    }
    /*
        softirq_vec[TASKLET_SOFTIRQ].action = tasklet_action;
        softirq_vec[HI_SOFTIRQ].action = tasklet_hi_action;
        如此,在内核中断处理框架的SOFTIRQ部分,如果发现本地CPU的__softirq_pending上
     TASKLET_SOFTIRQ或HI_SOFTIRQ 位被置1,就将调用tasklet_action或者tasklet_hi_action  
     */
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

驱动中的使用模板为:

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

/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
    ......
    /*向系统提交定义的tasklet*/
    tasklet_schedule(&xxx_tasklet);
    ......
}

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

/*设备驱动模块加载函数*/
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);
}

3)中断共享(IRQF_SHARED)

多个设备共享一根硬件中断线IRQ line的情况在实际的硬件系统中广泛存在, Linux支持这种中断共享。 使用共享中断的设备驱动程序的模板(仅包含与共享中断机制相关的部分):

/* 在中断到来时, 会遍历执行共享此中断的所有中断处理程序, 直到某一个函数返回
IRQ_HANDLED。 在中断处理程序顶半部中, 应根据硬件寄存器中的信息比照传入的dev参数
迅速地判断是否为本设备的中断, 若不是, 应迅速返回IRQ_NONE*/
irqreturn_t xxx_interrupt(int irq, void *dev)
{
    ......
    /*获知中断源*/
    int status = read_int_status();
    /*判断是否为本设备中断,若不是立即返回*/
    if(!is_myint(dev, status))
        return IRQ_NONE;
    
    /*若是本设备中断,进行处理*/
    ......
    /*表明中断已被处理*/
    return IRQ_HANDLED;
}

/*设备驱动模块加载函数*/
int xxx_init(void)
{
    ......
    /*共享中断的多个设备在申请中断时, 都应该使用IRQF_SHARED标志, 而且一个设备
    以IRQF_SHARED申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前
    申请该中断的所有设备也都以IRQF_SHARED标志申请该中断。
    内核为每个中断维护一个中断共享处理例程列表,dev就是区别不同处理例程的签名;因此
    最后一个参数dev必须唯一,任何指向模块地址空间的指针都行,但 dev绝不能设置为 NULL。
    一般将设备结构体指针作为参数。*/
    result = request_irq(sh_irq, xxx_interrupt, IRQF_SHARED, "xxx", xxx_dev);
    ......
}

/*设备驱动模块卸载*/
void xxx_exit(void)
{
    ......
    free_irq(xxx_irq, xxx_interrupt);
    ......  
}

一个使用共享处理例程的驱动需要小心:不能使用 enable_irq 或 disable_irq,否则,对其他共享这条线的设备就无法正常工作了。即便短时间禁止中断,另一设备也可能产生延时而为设备和其用户带来问题。

4)probe_irq_on()和probe_irq_off()

如果一个设备的驱动无法确定它所管理的设备的软件中断号irq,此时设备驱动程序可以使用irq的自动探测机制来获得真正在使用的irq。这种探测机制只限于非共享中断的情况。

探测前的情形是,该设备关联到了某个irq,但是因为设备驱动程序还不清楚是哪个irq,因此不可能调用request_irq来向该irq安装中断处理例程ISR,多以对应的irq的action为空。使用模板如下:

//探测要完成的任务是找到该设备所关联的irq
unsigned long irqs;
/*清除设备内部的中断*/
...
irqs = probe_irq_on();
/*等待5ms*/
msleep(5);
/*让设备产生一次中断*/
...
/*等待5ms*/
msleep(5);
/*得到探测到的中断号irq*/
irq = probe_irq_off(irqs);

三、使能/禁止中断API

1)禁止和使能某一个中断

void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)

用于使能和禁止指定的中断, irq 就是要禁止的中断号。disable_irq 函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数:

void disable_irq_nosync(unsigned int irq)

函数调用以后立即返回,不会等待当前中断处理程序执行完毕。

2)禁止和使能本地处理器的所有中断

/*关闭本地处理器的硬中断*/
#define local_irq_disable()                \
    do {                        \
        bool was_disabled = raw_irqs_disabled();\
        raw_local_irq_disable();        \
        if (!was_disabled)            \
            trace_hardirqs_off();        \
    } while (0)

/*打开本地处理器的硬中断*/
#define local_irq_enable()                \
    do {                        \
        trace_hardirqs_on();            \
        raw_local_irq_enable();            \
    } while (0)

/*关闭中断,相比local_irq_disable,会在关闭中断前前将处理器当前的标志位保存
在unsigned long flags中*/
#define local_irq_save(flags)                \
    do {                        \
        raw_local_irq_save(flags);        \
        if (!raw_irqs_disabled_flags(flags))    \
            trace_hardirqs_off();        \
    } while (0)

/*打开中断,相比local_irq_enable,会将保存在unsigned long flags中的值恢复到
处理器的FLAGS寄存器中。*/
#define local_irq_restore(flags)            \
    do {                        \
        if (!raw_irqs_disabled_flags(flags))    \
            trace_hardirqs_on();        \
        raw_local_irq_restore(flags);        \
    } while (0)

static inline void local_bh_disable(void)
{
    __local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}

static inline void local_bh_enable(void)
{
    __local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}

你可能感兴趣的:(Linux,Kernel,linux)