中断和异常
中断异常的硬件处理
- 按发射中断信号的时机分为“中断”和“异常”
- 中断(又叫异步中断):由设备的硬件寄存器(定时器、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线 LINT0 LINT1 - 给本地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内有一个本地的APIC(local APIC)。local APIC内部:
- 分为
- 异常的产生
- CPU探测到的:
- 故障(fault):可被修正,存的是当前eip, 需重新执行。如:页错误。
- 计算
- 除零
- 浮点数错误
- SIMD浮点异常
- 指令和内存
- 操作符无效
- 操作数地址无效
- 内存对齐错误
- 段、页
- 段越界
- 无效的TSS
- 段不存在
- 页错误
- 保护异常
- 其他
- 二次故障
- 设备无效
- 机器检查
- 计算
- 陷阱(trap):不用修正,处理完后执行下一条指令。存下一条指令地址。如:调试断点。
- 调试:当设置了TF位时,(地址在某个指定范围内就触发,debug寄存器存地址范围)
- 断点:int3指令触发。断点指令
- 溢出:当OF位设置了时,into指令触发
- 流产(abort):严重错误(硬件失败,或是系统数据出现错误); 或是CPU控制单元出错,无法保存eip。
- 处理程序能做的只有发信号终止进程
- 故障(fault):可被修正,存的是当前eip, 需重新执行。如:页错误。
- 程序设定的异常:
- 程序执行引发异常的指令
- 用于实现系统调用,或调试。
- 2号异常用于不可屏蔽中断
- CPU探测到的:
- 中断向量号分配[0, 255)
- 物理IRQ对应中断向量[32, 238]
- IBM兼容机, 某些设备的中断必须静态连接到特定的中断线上
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(禁止可屏蔽中断)。
- 用户态可以访问的叫“系统中断入口”:
- 描述符类型如下(默认都是内核态访问):
- 描述符格式:类型(第40-43bit表示类型),段选择符(全局段表中),偏移量, DPL,等。不同类型格式不太一样。
int3 | 3(断点) |
-
-
-
- 陷阱入口:控制转移后,不清除IF flag
- 用户态可以访问的叫“系统入口”:
- 陷阱入口:控制转移后,不清除IF flag
-
-
into | 4(溢出) |
bound | 5(地址边界检查)) |
int $0x80 | 128(系统调用) |
-
-
- 地址的内容:是一段汇编代码。内容大致是:
-
-
- 中断描述符表(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
- set_XXX_gate (n, addr)
- 设置中断描述符的函数
- 中断描述符表(IDT):255个描述符,地址在idtr中,lidt指令加载idtr.
- 中断和异常的硬件处理
- 中断进入
- 每执行完一条指令,检查有无中断
- 执行完一条指令之后, 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完全一样的的结构
- 异常栈 - 每进程一个, 即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的方式 返回
- 存大部分寄存器值到内核栈,调真正的handler(C函数,声明寄存器传参)。细节部分除了“设备无效”异常(FPU、页)外,都相同:
- 二次失败异常,用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共享。
- 中断到得很晚,与当前进程无关 - 不能用信号机制。 中断处理分成三部分:
- 四个基本动作:
- 保存IRQ,和寄存器到内核栈
- 发ACK到PIC,使能中断
- 执行该IRQ上的所有ISR。
- 跳到ret_from_inir 结束。
- 数据结构
- 每个IRQ一个链表,每个节点内是某个设备的中断服务程序(ISR); 所有链表在一个数组irq_desc里。
- 链表头的内容:
- PIC相关的:
- handler : 指向相关的PIC对象。PIC对象:对硬件PIC封装而成的一个数据结构,使在编写驱动时不用考虑PIC硬件差异。
- 名字: name
- 方法:
- startup, shundown
- enable disable
- handler : 指向相关的PIC对象。PIC对象:对硬件PIC封装而成的一个数据结构,使在编写驱动时不用考虑PIC硬件差异。
- PIC相关的:
-
-
-
-
-
- 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成功执行(用来统计无效中断)
- 清除设置相关位。判断有无其他CPU正在处理当前IRQ上的中断。
- 如果栈切换了,切回原来的栈
- 执行irq_exit宏:减少嵌套层次,执行可延迟函数
- 用ret_from_intr()结束中断处理
- 保存寄存器
- 遗漏的中断
- 某个CPU刚选中某个中断,还未上锁。就被另一个CPU禁用中断了。前者只设置pending,没有进入处理的循环
- enable_irq()的时候,如果pending了但还没有重发(replay位0);就设置replay位,让本地APIC给自己发中断
- 中断分类:I/O中断, timer, CPU之间
- CPU间的三个中断(IPI):
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。
- 调用set_ioapic_affinity_irq(n, mask),修改重定向表。(用户可通过/proc/irq/n/smp_affinity修改)
- 类型位irq_stat的irq_cpustat_t数组,每个CPU一个元素。保存每个CPU最近处理irq的情况。用于平衡每个CPU的irq处理。
- __softirq_pending: 悬挂的软中断个数
- idle_timestamp:只有CPU当前是idle时才有意义,开始idle的时间
- __nmi_count:处理nmi中断的次数
- apic_timer_irqs:处理局部apic 时间中断的次数
- 用修改I/O APIC的重定向表实现。由kirqd 线程实现, 根据irq_stat来平衡。
- 硬件支持仲裁优先级变换:
- 链表数组初始化 init_irq():
- 所有的IRQ都置成IRQ_DISABLE状态。用中段函数设置中断入口(中断描述表里的,从32开始)。
- 对称多处理:
softirq和tasklet
- 可延迟函数
- ISR要求必须串行执行,而且经常需要中途无中断发生;但是可延迟任务中途允许中断。
- 中断上下文: 中断handle和softirq
- 分类:
- 可延时函数包括softirq和tasklet。
- 在数据结构上,tasklets在softirq的结构内部,所有经常统称softirq.
- softirq:
- 静态分配,即编译时就确定了。
- 同类可并行,可同时在多CPU执行
- 自旋锁保护临界区
- 可重入。
- tasklet反之。使驱动编写简单。
- softirq:
- 基本操作:
- 初始化:定义一个新的可延迟函数。一般在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: 那何时激活?
- thread_info.preempt_count 。四个数的位或
- 如果都为零,说明不嵌套在中断上下文中。调用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的开始、结束时要禁止中断,恢复中断
- 设置本softirq的pending位,检查thread_info.preempt_count中的softirq次数与hardirq次数,是否都是零。
- 执行
- 一次执行: do_softirq()
- 前期准备:检查preempt_count, 禁止中断。如果时多内核栈,且不再softirq栈,切到softirq栈
- __soft_irq真正执行:
- 后期收尾:恢复以前的栈,恢复中断
- softirqd不断检查、执行
- softirqd不断被唤醒;反复检查有无激活的函数并执行;直到没有pending的函数,进入可中断睡眠。
- 每次执行调do_softirq(前后要禁用、启用抢占); 然后cond_resched(如果current thread_info的TIF_NEED_RESCHED为1,就执行调度)
- 一次执行: do_softirq()
- 数据结构:softirq_action softirq_vec [32], 32元素的数组,执行时从0开始,0的优先级最高。
- tasklet
- 通过链表组织的可延迟函数。
- 每CPU一个链表,所有链表在链表数组里。按优先级分成两个链表数组(优先级高函数有"hi")。
- softirq_vec的前两个元素,保存链表指针和遍历链表的函数指针
- 节点tasklet_struct:
- state: 2个位:
- TASKLET_STATE_SCHED: 表示pending
- TASKLET_STATE_RUN: 表明正在执行,多CPU时才有用
- count:禁用次数。FIXME: 为什么其他softirq没有单独的count
- func
- data
- next
- state: 2个位:
- 接口:
- 分配一个节点: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以后才来的。
- 一个workqueue_struct,包含一个多CPU数组。每一个元素是一个链表头(cpu_workqueue_struct):
- 接口
- 创建工作列队:
- 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后,要重新检查上述两条件
- 在允许抢占(thread_info.preempt_count==0)的情况下: 如果以下两个条件都满足,执行抢占调度让出CPU(preempt_schedule_irq)。否则恢复上层KCP
- 恢复到用户空间:
- 检查有无剩余事情要做(调度、恢复虚拟8086状态,悬挂信号、恢复单步执行,):
- 如果有等待调度的进程,调度(schedule())。再次获得CPU后,重新检查。直到没有新来的调度请求了。
- 如果要恢复虚拟8086状态,在用户空间建立相应的数据结构(FIXME: 以后研究)
- 处理其他的剩余事情(悬挂信号、恢复单步执行)。
- 检查有无剩余事情要做(调度、恢复虚拟8086状态,悬挂信号、恢复单步执行,):
- 恢复:
- SAVE_ALL保存的寄存器出栈。
- iret指令。