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

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


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

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

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

IRQ数据结构


IRQ 线的动态分配
驱动程序调用r equest _ irq () 这个 函数建立一个新的 irqaction 描述符 ,并 用参数值初始化它。然后调用 setup _i rq () 函数把这个描述符插入到合适的 IRQ 链表.如果 setup _ir q () 返回一个出错码,设备 驱动程序中止操作,这意味着IRQ 线已由另一个设备所使用,而这个设备不允许中断共 享.当设备操作结束时,驱动程序调用 fre e _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 个元素.  s oftirq_action 数据结构包括两个 字段: 指向软中断函数的 个action 指针和指向软中断函数需要的通用数据结构的 data 指针。 有数组的前六个 素被有效地使用:

一个软中断的优先级是相应的softirq_action 元数组内的下标。

另外 个关键的字段是32 位的 pre empt_ count 字段.用它来跟踪内核抢占和内核控制 路径的嵌套,该字段存放在每个进程描述符的 thr ead _info 字 段中:

第一个计数器记录显式禁用本地CPU 内核抢占的次数,值等于0 表示允许内核抢占.第 个计数器表 可延迟函数被禁用的程度(值为0表 可延迟函数处于激活状态,也就是允许执行软中断,大于0表示禁止,保证了软中断执行的串行性)。第
个计数器表示在本地CPU 中断处理程序的嵌套数( irq _e nter () 宏递增它的值, irq_exit ()宏递减它的值)
给preempt_count字段起这个名字的理由是很充分的:当内核代码明确不允许发生抢占 (抢占计数器不等于0)或当内核正在中断上 文中运行时,必须禁用内核的抢占功能.
因此,为了确定是能够抢占当前进程.内核快速检查preempt_count字段中的相应值 是否等 于0

ir q_ s ta t 数组包含NR_CPUS 个元素,系统中的每个C PU 对应 个元素.每个 元素的类型为 ir q_ cpu st at _ t , 该类型包含几个计数器和内核记录CPU正在做什么的标

__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_v ec,所有软中断处理函数在不同cpu上可能同时执行,这就要求软中断处理函数必须是可重入的。

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

如果在这样的 个检查点( loc a l_softir q_pe nd ing ( 不为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 ()函 数或 task let_hi _schedul e()函数将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

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

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


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


为何中断处理函数不能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 描述符 thr ead_ union结构 中的内核栈紧邻,而根据内核编译时的选项不同、 thread_ union 结 构可能占 个页框或两个页框.如果threa d_union 结构的大小为8K B ,那么当前进程 的内核栈被用于所有类型的内核控制路径:异常 、中 断和 可延迟的 函数 相反,如果th read_unio n 结构的大小为4K B ,内核就使用 种类 型的内核栈:
  • 异常栈,用于处理异常(包括系统调用) .这个栈包含在每个进程的thread_union数据结构中,因此对系统中的每个进程,内核使用不同的异常栈。
  • 硬中断请求栈,用于处理中断.系统中的每个C P U 都有个硬中断请求栈,而且 每个栈占用 个单独的页框。
  • 软中断请求栈 用于处理延迟的函数(软中断或tasklet  ).系统中的每个CPU都有 个软中断请求栈.而且每个栈占用一个单独的页框.
所有的硬中请求存放在hardirq_stack 数组中,而所有的软中断请求存放在softirq_stack 组中,每个数组 素都是跨越 个单独页框的irq_ ct x 类型的联合体.

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

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



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


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值