zoukankan      html  css  js  c++  java
  • 【内核】——中断和异常

    中断和异常

    • 定义:中断通常被定义为一个事件,该事件改变了CPU的执行顺序。
    • 分类:中断常分为同步中断和异步中断。在intel微处理器中,把同步中断也称为异常,异步中断称为中断
      • 同步中断(异常):当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令执行终止执行后CPU才会发出中断。异常是程序的错误产生的,或者是由内核必须处理的异常条件产生的。
      • 异步中断(中断):其他设备发出的中断,具有"随机性",也叫外部中断。由间隔定时器和IO设备产生。
    • 中断或异常处理执行的代码不是一个进程,他是一个内核控制路径。另外,中断处理程序比一个进程要“轻”,因为中断的上下文很少,建立或终止中断处理需要的时间很少。
    • 中断优先级分级(另一种分类情况下的分级):故障中断>访管中断/程序性中断>外中断>IO中断
    • 中断处理是由内核执行的最敏感任务之一,针对中断的响应包括这么几部分:关中断,保存断点,针对中断源(紧急就准备执行,不紧急就排队)。
      中断响应的时刻图

    IRQ和中断

    • 每个能发出中断请求的硬件设备控制器都有一条IRQ(interrupt ReQuest)的输出线。而所有IRQ线都和一个可编程中断控制器(PIC)相连。响应中断的具体流程如下:
      • PIC监视IRQ
      • IRQ发出信号给PIC,PIC把这个信号转换为一个向量,并存放在一个IO端口,从而允许CPU通过总线读取这个向量
      • 发送给了CPU,即产生一个中断
      • 等待直到CPU通过这个中断信号写进PIC的一个IO端口来确认他。
      • 继续监视
    • 显然上述的情况是针对单个CPU的简单情况,现在已经是多CPU核心了,为了充分发挥SMP体系结构的并行性,能够把中断传递给系统中的每个CPU至关重要。因此,Intel引入了IO高级可编程控制器(IO APIC)(IO Advanced Programmable interrupt Controller)。使用方式如下图:
    • 关于irq的相关代码位于./arch/i386/kernel/iqr.c中(这里的例子是以i386(intel32位)体系架构为例,现在的PC体系架构应该是在./arch/x86_64/kernel中),这里展示部分代码:

    中断描述表IDT

    • 中断描述表(interrupt description table IDT)是一个系统表(其他的系统表还有进程proc表,file表,inode表等等),每一个向量在表中有相应的中断或异常处理程序的入口地址。
    • IDT有三种类型的描述符。分别是任务门描述符,中断门描述符和陷阱门描述符。linux利用中断门处理中断,利用陷阱门处理异常
    • 中断和异常的硬件处理:从硬件的角度来看CPU如何处理中断和异常。可以参考:https://blog.csdn.net/windeal3203/article/details/44591609

    中断和异常处理程序的嵌套执行

    • 每个中断或异常都会引起一个内核控制路径,相应的内核控制路径的第一部分指令就是把那些寄存器的内容保存在内核堆栈的指令中,最后一部分指令就是恢复寄存器内容并让CPU返回到用户态的那些指令。而内核控制路径可以任意嵌套,和中断响应的时刻图类似。对中断进行处理的内核控制路径,其最后一部分指令并不总能使当前进程返回到用户态,如果嵌套深度大于1,这些指令将执行上次被打断的内核控制路径,此时CPU还在内核态。

    • /proc/interrupts文件会显示IRQ(interrupt request),从左至右依次是中断号,中断在各CPU发生的次数,中断设备名称,硬件中断号,中断处理函数:

    • /proc/iqr目录下面会为每个注册的irq创建一个以irq编号为名字的子目录,每个子目录下分别有以下条目:

      • smp_affinity irq和cpu之间的亲缘绑定关系,(default_smp_affinity=3,系统默认每个中断可以由两个cpu进行处理)
      • smp_affinity_hint 只读条目,用于用户空间做irq平衡只用;
      • spurious 可以获得该irq被处理和未被处理的次数的统计信息;
      • handler_name 驱动程序注册该irq时传入的处理程序的名字;

    异常处理

    • 处理异常中设置三个门结构,陷阱门,任务门和中断门初始化,其结构位于include/asm-x86_84desc.h定义,(这里的asm应该是汇编的意思吧,但是却是一个头文件):

    相应的实现则是在./arch/x84_64/kernel/traps.c中:

    • 异常处理结构有一共标准的结构,由以下三部分组成:
      • 在内核堆栈中保存大多数寄存器的内容(这部分由汇编实现),
      • 用高级的C函数处理异常
      • 通过ret_from_exception()函数从异常处理程序退出

    一个典型的异常处理程序被调用的步骤

    • 为异常程序保存寄存器的值:假设一个通用的异常处理的名字为handler_name,每一个异常处理程序都以下列的汇编指令开始:
      handler_name: pushl $0 /* only for some exceptions*/ pushl $do_handler_name jmp error_code
    • 当异常发生时,如果控制单元没有自动地把一个硬件出错代码插入到栈中,相应的汇编语言片段会包含一条pushl $0指令,在栈中垫一个空值,然后,把高级C函数的地址压栈,它的名字由异常处理程序和do_前缀构成。

    进入和离开异常处理程序

    • 执行异常处理程序的C函数名总是由do_前缀和处理程序组成。其中的大部分函数把硬件出错码和异常向量保存在当前进程的描述符中,然后向当前进程发送一个合适的信号。用代码描述如下:
      ··
      current->thread.error_code = error_code;
      current->thread.trap_no = vector;
      force_sig(sig_number, current);
      ··
    • 异常处理程序一终止,当前进程就关注这个信号,该信号要么由用户态进程自己处理,要么由内核态直接杀死。
    • 异常处理程序总是会检查异常是发生在用户态还是内核态,后一种情况下还要进行动态地址检查,因为出现在内核态的任何其他异常都是由于内核bug引起的。在这种情况下,异常处理程序会认为内核行为失常了,那么就要防止磁盘上数据崩溃,调用die()函数处理。

    中断处理

    • 中断处理不同于异常处理,中断处理依赖于中断类型,主要分为3类:
      • IO中断处理:包括IRQ线共享,动态分配,do_IRQ函数,IRQ在多处理器系统上的分发等等

      • 处理器间中断处理:处理器间中断允许一个CPU想系统中的其他CPU发送中断信号。处理器间的中断(IPI)不是通过IRQ线传输的,而是作为信号直接放在连接所有CPU本地APIC的总线上。

    软中断

    一个中断处理程序的几个中断服务例程之间是串行执行的,并且通常在一个中断的处理程序结束前,不应该再次出现这个中断。(这种情况下,系统性能瓶颈是CPU资源,需要增加更多的CPU来解决该问题)相反,可延迟中断可以在开中断的情况下执行。把可延迟中断从中断处理程序中抽出来有助于使内核保持较短的响应时间。

    linux2.6有两种非紧迫,可中断的内核函数:

    • 延迟函数(包括软中断和tasklet)
    • 通过工作队列执行的函数

    软中断和tasklet有密切关系,tasklet是在软中断的基础上实现。而出现在内核代码中的属于“软中断(softirq)”常常表示可延时函数的所有种类。而“中断上下文”表示内核当前正在执行一个中断处理程序或一个可延迟的函数。

    软中断 tasklet
    分配是静态的(编译时定义) 分配和初始化可以在运行时进行
    可以并发地运行在多个CPU上 总是被串行执行,不能在两个CPU上同时运行相同类型的tasklet
    可重入的 不必是可重入的

    可延迟函数可以执行四种操作:

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

    NIC中断处理过程

    当网卡控制器的FIFO收到的来自以太网的数据的时候(例如半满的时候,可以软件设定),可以将该事件通过irq signal送达Interrupt Controller。Interrupt Controller可以把中断分发给系统中的Processor A or B。

    NIC的中断处理过程大概包括:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>handle Data in the ram----------->unmask interrupt controller

    我们先假设Processor A处理了这个网卡中断事件,于是NIC的中断handler在Processor A上欢快的执行,这时候,Processor A的本地中断是disable的。NIC的中断handler在执行的过程中,网络数据仍然源源不断的到来,但是,如果NIC的中断handler不操作NIC的寄存器来ack这个中断的话,NIC是不会触发下一次中断的。还好,我们的NIC interrupt handler总是在最开始就会ack,因此,这不会导致性能问题。ack之后,NIC已经具体再次trigger中断的能力。当Processor A上的handler 在处理接收来自网络的数据的时候,NIC的FIFO很可能又收到新的数据,并trigger了中断,这时候,Interrupt controller还没有unmask,因此,即便还有Processor B(也就是说有处理器资源),中断控制器也无法把这个中断送达处理器系统。因此,只能眼睁睁的看着NIC FIFO填满数据,数据溢出,或者向对端发出拥塞信号,无论如何,整体的系统性能是受到严重的影响。

    注意:对于新的interrupt controller,可能没有mask和unmask操作,但是原理是一样的,只不过NIC的handler执行完毕要发生EOI而已。

    要解决上面的问题,最重要的是尽快的执行完中断handler,打开中断,unmask IRQ(或者发送EOI),方法就是把耗时的handle Data in the ram这个步骤踢出handler,让其在bottom half中执行。

    为什么会有softirq

    • linux内核把中断分为top half(handler 硬中断)和bottom half。bottom half提供了softirq、taskleth和workqueue机制

    • workqueue和softirq、tasklet有本质的区别:workqueue运行在process context,而softirq和tasklet运行在interrupt context。

    • 对于中断而言,是硬件通过触发信号,导致内核调用中断处理程序,进入内核空间,这个过程中硬件的一些变量和参数也要传递给内核,这些参数和被中断的进程的上下文就是中断上下文。对于用户进程而言,则是通过内核调用,进入内核空间,切换进程上下文的。

    • 因此,出现workqueue是不奇怪的,在有sleep需求的场景中,defering task必须延迟到kernel thread中执行,也就是说必须使用workqueue机制。softirq和tasklet是怎么回事呢?从本质上将,bottom half机制的设计有两方面的需求,一个是性能,一个是易用性。设计一个通用的bottom half机制来满足这两个需求非常的困难,因此,内核提供了softirq和tasklet两种机制。softirq更倾向于性能,而tasklet更倾向于易用性。

    • 为了性能,同一类型的softirq有可能在不同的CPU上并发执行,这给使用者带来了极大的痛苦,因为驱动工程师在撰写softirq的回调函数的时候要考虑重入,考虑并发,要引入同步机制。但是,为了性能,我们必须如此。

    • 而tasklet是串行执行的(注:不同的tasklet还是会并发的),不需要考虑重入的问题

    理解softirq

    • preempt_count:这个成员被用来判断当前进程是否可以被抢占。当内核代码明确不允许发生抢占的或内核正在中断上下文运行时,必须禁止内核的抢占功能。如果preempt_count不等于0(可能是代码调用preempt_disable显式的禁止了抢占,也可能是处于中断上下文等),说明当前不能进行抢占,如果preempt_count等于0,说明已经具备了抢占的条件(当然具体是否要抢占当前进程还是要看看thread info中的flag成员是否设定了_TIF_NEED_RESCHED这个标记,可能是当前的进程的时间片用完了,也可能是由于中断唤醒了优先级更高的进程)。 具体preempt_count的数据格式可以参考下图。


    • 对softirq count进行操作有两个场景:

      (1)也是在进入soft irq handler之前给 softirq count加一,退出soft irq handler之后给 softirq count减去一。由于soft irq handler在一个CPU上是不会并发的,总是串行执行,因此,这个场景下只需要一个bit就够了,也就是上图中的bit 8。通过该bit可以知道当前task是否在sofirq context。

      (2)由于内核同步的需求,进程上下文需要禁止softirq。这时候,kernel提供了local_bh_enable和local_bh_disable这样的接口函数。这部分的概念是和preempt disable/enable类似的,占用了bit9~15,最大可以支持127次嵌套。

      (3)本质上,关本地中断(irqs_disabled)是一种比关本地bottom half更强劲的锁,关本地中断实际上是禁止了top half和bottom half抢占当前进程上下文的运行。也许你会说:这也没有什么,就是有些浪费,至少代码逻辑没有问题。但事情没有这么简单,在local_bh_enable--->do_softirq--->__do_softirq中,有一条无条件打开当前中断的操作,也就是说,原本想通过local_irq_disable/local_irq_enable保护的临界区被破坏了,其他的中断handler可以插入执行,从而无法保证local_irq_disable/local_irq_enable保护的临界区的原子性,从而破坏了代码逻辑。(bh:bottom half虽然已经没有了,但是还是有这么一个特殊类型的可延迟函数)

      (4)barrier是优化屏障(Optimization barrier).

    • 一个task的各种上下文,简而言之就是IRQ context + softirq context+NMI (Non Maskable Interrupt不可屏蔽上下文)context。

      • IRQ context这里其实是硬中断上下文。也就是说明当前正在执行中断handler(top half),只要preempt_count中的hardirq count大于0,那么就是IRQ context。
      • softirq context并没有那么的直接,一般人会认为当sofirq handler正在执行的时候就是softirq context。

    softirq机制

    • softirq number:和IRQ number一样,对于软中断,linux kernel也是用一个softirq number唯一标识一个softirq。主要定义了6中软中断

    • softirq的主要数据结构是softirq_vec数组,包含类型为softirq_action的32个元素。另外一个重要的数据结构就是上面的preempt_count字段(int类型32位)

    处理软中断

    • 相关函数大部分位于kernelsoftirq.c中,包括软中断的相关函数和tasklet的相关函数。

    • open_softirq()函数处理软中断的初始化。

    • raise_softirq()函数用于激活软中断,触发softirq的接口函数有两个版本,一个是raise_softirq,有关中断的保护,另外一个是raise_softirq_irqoff,调用者已经关闭了中断,不需要关中断来保护“soft irq status register”。具体过程如下:

      • 执行local_irq_save宏以保存eflags寄存器的IF标志的状态值并禁用本地CPU上的中断。
    • do_softirq()函数:如果在这样的一个检查点(local_softirq_pending不为0)检测到挂起的软中断,内核就调用do_softirq来处理他们。具体就是调用__do_softirq()函数,读取本地CPU的软中断掩码并执行与每个设置位相关的可延迟函数。

      • 为了防止__do_softirq运行很长时间,而大大延迟用户态进程的执行,因此他每次制作固定次数的循环,然后就返回,如果还有其余挂起的软中断,内核线程ksoftirqd将会在预期的时间内处理他们。
      • ksoftirqd内核线程:当内核线程被唤醒时,就检查local_softirq_pending()中的软中断位掩码并在必要时调用do_softirq()。如果没有挂起的软中断,函数吧当前进程状态设置为TASK_INTERRUPTABLE,随后,如果当前进程需要就调用cond_resched()函数来实现进程切换。ksoftirqd/n内核线程为重要而难以平衡的问题提供了解决方案,软中断的连续高流量可能会产生问题,该问题就是由引入的内核线程来解决。

      • 如果开发人员自己解决,就有两种方案,一个是忽略在do_softirq运行时新出现的软中断。那么可能存在机器空闲,也只有在下一次时间中断到来时,中断才被执行。另一个是不断地检查挂起的软中断。do_softirq()函数只有在没有挂起的软中断才终止。这会使得用户态进程无法执行。(零拷贝技术)

    tasklet

    为什么要tasklet?

    将中断处理分成top half(cpu和外设之间的交互,获取状态,ack状态,收发数据等)和bottom half(后段的数据处理)已经深入人心,对于任何的OS都一样,将不那么紧急的事情推迟到bottom half中执行是OK的,具体如何推迟执行分成两种类型:有具体时间要求的(对应linux kernel中的低精度timer和高精度timer)和没有具体时间要求的。对于没有具体时间要求的又可以分成两种:

    1. 越快越好型,这种实际上是有性能要求的,除了中断top half可以抢占其执行,其他的进程上下文(无论该进程的优先级多么的高)是不会影响其执行的,一言以蔽之,在不影响中断延迟的情况下,OS会尽快处理。
    2. 随遇而安型。这种属于那种没有性能需求的,其调度执行依赖系统的调度器。

    本质上讲,越快越好型的bottom half不应该太多,而且tasklet的callback函数不能执行时间过长,否则会产生进程调度延迟过大的现象,甚至是非常长而且不确定的延迟,对real time的系统会产生很坏的影响。

    在linux kernel中,“越快越好型”有两种,softirq和tasklet,“随遇而安型”也有两种,workqueue和threaded irq handler。“越快越好型”能否只留下一个softirq呢?对于崇尚简单就是美的程序员当然希望如此。为了回答这个问题,我们先看看tasklet对于softirq而言有哪些好处:

    1. tasklet可以动态分配,也可以静态分配,数量不限。
    2. 同一种tasklet在多个cpu上也不会并行执行,这使得程序员在撰写tasklet function的时候比较方便,减少了对并发的考虑(当然损失了性能)。
    3. tasklet和softirq都不能sleep

    tasklet的基本原理

    tasklet建立在两个叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的软中断之上。几个tasklet可以与同一个软中断相关联,每个tasklet都有自己的函数。tasklet和高优先级的tasklet分别存放在tasklet_vec和tasklet_hi_vec数组中。两者都包括类型为tasklet_head的多个元素,每个元素都由一个指向tasklet描述符的指针组成。而tasklet描述符具体结构体如下所示(位于interrupt.h):

    struct tasklet_struct
    {
    	struct tasklet_struct *next;  // 指向CPU维持的链表中的下一个tasklet
    	unsigned long state;  // tasklet的状态,包括TASKLET_STATE_SCHED和TASKLET_STATE_RUN
    	atomic_t count;  // 锁计数器
        // func和data成员描述了该tasklet的callback函数
    	void (*func)(unsigned long);  // func是调用函数
    	unsigned long data;  // data是传递给func的参数
    };
    

    tasklet属于一种特殊的软中断(从他的相关函数都放在softirq.c中就可以看出来),tasklet的执行和softirq是由不同的driver函数执行,一个是do_softirq函数(背后是softirq_action结构体),一个直接是tasklet_action函数(参数是softirq_action结构体)。两者的操作基本相似:

    1. 禁止本地中断
    2. 获得本地CPU的逻辑号n
    3. 把tasklet_vec[n]或tasklet_hi_vec[n](存放softirq_action结构体具体成员的地方)指向的链表的地址存入局部变量list中
    4. 上面的数组赋值为0,因此,已经调度了tasklet描述符的链表被清空。
    5. 打开本地中断

    注意:除非tasklet函数重新激活自己,否则,tasklet的每次激活至多触发tasklet函数的一次执行。

    推荐阅读:http://www.wowotech.net/irq_subsystem/tasklet.html

    工作队列

    ​ linux内核2.6版本的工作队列(workqueue)对应于之前2.4版本的任务队列。这个版本中要引入了CMWQ(concurrency managed workerqueue)。可延迟函数和工作队列非常相似,但是可延迟函数运行在中断上下文中,而工作队列运行在进程上下文中。执行可阻塞函数(比如访问磁盘数据块的函数)的唯一方式是在进程上下文中。因为,中断上下文不能发生进程切换。而工作队列和可延迟函数都不能访问用户态地址空间。

    中断上下文和进程上下文

    • 中断上下文包括:
      • 执行该中断的处理函数(interrupt handler 或者top half),也就是hard interrupt context。
      • 执行软中断处理函数,执行tasklet函数,执行timer_callback函数。统称为bottom half,也就是software interrupt context。
      • 中断上下文没有自己的栈。当中断发生的时候,遇到哪一个进程就借用哪一个进程的资源,内核态的栈。
    • 进程上下文,在UNIX System V中,进程上下文由用户级上下文,寄存器上下文以及系统级上下文组成。所有进程(包括内核进程和普通进程)都有一个内核栈,在x86的32位机器上内核栈大小可以为4KB或8KB,这个可以在编译内核的时候配置。
      • 用户级上下文:DS(data segment)数据段寄存器,SS(stack segment)栈段寄存器,IP(instruction pointer)指令指针寄存器,CS(code segment)代码段寄存器(正文段)
        • 在 8086PC 机中,任意时刻,CPU 将 CS:IP 指向的内容当做指令执行。
        • 指令和数据在内存中没有区别,它们都是一些二进制信息,把这些二进制信息看作指令还是数据,是让编程人员通过控制 CPU 中的寄存器来完成的
        • 进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
      • 寄存器上下文:也称为硬件上下文,就是共享CPU寄存器的值。
        • 硬件上下文存放在TTS中,任务状态段。
      • 系统级上下文:进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。

    如何判断当前的上下文

    • in_irq()是用来判断是否在hard interrupt context的

    为什么中断上下文不能sleep

    本来呢interrupt context身轻如燕,没有依赖的task,调度器其实是不知道如何调度interrupt context的(它处理的都是task),在interrupt context借了一个外壳后,从理论上将,调度器是完全可以block该interrupt context执行,并将其他的task调入进入running状态。然而,block该interrupt context执行也就block其外壳task的执行,多么的不公平,多么的不确定,中断命中你,你就活该被schedule out,拥有正常思维的linux应该不会这么做的。因此,在中断上下文中(包括hard interrupt context和software interrupt context)不能睡眠。

    为何需要workqueue

    workqueue和其他的bottom half最大的不同是它是运行在进程上下文中的,它可以睡眠,这和其他bottom half机制有本质的不同,大大方便了驱动工程师撰写中断处理代码。当然,驱动模块也可以自己创建一个kernel thread来解决defering work,但是,如果每个driver都创建自己的kernel thread,那么内核线程数量过多,这会影响整体的性能。因此,最好的方法就是把这些需求汇集起来,提供一个统一的机制,也就是传说中的work queue了。

    struct cpu_workqueue_struct {  // 每个CPU的workqueue结构
    
    	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; // 当前正在处理的work
    
    	struct workqueue_struct *wq;  // 指向work queue struct,其中包含该描述符
    	task_t *thread;  // worker thread task(工作线程任务描述符指针)
    	struct completion exit; 
    
    } ____cacheline_aligned;
    
    

    worker thread要处理work,这些work被挂入work queue中的链表结构。由于每个processor都需要处理自己的work,因此这个work list是per cpu的。worklist成员就是这个per cpu的链表头,当worker thread被调度到的时候,就从这个队列中一个个的摘下work来处理。

    然后看看work_struct(其实就是deferrable task抽象而来的数据结构):

    struct work_struct {
    	unsigned long pending;  //如果函数位于工作队列,置位1否则为0
    	struct list_head entry; //挂起函数链表前一个或后一个元素的指针
    	void (*func)(void *); // 挂起函数的地址
    	void *data; // 函数的参数
    	void *wq_data;  //指向cpu_workqueue_struct描述符的父节点的指针
    	struct timer_list timer;  //延迟挂起函数执行的软定时器
    };
    

    所谓work就是异步执行的函数。如果该函数的代码中有些需要sleep的场景的时候,那么在中断上下文中直接调用将产生严重的问题。这时候,就需要到进程上下文中异步执行。下面我们仔细看看各个成员:func就是这个异步执行的函数,当work被调度执行的时候其实就是调用func这个callback函数。work对应的callback函数需要传递该work的struct作为callback函数的参数。work是被组织成队列的,entry成员就是挂入队列的那个节点,data包含了该work的状态flag和挂入workqueue的信息。

    导致worker thread进入sleep状态有三个条件:(a)电源管理模块没有请求冻结该worker thread。(b)该thread没有被其他模块请求停掉。(c)work list为空,也就是说没有work要处理。

    从中断和异常中返回

    内容实在太多,这里mark一下,以后有机会再细读。

    中断的个人理解

    在我看关于中断部分的相关资料的时候(《深入理解linux内核原理》和wowo大神关于中断部分的讲解),深刻感受到中断必然和驱动相关。而这是上面很少提到的一个点。确实中断大部分是关于外中断的,而外中断大部分是外设硬件发出的信号,那么什么人经常去深入了解中断,我们应该从什么角度去理解中断?很显然还是需要站在驱动工程师这个角度来看。而对驱动工程师写driver的时候需要考虑一下几个点:

    1. 正确使用内核提供的API。
    2. 正确使用同步机制保护驱动代码中的临界区
    3. 正确使用kernel提供的softirq、tasklet、workqueue等机制来完成具体的中断处理。

    应该了解的最基本的点:

    • CPU、PIC(APIC)和外设提供的request line之间的整体关系

      • CPU和PIC的映射关系
      • PIC把IRQ送给哪个CPU?
    • 外设信号HM Interrupt如何转换为IRQ数字的(偏硬件)

    中断子系统相关的软件框架:

    由上面的block图,我们可知linux kernel的中断子系统分成4个部分:

    1. 硬件无关的代码,我们称之Linux kernel通用中断处理模块。
    2. CPU architecture相关的中断处理, 和系统使用的具体的CPU architecture相关。
    3. Interrupt controller驱动代码 。
    4. 普通外设的驱动。

    名词解释:

    1. TTS(task state segment):任务状态段,用来存放硬件上下文。
    2. GDT(global description table) 全局描述表:又叫段描述符表。在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。
    3. /proc是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。用户和应用程序可以通过 proc得到系统的信息,并可以改变内核的某些参数。由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统是 动态从系统内核读出所需信息并提交的。
    4. 软中断通常包括:可延迟函数,编程异常。上面是为了区别,才换了个名字。
    5. 软中断和硬中断和开中断关中断的区别:一个是硬件框图,一个是流程。

    推荐阅读:

    GDT:https://www.cnblogs.com/bajdcc/p/8972946.html

    关于并发

    tasklet 通过 TASKLET_STATE_SCHED 和 TASKLET_STATE_RUN 来防止并发,threaded irq hander 也通过IRQS_ONESHOT防止并发,workqueue上的流程1、挂入队列。2、执行work。挂入到multi thread或者说per cpu workqueue上的指定的work,其callback是不会在一个cpu上并发执行(也就是说在多个cpu上可以并发执行)。

  • 相关阅读:
    第十周进度条
    冲刺阶段第十天
    冲刺阶段第九天
    冲刺阶段第八天
    冲刺阶段第七天
    冲刺阶段第六天
    第一次冲刺阶段(十一)
    第一次冲刺阶段(十)
    第一次冲刺阶段(九)
    第一次冲刺阶段(八)
  • 原文地址:https://www.cnblogs.com/SsoZhNO-1/p/12559506.html
Copyright © 2011-2022 走看看