linux 中断与异常---基本概念(二)

可编程中断控制器PIC
    +-------------+
    |         cpu          |
    +-------------+
           ↑ INTR
           |
                    ↓
+------------------------+
|            PIC         |
+------------------------+
         ↑   IRQ1            ↑ IRQn
     |            |
     ↓            
+---------+   +---------+
| device1 |   | devicen        
+---------+   +---------+


几个设备可以共享同一个IRQ线,1/0 中断处理程序必须足够灵活以给多个设备同时提供服务。
中断处理程序的灵活性是以两种不同的方式实现的:
IRQ 共享
中断处理程序执行多个中断服务倒程{interrupt service routine,ISR). 每个ISR是一个与单独设备(共享IRQ 线)相关的函数.因为不可能预先知道哪个特定的设备产生IRQ 因此,每个ISR 都被执行,以验证它的设备是否需要关注,如果是,当设备产生中断时 就执行需要执行的所有操作.

IRQ 动态分配
一条IRQ 线在可能的最后时刻才与一个设备驱动程序相关联,例如,软盘设备的IRQ 线只有在用户访问软盘设备时才被分配。这样,同一个IRQ 向量也可以由这几个设备在不同时刻使用

建立异常向量:trap_init();
建立中断向量:init_IRQ();

IRQ数据结构
linux 中断与异常---基本概念(二)

IRQ 线的动态分配
驱动程序调用request_irq ()这个函数建立一个新的irqaction描述符,并用参数值初始化它。然后调用setup _irq ()函数把这个描述符插入到合适的IRQ 链表.如果setup _irq () 返回一个出错码,设备驱动程序中止操作,这意味着IRQ 线已由另一个设备所使用,而这个设备不允许中断共享.当设备操作结束时,驱动程序调用free_irq()数从IRQ 链表中删除这个描述符,并将放相应的内存区.
int request_irq(unsigned int irq,
        irqreturn_t (*handler)(int, void *, struct pt_regs *),
        unsigned long irqflags, const char * devname, void *dev_id)
其中第一个参数irq是由硬件连线决定的。

top half & bottom half
To have low interrupt latency -- to split interrupt routines into 
 a `top half', which receives the hardware interrupt and do minimum work and return (ISR)
 a `bottom half', which does the lengthy processing.
把可延迟中断从中断处理程序中抽出来有助于使内核保持较短的响应时间.这对于那些期望它们的中断能在几毫秒内得到处理的“急迫”应用来说是非常重要的。
 Top halves have following properties (requirements) 
 need to run as quickly as possible 
 run with some (or all) interrupt levels disabled 
 are often time-critical and they deal with HW 
 do not run in process context and cannot block 

 Bottom halves are to defer work later 
 “Later” is often simply “not now” 
 Often, bottom halves run immediately after interrupt returns 
 They run with all interrupts enabled

Multiple mechanisms are available for bottom halves:
 softirq: (available since 2.3)
 tasklet: (available since 2.3)
 work queues: (available since 2.5)

软中断(softirq)
示软中断的主要数据结构是softirq_vec数组,该数组包含类型为softirq_action的32 个元素. softirq_action 数据结构包括两个字段: 指向软中断函数的个action指针和指向软中断函数需要的通用数据结构的data 指针。有数组的前六个素被有效地使用:
linux 中断与异常---基本概念(二)
一个软中断的优先级是相应的softirq_action 元数组内的下标。

另外个关键的字段是32 位的preempt_count 字段.用它来跟踪内核抢占和内核控制路径的嵌套,该字段存放在每个进程描述符的thread_info 字段中:
linux 中断与异常---基本概念(二)
第一个计数器记录显式禁用本地CPU 内核抢占的次数,值等于0 表示允许内核抢占.第个计数器表可延迟函数被禁用的程度(值为0表可延迟函数处于**状态,也就是允许执行软中断,大于0表示禁止,保证了软中断执行的串行性)。第
个计数器表示在本地CPU中断处理程序的嵌套数(irq_enter () 宏递增它的值,irq_exit ()宏递减它的值)
给preempt_count字段起这个名字的理由是很充分的:当内核代码明确不允许发生抢占(抢占计数器不等于0)或当内核正在中断上文中运行时,必须禁用内核的抢占功能.
因此,为了确定是能够抢占当前进程.内核快速检查preempt_count字段中的相应值是否等于0

irq_stat 数组包含NR_CPUS个元素,系统中的每个CPU 对应个元素.每个元素的类型为irq_cpustat_t , 该类型包含几个计数器和内核记录CPU正在做什么的标
linux 中断与异常---基本概念(二)
__softirq_pending段用于设置设置本地CPU软中断位掩码

void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
    softirq_vec[nr].data = data;
    softirq_vec[nr].action = action;
}
void fastcall raise_softirq(unsigned int nr)
{
    unsigned long flags;

    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}
也就是说软中断**标志是每cpu的:__softirq_pending,而软中断处理函数是所有cpu共用的:softirq_vec,所有软中断处理函数在不同cpu上可能同时执行,这就要求软中断处理函数必须是可重入的。

处理软中断
应该周期性地(但又不能太频繁地)检查活动(挂起)的软中断,检查是在内核代码的几个点上进行的。这在下列几种情况下进行(注意.检查点的个数和位置随内核版本和所支持的硬件结构而变化):
1.当内核调用local_bh_enabl e ()函**本地CPU 的软中断时;
2.当do_IRQ ()完成了I/O中断的处理时或调用irq_exit() 宏时;
3.如果系统使用I/O APIC ,则当smp_apic_timer_int errupt () 函数处理完本地定时器中断时
4.在多处理器系统中,当CPU处理完被CALL_FUNCTION_VECTOR处理器间中断所触发的函数时;
5.当个特殊的ksoftirqd/n 内核线程被唤醒时;

如果在这样的个检查点( loca l_softirq_pending ( 不为0 )检测到挂起的软中断,内核就调用do_softirq ()来处理它们。由于正在执行一个软中断函数时可能出现新挂起的软中断,所以为了保证可延迟函数的低延迟性,__do_softirq()直运行到执行完所有挂起的软中断。但是,这种机制可能迫使__do_softirq ()运行很长段时间,因而大大延迟用户态进程的执行.因此,__do_softirq ()只做固定次数的循环,然后就返回.如果还有其余挂起的软中断,那么内核线程ksoftirqd 将会在预期的时间内处理它们.

ksoftirqd 内核线程
每个CPU 都有自己的ksoftirqd/n 内核线程,每个ksoftirqd/n 内核线程都运行ksoftirqd () 函数

tasklet
tasklet 建立在两个叫做HI_SOFTIRQ 和TASKLET_SOFTIRQ 的软中断之上。几个tasklet可以与同一个软中断相关联,每个tasklet 执行自己的函数。两个软中断之间没有真正的区别,只不过do_softirq ()先执行HI_SOFTIRQ 的tasklet ,后执行TASKLET_SOFTIRQ 的tasklet.

tasklet 和高优先级的tasklet 分别存放在tasklet_vec 和tasklet_hi_vec 数组中, 者都包含类型为tasklet_head 的NR_CPUS 个元素,每个元素都由一个指向tasklet 描述符链表的指针组成。tasklet 描述符是个tasklet_struct 类型的数据结构:
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec) = { NULL };
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec) = { NULL };

/* Tasklets */
struct tasklet_head
{
    struct tasklet_struct *list;
};

void __init softirq_init(void)
{
    open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}

static void tasklet_action(struct softirq_action *a)
{
    struct tasklet_struct *list;

    local_irq_disable();
    list = __get_cpu_var(tasklet_vec).list;
    __get_cpu_var(tasklet_vec).list = NULL;
    local_irq_enable();

    while (list) {
        struct tasklet_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 = __get_cpu_var(tasklet_vec).list;
        __get_cpu_var(tasklet_vec).list = t;
        __raise_softirq_irqoff(TASKLET_SOFTIRQ);
        local_irq_enable();
    }
}
static inline int tasklet_trylock(struct tasklet_struct *t)
{
    return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}

static inline void tasklet_unlock(struct tasklet_struct *t)
{
    smp_mb__before_clear_bit();
    clear_bit(TASKLET_STATE_RUN, &(t)->state);
}

static inline void tasklet_unlock_wait(struct tasklet_struct *t)
{
    while (test_bit(TASKLET_STATE_RUN, &(t)->state)) { barrier(); }
}
state的TASKLET_STATE_RUN状态保证同类型的tasklet在任一时刻只在一个cpu上运行,这就保证了tasklet的串行化执行,也就是说tasklet对可重入性不再有要求。

为了**tasklet ,你该根据自己tasklet 需要的优先级.调用tasklet_schedule ()函数或tasklet_hi_schedule()函数将tasklet_struct插入tasklet_vec和tasklet_hi_vec中。

工作队列
在Linux 2.6 中引入了工作队列,它与Linux 2.4 中的任务队列相似的构造,用来代替任务队列。它们允许内核函数(非常像可延迟函数)被**,而且稍后由一种叫做作者线程( worker thread ) 的特殊内核线程来执行
尽管可延迟函数和工作队列)非常相似,但是它们的区别还是很大的.主要区别在于:可延迟函数运行在中断上下文,而工作队列中的函数运行在进程上下文中。执行可阻塞函数的唯一方式是在进程上下文中运行. 因为,在中断上下文中不可能发生进程切换.

struct workqueue_struct {
    struct cpu_workqueue_struct cpu_wq[NR_CPUS];
    const char *name;
    struct list_head list;     /* Empty if single thread */
};
struct workqueue_struct表示一个工作队列,2.6.11系统中存在如下工作队列:
aio_wq = create_workqueue("aio");
commit_wq = create_workqueue("reiserfs");
kblockd_workqueue = create_workqueue("kblockd");
wq = create_workqueue("rpciod");
wanpipe_wq = create_workqueue("wanpipe_wq");
keventd_wq = create_workqueue("events");
每个工作队列可以用单线程工作者或者在每个cpu上运行一个线程工作者来实现,struct cpu_workqueue_struct表示每个cpu上的一个工作者线程:
struct cpu_workqueue_struct {
    spinlock_t lock;
    long remove_sequence;    /* Least-recently added (next to run) */
    long insert_sequence;    /* Next to add */
    struct list_head worklist;       //要做的工作链表
    wait_queue_head_t more_work;
    wait_queue_head_t work_done;
    struct workqueue_struct *wq;
    task_t *thread;
    int run_depth;        /* Detect run_workqueue() recursion depth */
}
int  queue_work(struct workqueue_struct *wq, struct work_struct *work)函数用于将工作交给工作队列,如果工作队列对应多个工作者线程,则将工作放到调用该函数对应cpu的工作者线程上,具体为插入cpu_workqueue_struct的worklist链表中。

struct work_struct {
    unsigned long pending;
    struct list_head entry;
    void (*func)(void *);
    void *data;
    void *wq_data;
    struct timer_list timer;
};
struct work_struct结构表示每个要做的work

每个工作者线程在worker_thread () 函数内部不断地执行循环操作,因而,线程在绝大多数时间里处于睡眠状态并等待某些工作被插入队列.工作线程一旦被唤醒就调用run_workqueue ( 函数,该函数从工作者线程的工作队列链表中删除所有work_struct描述符并执行相应的挂起函数.由工作队列函数可以阻塞.因此.可以让工作者线程
睡眠,甚至可以让它迁移到另一个CPU 上恢复执行。

有些时候,内核必须等待工作队列中的所有挂起函数执行完毕. flush_workqueue ()函数接收workqueue_struct描述符的地址,并且在工作队列中的所有挂起函数结束之前
使调用进程一直处于阻塞状态.但是该函数不会等待在调用flush_workqueue ()之后加入作队列的挂起函数 每个cpu_workqueue_struct描述符的remove_sequence字
段和insert_sequence 字段用于识别新增加的挂起函数


绝大多数情况下,为了运行个函数而创建整个作者线程开销太大了。因此,内核引入叫做events 的预定义工作队列,所有的内核开发者都可以随意使用它.预定义工作
队列只是个包括不同内核层函数和I/O 驱动程序的标准作队列.它的workqueue_struct 描述符存放在keventd_wq 全局变量指针中.
linux 中断与异常---基本概念(二)

为何中断处理函数不能block呢
ulk的原话是:
The price to pay for allowing nested kernel control paths is that an interrupt handler
must never block, that is, no process switch can take place until an interrupt handler
is running. In fact, all the data needed to resume a nested kernel control path is
stored in the Kernel Mode stack, which is tightly bound to the current process.
把不能block归结为允许中断嵌套,我觉得这不是真正的原因,真的的原因我觉得是后面这句话,因为栈的原因,每个进程的thread_info 描述符thread_union结构中的内核栈紧邻,而根据内核编译时的选项不同、thread_union 结构可能占个页框或两个页框.如果thread_union 结构的大小为8K B ,那么当前进程的内核栈被用于所有类型的内核控制路径:异常、中断和可延迟的函数 相反,如果thread_union 结构的大小为4K B ,内核就使用种类型的内核栈:
  • 异常栈,用于处理异常(包括系统调用) .这个栈包含在每个进程的thread_union数据结构中,因此对系统中的每个进程,内核使用不同的异常栈。
  • 硬中断请求栈,用于处理中断.系统中的每个C P U 都有个硬中断请求栈,而且每个栈占用个单独的页框。
  • 软中断请求栈 用于处理延迟的函数(软中断或tasklet ).系统中的每个CPU都有个软中断请求栈.而且每个栈占用一个单独的页框.
所有的硬中请求存放在hardirq_stack 数组中,而所有的软中断请求存放在softirq_stack组中,每个数组素都是跨越个单独页框的irq_ctx 类型的联合体.

假设使用不同类型的内核栈,例如使用单独的硬中断请求栈,在中断处理时block会发生什么?这时上下文保留在硬中断请求栈中,block后进行进程切换,esp,eip等寄存器的值保存在被中断的进程的thread_info中,切换到一个新进程后开始新进程的执行,如果这时候又发生了中断会怎么样?因为硬中断请求栈是cpu共用的,但是这时它对硬件请求栈中的内容并不知晓,所以会覆盖掉请求栈中的内容,这时悲剧就发生了,前一个被block切换出去的上下文被破坏了,所以在使用不同类型的内核栈时,block是绝对不允许的,根本原因在于栈时cpu共用的而不是每个进程分开的,因为异常使用了进程内核栈,所以异常处理函数是可以block的。

正是因为linux允许使用不同的内核栈,所以为了统一起见,在硬中断处理,软中断处理和tasklet中都不允许block



参考资料:
《understanding the linux kernel 3rd edition》