linux 中断-很全

来源:互联网 发布:postgresql mysql 编辑:程序博客网 时间:2024/05/21 20:05

转自Rock3的Linux博客


中断

tasklet原理

没有评论

        tasklet是Linux内核中“可延迟执行”机制、或“中断下半部”的一种。基于软中断实现,但比软中断灵活,tasklet有的地方翻译作“任务蕾”,大部分书籍没找到合适的词汇去翻译它。本篇博客主要介绍tasklet的设计原理、使用方法。

        本篇博客耗时8小时。

一、tasklet解决什么问题?

        先看下tasklet在一些书籍上的介绍:

  • tasklet是I/O驱动程序中实现可延迟函数的首选方法(我猜某个内核版本开始,块设备从tasklet中独立出来成为单独的软中断BLOCK_SOFTIRQ和BLOCK_IOPOLL_SOFTIRQ)——ULK。
  • tasklet和工作队列是延期执行工作的机制,其实现基于软中断,但他们更易于使用,因而更适合与设备驱动程序...tasklet是“小进程”,执行一些迷你任务,对这些人物使用全功能进程可能比较浪费——PLKA。
  • tasklet是并行可执行(但是是锁密集型的)软件中断和旧下半区的一种混合体,这里既谈不上并行性,也谈不上性能。引入tasklet是为了替代原来的下半区。

        下面这段来自于PLKA的话我也很想留在这里:软中断是将操作推迟到未来时刻执行的最有效的方法。但该延期机制处理起来非常复杂。因为多个处理器可以同时且独立的处理软中断,同一个软中断的处理程序可以在几个CPU上同时运行。对软中断的效率来说,这是一个关键,多处理器系统上的网络实现显然受惠于此。但处理程序的设计必须是完全可重入且线程安全的。另外,临界区必须用自旋锁保护(或其他IPC机制),而这需要大量审慎的考虑。

        我自己的理解,由于软中断以ksoftirqd的形式与用户进程共同调度,这将关系到OS整体的性能,因此软中断在Linux内核中也仅仅就几个(网络、时钟、调度以及Tasklet等),在内核编译时确定。软中断这种方法显然不是面向硬件驱动的,而是驱动更上一层:不关心如何从具体的网卡接收数据包,但是从所有的网卡接收的数据包都要经过内核协议栈的处理。而且软中断比较“硬”——数量固定、编译时确定、操作函数必须可重入、需要慎重考虑锁的问题,不适合驱动直接调用,因此Linux内核为驱动直接提供了一种使用软中断的方法,就是tasklet。

二、tasklet数据结构

        tasklet通过软中断实现,软中断中有两种类型属于tasklet,分别是级别最高的HI_SOFTIRQ和TASKLET_SOFTIRQ。

        Linux内核采用两个PER_CPU的数组tasklet_vec[]和tasklet_hi_vec[]维护系统种的所有tasklet(kernel/softirq.c),分别维护TASKLET_SOFTIRQ级别和HI_SOFTIRQ级别的tasklet:

1
2
3
4
5
6
7
8
structtasklet_head
{
    structtasklet_struct *head;
    structtasklet_struct **tail;
};
 
staticDEFINE_PER_CPU(structtasklet_head, tasklet_vec);
staticDEFINE_PER_CPU(structtasklet_head, tasklet_hi_vec);

    tasklet_vec

        tasklet的核心结构体如下(include/linux/interrupt.h):

1
2
3
4
5
6
7
8
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_tcount;
    void (*func)(unsigned long);
    unsigned long data;
};

        习惯上称之为tasklet描述符,func指针是具体的处理函数指针,data为可选参数,state表示该tasklet的状态,分别使用不同的bit表示两个状态:TASKLET_STATE_SCHED和TASKLET_STATE_RUN:

  • TASKLET_STATE_SCHED置位表示已经被调度(挂起),也意味着tasklet描述符被插入到了tasklet_vec和tasklet_hi_vec数组的其中一个链表中,可以被执行。
  • TASKLET_STATE_RUN置位表示该tasklet正在某个CPU上执行,单个处理器系统上并不校验该标志,因为没必要检查特定的tasklet是否正在运行。

        count为原子计数器,用于禁用已经调度的tasklet,如果该值不为0,则不予以执行。

三、tasklet操作接口

        tasklet对驱动开放的常用操作包括:

  • 初始化,tasklet_init(),初始化一个tasklet描述符。
  • 调度,tasklet_schedule()和tasklet_hi_schedule(),将taslet置位TASKLET_STATE_SCHED,并尝试激活所在的软中断。
  • 禁用/启动,tasklet_disable_nosync()、tasklet_disable()、task_enable(),通过count计数器实现。
  • 执行,tasklet_action()和tasklet_hi_action(),具体的执行软中断。
  • 杀死,tasklet_kill(),。。。

        tasklet_int()函数实现如下(kernel/softirq.c):

1
2
3
4
5
6
7
8
9
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;
}

        tasklet_schedule()函数与tasklet_hi_schedule()函数的实现很类似,这里只列tasklet_schedule()函数的实现(kernel/softirq.c),都挺明白就不描述了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
staticinline void tasklet_schedule(struct tasklet_struct *t)
{
    if(!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_schedule(t);
}
 
void __tasklet_schedule(struct tasklet_struct *t)
{  
    unsigned long flags;
         
    local_irq_save(flags);
    t->next = NULL;
    *__this_cpu_read(tasklet_vec.tail) = t;
    __this_cpu_write(tasklet_vec.tail, &(t->next));
    raise_softirq_irqoff(TASKLET_SOFTIRQ);
    local_irq_restore(flags);
}

        tasklet_disable()函数、task_enable()函数以及tasklet_disable_nosync()函数(include/linux/interrupt.h),不说了只列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
staticinline void tasklet_disable_nosync(structtasklet_struct *t)
{
    atomic_inc(&t->count);
    smp_mb__after_atomic_inc();
}
 
staticinline void tasklet_disable(structtasklet_struct *t)
{
    tasklet_disable_nosync(t);
    tasklet_unlock_wait(t);
    smp_mb();
}
 
staticinline void tasklet_enable(structtasklet_struct *t)
{
    smp_mb__before_atomic_dec();
    atomic_dec(&t->count);
}

        只列tasklet_action()函数(kernel/softirq.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
staticvoid tasklet_action(structsoftirq_action *a)
{
    structtasklet_struct *list;
 
    local_irq_disable();
    list = __this_cpu_read(tasklet_vec.head);
    __this_cpu_write(tasklet_vec.head, NULL);
    __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);
    local_irq_enable();
 
    while(list) {
        structtasklet_struct *t = list;
 
        list = list->next;
 
        if(tasklet_trylock(t)) {
            if(!atomic_read(&t->count)) {
                if(!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
                    BUG();
                t->func(t->data);
                tasklet_unlock(t);
                continue;
            }
            tasklet_unlock(t);
        }
 
        local_irq_disable();
        t->next = NULL;
        *__this_cpu_read(tasklet_vec.tail) = t;
        __this_cpu_write(tasklet_vec.tail, &(t->next));
        __raise_softirq_irqoff(TASKLET_SOFTIRQ);
        local_irq_enable();
    }
}

        tasklet_action()函数在softirq_init()函数中被调用:

1
2
3
4
5
6
void__init softirq_init(void)
{
    ...
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

        tasklet_kill()实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
voidtasklet_kill(structtasklet_struct *t)
{
    if(in_interrupt())
        printk("Attempt to kill tasklet from interrupt\n");
 
    while(test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
        do{
            yield();
        }while(test_bit(TASKLET_STATE_SCHED, &t->state));
    }
    tasklet_unlock_wait(t);
    clear_bit(TASKLET_STATE_SCHED, &t->state);
}

        yeild()函数是个值的研究的点。

四、一个tasklet调用例子

        找了一个tasklet的例子看一下(drivers/usb/atm,usb摄像头),在其自举函数usbatm_usb_probe()中调用了tasklet_init()初始化了两个tasklet描述符用于接收和发送的“可延迟操作处理”,但此是并没有将其加入到tasklet_vec[]或tasklet_hi_vec[]中:

1
2
3
4
    tasklet_init(&instance->rx_channel.tasklet,
usbatm_rx_process, (unsigned long)instance);
    tasklet_init(&instance->tx_channel.tasklet,
usbatm_tx_process, (unsigned long)instance);

        在其发送接口usbatm_atm_send()函数调用tasklet_schedule()函数将所初始化的tasklet加入到当前cpu的tasklet_vec链表尾部,并尝试调用do_softirq_irqoff()执行软中断TASKLET_SOFTIRQ:

1
2
3
4
5
6
staticint usbatm_atm_send(struct atm_vcc *vcc, struct sk_buff *skb)
{
    ...
    tasklet_schedule(&instance->tx_channel.tasklet);
    ...
}

        在其断开设备的接口usbatm_usb_disconnect()中调用tasklet_disable()函数和tasklet_enable()函数重新启动其收发tasklet(具体原因不详,这个地方可能就是由这个需要,暂时重启收发tasklet):

1
2
3
4
5
6
7
8
9
10
void usbatm_usb_disconnect(struct usb_interface *intf)
{
    ...
    tasklet_disable(&instance->rx_channel.tasklet);
    tasklet_disable(&instance->tx_channel.tasklet);
    ...
    tasklet_enable(&instance->rx_channel.tasklet);
    tasklet_enable(&instance->tx_channel.tasklet);
    ...
}

        在其销毁接口usbatm_destroy_instance()中调用tasklet_kill()函数,强行将该tasklet踢出调度队列。

        从上述过程以及tasklet的设计可以看出,tasklet整体是这么运行的:驱动应该在其硬中断处理函数的莫为调用tasklet_schedule()接口激活该taskle,内核经常调用do_softirq()执行软中断,通过softirq执行tasket,如下图所示。图中灰色部分为禁止硬中断部分,为保护软中断pending位图和tasklet_vec链表数组,count的改变均为原子操作,count确保SMP架构下同时只有一个CPU在执行该tasklet:

tasklet_action

五、tasklet同步

        主要看两个参数,一个state,一个count。

        state用于校验在tasklet_action()或tasklet_schedule()时,是否执行该tasklet的handler。state被tasklet_schedule()函数、tasklet_hi_schedule()函数、tasklet_action()函数以及tasklet_kill()函数所修改:

  • tasklet_schedule()函数、tasklet_hi_schedule()函数将state置位TASKLET_STATE_SCHED。
  • tasklet_action()函数将state的TASKLET_STATE_SCHED清除,并设置TASKLET_STATE_RUN。
  • tasklet_kill()函数将state的TASKLET_STATE_SCHED清除。

        tasklet_action()函数在设置TASKLET_STATE_RUN标志时,使用了tasklet_trylock()、tasklet_unlock()等接口:

​        count用于smp同步,count不为0,则表示该tasklet正在某CPU上执行,其他CPU则不执行该tasklet,count保证某个tasklet同时只能在一个CPU上执行。count的操作都是原子操作:

  • tasklet_disable()函数/tasklet_disable_nosync()函数将count原子减1。
  • tasklet_enablle()函数将count原子加1。

​        另外,tasklet的操作中还所使用了local_irq_save()/local_irq_disable()等禁止本地中断的函数,早保护对象被修改完毕后立即使用local_irq_resore()/local_irq_enable()开启:

  • tasklet_schedule()函数中,用于保护tasklet_vec[]链表和软中断的pending位图的更改。因为硬中断的激发能导致二者的更改。
  • tasklet_action()函数中,用于保护tasklet_vec[]链表和软中断的pending位图的更改。因为硬中断的激发能导致二者的更改。

六、总结

        tasklet是一种“可延迟执行”机制中的一种,基于软中断实现,主要面向驱动程序。tasklet与软中断的区别在于每个CPU上不能同时执行相同的tasklet,tasklet函数本身也不必是可重入的。与软中断一样,为了保证tasklet和硬中断之间在同一个CPU上是串行执行的,维护其PER_CPU的链表时,需要屏蔽硬中断。

        不要为了列代码而列代码,不要为了抄书而抄书。

七、细节

1、原子操作

        tasklet使用taskle_disable()函数和tasklet_enable()函数对count位进行增减操作,以保证SMP架构下,不在不同的CPU上同时运行相同的tasklet。这里使用了原子操作atomic_inc()和atomic_dec():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
staticinline void atomic_inc(atomic_t *v)
{
    asmvolatile(LOCK_PREFIX"incl %0"
             :"+m"(v->counter));
}
 
staticinline void atomic_dec(atomic_t *v)
{
    asmvolatile(LOCK_PREFIX"decl %0"
             :"+m"(v->counter));
}
 
#ifdef CONFIG_SMP
#define LOCK_PREFIX_HERE \
        ".pushsection .smp_locks,\"a\"\n"   \
        ".balign 4\n"               \
        ".long 671f - .\n" /* offset */     \
        ".popsection\n"            \
        "671:"
 
#define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "
 
#else /* ! CONFIG_SMP */
#define LOCK_PREFIX_HERE ""
#define LOCK_PREFIX ""
#endif

2、memory barrier

 

参考资料:

1、Understanding Linux Kernel

2、Professinal Linux Kernel Architecture

Published in 中断 and tagged tasklet on 2013年11月22日 by rock3

软中断原理

    Linux内核中将不太紧急的中断处理工作留给软中断去处理,这部分工作通常称为“可延期执行”任务。术语“软中断(softirq)”常常表示可延迟函数的所有种类。另一种被广泛使用的术语是“中断上下文”:表示内核当前正在执行一个中断处理程序或一个可延迟的函数。软中断机制目前包含几种:softirq、tasklet、工作队列。讨厌的是softirq本身也叫软中断,本文定位介绍softirq机制,而不包含tasklet和工作队列。由于本文比较仓促,如有错误之处敬请指出。

   本文中使用的内核代码版本为3.10.9。

   本篇博客耗时:18小时。

一、软中断的目的

    软中断,就是一种“可延迟执行”任务的处理方式,一种有别于硬件中断处理。举个例子,网卡接收数据包,从网卡产生中断信号,CPU将网络数据包拷贝到内核,然后进行协议栈的处理,最后将数据部分传递给用户空间,这个过程都可以说是中断处理函数需要做的部分,但硬件中断处理仅仅做从网卡拷贝数据的工作,而协议栈的处理的工作就交给“可延迟执行”部分处理。而软中断,正是”可延迟处理“部分的一种机制,之所以叫软中断,是因为类似于硬件中断的过程,产生中断信号,维护软中断向量,进行中断处理。

    可延迟函数在执行上一般有四个流程:

  • 初始化,定义一个新的可延迟函数,这个操作通常在内核自身初始化或加载过模块时进行。
  • 激活,标记一个可延迟函数为“挂起”状态(可延迟函数的下一轮调度中可执行),激活可以在任何时后进行(即是正在处理中断)。
  • 屏蔽,有选择的屏蔽一个可延迟函数,这样,即使它被激活,内核也不执行它。
  • 执行,执行一个挂起的可延迟函数和同类型的其他所有挂起的可延迟函数;执行是在特定的时间内进行的。

二、软中断内核数据机构

    Linux内核中定义为数不多的几种软中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,   
    /* Preferable RCU should always be the last softirq */
 
    NR_SOFTIRQS
};

软中断全局数组:

1
2
3
4
5
6
staticstruct softirq_action softirq_vec[NR_SOFTIRQS]
__cacheline_aligned_in_smp;
structsoftirq_action
{
    void   (*action)(structsoftirq_action *);
};

    softirq_vec[]数组元素下标就是软中断的向量号,内容为对应的处理函数的指针。

    参数如何传递?函数执行时,以softirq_action指针为参数,怎么获得相关的处理数据指针,比如网卡数据接收?

三、软中断的操作接口

    软中断对应的可延迟函数的四步分别为:

  • open_softirq(),注册软中断
  • raise_softirq(),激活软中断
  • local_bh_disable(),整体屏蔽所有可延迟机制
  • do_softirq(),执行软中断

    驱动通过open_softirq()函数初始化softirq_vec[]数组元素,完成对softirq_vec[]数组元素的初始化(kerne/softirq.c)

1
2
3
4
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

   使用raise_softirq()函数来激活软中断,等到下次调度时使用,raise_softirq()函数执行下面的操作:

  • local_irq_save()保存EFLAGS寄存器的IF位。
  • 将per_cpu的pending位图的相应bit置位,表示处与“挂起”状态(“挂起”表示可被执行),关于pending位图的细节请参考“六、一些细节”。
  • 如果当前进程未处于中断上下文中,则wakeup_softirqd(),尝试执行所有的“挂起”的softirq。
  • local_irq_restore()保存EFLAGS恢复寄存器的IF位。

    起先,对上述过程,我有两点不太明白:

  • raise_softirq(nr)函数主要功能是将软中断号为nr的软中断“挂起”,也即置位工作,是一个纯粹的软件操作,为什么要保存eflags寄存器的硬件中断的响应标志?
  • 如果当前进程没有处于中断的上下文中(软中断、硬中断),则唤醒ksoftirqd线程。这是什么意思?为什么不单独使用do_softirq()去做执行软中断的事情?

    第一个问题,在“五、同步”一节讨论。

    第二个问题,在于两个接口(do_softirq()和raise_softirq())都可以轮询softirq函数,这样做的好处,根据ULK上的说法,为了尽量不要让软中断像硬中断一样中断用户进程的操作,而尝试使用内核线程的方式与用户进程一同调度,更加公平。经过查找,在x86架构下,单独调用do_softirq()的地方很少:

    do_softirq

   这也说明了do_softirq()和raise_softirq()的区别:do_softirq()尝试在当前进程中或者内核softirq_ctx中执行轮询pending的softirq并执行他们(do_softirq()也尝试使用ksoftirqd,但是次要的),而raise_softirq()将当前的softirq置位pending,并尝试在ksoftirqd中轮询并执行所有状态为pending的softirq。这也是为什么do_softirq()和raise_softirq()都能轮询并执行软中断数组的原因,内核想在更多的地方执行软中断。

    我猜这里的主要矛盾是硬中断、软中断、用户进程之间的优先级问题,硬中断肯定可以中断软中断和用户进程,而软中断和用户进程之间的优先级应该是软中断略高于用户进程,想象一个socket程序在使用read接口来读取网络数据,那么如果接收并处理网络数据的软中断(NET_RX_SOFTIRQ)优先级没有用户进程(当然不一定是read()接口,还有其他用户进程,如kde什么的)的高,那么它将整体弱于用户进程而执行,那么将导致read接收数据效率低下,甚至丢包。反过来,如果软中断可以任意打断用户进程(这样软中断就和硬中断没什么区别了),那么在接收网络数据包的同时,会让KDE桌面显得很卡(用户空间驱动)。为了平衡二者之间的关系,Linux内核发明了raise_softirq()和do_softirq()两个接口,一个通过ksoftirqd主动触发软中断,一个在不少位置(详见“四、何时调用软中断”)在不同的内核栈里执行软中断。而ksoftirqd与普通用户进程通过调度器竞争CPU。内核大量的调用软中断的方式还是do_softirq(),大量的local_bh_enable()和irq_exit(),伴随着每次do_IRQ()都会被调用,而raise_softirq()函数往往在硬中断处理函数中决定是否要调用它。

     do_softirq()函数代码(64位,32位代码比64位略微复杂一些):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
asmlinkagevoiddo_softirq(void)
{
    __u32 pending;
    unsignedlongflags;
 
    if(in_interrupt())
        return;
 
    local_irq_save(flags);
    pending = local_softirq_pending();
    /* Switch to interrupt stack */
    if(pending) {
        call_softirq();
        WARN_ON_ONCE(softirq_count());
    }  
    local_irq_restore(flags);
}

    call_softirq()为arch/x86/kernel/entry_64.S中定义,call_softirq()函数指向__do_softirq()函数,__do_softirq()函数在复杂的条件下轮询softirq_vec[]数组,执行所有挂起的软中断处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
asmlinkage void __do_softirq(void)
{
    struct softirq_action *h;
    __u32 pending;
    unsigned long end= jiffies + MAX_SOFTIRQ_TIME;
    int cpu;
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;
 
    /*
     * Mask out PF_MEMALLOC s current task context is borrowed for the
     * softirq. A softirq handled such as network RX might set PF_MEMALLOC
     * again if the socket is related to swap
     */
    current->flags &= ~PF_MEMALLOC;
 
    pending = local_softirq_pending();
    account_irq_enter_time(current);
 
    __local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET);
    lockdep_softirq_enter();
 
    cpu = smp_processor_id();
restart:
    /* Reset the pending bitmask before enabling irqs */
    set_softirq_pending(0);
 
    local_irq_enable();
 
    h = softirq_vec;
 
    do{
        if(pending & 1) {
            unsigned int vec_nr = h - softirq_vec;
            int prev_count = preempt_count();
 
            kstat_incr_softirqs_this_cpu(vec_nr);
 
            trace_softirq_entry(vec_nr);
            h->action(h);
            trace_softirq_exit(vec_nr);
            if(unlikely(prev_count != preempt_count())) {
                printk(KERN_ERR"huh, entered softirq %u %s %p"
                       "with preempt_count %08x,"
                       " exited with %08x?\n", vec_nr,
                       softirq_to_name[vec_nr], h->action,
                       prev_count, preempt_count());
                preempt_count() = prev_count;
            }
 
            rcu_bh_qs(cpu);
        }
        h++;
        pending >>= 1;
    }while(pending);
 
    local_irq_disable();
 
    pending = local_softirq_pending();
    if(pending) {
        if(time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            gotorestart;
 
        wakeup_softirqd();
    }
 
    lockdep_softirq_exit();
 
    account_irq_exit_time(current);
    __local_bh_enable(SOFTIRQ_OFFSET);
    tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

    去掉trace部分以及一些状态和统计数据(lockdep_softirq_enter是更新统计数据)的更新,__do_softirq()函数通过判断percpu的pending标志来执行软中断数组上所有pending的action,但一下几点需要注意

  • 在执行do{}while()循环前,local_irq_enable(),执行完毕后local_irq_disable()。先enable,后disable,是因为do_softirq()中先local_irq_save(),后local_irq_restore(),详细原因将在“五、同步”中讨论。
  • 在进入__do_softirq()函数后,要使用__local_bh_disable(__RET_IP_,SOFTIRQ_OFFSET)来关闭下班部,所有事情做完后,需要通过__local_bh_enable(SOFTIRQ_OFFSET)来激活下班部(“下班部”这个次其实已经过时了,就是激活所有“可延迟执行”机制),这是因为下班部机制没必要在同一个CPU上同时执行两遍。

四、何时调用软中断

    Linux内核在以下几个位置会查看local cpu的软中断掩码位,执行所有pending的软中断处理函数,:

  • do_IRQ()执行完硬中断处理,irq_exit()函数调用invoke_softirq()函数,再调用do_softirq()函数。
  • local_bh_enable()函数执行时,通过__local_bh_enable()函数调用了do_softirq()函数。
  • smp_apic_timer_interrupt()函数执行完毕时,调用了irq_exit()函数。
  • wakeup_softirqd()唤醒ksoftirqd/n时。
  • 其他情况。

    do_IRQ()为硬中断处理函数,在do_IRQ()执行完毕desc->handle_irq()后,将通过irq_exit()接口转入软中断处理。这也是硬中断和软中断衔接的位置。(记得desc->action->thread_fn,在handle_irq_event()里面有处理ONESHOT的情况,看来与软中断无关了,这个地方还得再研究一下)。

    五、同步

    软中断数组虽然不是PER_CPU的,但是pending位图却是PER_CPU的。每个CPU决定自己要将哪个softirq设置为pending,要将执行哪些softirq(pending状态的)。两个CPU执行同时同一个软中断也是没有问题的。有几个位置需要分析一下,它们分别是:

  • in_interrupt()函数判断是否处于中断上下文;
  • __do_softirq()函数执行前的local_irq_disable(),__do_softirq()函数后的local_irq_enable();
  • __do_softirq()函数执行过程中,需要先__local_bh_disable(),执行完毕后__local_bh_enable();
  • __do_softirq()函数中,在具体的循环执行软中断处理函数的过程中,先local_irq_enable()开中断,然后local_irq_disable()关中断;

local_irq_disable

  • 紫色,主要工作函数
  • 粉色,current->preempt_count的改变点
  • 青色,禁止、开启本地中断点
  • 黄色,硬中断和软中断的主要工作
  • 灰色,存取本定pending位图的位置

    注意,这里所有的同步操作,都是local的(pending位图,local_irq,local_bh)软中断不管其他CPU上是否正在执行软中断,因此不操心其他CPU发生的事情,考虑嵌套的情况:如果在执行do_softirq()函数的某一步,被do_IRQ()函数打断,do_IRQ()函数在结束时又会调用do_softirq()函数,如果再被打断,那么将会形成反复嵌套:do_IRQ():1->do_softirq():1->do_IRQ():2->do_softirq():2->do_softirq():2 return->do_IRQ():2 return->do softirq():1 return->do_IRQ():1return。先假设不用禁止硬中断,因为__do_softirq()函数通过pending位图来判断是否执行该action,do_IRQ():2打断do_softirq():1的点的不同可能出现以下几种情况:

  • 在__local_bh_disable()前打断,do_softirq():1等do_IRQ():2返回后继续执行,但次是pending位图已经清0,前取出的pending实际上已经发生了变化,没必要再执行一遍软中断,但还是执行了(软中断可重入),降低效率。
  • 在__local_bh_disable()后打断,do_softirq():1等do_IRQ():2返回后继续执行,由于do_softirq():1已经通过__local_bh_disable()禁止了“可延迟执行”机制,那么do_softirq():2就直接从in_interrupt()处返回了,do_softirq():2可以正常的执行所有pending的action,但是时间延后了(这也算正常,因为硬中断有先级高于软中断)。

    为了避免第一种情况,就要保护PER_CPU的pending位图与实际执行的action是同步的,要做到这点其实不难,因为在同一个CPU上代码串行执行,在这段加个全局标志就可以,将pending位图和main work视为一体,前置标志,后清标志。这个标志在这就是SOFTIRQ_OFFSET,就像下面这样:

local_irq_disable_myself

    但实际上Linux没有这么做,而是在__do_softirq()前后分别屏蔽中断和开启中断,并在__do_softirq()的main work前后作反向操作,这样做的唯一解释就是硬中断中除了do_softirq()本身的操作外,还有破坏非原子操作local_softirq_pending()和set_softirq_pending(0)两步的操作,那就需要查一下都有在什么位置设置了pengding位图的操作了。时钟中断就是这样的,在timer_interrupt(irq_desc[0]->action->handler)中调update_process_timers,最终调用了rasie_softirq()。也就是说,硬中断有可能改变softirq的pending位图(其实本来就这么设计的,是否启动软中断,是硬中断处理函数说的算):

irq0

 

    按照ULK上的解释,如果in_interrupt()返回1,则说明当前进程要么在中断上下文中调用了do_softirq()函数,要么当前禁止软中断。

    另外,关于pending位图,经过查找,发现除了raise_softirq()函数会将相应的软中断置位以外,没有发现别的点。(调用or_softirq_pending宏)。

六、一些细节

1、in_interrupt()函数的实现

    软中断(raise_softirq()函数和do_softirq()函数)通过in_interrupt()函数来判断是否处于中断上下文中(include/linux/preempt_mask.h),如果不处于中断上下文中,才执行软中断轮询操作。in_interrupt()函数实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
* - bit 27 is the PREEMPT_ACTIVE flag
 *
 * PREEMPT_MASK: 0x000000ff
 * SOFTIRQ_MASK: 0x0000ff00
 * HARDIRQ_MASK: 0x03ff0000
 *     NMI_MASK: 0x04000000
 */
 
#define in_interrupt()      (irq_count())
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
                 | NMI_MASK))
#define preempt_count() (current_thread_info()->preempt_count)

    而current_thread_info()->preempt_count是进程用于描述被抢占的属性,它包含3个计数器,分别用于统计硬中断、软中断、以及抢占的次数。在下列点被增加:

  • do_IRQ()函数调用irq_enter()函数,再调用__irq_enter()函数,将改变量增加HARDIRQ_OFFSET(0x10000)
  • __local_bh_disable()函数被执行时,也即“可延迟函数”被禁止时,将该变量增加SOFTIRQ_DISABLE_OFFSET(0x200)
  • preempt_disable()函数被执行时,也即内核禁止抢占时,该变量将被增加1。

    在下列点被减少(是增加点的逆向操作):

  • do_IRQ()函数调用irq_exit()函数,再调用sub_preempt_count()函数,将改变量减少HARDIRQ_OFFSET(0x10000)。
  • __local_bh_enable()函数被执行时,也即“可延迟函数”被激活时,将该变量减少SOFTIRQ_DISABLE_OFFSET(0x200)。
  • preempt_enable()函数被执行时,也即内核激活抢占时,该变量将被减少1。

    in_interrupt()函数只看irq_count()也即软中断、硬中断以及NMI中断是否正在处理。也是当前进程是否正处于“中断上下文”当中,一旦处于中断上下文当中,说名current进程要么还有硬中断没执行完,要么还有软中断没执行完,那么就没有必要在执行softirq了。当然,改变current_thread_info()->preempt_count计数器的点可能不止这些,这里就不找那么复杂了。

2、pending位图操作

     pending位图是PER_CPU的,每个CPU都有一个对应的软中断挂起位图,只要相应的位被置位,则说明该softirq可以被执行。pending位图是irq_stat结构体变量的一个成员(arch/x86/include/asm/hardirq.h):

1
2
3
4
5
6
typedefstruct {
    unsignedint__softirq_pending;
    ...
} ____cacheline_aligned irq_cpustat_t;
 
DECLARE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);

    对pending位图的基本操作有(arch/x86/include/asm/hardirq.h):

  • local_softirq_pending(),取本地pending值,__do_softirq()函数时会调用。
  • set_softirq_pending(),设置本地pending值,__do_softirq()函数时会调用。
  • or_softirq_pending(),设置本地pending的某一位,raise_softirq()函数会调用。
1
2
3
4
5
#define inc_irq_stat(member)    this_cpu_inc(irq_stat.member)
 
#define set_softirq_pending(x)  \
        this_cpu_write(irq_stat.__softirq_pending, (x))
#define or_softirq_pending(x)   this_cpu_or(irq_stat.__softirq_pending, (x))

3、开关中断操作

    开关中断通过以下两组函数进行:

  • local_irq_enable()和local_irq_disable(),开启和关闭本地CPU上的中断处理
  • local_irq_save()和local_irq_restore(),开启和关闭本地CPU上的中断处理,并保存和恢复EFLAGS寄存器的IF位。

​    他们的实现如下(省略掉嵌套调用部分,arch/x86/include/asm/irqflags.h),也就是几个汇编指令:cli(clear interrupt flag)禁止IRQ,sti(set interrupt flag)启动IRQ,关于c内嵌汇编,详见这篇帖子”__asm__ __volatile__内嵌汇编用法概述“:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
staticinline unsigned long native_save_fl(void)
{
    unsigned long flags;
 
    /*
     * "=rm" is safe here, because "pop" adjusts the stack before
     * it evaluates its effective address -- this is part of the
     * documented behavior of the "pop" instruction.
     */
    asm volatile("# __raw_save_flags\n\t"
             "pushf ; pop %0"
             :"=rm"(flags)
             :/* no input */
             :"memory");
 
    returnflags;
}
 
staticinline void native_restore_fl(unsigned long flags)
{
    asm volatile("push %0 ; popf"
             :/* no output */
             :"g"(flags)
             :"memory","cc");
}
 
staticinline void native_irq_disable(void)
{
    asm volatile("cli": : :"memory");
}
 
staticinline void native_irq_enable(void)
{
    asm volatile("sti": : :"memory");
}

4、开关下半部操作

    通过__local_bh_enable()函数和__local_bh_disable()函数来实现开关下半部操作,所谓的下半部机制就是“可延迟操作”机制的总称。__local_bh_enable()函数和__local_bh_disable()函数仅仅用作current_thread_info()->preempt_count的置位和清除,并不做其他工作(只列一个,kernel/softirq.c):

1
2
3
4
5
6
7
8
9
staticvoid __local_bh_enable(unsigned int cnt)
{
    WARN_ON_ONCE(in_irq());
    WARN_ON_ONCE(!irqs_disabled());
 
    if(softirq_count() == cnt)
        trace_softirqs_on(_RET_IP_);
    sub_preempt_count(cnt);
}

   他们分别被local_bh_enable()函数和local_bh_disable()函数调用,除了置位操作,还尝试执行do_softirq()函数,这里就不列了。

 5、内核栈切换

    如果内核栈是8k的,则软中断运行于进程的内核栈,如果内核栈是4k的情况,则软中断运行于PER_CPU的softirq_ctx栈上,这一点与硬中断很像。32位有这个操作,但64位没有,因为64位都是8K的内核栈。看下32位的do_softirq()函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
asmlinkagevoiddo_softirq(void)
{
    unsignedlongflags;
    structthread_info *curctx;
    unionirq_ctx *irqctx;
    u32 *isp;
 
    if(in_interrupt())
        return;
 
    local_irq_save(flags);
 
    if(local_softirq_pending()) {
        curctx = current_thread_info();
        irqctx = __this_cpu_read(softirq_ctx);
        irqctx->tinfo.task = curctx->task;
        irqctx->tinfo.previous_esp = current_stack_pointer;
 
        /* build the stack frame on the softirq stack */
        isp = (u32 *) ((char*)irqctx + sizeof(*irqctx));
 
        call_on_stack(__do_softirq, isp);
        /* 
         * Shouldn't happen, we returned above if in_interrupt():
         */
        WARN_ON_ONCE(softirq_count());
    }  
 
    local_irq_restore(flags);
}

    call_on_stack()函数用于切换内核栈:

1
2
3
4
5
6
7
8
9
10
staticvoid call_on_stack(void*func, void*stack)
{
    asmvolatile("xchgl %%ebx,%%esp \n"
             "call  *%%edi      \n"
             "movl  %%ebx,%%esp \n"
             :"=b"(stack)
             :"0"(stack),
               "D"(func)
             :"memory","cc","edx","ecx","eax");
}

七、总结

    Linux内核中,软中断是硬中断的一种辅助处理方式,类似于硬中断,软中断也有软中断数组维护,基本操作有注册、激活、屏蔽、执行等。软中断的注册是静态的,伴随内核编译完毕而完成。软中断在几个位置被执行,其中比较总要的一个位置就是do_IRQ()执行完毕时,irq_exit()函数退出时调用do_softirq(),这也是软中断和硬中断之间的联系。

    软中断通过PER_CPU的pending位图来维护它的可执行状态,当软中断被执行后,将清除其pending位图,在下次硬中断执行过程中,将决定是否将其pending位图置位。pengding位图的同步通过禁止本地中断来实现。

    如果进程的内核栈为8k,则软中断运行于进程的内核栈,如果内核栈为4k,则软中断运行于内核的单独的软中断处理栈softirq_ctx。

    软中断函数的设计必须是可重入的。

八、遗留问题

1、内核栈切换细节?

2、中断与抢占有什么区别?

3、ksfotirqd是内核进程?内核线程?

4、在SMP架构下,PIC如何将每一次硬件中断分配给每一个CPU的?

5、软中断的可重入意味着什么?

6、硬件驱动如何与软中断挂接?给一个例子

 

 

参考资料:

1、Understanding Linux Kernel

2、Professinal Linux Kernel Architecture

3、内核随记(二)——内核抢占与中断返回

4、浅析Linux的软中断的实现

Published in 中断 and tagged softirq, 软中断 on 2013年11月20日 by rock3

irq_desc操作

     irq_desc[]数组是linux内核中用于维护IRQ资源的管理单元,它存储了某IRQ号对应的哪些处理函数,属于哪个PIC管理、来自哪个设备、IRQ自身的属性、资源等,是内核中断子系统的一个核心数组,习惯上称其为“irq数组”(个人爱好,下标就irq号)。本篇博客着重学习irq_desc[]数组的一些操作的过程和方法,如初始化、中断处理、中断号申请、中断线程等,而对于辅助性的8259A和APIC等设备的初始化过程,不详细讨论,对于某些图片或代码,也将其省略掉了。

    本文中出现的irq_desc->和desc->均表示具体的irq数组变量,称其中的一个个体为irq_desc[]数组元素,描述个体时也直接时用字符串desc。为了区别PIC的handle和driver的handle,将前者称为中断处理函数(对应desc->handle_irq,实际上对应handle_xxx_irq()),而将后者称为中断处理操作(对应desc->action)。本文中将以irq_descp[]数组为操作对象的层称为irq层。本文使用的内核代码版本为3.10.9。

    一篇好的博客应该是尽量多的说明,配少量的核心代码,这里偷懒了,很多部分实际是代码分析的过程,也没有省略掉。本篇博客耗时48小时。

一、irq_desc结构和irq_desc[]数组

    irq_desc[]数组,在kernel/irq/irqdesc.c中声明,用于内核管理中断请求,例如中断请求来自哪个设备,使用什么函数处理,同步资源等:

1
2
3
4
5
6
7
structirq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
    [0 ... NR_IRQS-1] = {
        .handle_irq = handle_bad_irq,
        .depth      = 1,
        .lock       = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
    }  
};

    整体上,关于irq_desc结构体,如下图所示:irq_desc

 

    struct irq_desc结构体(以前的版本结构体的名字是irq_desc_t)定义如下所示(简化过,include/linux/irqdesc.h)。大部分成员都是辅助性的,关键的成员是irq_data、handle_irqs、action、depth、lock、istat,所谓irq_desc[]数组的初始化,看其主要成员的初始化的过程,在这里做简单的说明:

  • action指针指向具体的设备驱动提供的中断处理操作,就是所为的ISR,action本身是一个单向链表结构体,由next指针指向下一个操作,因此action实际上是一个操作链,可以用于共享IRQ线的情况。
  • handle_irq是irq_desc结构中与PIC相关的中断处理函数的接口,通常称作”hard irq handler“。此函数对应了PIC中的handle_xxx_irq()系列函数(xxx代表触发方式),do_IRQ()就会调用该函数,此函数最终会执行desc->action。
  • irq_data用于描述PIC方法使用的数据,irq_data下面有两个比较重要的结构:chip和state_use_accessors,前者表示此irq_desc[]元素时用的PIC芯片类型,其中包含对该芯片的基本操作方法的指针;后者表示该chip的状态和属性,其中有些用于判断irq_desc本身应该所处的状态。
  • lock用于SMP下不同core下的同步。
  • depth表示中断嵌套深度,也即一个中断打断了几个其他中断。
  • istate表示该desc目前的状态,将在“六、istate状态”中描述。
1
2
3
4
5
6
7
8
9
10
11
12
13
structirq_desc {
    structirq_data     irq_data;
    irq_flow_handler_t  handle_irq;
    ...
    structirqaction    *action;    /* IRQ action list */
    unsignedint       status_use_accessors;
    unsignedint       core_internal_state__do_not_mess_with_it;
    unsignedint       depth;      /* nested irq disables */
    raw_spinlock_t      lock;
    ...
    structmodule       *owner;
    constchar      *name;
} ____cacheline_internodealigned_in_smp;

   这里还是看一下irqaction结构体,action的handler是具体的中断服务程序,next指针用于指向同一个链上的后一个的irqaction,thread_fn用于描述软中断处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef irqreturn_t (*irq_handler_t)(int, void *);
 
struct irqaction {
    irq_handler_t       handler;
    void            *dev_id;
    void __percpu       *percpu_dev_id;
    struct irqaction    *next;
    irq_handler_t       thread_fn;
    struct task_struct  *thread;
    unsigned int        irq;
    unsigned int        flags;
    unsigned long       thread_flags;
    unsigned long       thread_mask;
    constchar      *name;
    struct proc_dir_entry   *dir;
} ____cacheline_internodealigned_in_smp;

    这意味着所有的驱动在写中断处理函数时,必须以irqreturn_t为类型:

1
2
3
4
5
6
7
8
// intel e1000
staticirqreturn_t e1000_intr(int irq, void *data);
// acpi
staticirqreturn_t acpi_irq(int irq, void *dev_id)
// hd
staticirqreturn_t hd_interrupt(int irq, void *dev_id)
// ac97
staticirqreturn_t atmel_ac97c_interrupt(int irq, void *dev)

    在这里,很容易产生一个问题,就是驱动程序处理的数据在哪?总要有些数据要处理,是从void参数吗?那么这个数据怎么获取的?handle_irq_event_percpu()函数里有具体的action的调用方式:

1
res = action->handler(irq, action->dev_id);

    那么,void *参数来自action->dev_id,而dev_id是驱动程序注册时,调用request_irq()函数传递给内核的。而这个dev_id通常指向一个device设备,驱动程序就通过该device设备将需要的数据接收上来,并进行处理。

二、irq_desc[]的初始化——8259A

   irq_desc[]数组是内核维护中断请求资源的核心数组,它必须在合适的时机予以初始化。内核起动后,有步骤的初始化内核各个子系统,init_IRQ()函数主要负责完成内核中断子系统的主要初始化。irq_desc[]数组伴随着init_IRQ()函数的执行而完成其一部分的初始化。
   init_IRQ()函数的调用路径为main()->...->start_kernel()->init_IRQ()->native_init_IRQ()。init_IRQ()函数与irq_desc[]数组初始化或者IDT、interrupt[]数组的设置有关的函数或过程,关于init_IRQ的内部调用关系,如下图所示:

init_IRQ

    下面是具体的代码分析过程:

    从init_IRQ()函数开始分析,init_IRQ在arch/x86/kernel/irqinit.c中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void__init init_IRQ(void)
{
    inti;
 
    /* 
     * We probably need a better place for this, but it works for
     * now ...
     */
    x86_add_irq_domains();
 
    /* 
     * On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15.
     * If these IRQ's are handled by legacy interrupt-controllers like PIC,
     * then this configuration will likely be static after the boot. If
     * these IRQ's are handled by more mordern controllers like IO-APIC,
     * then this vector space can be freed and re-used dynamically as the
     * irq's migrate etc.
     */
    for(i = 0; i < legacy_pic->nr_legacy_irqs; i++)
        per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i;
 
    x86_init.irqs.intr_init();
}

    x86_add_irq_domains()直接略过。这里的注释还时很有用的,这里说开始时使用8259A注册这些中断向量号,如果系统使用IO APIC,将覆盖这些中断向量号,并且能够动态的重新使用。vector_irq为在arch/x86/include/asm/hw_irq.h中定义的per_cpu整形数组,长度为256,用于描述每个CPU的中断向量号,即vector_irq[](vector_irq[]元素初始化时被赋值为-1)中存储着系统可以使用的中断向量号。这里需要注意,vector_irq[]数组时PER_CPU的。

    legacy_pic字面意思为“遗留的PIC”,就是指8259A,legacy_pic定义在arch/x86/kernel/i8259.c,其中NR_IRQS_LEGACY为16:

1
2
3
4
5
6
7
8
9
10
11
12
13
structlegacy_pic default_legacy_pic = {
    .nr_legacy_irqs = NR_IRQS_LEGACY,
    .chip  = &i8259A_chip,
    .mask = mask_8259A_irq,
    .unmask = unmask_8259A_irq,
    .mask_all = mask_8259A,
    .restore_mask = unmask_8259A,
    .init = init_8259A,
    .irq_pending = i8259A_irq_pending,
    .make_irq = make_8259A_irq,
};
 
structlegacy_pic *legacy_pic = &default_legacy_pic;

    a) native_inti_IRQ()

    init_IRQ()将vector_irq[]逐个赋值(就赋值了16个,从0x30到0x39)。x86_init为x86架构初始化时的一个全局变量,记录了各个子系统(irq,paging,timer,iommu,pci等)初始化使用的具体函数。而实际的x86_init.irqs.intr_init指针指向native_init_IRQ()函数(arch/x86/kernel/irqinit.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void__init native_init_IRQ(void)
{
    inti;
 
    /* Execute any quirks before the call gates are initialised: */
    x86_init.irqs.pre_vector_init();
 
    apic_intr_init();
 
    /* 
     * Cover the whole vector space, no vector can escape
     * us. (some of these will be overridden and become
     * 'special' SMP interrupts)
     */
    i = FIRST_EXTERNAL_VECTOR;
    for_each_clear_bit_from(i, used_vectors, NR_VECTORS) {
        /* IA32_SYSCALL_VECTOR could be used in trap_init already. */
        set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
    }  
 
    if(!acpi_ioapic && !of_ioapic)
        setup_irq(2, &irq2);
 
#ifdef CONFIG_X86_32
    irq_ctx_init(smp_processor_id());
#endif
}
   x86_init.irqs.pre_vector_init指针指向init_ISA_irqs()函数,主要完成8259A/Local APIC的初始化,apic_intr_init()函数主要完成apic相关的中断的初始化。接着,native_init_IRQ()函数将调用set_intr_gate()函数设置中断门,将interrupt[]数组设置的地址设置到相应的中断门。注意,这里只是对没有used_vectors进行set_intr_gate()的赋值,并不是从FIRST_EXTERNAL_VECTOR到NR_VECTORS全部赋值,因为有些特殊情况会预留(关于used_vectors和vector_irq的关系,详见“七、中断向量、锁和CPU”)。余下的两个接口处理了一些特殊情况,这里不展开了。

    实际上init_IRQ()主要调用了native_init_IRQ(),除了使用set_intr_gate()来初始化Interrupt describptor外,后者主要干了两件事:init_ISA_irqs()和apic_intr_init()。先从简单的看起,apic_intr_init()函数实际上是一系列的set_intr_gate,但不通过interrupt[]数组,也不通过irq_desc[](这就是native_init_IRQ()函数中所为的“特殊情况”,属于used_vectors的范围):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
staticvoid __init apic_intr_init(void)
{
    smp_intr_init();
 
#ifdef CONFIG_X86_THERMAL_VECTOR
    alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt);
#endif
    ...
#ifdef CONFIG_HAVE_KVM
    /* IPI for KVM to deliver posted interrupt */
    alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi);
#endif
    ...
}

    而smp_intr_init()函数如下执行apic_intr_intr()函数类似的操作,也通过set_intr_gate()函数设置了一些中断门。

    这些中断门没有通过interrupt数组,也没有irq_desc数组,而是直接使用set_intr_gate()接口将其IDT中的中断门描述符初始化。而这些中断在/proc/interrupt中显示比较特殊,并不以中断向量号的形式显示,而是以名字的形式,比如NMI,本身也不连接任何的PIC(截取一部分):

1
2
3
4
5
6
7
8
9
10
[rock3@e4310 linux-stable]$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3      
...
 44:         66         80         77         72   PCI-MSI-edge      snd_hda_intel
 45:   14948296          0          0          0   PCI-MSI-edge      iwlwifi
NMI:       1539      19912      17314      17232   Non-maskable interrupts
LOC:   45133746   42836772   33584448   33666542   Local timer interrupts
SPU:          0          0          0          0   Spurious interrupts
PMI:       1539      19912      17314      17232   Performance monitoring interrupts
IWI:     641572     409182     330064     302186   IRQ work interrupts

    然后看比较复杂的init_ISA_irqs()函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void__init init_ISA_irqs(void)
{
    structirq_chip *chip = legacy_pic->chip;
    constchar *name = chip->name;
    inti;
 
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
    init_bsp_APIC();
#endif
    legacy_pic->init(0);
 
    for(i = 0; i < legacy_pic->nr_legacy_irqs; i++)
        irq_set_chip_and_handler_name(i, chip, handle_level_irq, name);
}

    legacy_pic->init指针指向init_8259A()函数,因此init_ISA_irqs执行了init_8259A(0)。irq_set_chip_and_handler_name()函数用于设置irq_desc[]数组的handle_irq、name、chip等成员。因此init_ISA_irqs()函数做了三件事:init_bsp_APIC()、init_8259A()、irq_set_chip_and_handler_name()。此时legacy_pic->nr_legacy_irqs为16。

    init_bsp_APIC()为对Local APIC的某种初始化操作,与irq_desc[]数组初始化无关,不讨论了。

   init_8259A(0)为对8259A的某种初始化操作,与Irq_desc[]数组的初始化无关,不讨论了。

  irq_set_chip_and_handler_name()函数如下(kernel/irq/chip.c):

1
2
3
4
5
6
7
void
irq_set_chip_and_handler_name(unsignedintirq, structirq_chip *chip,
                  irq_flow_handler_t handle, constchar *name)
{
    irq_set_chip(irq, chip);
    __irq_set_handler(irq, handle, 0, name);
}

    irq_set_chip()将irq_descp[]数组的*action的chip成员,主要是__irq_set_handler()函数(kernel/irq/chip.c),看下__irq_set_handler()函数都设置了irq_desc[]数组的什么成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void
__irq_set_handler(unsignedintirq, irq_flow_handler_t handle, intis_chained,
          constchar *name)
{
    unsignedlongflags;
    structirq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
 
    if(!desc)
        return;
 
    if(!handle) {
        handle = handle_bad_irq;
    }else{
        if(WARN_ON(desc->irq_data.chip == &no_irq_chip))
            gotoout;
    }
 
    /* Uninstall? */
    if(handle == handle_bad_irq) {
        if(desc->irq_data.chip != &no_irq_chip)
            mask_ack_irq(desc);
        irq_state_set_disabled(desc);
        desc->depth = 1;
    }
    desc->handle_irq = handle;
    desc->name = name;
 
    if(handle != handle_bad_irq && is_chained) {
        irq_settings_set_noprobe(desc);
        irq_settings_set_norequest(desc);
        irq_settings_set_nothread(desc);
        irq_startup(desc,true);
    }
out:
    irq_put_desc_busunlock(desc, flags);
}

    主要就设置了两个成员:handle_irq全部设置为handle_level_irq,name设置为“XT-PIC”(8259A)。而irq_desc[]数组中的handle_irq成员在do_IRQ()中被调用来执行具体的ISA。这个位置使用了buslock,也即desc->irq_data.chip->irq_bus_lock,而不是desc->lock。buslock用于中断控制器的操作,desc->lock用于IRQ中断处理函数的操作。

Linux Kernel中断机制3——硬件支撑

没有评论

    中断(Interrupt)包括中断和异常两种类型,异常通常由CPU上执行的指令直接触发,而中断是由外设发出的电信号触发的,但是那么是否所有的外设都直接接在CPU的中断PIN脚上触发中断?CPU有多少负责中断额PIN脚?CPU如何区别可屏蔽中断和非可屏蔽中断?CPU如何区别Faults、Traps、Aborts?本篇文章主要来搞懂这些问题。

一、中断控制器

    首先,CPU肯定不会为允许所有的外设都直接接在其上(否则要总线干什么),即使是通知发生中断这一种功能。应该有专门的中间设备/元器件负责这个工作,这个中间设备就是中断控制器。我刚刚学习中断的时候,甚至都不清楚中断控制器是个硬件还是软件,是否是CPU的一部分,那么这个地方先给出两个中断控制器的外观图片:

Intel-P8259Aich10

82093aa

    以上三张图片分别是Intel的IP8259A芯片、intel的ICH10南桥芯片以及Intel的S82093AA芯片,他们都有中断控制器的功能,其中8259A芯片比较老式,目前已经基本淘汰;Intel平台流行的做法是高级可编程将中断控制器(APIC)集成到南桥芯片(I/O Controller Hub)中,而较老的S82093AA芯片是最初的APIC的形态。

    可编程控制器(Programmable Interrupt Controller)是通常由两片 8259A 风格的外部芯片以“级联”的方式连接在一起。每个芯片可处理多达 8 个不同的 中断请求。因为从 PIC 的 INT 输出线连接到主 PIC 的 IRQ2 引脚,所以可用 IRQ 线的个数达到 15 个,如下图所示(示意图,并不代表一定这么连接):

8259A

    8259A除了起到向CPU引入多个外部中断源的作用外,还有一些基本功能,如中断分级、中断屏蔽,中断管理提(存储中断向量)等。

    随着SMP架构的发展,Intel在2000年左右的时候率先引入一种名为 高级可编程控制器的新组件(Advanced Programmable Interrupt Controller),来替代老式的 8259A 可编程中断控制器。APIC包括两部分:一是“本地 APIC(Local APIC)”,主要负责传递中断信号到指定的处理器,本地APIC通常集成到CPU内部,之所以成为Local,是相对CPU而言。另外一个重要的部分是 I/O APIC,主要是收集来自 I/O 设备的 Interrupt 信号且将中断时发送信号到本地 APIC。

    每个本地 APIC 都有 32 位的寄存器,一个内部时钟,一个本地定时设备以及为本地中断保留的两条额外的 IRQ 线 LINT0 和 LINT1。所有本地 APIC 都连接到 I/O APIC,形成一个多级 APIC 系统,如下图所示(示意图,并不代表一定这么连接):

ioapic

    当然,本地APIC除了接收来自IO APIC的中断信号,还可以接收其他来源的中断,比如接在CPU LINT0和LINT1管脚上的中断、IPI中断(核间中断)、APIC定时器产生中断、性能监视计数器中断、热传感器中断、APIC内部错误中断等。无论是PIC还是APIC,都通过某种方式与CPU相连(有的时候并不直接相连),这解决两个问题:

(1)CPU对多个外设的中断的管理

(2)多CPU的中断管理(APIC)

    当然,APIC自有一套硬件逻辑去实现这些功能,Intel也提供了相关的用户手册,这里就不深究了,有兴趣可以参考资料3——Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 Chapter 10 advanced programmable interrupt controller(APIC).

    以下为ULK中关于IRQ线以及中断控制器的工作逻辑的描述:

    每个能够发出中断请求的硬件设备控制器都有一条名为IRQ(Interrupt ReQuest)的输出线。所有现有的IRQ线都与PIC的硬件段路的输入引脚相连,PIC执行下列动作:

1、监视IRQ线,检查产生的信号。如果有两条或两条以上的IRQ线产生信号,就选择引脚编号较小的IRQ线。

2、如果一个引发信号出现在IRQ线上:

  • a.把接收到的引发信号转换成对应的向量。
  • b.把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读此向量。
  • c.把引发信号发送哦哦嗯到处理器的INTR引脚,产生一个中断。
  • d.等待,知道CPU通过把这个中断信号写进PIC的一个I/O端口来确认它;当这种情况发生时,清INTR线。

3、返回到第一步。

二、Intel x86 CPU中断管脚

    APIC系统主要作用是管理外设产生的异步中断,而对Intel x86架构下的各种异常,如故障、陷阱以及终止,系统是如何管理的那?这得看Intel手册:Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 的Chaper 2 system architecture overview 和Chapter 6 interrupt and exception以及Chapter 10 APIC。

    Intel x86架构提供LINT0和LINT1两个中断引脚,他们通常与Local APIC相连,用于接收Local APIC传递的中断信号,另外,当Local APIC被禁用的时候,LINT0和LINT1即被配置为INTR和NMI管脚,即外部IO中断管脚和非屏蔽中断管脚。INTR引脚负责向处理器通知发生了外部中断,处理器从系统总线上读出由外部中断控制器提供的中断向量(Interrupt Vector)号,例如从8259a提供。NMI中断使用2号中断向量。

    通常一个INTR引脚能够接收64个中断源,至于CPU内部怎么通过LINT0和LINT1进行中断的处理,就不管了,抑或还有其他的中断引脚什么的...

三、Intel x86架构对中断的支持——中断向量和IDT表

    Intel采用中断向量表(Interrupt Describtor Table)的方式去管理中断中断 。对于中断、陷阱、故障以及终止,有一些是Intel自己在设计CPU架构的时候就能够预知的,例如在执行除零时就会出现异常,在页式管理机制就可能出现缺页异常,有一些是Intel无法预估的,比如一个未来设计的设备产生的中断。对于前者,Intel称之为Intel 64 and IA-32 architectures for architecture-defined exceptions and interrupts,即Intel64和IA-32架构定义的架构相关的异常和中断,姑且简称为架构相关的中断和异常。

    为了区别这些中断和异常,Intel给出了中断向量(Interrupt Vector),即使用一个数字代表一个特殊的中断或者异常类型,Intel规定中断向量号的范围是0~255,0~31号为架构相关的异常和中断,32~255为User Defined Interrupt(保护模式):

中断向量号
助记符
描述
类型
0#DEDivide ErrorFault1#DBRESERVEDFault/ Trap2—NMI InterruptInterrupt3#BPBreakpoint(INT 3)Trap4#OFOverflow(INTO 0)Trap5#BRBOUND Range ExceededFault6#UDInvalid Opcode (Undefined Opcode)Fault7#NMDevice Not Available (No Math Coprocessor)Fault8#DFDouble FaultAbort9 Coprocessor Segment Overrun (reserved)Fault
显示第 1 至 10 项结果,共 23 项
上页下页

    中断向量是中断向量表(IDT)的索引,而中断向量表存在于内存的某个位置,由Intel的寄存器IDTR负责记录其基址(线性地址)和大小。IDT表中包含了操作系统中注册的外部IO中断的处理程序的入口地址,以及其他操作系统实现的架构相关的中断和异常的处理函数入口地址(这些地址又存放在所谓的gate destribtor中)。INTR和IDT的关系如下图所示:

IDTRandIDT

    上图中的Gate for Interrupt #n实际上分为三类:task gate,interrupt gate,trap gate,结构如下图所示:

gate_descriptor

    gate在Linux内核(3.11.1)中数据结构如下(32bits):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
structdesc_struct {
union{
struct{
unsignedinta;
unsignedintb;
};
struct{
u16 limit0;
u16 base0;
unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
};
};
} __attribute__((packed));

64bits的gate describtor:

1
2
3
4
5
6
7
8
9
/* 16byte gate */
structgate_struct64 {
u16 offset_low;
u16 segment;
unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1;
u16 offset_middle;
u32 offset_high;
u32 zero1;
} __attribute__((packed));

    IDT中的Gate可以以某种方式获取到具体的中断/异常处理函数的入口地址,从而可以执行该中断/异常处理函数:

interrupt_procedure_call

    GDT和LDT分别为全局描述符表(Global Destribtor Table)和本地描述附表(Local Destribtor Table),其中存储的内容为整个操作系统对各segment的描述,GDT、LDT、segment牵扯到内存管理的机制将在《Linux Kernel内存管理》系列中被具体学习,此处关心gate整个概念。

四、中断过程

    PIC和APIC是用于解决多设备中断管理问题的硬件设备,而APIC还可以解决SMP架构中断管理的问题。PIC通常指两片8259a级联,而APIC通常包括两个部分Local APIC和IO APIC,Local APIC通常集成在CPU内部(Intel),外部与IO APIC相连,内部与CPU管脚LINT0和LINT1相连。

    Intel 64和IA-32架构CPU对外提供中断向量和中断描述符表(IDT)的机制来处理中断和异常。中断按照下列逻辑触发并被执行(以键盘为例):

1、用户按下键盘按键;

2、电平信号变化通知中断控制器发生了一次中断;

3、中断控制器通知CPU此次中断的中断向量号;

4、CPU判定是否要处理此次中断,如果要处理,转5,否则退出;

5、从IDTR寄存器读取IDT的基址+中断向量号,找到对应的中断处理函数入口地址;

6、CPU按照某种软件策略执行该中断处理函数;

五、中断硬件处理细节(IA32/Intel 64)

对于第四节中断过程的步骤4、步骤5的一些细节,描述如下(摘自ULK):

当执行了一条指令后,cs和eip这对寄存器包含了下一条要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否存在已经发生了一个中断或者异常。如果发生了一个中断或一场,那么控制单元执行下列操作:

1、确定与中断或异常关联的向量i(i的范围为0~255)。

2、读由idtr寄存器指向的IDT表中的第i项(在下面的描述中,我们假定IDT表项中包含的是一个中断门或一个陷阱门)。

3、从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。

4、确信中断是授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较,如果CPL小于DPL,就产生一个“General Protection”异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“General Protection”异常。这最后一个检查可以避免用户程序访问特殊的陷阱门或中断门。

5、检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点:

  • a.读tr寄存器,以访问运行进程的TSS段。
  • b.用与新特权级相关的栈段和桟指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到。
  • c.在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。

6、如果发生的是“故障(Fault)”,用引起异常的指令地址装载cs和eip寄存器,从而使得这条指令能再次被执行。

7、在栈中保存eflag、cs及eip的内容。

8、如果硬件产生了一个错误码,则将它保存在栈中。

9、装载cs和eip寄存器,其值分别是IDT表中的第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。

控制单元所执行的最后一步就是跳转到中断或异常处理程序。还句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。

中断或异常被处理后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:

1、用保存的栈中的值状态cs、eip或eflag寄存器。如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码。

2、检查处理程序的CPL是否等于cs中的最低两位的值(这意味着被中断的进程与处理程序运行在同一特权级)。如果是,iret终止执行;否则,转入下一步。

3、从栈中装载ss和esp寄存器,因此,返回到与旧特权级相关的栈。

4、检查ds、es、fs以及gs寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且DPL值小于CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这个寄存器,怀有恶意的用户态程序就能利用他们来访问内核地址空间。

存疑

Q1:对门(gate)的概念一直比较含糊,computer sicence + gate,让人很容易直接反应到门电路——实现与、或、非等逻辑功能的电路,显然是个硬件。但是在Intel 64和IA-32手册中,门是这样被描述的:

The architecture also defines a set of special descriptors called gates (call gates, interrupt gates, trap gates, and task gates). These provide protected gateways to system procedures and handlers that may operate at a different privilege level than application programs and most procedures. For example, a CALL to a call gate can provide access to a procedure in a code segment that is at the same or a numerically lower privilege level ( more privileged) than the current code segment. To access a procedure through a call gate, the calling procedure1 supplies the selector for the call gate. The processor then performs an access rights check on the call gate, comparing the CPL with the privilege level of the call gate and the destination code segment pointed to by the call gate.

架构同时定义了一些列的被称作门的描述符(call门、interrupt门、trap门、task门)。这些描述符向系统程序和处理函数提供了保护性的网关,这可以让他们运行在与大多数应用程序不同的权限级别上。例如,通过call gate的一次调用,能够向代码段程序同时或周期性的提供比当前的代码段更低权限数值(更多的权限)。要通过call gate调用一个程序,调用者需要提供当前call gate的selector,接着,处理器就提供了再当前call gate上校验过的权限,通过比较当前call gate的CPL的权限级别和通过当前call gate指向目标代码段的权限。

那么,在Intel架构中,门是一个虚拟的概念,而不是一个硬件,是一个描述符,是Intel对外提供的用于某些特殊权限需要时的程序调用,系统程序向CPU提供门描述符(包含程序代码入口),而CPU根据权限判定是否允许执行。之所以分为这么多门,是因为权限的不同。不知道理解的对不对哈。

Q2:关于栈的切换的疑问。引发中断的程序的优先级不能高于中断处理程序的优先级,否则引发异常,这好理解,但是一旦优先级发生变化时,进行堆栈的切换过程,有些迷惑,主要是对栈的理解:我理解栈是一段连续的内存空间,方便从C程序转换为CPU指令的执行(主要是函数嵌套调用,过程调用),由于运行程序的权限不同,分为内核栈和用户栈,每个进程都有自己的栈,但中断处理函数并没有自己的栈,而是与进程的内核栈共享。中断处理函数在内核启动后,即被装载在属于内核段的某处内存中,要被压入具体的进程的内核栈?cs和eip被设置后,压栈是自动的?慢不慢?类似与call?普通函数调用libc影射的方式?混乱..

参考资料:

1、Linux 内核中断内幕

2、I/O Controller Hub——wiki

3、Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3

4、Intel® 82093AA I/O Advanced Programmable Interrupt Controller (I/O APIC) Datasheet

5、Intel x86架构之APIC

6、Understanding the Linux Kernel.

Published in 中断, 内核 and tagged 8259a, APIC, gate,门, IDT on 2013年9月30日 by rock3

Linux Kernel中断机制2——中断分类

没有评论

在《Linux Kernel中断机制1——中断概念》一节中描述了中断的基本概念,其中professional linux kernel architecture中提到中断可以有异常(exception)和错误(error)产生,本节研究中断的分类和硬件相关的部分。本节主要来源于Understanding the Linux Kernel 3rd.

一、同步中断和异步中断

同步中断和异步中断的概念经常被提到(understanding the linux kernel 3rd):

同步中断是当指令执行时由 CPU 控制单元产生,之所以称为同步,是因为只有在一条指令执行完毕后 CPU 才会发出中断,而不是发生在代码指令执行期间,比如系统调用。
异步中断是指由其他硬件设备依照 CPU 时钟信号随机产生,即意味着中断能够在指令之间发生,例如键盘中断。

Intel微处理器手册称同步中断为“异常(Exception)”,称异步中断为“中断(Interrupt)”,而平时所说的中断,两者都包含。

二、中断和异常

Intel手册中Interrupt(实际上就是我们平时说的异步中断)又可以分为可屏蔽中断(maskable interrupt)和不可屏蔽中断(unmaskable interrupt)。可屏蔽中断通常由外部IO设备发出,处理器根据该中断是否设置屏蔽位。中断的执行逻辑如下图所示:

interrupt

可屏蔽中断(Maskable interrupts)

所有的I/O设备产生的中断请求都是可屏蔽中断,可屏蔽中断有两种状态,masked和unmasked;只要一个中断还处于maked状态,控制单元将忽略它发出的中断请求。

不可屏蔽中断(Nonmaskable interrupts)

只有极少数的关键事件,像硬件错误等,会发出不可屏蔽中断,不可屏蔽总是被CPU识别

而异常又分为故障(fault)、陷阱(trap)和终止(abort)三类:

处理器侦测异常(Processor-detected exceptions),当CPU正在执行某条指令时侦测到异常状况时,产生的异常,这些异常又根据EIP的值分为3类,CPU的控制单元产生异常时EIP的值被保存在内核栈中

  • 故障(Faults)

能被纠正,一旦发生,程序允许在不影响连贯性的情况下重启.EIP中保存的值是引起故障的指令,因此当异常处理程序终止时,该指令可以被重新执行。fault

  • 陷阱(Traps)

在陷阱指令执行后立即报告;在内核把控制权返回给程序后,程序还可以不影响连贯性的继续执行。EIP中保存的值是陷阱指令后应该执行的指令的地址。只有在没有必要重复执行已经终止的指令时,才出发陷阱。陷阱的主要功能是调试,在这种情况下,中断信号的作用是通知debugger一条特殊的指令已经被执行(例如,到断点位置了)。一旦用户检查过debugger提供的数据,她很可能会继续执行程序的下一条指令。traps

  • 终止(Aborts)

严重错误发生了,导致控制单元无法将引起异常的指令地址存入EIP,Abort就是被用于报告这些错误的,例如硬件故障和系统表中非法或不一致的值。控制单元发送的中断信号是紧急信号,用来把控制权切换到相应的异常终止处理程序,该异常终止处理程序除了强制受影响的进程终止,别无他法abort

编程异常(Programmed exceptions),由编程者请求发出,通常使用int或者int0/int3指令;into(检查溢出)和bound(检查地址边界)在他们检测的结果不为真的时候也会引发编程异常。编程异常被控制单元当作陷阱处理,它们经常称作software interrupts。这类异常通常有两类用处:实现系统调用和通知debugger一个特殊的时间发生。

三、总结

中断,Interrupt在x86架构下,根据发生中断的时刻处理器上是否执行完毕当前指令,分为同步中断(synchronous interrupt)和异步中断(asynchronous interrupt),Intel通常将同步中断成为异常(Exception),而将异步中断称为中断(Interrupt)。同步中断多为CPU上正在执行的指令产生直接产生,而异步中断来自于I/O设备。如下图所示:interrupt

Intel的“异常”(Exception)又根据严重程度和EIP存储哪条指令分为三类:故障(Fault)、陷阱(Trap)和终止(Abort),而这三类异常的处理方法不同:

类别
原因
同步/异步
返回的行为
中断(Interrupt)来自I/O设备的电信号异步总是返回到下一条指令故障(Fault)潜在可恢复的错误同步返回到当前指令陷阱(Trap)有意的异常同步总是返回到下一条指令终止(Abort)不可恢复的错误同步不会返回
显示第 1 至 4 项结果,共 4 项
上页下页

四、遗留问题

Q1:异步中断分为可屏蔽中断和非可屏蔽中断,这种分发来源于Intel 64和IA32技术手册,也就是说Intel相关架构的CPU在处理外围硬件设备中断的时候,这样区分,并且有硬件上的办法做到了这一点(猜测有相关的CPU的PIN角负责相关的中断)。但是,这仅仅是Intel或者x86架构下对常规外设中断的处理方式,还是计算机系统常用的架构方法那,暂时不得而知(估计是)。

Q2:这里牵扯到一个问题:Only a few critical events (such as hardware failures) give rise to nonmaskable interrupts. 这里面give raise to是产生的意思,也就是说非屏蔽中断事由一些关键事件产生的(这里没说谁产生的这些关键事件,应该也是外围硬件产生的,比如总线错误),这就是说有个机制问题,谁决定某中断是非可屏蔽中断或者可屏蔽中断,是CPU和中断控制器以及外设的布线、内核设置或者外围设备自己?

0 0
原创粉丝点击