`

中断和异常

 
阅读更多
中断和异常
  • 按发射中断信号的时机分为“中断”和“异常”
    • 中断(又叫异步中断):由设备的硬件寄存器(定时器、I/O设备)产生,可能在任何时候发出
    • 异常(又叫同步中断):CPU发出的,控制单元只在终止指令执行后,发出
      • 由于程序本身的错误而产生: kernel发信号给进程
      • 由于异常的外部情况而产生: kernel尽量恢复错误,恢复程序执行
  • 中断信号的处理方式:
    • 切换到中断处理时:
      • 只需要保存和设置相关计数器(eip, cs等),而不用进程切换,所以不用切换硬件上下文。但要保存中断处理要用的部分寄存器。
    • 分紧急部分和不紧急部分
      • 紧急部分:在当前进程中执行,
      • 不紧急部分:稍候在"可延时函数"中执行
    • 中断处理代码必须能够重入,以便能够中断嵌套
      • 内核中有“中断禁用”的临界区,应该尽量小。
中断和异常的产生
  • 中断的产生:
    • 分为
      • 可屏蔽中断 - I/O设备。
      • 不可屏蔽中断 - 一些严重的错误,例如:硬件失败。
    • 一个IRQ(Interrupt ReQuest)代表中断控制器上的一根中断线,和一个中断向量
    • 单CPU:可编程中断控制器(PIC)
      • 中断请求(中断线上的信号)优先级排序。
      • 把中断线变成中断向量, 传给CPU(通过PIC的I/O port)
      • 通过CPU的INTR针,与CPU通信:PIC告知有中断(set INTR),CPU发回ACK (clear INTR)
      • 可disable某一根中断线,但上次中断向量的值会保存。这样可以在一根线上串行处理不同的设备。
    • 多CPU:改进的可编程中断控制器(APIC):
      • 每个CPU内有一个本地的APIC(local APIC)。local APIC内部:
        • 32bit寄存器,
        • 一个内部时钟、一个定时器设备 - 用来产生定时器中断。
        • 两条附加IRQ线 LINT0LINT1 - 给本地IRQ用
      • 一个额外的APIC负责连接外部中断线(I/O APIC),并与每个local APIC通信。I/O APIC内部:
        • 24跟IRQ线
        • 中断重定位表(24个条目) - 与IRQ线对应。每个条目的内容如下:
          • 描述某个中断向量发给哪个,或哪几个CPU,或怎么选择目标CPU
        • 可编程寄存器
        • 消息单元 (跟locel APIC通信)
      • 选择目标CPU的方式:
        • 静态:指定一次发送给哪个,哪几个,或是所有CPU
        • 动态:由CPU的任务优先级(先考虑,找最低),本地APIC获得IRQ的仲裁优先级确定(后考虑,找最高。得到者变最低,动态变化,轮流获得机会)。
          • CPU当前任务的优先级,存在local APIC 的TPR寄存器中
      • 每APIC一个ID,CPU可通过它互相发中断(在ICR(中断命令寄存器)中,指定目标APIC的ID和中断向量就可以)。
      • APIC在单CPU系统也可用
  • 异常的产生
    • CPU探测到的:
      • 故障(fault):可被修正,存的是当前eip, 需重新执行。如:页错误。
        • 计算
          • 除零
          • 浮点数错误
          • SIMD浮点异常
        • 指令和内存
          • 操作符无效
          • 操作数地址无效
          • 内存对齐错误
        • 段、页
          • 段越界
          • 无效的TSS
          • 段不存在
          • 页错误
          • 保护异常
        • 其他
          • 二次故障
          • 设备无效
          • 机器检查
      • 陷阱(trap):不用修正,处理完后执行下一条指令。存下一条指令地址。如:调试断点。
        • 调试:当设置了TF位时,(地址在某个指定范围内就触发,debug寄存器存地址范围)
        • 断点:int3指令触发。断点指令
        • 溢出:当OF位设置了时,into指令触发
      • 流产(abort):严重错误(硬件失败,或是系统数据出现错误); 或是CPU控制单元出错,无法保存eip。
        • 处理程序能做的只有发信号终止进程
    • 程序设定的异常:
      • 程序执行引发异常的指令
      • 用于实现系统调用,或调试。
    • 2号异常用于不可屏蔽中断
  • 中断向量号分配[0, 255)
    • 物理IRQ对应中断向量[32, 238]
    • IBM兼容机, 某些设备的中断必须静态连接到特定的中断线上
[tr]中断号分配[/tr]
0 - 19 不可屏蔽的中断和异常
20 - 31 intel 保留
32 - 127 IRQ
128 系统调用用到的软件异常
129-238 IRQ
239 本地APIC的timer
240 本地APIC的热中断 (thermal interrupt)
241 - 250 linux扩展
251 - 253 CPU之间互发中断消息
254 错误中断,本地APIC探测到一个错误状况
255 本地APIC 伪造中断。告知设备发出了一个被屏蔽的中断
中断异常的硬件处理
  • 中断描述符:
    • 描述符格式:类型(第40-43bit表示类型),段选择符(全局段表中),偏移量, DPL,等。不同类型格式不太一样。
      • 描述符类型如下(默认都是内核态访问):
        • 任务入口:需要替换当前进程的。段选择符位置放的是TSS选择符,没有偏移量
        • 中断入口:控制转移后,清除IF flag(禁止可屏蔽中断)。
          • 用户态可以访问的叫“系统中断入口”:
[tr]指令中断号[/tr]
int3 3(断点)
        • 陷阱入口:控制转移后,不清除IF flag
          • 用户态可以访问的叫“系统入口”:
[tr]指令中断号[/tr]
into 4(溢出)
bound 5(地址边界检查))
int $0x80 128(系统调用)
      • 地址的内容:是一段汇编代码。内容大致是:
handler_name: /*保存寄存器的汇编代码*/ call do_handle_name /*真正的处理, C函数*/
    • 中断描述符表(IDT):255个描述符,地址在idtr中,lidt指令加载idtr.
      • 设置中断描述符的函数
        • set_XXX_gate (n, addr)
          • n:条目索引
          • addr: 段偏移量。段选择符就是内核代码段。
          • XXX:有system, DPL位3;否则DPL位0
        • set_task_gate (n, gdt)
          • 只有一次调用:gdt传31, 最后一个段。专门存放处理二次失败中断的进程TSS
  • 中断和异常的硬件处理
    • 中断进入
      • 每执行完一条指令,检查有无中断
        • 执行完一条指令之后, eip变成下一条,
        • 检查执行前一条指令时有无中断、异常。
      • 若有中断
        • 根据中断向量数和idtr, 找IDT条目;根据IDT里的段选择符,找段描述符。
        • 检查权限:CPL与段DPL,与IDT条目里的DPL比较。(CPL都要被许可)
        • 如果段DPL与CPL不同,栈切换。
          • tr寄存器存了当前CPU的TSS。
          • 在TSS里,用段DPL找到对应的栈(即选择内核栈还是用户栈,FIXME: 硬件能办到吗);设置ss, esp。
          • 在新栈里存原来的ss, esp。
        • 保存eflags, cs, eip到栈中,以备恢复。
          • 如果是fault(可以恢复的),保存之前先用异常时的cs eip装载cs eip寄存器
        • 如果异常带来一个硬件错误码,存在栈里。
        • load 中断处理的的cs(段选择符), eip (段选择符和offset),开始处理
  • 中断返回: 用iret指令返回. 控制单元做的事:
    • 从栈中装载cs, eip, eflags, 恢复eip
    • 如果中断进入时发生了栈切换(原来存的CPL(刚从栈中放到cs的),与中断处理段的DPL不相等):
      • 从栈中装载ss, esp以恢复处理前的栈
      • 清除遗留段地址 - 清除权限值比CPL还要低(权力大)的段寄存器;因为权限保护,不能访问。
中断、异常的软件处理
  • 可嵌套 (实质是“内核控制路径(kernel control path)”可以嵌套执行)
    • 可嵌套的前提条件:开始存reg, 最后又恢复了,所以说可以嵌套。
    • 代价:中断处理不能阻塞(中断处理时,不允许进程切换。FIXME 用于分时的定时器中断被禁用了? )。
    • 可行性:
      • 只有处理“页故障”时才会阻塞。而中断处理不会产生“页故障”,因为中断处理程序的代码、数据永远在内存中。
      • 假设内核没有bug,就不会产生异常(“页故障”除外),异常只产生在用户态(程序bug,或调试时)。
      • 只有“页故障”才会阻塞,且处理它时不会再产生异常。所以不会再次引发二次“页故障”
    • 嵌套的好处:
      • 提高吞吐量:先禁用中断线,直到CPU发出ack, 然后启用中断。此时CPU处理剩下的部分(此时可嵌套)。
      • 不用考虑优先级:由于可嵌套,就不用考虑中断优先级,简化了软硬件设计。
      • 多CPU时,如果中断处理被分给一个要切换进程的CPU,可以迁移到其他CPU。
  • 多个内核栈
    • thread_info编译时8K:所有都在同一个内核栈
    • thread_info编译时4K:三个内核栈,每个大小是4K
      • 异常栈 - 每进程一个, 即thread_info
        • thread_info存着后两个栈的基址指针。
      • 硬中断 - 每CPU一个 ,所有CPU的在一个数组理:irq_ctx hardirq_stack [nr_cpu]。 hardirq_ctx[nr_cpu]存着每个栈的基址,用于快速定位
      • 软中断 - 每CPU一个, 所有CPU的在一个数组理:irq_ctx softirq_stack [nr_cpu]。 softirq_ctx[nr_cpu]存着每个栈的基址,用于快速定位。
        • irq_ctx是一个与thread_info完全一样的的结构
异常的软件处理:
  • 非二次失败异常
    • 存大部分寄存器值到内核栈,调真正的handler(C函数,声明寄存器传参)。细节部分除了“设备无效”异常(FPU、页)外,都相同:
      • 压入异常码,处理handler的地址(两者入栈是为了下面的汇编代码都相同)
      • 保存并设置相应寄存器:
        • 压入handler可能用到的Regs
        • 设置DF,使edi, esi用于string指令(块拷贝等)时会自增(cld指令, clear direction)。FIXME: why?
      • 准备函数地址、变量,即把它们都放入寄存器:
        • 栈内错误码放进edx, 栈内的错误码置为-1.
        • 栈内的hander放入 edi; es放入栈内handler的位置
        • 装载用户数据段(__USER_DS)到ds和es (考虑:上述edi, esi的设置)
        • 栈顶位置-> eax,
      • 调用hander:
        • call edi(handler), 参数已放入 eax(栈顶位置), edx(错误码) 进入异常的handle
    • handler的处理方式:
      • 调do_trap()记录异常代码,给current进程发信号; 进程再处理信号(通过处理信号来处理异常),选择修复或流产(MS_DOS模式下,处理不同)。
      • 用异常机制管理硬件资源(需要时加载的策略):
        • 进程切换时, 用“设备无效异常”来实现FPU、MMX、XMM的保存与恢复
        • 用页故障来启动换页。
    • 以jmp到汇编函数ret_from_exception的方式 返回
  • 二次失败异常,用task_gate处理
    • 严重错误,esp以无效。
    • 处理时,load TSS内的eip, esp。在TSS自己的栈上执行doublefault_fn()
    • FIXME: 当前进程怎么办?
  • 处理没实现时,用ignore_init替代:
    • 保存一些寄存器,printk "未知中断"(实际上现在还不能执行,因为打印到console, 还是写到日志文件都需要处理设备中断)。
    • 恢复寄存器,执行iret.
  • 如果内核态出异常:
    • 系统调用参数错误 - 后面的章节介绍
    • 内核的bug - 打印所有Reg值,退出进程
中断的软件处理
  • 特点
    • 中断到得很晚,与当前进程无关 - 不能用信号机制。 中断处理分成三部分:
      • 紧急部分:
        • 发ACK,重新编程PIC、设备控制器,操作与CPU、设备都关联的数据。
        • 要立即完成。过程中,中断禁用。
      • 不紧急部分: 仅与CPU相关,可以快速完成。过程中,中断使能。
      • 可延时部分: 用于向进程(不一定是当前进程)的用户空间拷贝数据。可被延时较长时间。
    • IRQ少,设备多
      • IRQ共享。
        • 每个设备一个服务程序。
        • 来了中断,IRQ上的所有设备服务程序要遍历。
      • IRQ动态分配
        • 程序需要哪个设备,临时给哪个设备分配IRQ。
      • 给一个设备分配IRQ
        • 硬件跳线
        • 安装时运行程序分配(询问用户或系统)。
        • 根据硬件协议,设备声明用那根,系统根据情况分配一个。handle通过设备的I/O port,获得分配到的IRQ。
  • 四个基本动作:
    • 保存IRQ,和寄存器到内核栈
    • 发ACK到PIC,使能中断
    • 执行该IRQ上的所有ISR。
    • 跳到ret_from_inir 结束。
  • 数据结构
    • 每个IRQ一个链表,每个节点内是某个设备的中断服务程序(ISR); 所有链表在一个数组irq_desc里。
    • 链表头的内容:
      • PIC相关的:
        • handler : 指向相关的PIC对象。PIC对象:对硬件PIC封装而成的一个数据结构,使在编写驱动时不用考虑PIC硬件差异。
          • 名字: name
          • 方法:
            • startup, shundown
            • enable disable
            • ack - 给PIC发ACK
            • end - 告诉PIC处理完成
            • set_affinity - 多CPU时,该PIC倾向处理某些特定的IRQ
        • handler_data :PIC对象里的函数用到的数据
        • action:这个IRQ上的所有ISR, 以链表形式。每一个节点对应一个设备,节点的结构irq_action:
          • name - 设备名
          • dev_id - 设备号 (主设备号,副设备号)
          • hander - ISR
          • irq - irq线号
          • dir - irq线所在路径/proc/irq/n
          • flags - 描述IRQ线与设备的关系
            • SA_INERRUPR - 该handler, 不可被中断
            • SA_SHIRQ - 该设备允许IRQ共享
            • SA_SAMPLE_RADOM - 该设备可以所谓随机数发生器。(FIXME 跟中断有什么关系)
          • mask- 没用
          • next - 下一个节点指针
      • 多CPU合作: lock, 互斥锁
      • 状态位的组合: status
        • IRQ_INPROGRESS: 正被处理
        • IRQ_DISABLE - 该IRQ被禁用了
        • IRQ_PENDING - 发回ACK了,未服务
        • IRQ_REPLAY - 当该IRQ刚要被处理,还未上锁时,被其他CPU禁用了。然后被另外的程序手动置成该状态, 表示重发
        • IRQ_AUTODETCT -
        • IRQ_WAITING -
        • IRQ_LEVEL:x86架构中不用
        • IRQ_PER_CPU:x86架构中不用
        • IRQ_MASKED:不用
      • depth:IRQ被禁用的层次
        • disable_irq()时, depth++,enable_irq()时, depth--。
        • 遇到0时,才真正disable或enable IRQ。 0位,使能状态。
      • 统计信息,用来防止意外中断:
        • irq_count:中断发生总次数
        • irqs_unhandled:没处理中断次数。IRQ上的ISR都不识别,或是该IRQ上没有ISR
        • 如果某个IRQ线,总有意外中断,禁用该IRQ线。
        • 根据未处理的中断次数,和中断总数来判断。例如每100,000个中断,有99000是没有处理的,就禁用改中断线。
  • 中断处理:
    • 保存寄存器
      • 入栈:向量减去256。 (大于256的向量是系统调用)
      • 要保护的寄存器入栈 (再加上下步“装载用户数据” 组成:SAVE_ALL宏)
    • 装载用户数据段
      • (__USER_DS) 放到 ds和es
    • 准备参数: 栈指针 -> eax
      • eax既是栈指针,又指出了保存的寄存器的值
    • 调用do_IRQ (参数在eax中): 起封装“栈切换”的作用
      • 嵌套层次++;(thread_info.preempt_count)
      • 如果内核栈是4K,检查是否切到了硬件中断栈。如果没有,切换到硬件中断栈。
        • 检查:通过esp确定当前使用的内核栈基地址 ( current_thread_info() ), 再与hardirq_ctx存的地址比较。
        • 栈切换:存当前的pd, esp到irq_ctx中, 装入irq_ctx对应的esp.
      • 调用__do_IRQ() (解决多CPU问题,每次改变status都有加锁)
        • 清除设置相关位。判断有无其他CPU正在处理当前IRQ上的中断。
          • 如果有了,当前交由那个CPU处理,本CPU仅设置pending位,不处理。
          • 如果没有,本CPU反复调用handle_IRQ_event()处理,直到没有CPU再设置pending(把处理交给自己)。handle_IRQ_event:
            • 如果该IRQ可以被中断,使能中断(sti指令);
            • 链表上所有的handler都执行;
            • 禁中断(cli指令);
            • 返回是否有handler成功执行(用来统计无效中断)
      • 如果栈切换了,切回原来的栈
      • 执行irq_exit宏:减少嵌套层次,执行可延迟函数
      • 用ret_from_intr()结束中断处理
  • 遗漏的中断
    • 某个CPU刚选中某个中断,还未上锁。就被另一个CPU禁用中断了。前者只设置pending,没有进入处理的循环
    • enable_irq()的时候,如果pending了但还没有重发(replay位0);就设置replay位,让本地APIC给自己发中断
  • 中断分类:I/O中断, timer, CPU之间
    • CPU间的三个中断(IPI):
[tr]向量号(_VECTOR左边的是中断名字)目标CPU作用接收者的动作附加说明[/tr]
CALL_FUNCTION_VECTOR(0xfb) 除发出者外的所有CPU 使接收者执行某一个函数 发回ack, 执行函数 函数指针call_data里
RESCHEDUELE_VECTOR(0xfc) 某一个,某几个CPU 使其调度 发回ack, 调度
INVALID_TLB_VECTOR(0xfd) 所有CPU 强制其TLB无效
    • 发射函数: send_IPI_xyz
目标CPU 所有 除了自己 自己 指定
函数xyz all allbutself self maks
    • 处理:BUILD_INTERRUPT宏,内部调C函数smp_name_interrupt() (name是中断名字)
  • 软件上IRQ线的分配(在链表里插入节点):
    • 创建一个irq_action节点:request_irq(irq_num, ISR, flags,name, ......)
    • 插入到相应链表: setup_irq().
      • 检查已有节点是否有SA_SHIRQ属性
      • 插入链
      • 如果是该链上第一个节点,运行PIC对象的setup方法,使能这个IRQ号
    • 释放一个节点(FIXME: 链表上所有节点?):free_irq
  • irq信号在多CPU间分配
    • 对称多处理:
      • 每个CPU执行中断处理时,时间片相同。
      • 每个CPU轮流得到irq信号.
        • 硬件支持仲裁优先级变换:
          • local APIC 初始化,任务优先级都相同。
          • 仲裁优先级不断变换,实现轮流得到irq
        • 如果硬件不支持仲裁优先级变换:
          • 用修改I/O APIC的重定向表实现。由kirqd 线程实现, 根据irq_stat来平衡。
            • 调用set_ioapic_affinity_irq(n, mask),修改重定向表。(用户可通过/proc/irq/n/smp_affinity修改)
              • n:中断向量
              • 32bit mask:哪几个CPU可以收到irq。
          • 类型位irq_stat的irq_cpustat_t数组,每个CPU一个元素。保存每个CPU最近处理irq的情况。用于平衡每个CPU的irq处理。
            • __softirq_pending: 悬挂的软中断个数
            • idle_timestamp:只有CPU当前是idle时才有意义,开始idle的时间
            • __nmi_count:处理nmi中断的次数
            • apic_timer_irqs:处理局部apic 时间中断的次数
    • 链表数组初始化 init_irq():
      • 所有的IRQ都置成IRQ_DISABLE状态。用中段函数设置中断入口(中断描述表里的,从32开始)。


softirq和tasklet
  • 可延迟函数
    • ISR要求必须串行执行,而且经常需要中途无中断发生;但是可延迟任务中途允许中断。
    • 中断上下文: 中断handle和softirq
    • 分类:
      • 可延时函数包括softirq和tasklet。
      • 在数据结构上,tasklets在softirq的结构内部,所有经常统称softirq.
        • softirq:
          • 静态分配,即编译时就确定了。
          • 同类可并行,可同时在多CPU执行
            • 自旋锁保护临界区
          • 可重入。
        • tasklet反之。使驱动编写简单。
    • 基本操作:
      • 初始化:定义一个新的可延迟函数。一般在kernel初始化,或添加一个模块的时候用
      • 激活:设一个可延迟函数的状态为pending. 表示希望被执行
      • 屏蔽:disable一个可延迟函数
      • 执行:在某个检查点,检查有无pending的函数,并执行它
        • 对于一个可延迟函数,执行它的CPU必须是激活它的CPU。为了硬件cache考虑
  • softirq
    • 数据结构:softirq_action softirq_vec [32], 32元素的数组,执行时从0开始,0的优先级最高。
      • action: 函数指针
      • data: 通用指针,action的参数
    • 设置,即把一个函数设置在softirq_vec的一个元素:open_softirq(indx, action, data)
    • 激活,raise_softirq(index).
      • 设置本softirq的pending位,检查thread_info.preempt_count中的softirq次数与hardirq次数,是否都是零。
        • thread_info.preempt_count 。四个数的位或
          • 抢占次数:抢占被禁用次数
          • softirq次数:softirq被禁用次数
          • hardirq次数:irq处理嵌套次数
          • PREEMP_ACTIVE标志位
          • 方便:只要检查preempt_count==0, 就可判断是否内核可抢占。
        • 多内核栈时,用irq_ctx内的preempt_count, 但它一般是个正数。FIXME: 那何时激活?
      • 如果都为零,说明不嵌套在中断上下文中。调用wakeup_softirqd()来唤醒执行软中断的内核线程ksoftirqd
        • 内核线程ksoftirqd/n (n指某个CPU, ksoftirqd每个CPU一个,FIXME: 且可以迁移),不断检查,执行。
        • 其他softirq的执行点(只执行一次):
          • 使能本地CPU的softirq:local_bh_enable()
          • 中断处理完成后:
            • do_IRQ完成处理后:irq_exit宏内
            • 处理完本地timer中断后
            • 处理完IPI: CALL_FUNCTION_VECTOR以后
      • raise_softirq的开始、结束时要禁止中断,恢复中断
    • 执行
      • 一次执行: do_softirq()
        • 前期准备:检查preempt_count, 禁止中断。如果时多内核栈,且不再softirq栈,切到softirq栈
        • __soft_irq真正执行:
        • 后期收尾:恢复以前的栈,恢复中断
      • softirqd不断检查、执行
        • softirqd不断被唤醒;反复检查有无激活的函数并执行;直到没有pending的函数,进入可中断睡眠。
        • 每次执行调do_softirq(前后要禁用、启用抢占); 然后cond_resched(如果current thread_info的TIF_NEED_RESCHED为1,就执行调度)
  • tasklet
    • 通过链表组织的可延迟函数。
    • 每CPU一个链表,所有链表在链表数组里。按优先级分成两个链表数组(优先级高函数有"hi")。
    • softirq_vec的前两个元素,保存链表指针和遍历链表的函数指针
    • 节点tasklet_struct:
      • state: 2个位:
        • TASKLET_STATE_SCHED: 表示pending
        • TASKLET_STATE_RUN: 表明正在执行,多CPU时才有用
      • count:禁用次数。FIXME: 为什么其他softirq没有单独的count
      • func
      • data
      • next
    • 接口:
      • 分配一个节点:tasklet_init()
      • 禁用、使能: tasklet_disable_nosyns, tasklet_disable, tasklet_enable
      • 激活tasklet_schedule、tasklet_hi_schedule
        • 设置pending位,添加到相应表头,激活softirq_vec中的相应元素。后两步要禁用本地中断
      • 执行:就是softirq_vec里前两个元素的函数是tasklet_hi_action, tasklet_action
        • 拷贝清空本地CPU的链表头指针,禁中断下进行
        • 遍历链表:
          • TASKLET_STATE_RUN的节点:表示现在有其他CPU在执行这个函数。在链表数组里本地CPU的位置,重新插入该节点。
          • count>0的节点:被禁用了,重新插入该节点
          • count<=0的节点:清除pending位,执行函数。注意:每个函数最多执行一次,执行完了不再插入列队
  • work queue(工作列队)
    • 内核函数被激活,稍候被特殊的内核线程(worker thread)执行。
    • 与可延迟函数的区别:工作列队里的函数可以阻塞
    • 数据结构
      • 一个workqueue_struct,包含一个多CPU数组。每一个元素是一个链表头(cpu_workqueue_struct):
        • lock
        • wq:上级指针
        • worklist: 函数链表。节点结构如下:
          • pending:
          • entry: 内嵌链表
          • wq_data: 上级指针
          • timer: 用于延迟插入的软件定时器
          • func: 函数指针
          • data: 通用指针,函数func的参数。
        • thread:执行该链表函数的工作线程
        • run_depth:
        • more_work:等待列队,等待新的函数的工作线程阻塞在这里
        • work_done:等待列队, 等待工作列队flush完毕的进程阻塞在这里
        • remove_sequence:用于判断哪些哪些函数是flush以后才来的。
        • insert_sequence:用于判断哪些哪些函数是flush以后才来的。
    • 接口
      • 创建工作列队:
        • create_workqueue: 一共NR_CPU个工作线程,每个链表一个。每个线程都可以到任意的CPU执行
        • create_singlethread_workqueue:只有一个工作线程。
      • 插入一个函数:
        • queue_work():把一个节点插入列队,设置pending位。(插入到local CPU的链表)
        • queue_delayed_work():多一个timer参数,延迟插入。(设置一个timer,定时器到了再插入)
        • cancel_delayed_work(): 取消插入,必须在实际插入之前调用。
      • 执行:
        • 每一个工作线程都进入worker_thread()内部的一个循环中。
        • 大部分时间睡眠,有函数时唤醒,唤醒后调run_workqueue(摘下该线程对应链表的所有的节点,执行里面的pending函数)
      • 等待执行完:
        • flush_work_queue: 阻塞当前进程,直到所有的pending函数执行完毕。
        • 但不包括新来的函数。用remove_sequece, insert_sequece两个计数判断哪些是新来的函数
    • 内核预定义的工作列队events: 包含不同kernel层的函数和I/O驱动。 在keventd_wq数组里存着不同的工作列队
从中断和异常中返回
  • 如果内核支持抢占,那么返回时禁中断
    • 中断处理末尾就是禁中断的,所以从异常返回时要首先禁中断。
  • 如果有嵌套的KCP(kernel control path),且不是虚拟8086模式,则恢复kernel。否则恢复用户空间
    • 判断栈中保存的cs的权限位,和eflags的VM位
    • 恢复到内核后:
      • 在允许抢占(thread_info.preempt_count==0)的情况下:如果以下两个条件都满足,执行抢占调度让出CPU(preempt_schedule_irq)。否则恢复上层KCP
        • 有等待调度的进程(current->thread_info.flags的TIF_NEED_RESCHED被设置了)。
        • 上层要恢复的KCP允许中断(本级是异常时,才有可能“要恢复的KCP不允许中断”)。
        • 被抢占,再次获得CPU后,要重新检查上述两条件
    • 恢复到用户空间:
      • 检查有无剩余事情要做(调度、恢复虚拟8086状态,悬挂信号、恢复单步执行,):
        • 如果有等待调度的进程,调度(schedule())。再次获得CPU后,重新检查。直到没有新来的调度请求了。
        • 如果要恢复虚拟8086状态,在用户空间建立相应的数据结构(FIXME: 以后研究)
        • 处理其他的剩余事情(悬挂信号、恢复单步执行)。
  • 恢复:
    • SAVE_ALL保存的寄存器出栈。
    • iret指令。
中断和异常
  • 按发射中断信号的时机分为“中断”和“异常”
    • 中断(又叫异步中断):由设备的硬件寄存器(定时器、I/O设备)产生,可能在任何时候发出
    • 异常(又叫同步中断):CPU发出的,控制单元只在终止指令执行后,发出
      • 由于程序本身的错误而产生: kernel发信号给进程
      • 由于异常的外部情况而产生: kernel尽量恢复错误,恢复程序执行
  • 中断信号的处理方式:
    • 切换到中断处理时:
      • 只需要保存和设置相关计数器(eip, cs等),而不用进程切换,所以不用切换硬件上下文。但要保存中断处理要用的部分寄存器。
    • 分紧急部分和不紧急部分
      • 紧急部分:在当前进程中执行,
      • 不紧急部分:稍候在"可延时函数"中执行
    • 中断处理代码必须能够重入,以便能够中断嵌套
      • 内核中有“中断禁用”的临界区,应该尽量小。
中断和异常的产生
  • 中断的产生:
    • 分为
      • 可屏蔽中断 - I/O设备。
      • 不可屏蔽中断 - 一些严重的错误,例如:硬件失败。
    • 一个IRQ(Interrupt ReQuest)代表中断控制器上的一根中断线,和一个中断向量
    • 单CPU:可编程中断控制器(PIC)
      • 中断请求(中断线上的信号)优先级排序。
      • 把中断线变成中断向量, 传给CPU(通过PIC的I/O port)
      • 通过CPU的INTR针,与CPU通信:PIC告知有中断(set INTR),CPU发回ACK (clear INTR)
      • 可disable某一根中断线,但上次中断向量的值会保存。这样可以在一根线上串行处理不同的设备。
    • 多CPU:改进的可编程中断控制器(APIC):
      • 每个CPU内有一个本地的APIC(local APIC)。local APIC内部:
        • 32bit寄存器,
        • 一个内部时钟、一个定时器设备 - 用来产生定时器中断。
        • 两条附加IRQ线 LINT0LINT1 - 给本地IRQ用
      • 一个额外的APIC负责连接外部中断线(I/O APIC),并与每个local APIC通信。I/O APIC内部:
        • 24跟IRQ线
        • 中断重定位表(24个条目) - 与IRQ线对应。每个条目的内容如下:
          • 描述某个中断向量发给哪个,或哪几个CPU,或怎么选择目标CPU
        • 可编程寄存器
        • 消息单元 (跟locel APIC通信)
      • 选择目标CPU的方式:
        • 静态:指定一次发送给哪个,哪几个,或是所有CPU
        • 动态:由CPU的任务优先级(先考虑,找最低),本地APIC获得IRQ的仲裁优先级确定(后考虑,找最高。得到者变最低,动态变化,轮流获得机会)。
          • CPU当前任务的优先级,存在local APIC 的TPR寄存器中
      • 每APIC一个ID,CPU可通过它互相发中断(在ICR(中断命令寄存器)中,指定目标APIC的ID和中断向量就可以)。
      • APIC在单CPU系统也可用
  • 异常的产生
    • CPU探测到的:
      • 故障(fault):可被修正,存的是当前eip, 需重新执行。如:页错误。
        • 计算
          • 除零
          • 浮点数错误
          • SIMD浮点异常
        • 指令和内存
          • 操作符无效
          • 操作数地址无效
          • 内存对齐错误
        • 段、页
          • 段越界
          • 无效的TSS
          • 段不存在
          • 页错误
          • 保护异常
        • 其他
          • 二次故障
          • 设备无效
          • 机器检查
      • 陷阱(trap):不用修正,处理完后执行下一条指令。存下一条指令地址。如:调试断点。
        • 调试:当设置了TF位时,(地址在某个指定范围内就触发,debug寄存器存地址范围)
        • 断点:int3指令触发。断点指令
        • 溢出:当OF位设置了时,into指令触发
      • 流产(abort):严重错误(硬件失败,或是系统数据出现错误); 或是CPU控制单元出错,无法保存eip。
        • 处理程序能做的只有发信号终止进程
    • 程序设定的异常:
      • 程序执行引发异常的指令
      • 用于实现系统调用,或调试。
    • 2号异常用于不可屏蔽中断
  • 中断向量号分配[0, 255)
    • 物理IRQ对应中断向量[32, 238]
    • IBM兼容机, 某些设备的中断必须静态连接到特定的中断线上
[tr]中断号分配[/tr]
0 - 19 不可屏蔽的中断和异常
20 - 31 intel 保留
32 - 127 IRQ
128 系统调用用到的软件异常
129-238 IRQ
239 本地APIC的timer
240 本地APIC的热中断 (thermal interrupt)
241 - 250 linux扩展
251 - 253 CPU之间互发中断消息
254 错误中断,本地APIC探测到一个错误状况
255 本地APIC 伪造中断。告知设备发出了一个被屏蔽的中断
中断异常的硬件处理
  • 中断描述符:
    • 描述符格式:类型(第40-43bit表示类型),段选择符(全局段表中),偏移量, DPL,等。不同类型格式不太一样。
      • 描述符类型如下(默认都是内核态访问):
        • 任务入口:需要替换当前进程的。段选择符位置放的是TSS选择符,没有偏移量
        • 中断入口:控制转移后,清除IF flag(禁止可屏蔽中断)。
          • 用户态可以访问的叫“系统中断入口”:
[tr]指令中断号[/tr]
int3 3(断点)
        • 陷阱入口:控制转移后,不清除IF flag
          • 用户态可以访问的叫“系统入口”:
[tr]指令中断号[/tr]
into 4(溢出)
bound 5(地址边界检查))
int $0x80 128(系统调用)
      • 地址的内容:是一段汇编代码。内容大致是:
handler_name: /*保存寄存器的汇编代码*/ call do_handle_name /*真正的处理, C函数*/
    • 中断描述符表(IDT):255个描述符,地址在idtr中,lidt指令加载idtr.
      • 设置中断描述符的函数
        • set_XXX_gate (n, addr)
          • n:条目索引
          • addr: 段偏移量。段选择符就是内核代码段。
          • XXX:有system, DPL位3;否则DPL位0
        • set_task_gate (n, gdt)
          • 只有一次调用:gdt传31, 最后一个段。专门存放处理二次失败中断的进程TSS
  • 中断和异常的硬件处理
    • 中断进入
      • 每执行完一条指令,检查有无中断
        • 执行完一条指令之后, eip变成下一条,
        • 检查执行前一条指令时有无中断、异常。
      • 若有中断
        • 根据中断向量数和idtr, 找IDT条目;根据IDT里的段选择符,找段描述符。
        • 检查权限:CPL与段DPL,与IDT条目里的DPL比较。(CPL都要被许可)
        • 如果段DPL与CPL不同,栈切换。
          • tr寄存器存了当前CPU的TSS。
          • 在TSS里,用段DPL找到对应的栈(即选择内核栈还是用户栈,FIXME: 硬件能办到吗);设置ss, esp。
          • 在新栈里存原来的ss, esp。
        • 保存eflags, cs, eip到栈中,以备恢复。
          • 如果是fault(可以恢复的),保存之前先用异常时的cs eip装载cs eip寄存器
        • 如果异常带来一个硬件错误码,存在栈里。
        • load 中断处理的的cs(段选择符), eip (段选择符和offset),开始处理
  • 中断返回: 用iret指令返回. 控制单元做的事:
    • 从栈中装载cs, eip, eflags, 恢复eip
    • 如果中断进入时发生了栈切换(原来存的CPL(刚从栈中放到cs的),与中断处理段的DPL不相等):
      • 从栈中装载ss, esp以恢复处理前的栈
      • 清除遗留段地址 - 清除权限值比CPL还要低(权力大)的段寄存器;因为权限保护,不能访问。
中断、异常的软件处理
  • 可嵌套 (实质是“内核控制路径(kernel control path)”可以嵌套执行)
    • 可嵌套的前提条件:开始存reg, 最后又恢复了,所以说可以嵌套。
    • 代价:中断处理不能阻塞(中断处理时,不允许进程切换。FIXME 用于分时的定时器中断被禁用了? )。
    • 可行性:
      • 只有处理“页故障”时才会阻塞。而中断处理不会产生“页故障”,因为中断处理程序的代码、数据永远在内存中。
      • 假设内核没有bug,就不会产生异常(“页故障”除外),异常只产生在用户态(程序bug,或调试时)。
      • 只有“页故障”才会阻塞,且处理它时不会再产生异常。所以不会再次引发二次“页故障”
    • 嵌套的好处:
      • 提高吞吐量:先禁用中断线,直到CPU发出ack, 然后启用中断。此时CPU处理剩下的部分(此时可嵌套)。
      • 不用考虑优先级:由于可嵌套,就不用考虑中断优先级,简化了软硬件设计。
      • 多CPU时,如果中断处理被分给一个要切换进程的CPU,可以迁移到其他CPU。
  • 多个内核栈
    • thread_info编译时8K:所有都在同一个内核栈
    • thread_info编译时4K:三个内核栈,每个大小是4K
      • 异常栈 - 每进程一个, 即thread_info
        • thread_info存着后两个栈的基址指针。
      • 硬中断 - 每CPU一个 ,所有CPU的在一个数组理:irq_ctx hardirq_stack [nr_cpu]。 hardirq_ctx[nr_cpu]存着每个栈的基址,用于快速定位
      • 软中断 - 每CPU一个, 所有CPU的在一个数组理:irq_ctx softirq_stack [nr_cpu]。 softirq_ctx[nr_cpu]存着每个栈的基址,用于快速定位。
        • irq_ctx是一个与thread_info完全一样的的结构
异常的软件处理:
  • 非二次失败异常
    • 存大部分寄存器值到内核栈,调真正的handler(C函数,声明寄存器传参)。细节部分除了“设备无效”异常(FPU、页)外,都相同:
      • 压入异常码,处理handler的地址(两者入栈是为了下面的汇编代码都相同)
      • 保存并设置相应寄存器:
        • 压入handler可能用到的Regs
        • 设置DF,使edi, esi用于string指令(块拷贝等)时会自增(cld指令, clear direction)。FIXME: why?
      • 准备函数地址、变量,即把它们都放入寄存器:
        • 栈内错误码放进edx, 栈内的错误码置为-1.
        • 栈内的hander放入 edi; es放入栈内handler的位置
        • 装载用户数据段(__USER_DS)到ds和es (考虑:上述edi, esi的设置)
        • 栈顶位置-> eax,
      • 调用hander:
        • call edi(handler), 参数已放入 eax(栈顶位置), edx(错误码) 进入异常的handle
    • handler的处理方式:
      • 调do_trap()记录异常代码,给current进程发信号; 进程再处理信号(通过处理信号来处理异常),选择修复或流产(MS_DOS模式下,处理不同)。
      • 用异常机制管理硬件资源(需要时加载的策略):
        • 进程切换时, 用“设备无效异常”来实现FPU、MMX、XMM的保存与恢复
        • 用页故障来启动换页。
    • 以jmp到汇编函数ret_from_exception的方式 返回
  • 二次失败异常,用task_gate处理
    • 严重错误,esp以无效。
    • 处理时,load TSS内的eip, esp。在TSS自己的栈上执行doublefault_fn()
    • FIXME: 当前进程怎么办?
  • 处理没实现时,用ignore_init替代:
    • 保存一些寄存器,printk "未知中断"(实际上现在还不能执行,因为打印到console, 还是写到日志文件都需要处理设备中断)。
    • 恢复寄存器,执行iret.
  • 如果内核态出异常:
    • 系统调用参数错误 - 后面的章节介绍
    • 内核的bug - 打印所有Reg值,退出进程
中断的软件处理
  • 特点
    • 中断到得很晚,与当前进程无关 - 不能用信号机制。 中断处理分成三部分:
      • 紧急部分:
        • 发ACK,重新编程PIC、设备控制器,操作与CPU、设备都关联的数据。
        • 要立即完成。过程中,中断禁用。
      • 不紧急部分: 仅与CPU相关,可以快速完成。过程中,中断使能。
      • 可延时部分: 用于向进程(不一定是当前进程)的用户空间拷贝数据。可被延时较长时间。
    • IRQ少,设备多
      • IRQ共享。
        • 每个设备一个服务程序。
        • 来了中断,IRQ上的所有设备服务程序要遍历。
      • IRQ动态分配
        • 程序需要哪个设备,临时给哪个设备分配IRQ。
      • 给一个设备分配IRQ
        • 硬件跳线
        • 安装时运行程序分配(询问用户或系统)。
        • 根据硬件协议,设备声明用那根,系统根据情况分配一个。handle通过设备的I/O port,获得分配到的IRQ。
  • 四个基本动作:
    • 保存IRQ,和寄存器到内核栈
    • 发ACK到PIC,使能中断
    • 执行该IRQ上的所有ISR。
    • 跳到ret_from_inir 结束。
  • 数据结构
    • 每个IRQ一个链表,每个节点内是某个设备的中断服务程序(ISR); 所有链表在一个数组irq_desc里。
    • 链表头的内容:
      • PIC相关的:
        • handler : 指向相关的PIC对象。PIC对象:对硬件PIC封装而成的一个数据结构,使在编写驱动时不用考虑PIC硬件差异。
          • 名字: name
          • 方法:
            • startup, shundown
            • enable disable
            • ack - 给PIC发ACK
            • end - 告诉PIC处理完成
            • set_affinity - 多CPU时,该PIC倾向处理某些特定的IRQ
        • handler_data :PIC对象里的函数用到的数据
        • action:这个IRQ上的所有ISR, 以链表形式。每一个节点对应一个设备,节点的结构irq_action:
          • name - 设备名
          • dev_id - 设备号 (主设备号,副设备号)
          • hander - ISR
          • irq - irq线号
          • dir - irq线所在路径/proc/irq/n
          • flags - 描述IRQ线与设备的关系
            • SA_INERRUPR - 该handler, 不可被中断
            • SA_SHIRQ - 该设备允许IRQ共享
            • SA_SAMPLE_RADOM - 该设备可以所谓随机数发生器。(FIXME 跟中断有什么关系)
          • mask- 没用
          • next - 下一个节点指针
      • 多CPU合作: lock, 互斥锁
      • 状态位的组合: status
        • IRQ_INPROGRESS: 正被处理
        • IRQ_DISABLE - 该IRQ被禁用了
        • IRQ_PENDING - 发回ACK了,未服务
        • IRQ_REPLAY - 当该IRQ刚要被处理,还未上锁时,被其他CPU禁用了。然后被另外的程序手动置成该状态, 表示重发
        • IRQ_AUTODETCT -
        • IRQ_WAITING -
        • IRQ_LEVEL:x86架构中不用
        • IRQ_PER_CPU:x86架构中不用
        • IRQ_MASKED:不用
      • depth:IRQ被禁用的层次
        • disable_irq()时, depth++,enable_irq()时, depth--。
        • 遇到0时,才真正disable或enable IRQ。 0位,使能状态。
      • 统计信息,用来防止意外中断:
        • irq_count:中断发生总次数
        • irqs_unhandled:没处理中断次数。IRQ上的ISR都不识别,或是该IRQ上没有ISR
        • 如果某个IRQ线,总有意外中断,禁用该IRQ线。
        • 根据未处理的中断次数,和中断总数来判断。例如每100,000个中断,有99000是没有处理的,就禁用改中断线。
  • 中断处理:
    • 保存寄存器
      • 入栈:向量减去256。 (大于256的向量是系统调用)
      • 要保护的寄存器入栈 (再加上下步“装载用户数据” 组成:SAVE_ALL宏)
    • 装载用户数据段
      • (__USER_DS) 放到 ds和es
    • 准备参数: 栈指针 -> eax
      • eax既是栈指针,又指出了保存的寄存器的值
    • 调用do_IRQ (参数在eax中): 起封装“栈切换”的作用
      • 嵌套层次++;(thread_info.preempt_count)
      • 如果内核栈是4K,检查是否切到了硬件中断栈。如果没有,切换到硬件中断栈。
        • 检查:通过esp确定当前使用的内核栈基地址 ( current_thread_info() ), 再与hardirq_ctx存的地址比较。
        • 栈切换:存当前的pd, esp到irq_ctx中, 装入irq_ctx对应的esp.
      • 调用__do_IRQ() (解决多CPU问题,每次改变status都有加锁)
        • 清除设置相关位。判断有无其他CPU正在处理当前IRQ上的中断。
          • 如果有了,当前交由那个CPU处理,本CPU仅设置pending位,不处理。
          • 如果没有,本CPU反复调用handle_IRQ_event()处理,直到没有CPU再设置pending(把处理交给自己)。handle_IRQ_event:
            • 如果该IRQ可以被中断,使能中断(sti指令);
            • 链表上所有的handler都执行;
            • 禁中断(cli指令);
            • 返回是否有handler成功执行(用来统计无效中断)
      • 如果栈切换了,切回原来的栈
      • 执行irq_exit宏:减少嵌套层次,执行可延迟函数
      • 用ret_from_intr()结束中断处理
  • 遗漏的中断
    • 某个CPU刚选中某个中断,还未上锁。就被另一个CPU禁用中断了。前者只设置pending,没有进入处理的循环
    • enable_irq()的时候,如果pending了但还没有重发(replay位0);就设置replay位,让本地APIC给自己发中断
  • 中断分类:I/O中断, timer, CPU之间
    • CPU间的三个中断(IPI):
[tr]向量号(_VECTOR左边的是中断名字)目标CPU作用接收者的动作附加说明[/tr]
CALL_FUNCTION_VECTOR(0xfb) 除发出者外的所有CPU 使接收者执行某一个函数 发回ack, 执行函数 函数指针call_data里
RESCHEDUELE_VECTOR(0xfc) 某一个,某几个CPU 使其调度 发回ack, 调度
INVALID_TLB_VECTOR(0xfd) 所有CPU 强制其TLB无效
    • 发射函数: send_IPI_xyz
目标CPU 所有 除了自己 自己 指定
函数xyz all allbutself self maks
    • 处理:BUILD_INTERRUPT宏,内部调C函数smp_name_interrupt() (name是中断名字)
  • 软件上IRQ线的分配(在链表里插入节点):
    • 创建一个irq_action节点:request_irq(irq_num, ISR, flags,name, ......)
    • 插入到相应链表: setup_irq().
      • 检查已有节点是否有SA_SHIRQ属性
      • 插入链
      • 如果是该链上第一个节点,运行PIC对象的setup方法,使能这个IRQ号
    • 释放一个节点(FIXME: 链表上所有节点?):free_irq
  • irq信号在多CPU间分配
    • 对称多处理:
      • 每个CPU执行中断处理时,时间片相同。
      • 每个CPU轮流得到irq信号.
        • 硬件支持仲裁优先级变换:
          • local APIC 初始化,任务优先级都相同。
          • 仲裁优先级不断变换,实现轮流得到irq
        • 如果硬件不支持仲裁优先级变换:
          • 用修改I/O APIC的重定向表实现。由kirqd 线程实现, 根据irq_stat来平衡。
            • 调用set_ioapic_affinity_irq(n, mask),修改重定向表。(用户可通过/proc/irq/n/smp_affinity修改)
              • n:中断向量
              • 32bit mask:哪几个CPU可以收到irq。
          • 类型位irq_stat的irq_cpustat_t数组,每个CPU一个元素。保存每个CPU最近处理irq的情况。用于平衡每个CPU的irq处理。
            • __softirq_pending: 悬挂的软中断个数
            • idle_timestamp:只有CPU当前是idle时才有意义,开始idle的时间
            • __nmi_count:处理nmi中断的次数
            • apic_timer_irqs:处理局部apic 时间中断的次数
    • 链表数组初始化 init_irq():
      • 所有的IRQ都置成IRQ_DISABLE状态。用中段函数设置中断入口(中断描述表里的,从32开始)。


softirq和tasklet
  • 可延迟函数
    • ISR要求必须串行执行,而且经常需要中途无中断发生;但是可延迟任务中途允许中断。
    • 中断上下文: 中断handle和softirq
    • 分类:
      • 可延时函数包括softirq和tasklet。
      • 在数据结构上,tasklets在softirq的结构内部,所有经常统称softirq.
        • softirq:
          • 静态分配,即编译时就确定了。
          • 同类可并行,可同时在多CPU执行
            • 自旋锁保护临界区
          • 可重入。
        • tasklet反之。使驱动编写简单。
    • 基本操作:
      • 初始化:定义一个新的可延迟函数。一般在kernel初始化,或添加一个模块的时候用
      • 激活:设一个可延迟函数的状态为pending. 表示希望被执行
      • 屏蔽:disable一个可延迟函数
      • 执行:在某个检查点,检查有无pending的函数,并执行它
        • 对于一个可延迟函数,执行它的CPU必须是激活它的CPU。为了硬件cache考虑
  • softirq
    • 数据结构:softirq_action softirq_vec [32], 32元素的数组,执行时从0开始,0的优先级最高。
      • action: 函数指针
      • data: 通用指针,action的参数
    • 设置,即把一个函数设置在softirq_vec的一个元素:open_softirq(indx, action, data)
    • 激活,raise_softirq(index).
      • 设置本softirq的pending位,检查thread_info.preempt_count中的softirq次数与hardirq次数,是否都是零。
        • thread_info.preempt_count 。四个数的位或
          • 抢占次数:抢占被禁用次数
          • softirq次数:softirq被禁用次数
          • hardirq次数:irq处理嵌套次数
          • PREEMP_ACTIVE标志位
          • 方便:只要检查preempt_count==0, 就可判断是否内核可抢占。
        • 多内核栈时,用irq_ctx内的preempt_count, 但它一般是个正数。FIXME: 那何时激活?
      • 如果都为零,说明不嵌套在中断上下文中。调用wakeup_softirqd()来唤醒执行软中断的内核线程ksoftirqd
        • 内核线程ksoftirqd/n (n指某个CPU, ksoftirqd每个CPU一个,FIXME: 且可以迁移),不断检查,执行。
        • 其他softirq的执行点(只执行一次):
          • 使能本地CPU的softirq:local_bh_enable()
          • 中断处理完成后:
            • do_IRQ完成处理后:irq_exit宏内
            • 处理完本地timer中断后
            • 处理完IPI: CALL_FUNCTION_VECTOR以后
      • raise_softirq的开始、结束时要禁止中断,恢复中断
    • 执行
      • 一次执行: do_softirq()
        • 前期准备:检查preempt_count, 禁止中断。如果时多内核栈,且不再softirq栈,切到softirq栈
        • __soft_irq真正执行:
        • 后期收尾:恢复以前的栈,恢复中断
      • softirqd不断检查、执行
        • softirqd不断被唤醒;反复检查有无激活的函数并执行;直到没有pending的函数,进入可中断睡眠。
        • 每次执行调do_softirq(前后要禁用、启用抢占); 然后cond_resched(如果current thread_info的TIF_NEED_RESCHED为1,就执行调度)
  • tasklet
    • 通过链表组织的可延迟函数。
    • 每CPU一个链表,所有链表在链表数组里。按优先级分成两个链表数组(优先级高函数有"hi")。
    • softirq_vec的前两个元素,保存链表指针和遍历链表的函数指针
    • 节点tasklet_struct:
      • state: 2个位:
        • TASKLET_STATE_SCHED: 表示pending
        • TASKLET_STATE_RUN: 表明正在执行,多CPU时才有用
      • count:禁用次数。FIXME: 为什么其他softirq没有单独的count
      • func
      • data
      • next
    • 接口:
      • 分配一个节点:tasklet_init()
      • 禁用、使能: tasklet_disable_nosyns, tasklet_disable, tasklet_enable
      • 激活tasklet_schedule、tasklet_hi_schedule
        • 设置pending位,添加到相应表头,激活softirq_vec中的相应元素。后两步要禁用本地中断
      • 执行:就是softirq_vec里前两个元素的函数是tasklet_hi_action, tasklet_action
        • 拷贝清空本地CPU的链表头指针,禁中断下进行
        • 遍历链表:
          • TASKLET_STATE_RUN的节点:表示现在有其他CPU在执行这个函数。在链表数组里本地CPU的位置,重新插入该节点。
          • count>0的节点:被禁用了,重新插入该节点
          • count<=0的节点:清除pending位,执行函数。注意:每个函数最多执行一次,执行完了不再插入列队
  • work queue(工作列队)
    • 内核函数被激活,稍候被特殊的内核线程(worker thread)执行。
    • 与可延迟函数的区别:工作列队里的函数可以阻塞
    • 数据结构
      • 一个workqueue_struct,包含一个多CPU数组。每一个元素是一个链表头(cpu_workqueue_struct):
        • lock
        • wq:上级指针
        • worklist: 函数链表。节点结构如下:
          • pending:
          • entry: 内嵌链表
          • wq_data: 上级指针
          • timer: 用于延迟插入的软件定时器
          • func: 函数指针
          • data: 通用指针,函数func的参数。
        • thread:执行该链表函数的工作线程
        • run_depth:
        • more_work:等待列队,等待新的函数的工作线程阻塞在这里
        • work_done:等待列队, 等待工作列队flush完毕的进程阻塞在这里
        • remove_sequence:用于判断哪些哪些函数是flush以后才来的。
        • insert_sequence:用于判断哪些哪些函数是flush以后才来的。
    • 接口
      • 创建工作列队:
        • create_workqueue: 一共NR_CPU个工作线程,每个链表一个。每个线程都可以到任意的CPU执行
        • create_singlethread_workqueue:只有一个工作线程。
      • 插入一个函数:
        • queue_work():把一个节点插入列队,设置pending位。(插入到local CPU的链表)
        • queue_delayed_work():多一个timer参数,延迟插入。(设置一个timer,定时器到了再插入)
        • cancel_delayed_work(): 取消插入,必须在实际插入之前调用。
      • 执行:
        • 每一个工作线程都进入worker_thread()内部的一个循环中。
        • 大部分时间睡眠,有函数时唤醒,唤醒后调run_workqueue(摘下该线程对应链表的所有的节点,执行里面的pending函数)
      • 等待执行完:
        • flush_work_queue: 阻塞当前进程,直到所有的pending函数执行完毕。
        • 但不包括新来的函数。用remove_sequece, insert_sequece两个计数判断哪些是新来的函数
    • 内核预定义的工作列队events: 包含不同kernel层的函数和I/O驱动。 在keventd_wq数组里存着不同的工作列队
从中断和异常中返回
  • 如果内核支持抢占,那么返回时禁中断
    • 中断处理末尾就是禁中断的,所以从异常返回时要首先禁中断。
  • 如果有嵌套的KCP(kernel control path),且不是虚拟8086模式,则恢复kernel。否则恢复用户空间
    • 判断栈中保存的cs的权限位,和eflags的VM位
    • 恢复到内核后:
      • 在允许抢占(thread_info.preempt_count==0)的情况下:如果以下两个条件都满足,执行抢占调度让出CPU(preempt_schedule_irq)。否则恢复上层KCP
        • 有等待调度的进程(current->thread_info.flags的TIF_NEED_RESCHED被设置了)。
        • 上层要恢复的KCP允许中断(本级是异常时,才有可能“要恢复的KCP不允许中断”)。
        • 被抢占,再次获得CPU后,要重新检查上述两条件
    • 恢复到用户空间:
      • 检查有无剩余事情要做(调度、恢复虚拟8086状态,悬挂信号、恢复单步执行,):
        • 如果有等待调度的进程,调度(schedule())。再次获得CPU后,重新检查。直到没有新来的调度请求了。
        • 如果要恢复虚拟8086状态,在用户空间建立相应的数据结构(FIXME: 以后研究)
        • 处理其他的剩余事情(悬挂信号、恢复单步执行)。
  • 恢复:
    • SAVE_ALL保存的寄存器出栈。
    • iret指令。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics