操作系统-异常处理流
Posted living_frontier
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统-异常处理流相关的知识,希望对你有一定的参考价值。
一、总论
1.1 软硬件分工
首先先明确,异常处理流是一个需要软硬件协作的流程。如果仅仅掌握了计组的知识,是没有办法对于异常处理流有一个很好的认识的。但是如果掌握了操作系统知识,而没有掌握计组知识,则不会有任何的实现负担。这是因为在 CPU 层面已经将异常处理流的硬件部分封装起来了(整个封装过程就是 P7,可以看到 P7 还是很难的),我们只要了解一下 CPU 提供给操作系统的接口,就可以利用这些接口提供的信息和功能完成异常处理流的软件部分。这是完全没有问题的。
我们有如下示意图:
其中蓝色的是硬件部分(也就是计组要解决的问题),黄色的是软件部分(也就是操作系统),其中红色的部分,就是两个部分的接口。
这幅图最重要的就是搞懂那两个接口寄存器的意义。Cause 为选择哪一个异常处理函数提供了决策的依据,EPC 为有朝一日返回这个进程接着执行提供了支持(注意异常处理完成后不一定会返回异常发生时所在的进程,比如由于时钟中断导致的进程切换)。
1.2 明确概念
这个问题其实在计组实践的时候,就已经出现了。因为我们要处理的 “意外情况” 特别的多,所以我们需要一些名词来描述概念,但是往往会有一些因为名词冲突,而导致概念混淆的情况。所以在开始文章之前,还是先介绍一下概念名词,这些名词只在这篇文章中有效。另外声明,这里的概念定义跟我在计组P7博客里的概念是冲突的(只能说年少无知太轻狂)。
概念有如下关系
所有的“控制流异常情况的原因”统称为“异常”,同步异常指的是处理器执行某条指令而导致的异常,异步异常指的是异常触发的原因与处理器当前正在执行的指令无关的异常。更低一级的概念并不能保证正确性。
1.3 异常处理流程
这里给出异常处理的一般流程
二、硬件部分
2.1 计组 CP0 结构
这里给出上个学期我实现 P7 的时候的源码,但是需要注意的是计组实现的 CPU 架构好像是 Mips32,与操作系统用的 R3000 并不相同,CP0 的结构有一些细节上的差异。
这是 CP0 的实现。
`timescale 1ns / 1ps
`define IM SR[15:10]
`define EXL SR[1]
`define IE SR[0]
`define BD Cause[31]
`define IP Cause[15:10]
`define ExcCode Cause[6:2]
module CP0
(
input clk,
input reset,
input en,
input [4:0] CP0Add,
input [31:0] CP0In,
output [31:0] CP0Out,
input [31:0] VPC,
input BDIn,
input [4:0] ExcCodeIn,
input [5:0] HWInt,
input EXLClr,
output [31:0] EPCOut,
output Req,
output tbReq
);
reg [31:0] SR, Cause, EPC, PrID;
//为了测试约定
assign tbReq = (HWInt[2] & SR[12] & (~`EXL) & `IE);
wire IntReq = (|(HWInt & `IM)) & (~`EXL) & `IE;
//无论有没有全局中断使能,都要响应异常
wire ExcReq = (|ExcCodeIn) & (~`EXL);
assign Req = IntReq | ExcReq;
//如果发生了中断,就要给更新 EPC,否则保持原值
wire [31:0] nextEPC = (Req)? ((BDIn)? VPC - 4 : VPC) :
EPC;
assign EPCOut = EPC;
assign CP0Out = (CP0Add == 5'd12)? SR :
(CP0Add == 5'd13)? Cause :
(CP0Add == 5'd14)? EPC :
(CP0Add == 5'd15)? PrID :
0;
initial begin
SR <= 0;
Cause <= 0;
EPC <= 0;
PrID <= 32'hDEAD_C0DE;
end
always @(posedge clk) begin
if(reset) begin
SR <= 0;
Cause <= 0;
EPC <= 0;
PrID <= 32'hDEAD_C0DE;
end
else begin
if(EXLClr) `EXL <= 0;
if(Req) begin //相当于Req来临的时候,是没有办法用mt来写东西的
`ExcCode <= IntReq ? 5'b0 : ExcCodeIn;
`EXL <= 1;
EPC <= nextEPC;
`BD <= BDIn;
end
else if(en) begin
if(CP0Add == 12) SR <= CP0In;
//if(CP0Add == 13) Cause <= CP0In;
if(CP0Add == 14) EPC <= CP0In;
//if(CP0Add == 15) PrID <= CP0In;
end
`IP <= HWInt;
end
end
endmodule
这是 CP0 的布线
CP0 cp0
(
.clk(clk),
.reset(reset),
.en(CP0En),
.CP0Add(M_rd),
.CP0In(M_FWrtOut),
.CP0Out(M_CP0Out),
.VPC(M_PC),
.BDIn(M_BD),
.ExcCodeIn(M_ExcCode),
.HWInt(HWInt),
.EXLClr(EXLClr),
.EPCOut(M_EPCOut),
.Req(Req),
.tbReq(tbReq)
);
2.2 SR 寄存器
这个寄存不是接口!!!也就是说,这个寄存器不需要给操作系统看到。那么这个寄存器在异常处理方面是干啥的呢(它不止在异常流控制这一个方面发挥作用),其实是控制异常的。也就是说,所有的异常,都需要经过和 SR 寄存器某些位的加工处理以后,才可以将生成的数据放在 Cause 中提供给操作系统。
在计组中,表现为这样的形式:
`define IM SR[15:10]
`define EXL SR[1]
`define IE SR[0]
wire IntReq = (|(HWInt & `IM)) & (~`EXL) & `IE;
wire ExcReq = (|ExcCodeIn) & (~`EXL);
assign Req = IntReq | ExcReq;
可以看到,SR 主要提供中断的掩码 IM
和中断使能 IE
,异常等级 EXL
三个位。之后通过这些位的运算后来确定到底要不要发出异常信号和更新 EPC 之类的一系列处理。重点其实就是掩码与使能。因为操作系统使用的是 R3000,所以重点介绍 R3000 的结构
虽然有很多位,而且每一位都很重要,但是其实真正用到的没有几个,首先是 IM
,他是中断掩码,也就是说,R3000 有 8 个外部中断源,只有该位置 1 ,对应的外部中断才会被引发异常,否则不会。
然后就是所谓的二重栈结构,这个结构在 SR[5:0]
,首先解释一下,KU
如果置 1,就说明当前进程处于内核态( 内核态可以使用的指令更多,访问的地址空间更大),IE
如果置 1,这说明当前允许异常发生(注意是异常,而不是中断,范围扩大了)。然后在解释一下下标,o
是 old 的意思,p
是 previous 的意思,c
是 current 的意思。
然后介绍一下这个所谓的二重三级栈,其实就是当发生异常的时候,previous 的内容会被拷贝到 old 中,current 的内容会被拷贝到 previous 中。然后当 eret
指令一下达,previous 的内容会被拷贝到 current 中,old 中内容会被拷贝到 previous 中。
关于这个神奇的栈设置,还是妄谈几句。R3000 应该是不希望允许嵌套中断的,所以当处理中断的时候,先把 IEc 设置为 0 ,这样就不允许其他的中断在处理这个中断的时候发生了,屏蔽了其他中断。但是似乎不允许嵌套中断,CPU 功能又实现不了了,如果缺页中断中又有一个缺页中断,那么就崩了(具体我不知道,下面截图),所以为了支持嵌套中断,所以必须有所记录。就跟我们用栈来支持函数嵌套一样,我们同样需要栈结构来支持,EPC
似乎自己找地方去了,然后 SR
这两位就是用这个栈结构。
2.3 Cause 寄存器
Cause 寄存器里的内容由 CP0 生成,然后操作系统会访问这个值,我们在计组中,有
`define BD Cause[31]
`define IP Cause[15:10]
`define ExcCode Cause[6:2]
if(Req) begin //相当于Req来临的时候,是没有办法用mt来写东西的
`ExcCode <= IntReq ? 5'b0 : ExcCodeIn;
`EXL <= 1;
EPC <= nextEPC;
`BD <= BDIn;
end
`IP <= HWInt;
可以看到,Cause 里比较重要的一个域就是 ExcCode
用来记录到底是哪一种异常。这样操作系统才可以根据信息来选择不同的异常处理函数去处理。
对于R3000架构来说,Cause 寄存器如图:
BD 这个域同样是接口,如果异常指令是一个分支延迟槽指令,那么这位就会被置 1。这是因为当异常指令为延迟槽指令的时候,EPC
的值会变成延迟槽指令的前一条指令(为了程序的正常执行)。我们在软件侧可以利用 BD
来获得真正发生异常的指令。
IP 是 Interrupt pending 的意思,他的意思结合代码来看就很容易,它记录的是现在发生的中断,但是这些中断不一定被响应,还有看 IM 的设置。IP 只是一个单纯的记录者
`IP <= HWInt;
最重要的是 ExcCode,异常码,CPU 会根据异常的不同,将这个域设置成不同的值,操作系统通过读取这里的值,就可以获得这次异常的信息,R3000 常用的异常码如下:
2.4 EPC 寄存器
计组 CP0 跟 R3000 这个是一样的,可以看代码
wire [31:0] nextEPC = (Req)? ((BDIn)? VPC - 4 : VPC) :
EPC;
就是保存一下当前的 PC,然后方便返回。
三、软件总论
3.1 文章结构
本文会分析先分析异常处理的共性,然后会每一章介绍一个异常的具体流程。
因为异常是一个很宏大的主题,期间逻辑错综复杂,所以我并没有能力把他们组织成一个逻辑完备的整体,所以可能有的时候只能放源码解读,而不是有理有据的总结。这个摆烂的部分可以看做是之前博文《MOS源码解读》的 lab3 和 lab4 部分。
我们要讲的异常处理函数有如下几个:
异常处理函数 | 编号 | 解释 |
---|---|---|
handle_reserved | 无 | 没有内容,用于初始化异常分发矩阵 |
handle_int | 0 | 用于处理时钟中断,主要是换进程调度 |
handle_mod | 1 | 当尝试写一个只读的虚拟页面的时候会触发,主要会进行一个写时复制处理 |
handle_tlb | 2 | 当 TLB 缺失的时候会触发,会把需要的页表项调入 TLB |
handle_tlb | 3 | 似乎与上面相同 |
handle_sys | 8 | 当使用 syscall 指令的时候调用,会根据系统调用号去决定异常处理的功能 |
3.2 直观理解
异常处理真的是很复杂,但是感觉第一次提出异常处理流并且利用它构建了各个抽象概念,比如进程的时候,简直就像在欣赏一件艺术品一样,实在是感到十分震撼。
我个人觉得异常处理跟进程抽象联系还是很紧密的,或者说是内核态用户态转换是很紧密的。异常处理的本质是异常处理流。换句话说,就是程序计数器的变化,执行指令的变化。通过这种异常处理,我们可以实现在各个功能的实现间的跳转。我们把一部分功能交给操作系统去实现,而让用户进程只考虑跟自己有关的事情。
落实到比较具体的,就是在异常处理的时候 PC 会怎么变化,为了恢复现场会怎么做,我们需要新的栈空间吗?之类的问题,是我们要关注的重点。
四、异常的共性
4.1 普遍流程
在 lib/genex.S
中有一个异常处理函数的构建宏,它总结了一些异常处理函数的普遍规律,我们可以通过这个函数来看一下异常的共性
.macro BUILD_HANDLER exception handler clear
.align 5
NESTED(handle_\\exception, TF_SIZE, sp)
nop
SAVE_ALL
__build_clear_\\clear
.set at
move a0, sp
jal \\handler
nop
j ret_from_exception
nop
END(handle_\\exception)
.endm
这里面虽然函数我都没有解释,但是从名字可以看出来,里面大概是先通过 SAVE_ALL
来保存现场,然后通过_build_clear_\\cli
来禁用中断,然后会直接到达一个真正的 handler
,这是各个异常处理的差异性体现,这个 handler 会接受一个参数,我们回头去讲。最后会通过 ret_from_exception
进行一个异常的返回。
需要强调的是,并不是所有异常处理都用了这个统一的模板,用这个模板的只有下面这几个
BUILD_HANDLER reserved do_reserved cli
BUILD_HANDLER tlb do_refill cli
BUILD_HANDLER mod page_fault_handler cli
但是其他的,比如说 handle_int
和 handle_sys
并没有使用这个模板处理。但是更需要强调的是,即使没有使用这个模板,但是他们的流程基本上也与模板类似,比如 handle_int
中就有如下代码片段,可以看出与模板是很类似的。
SAVE_ALL
CLI
j sched_yield
j ret_from_exception
但是显然光看函数和宏的名字是没有办法来了解异常处理流程的,这里先简单介绍一下我们大致有干什么,然后再结合每一章进行介绍:
- 选择内核栈:这是因为异常一定是由一个原来的进程转换到操作系统进程的,那么之前使用的栈肯定是不能用了(有可能异常的原因就是栈对应的虚拟页面 TLB 缺失),我们需要一个新的栈来保存上下文,完成异常处理函数的各个子函数的调用。
- 在内核栈上保存上下文:这里保存的上下文就是指通用寄存器、乘除寄存器和一些协处理器的寄存器(在 MOS 中这些东西被打包成
Trapframe
结构体),这些寄存器的值都来自发生异常的用户进程。我们要保存他们是因为我们的异常处理函数可能会覆盖这些值,而我们又需要这些值。主要是因为我们异常处理的时候有时候会用到他们(通过上下文指定政策),而我们在结束处理后还需要将其恢复(但是并不一定完全不变)。 - 禁用中断:MOS 并不支持嵌套中断,所以进入异常后就会把中断使能端关掉。
- 进行异常处理:这个根据具体情况分析,是特异性的体现。
- 恢复栈指针:因为之前使用了内核栈,所以我们现在要进行一个恢复,不过其实可以看做恢复上下文的一部分。
- 恢复上下文:将内核栈上保存的用户进程上下文恢复。
- 跳转回用户进程:将
PC
设置为EPC
,跳转回用户进程。
下面就会结合这个流程来进行进行介绍。
4.2 选择内核栈
4.2.1 内核中的栈
选择内核栈是通过重置栈指针实现的。为什么要重置栈指针?这是因为此时计算机刚刚处于异常后的状态,虽然 PC 已经被纠正到了 0x80000080
。但是栈指针还是用户进程的栈指针,这显然是不能正常发挥作用的,我们需要将栈指针设置成一个内核栈指针。
在内核上我们有三个栈(先别纠结为啥仨栈),一个栈的栈底是 0x8040_0000
一个栈的栈底是 KERNEL_SP
(在 settimer
中设置 )另一个栈的栈底是 TIMESTACK
。他们在内存中的分布是这样的
我们说这几个栈的作用是不一样的,Kernel Init Stack
是初始化操作系统的时候用到的栈,之后就不会再使用了,因为这个栈上没法减少,也就没法重复利用。
Kernel Stack
是在原来的栈上新增出的栈,这个栈的栈底是变量 KERNEL_SP
,这个变量在 set_timer
这个函数中设置
sw sp, KERNEL_SP
他将这个时刻的栈指针存储在一个叫做 KERNEL_SP
的变量中,这个变量就定义在 env_asm.S
中,就一个字大小。
为什么要存储这个栈指针,是因为当发生异常的时候,不仅 PC 需要从用户进程更新到操作系统的指令流中,同时栈指针也需要调整到操作系统的栈,这就需要我们确定一下异常之后操作系统的栈指针应该在那里,那么就应该在这里。这是因为 set_timer
被 klock_init
调用,而 mips_init
的最后一个函数调用就是 klock_init
。之后会执行一个死循环,直到被时钟中断改变。所以此时的栈结构是这样的:
第三个栈是 TIMESTACK,这个栈主要是处理时钟中断的时候需要使用。这里讨论一下为啥内核需要两个栈(用于异常处理),我初步的结论(与叶哥哥和郭哥哥讨论过后)觉得一个就够了,没必要用两个。姜姐姐提供了一种说法,认为这两种栈的设计可能是一个未完成的设计,这个设计可能是为了支持在进行其他异常处理的时候也可以响应时钟中断,但是没有完成这个设计,所以现在的 MOS 两个栈的设计看上去就是很冗余。
4.2.2 get_sp
这个宏就是用来选择栈的,我们来看一下其功能
.macro get_sp
mfc0 k1, CP0_CAUSE
// 0x107c = 0001_0000_0111_1100, which means the exccode and the time irq
andi k1, 0x107C
xori k1, 0x1000
// if it's not a time interupt,then jump
bnez k1, 1f
nop
// 0x82000000 is the TIMESTACK
li sp, 0x82000000
j 2f
nop
1:
// 当 sp 已经为内核栈的时候,不变
bltz sp, 2f
nop
lw sp, KERNEL_SP
nop
2: nop
.endm
总之这一套下来,有一个固定的结论:
- 如果发生的为时钟中断,那么 sp 会被设置为
TIMESTACK
- 如果是其他异常,那么 sp 会被设置为
KERNEL_SP
。 - 如果栈指针已经被设置为内核栈指针了(上面俩个中的一个),那么就不变,这个设计是为了支持嵌套异常。
再看 SAVE_ALL
中的代码就很清楚了
move k0,sp
get_sp
move k1,sp
subu sp,k1,TF_SIZE
sw k0,TF_REG29(sp)
sp
原本的值被保留了,而 sp
现在的值,作为了保存的基地址。
换句话说,当异常为时钟中断的时候,我们将进程的上下文保存在 TIMESTACK
,而当其他异常的时候,我们将进程的上下文保存在 KERNEL_SP
。然后我们会继续使用这附近的栈作为我们的内核栈空间。
4.3 保存上下文
主要通过 SAVEALL
中的部分实现,因为还有一部分是调用 get_sp
实现栈选择,对于 SAVE_ALL
之后的栈的结构,大概就是这样的
此时 sp
指向 Trapframe
的底部。之所以要强调这一点,是因为在这之后,有这个语句(在build里)
move a0, sp
这就表示如果是 C 函数,那么是可以完全利用这里面的上下文的,比如说
void page_fault_handler(struct Trapframe *tf);
但是这个用法太低级了,lab3-2 的考试题目可以用 C 来写,用法很高级,而且比汇编简单多了。我本来已经总结了,但是找不到了,摆烂放一张 banana 的图就好了(她没处理延迟槽)。可以看到可以利用这个指针获得基本上所有的信息。
4.4 禁止中断
主要通过 CLI
进行设置,具体的代码之前博客里有。但是这件事情有待商榷,因为据叶哥哥说,异常处理的禁用是在硬件层次上实现的,而 R3000 并没有实现这个功能(或者说实现了支持嵌套异常的功能),在软件方面是没有办法禁止中断的,我就不细讨论了。
4.5 进行异常处理
这个每个都不一样,具体的回头再说。
4.6 从异常返回
4.6.1 ret_from_exception
这个函数功能是从栈上恢复寄存器,并且跳转用户进程。
FEXPORT(ret_from_exception)
.set noat
.set noreorder
RESTORE_SOME
.set at
lw k0, TF_EPC(sp)
lw sp, TF_REG29(sp) /* Deallocate stack */
nop
jr k0
rfe
恢复寄存器采用的是 RESTORE_SOME
RESTORE_SOME
然后恢复栈指针和 PC 用的是下面的操作
lw k0, TF_EPC(sp)
lw sp, TF_REG29(sp) /* Deallocate stack */
nop
jr k0
rfe
4.6.2 restore_some
这个宏可以分为两个部分,一个是设置 STATUS 寄存器的状态,一个是恢复各种通用寄存器。
对于第一点,没有全部看懂,这只知道它应该是先开启了异常功能。(因为 MOS 不太允许嵌套异常,所以会在进入中断后关闭异常)。
// set the STATUS normal
mfc0 t0, CP0_STATUS
ori t0, 0x3
xori t0, 0x3
mtc0 t0, CP0_STATUS
// I don't know exactly what it had done
lw v0, TF_STATUS(sp)
li v1, 0xff00
and t0, v1
nor v1, $0, v1
and v0, v1
or v0, t0
mtc0 v0, CP0_STATUS
然后进行寄存器的恢复,利用的依然是栈指针,这里需要注意的是,为什么可以利用栈指针,这是因为当异常处理完事后,栈指针就会回到当时 SAVE_ALL 之后的位置,所以地址向上查找就可以恢复现场
lw v1, TF_LO(sp)
mtlo v1
lw v0, TF_HI(sp)
lw v1, TF_EPC(sp)
mthi v0
mtc0 v1,CP0_EPC
lw $31,TF_REG31(sp)
lw $30,TF_REG30(sp)
lw $28,TF_REG28(sp)
lw $25,TF_REG25(sp)
lw $24,TF_REG24(sp)
lw $23,TF_REG23(sp)
lw $22,TF_REG22(sp)
lw $21,TF_REG21(sp)
lw $20,TF_REG20(sp)
lw $19,TF_REG19(sp)
lw $18,TF_REG18(sp)
lw $17,TF_REG17(sp)
lw $16,TF_REG16(sp)
lw $15,TF_REG15(sp)
lw $14,TF_REG14(sp)
lw $13,TF_REG13(sp)
lw $12,TF_REG12(sp)
lw $11,TF_REG11(sp)
lw $10,TF_REG10(sp)
lw $9,TF_REG9(sp)
lw $8,TF_REG8(sp)
lw $7,TF_REG7(sp)
lw $6,TF_REG6(sp)
lw $5,TF_REG5(sp)
lw $4,TF_REG4(sp)
lw $3,TF_REG3(sp)
lw $2,TF_REG2(sp)
lw $1,TF_REG1(sp)
在这里没有恢复的寄存器是 PC 和 栈指针,二者的恢复是在其他函数里实现的(比如 ret_from_exception
) 这可能是因为设计的原因。
4.7 异常的分发
当发生异常的时候,就会自动跳转到一个固定的物理地址,然后执行一段固定的程序,这个程序就叫做异常分发程序,他根据 Cause 寄存器中的值决定到底要调用哪一个异常处理程序。
首先我们需要把程序链接到固定的位置,因此要修改链接脚本
. = 0x80000080;
.except_vec3 :
*(.text.exc_vec3)
然后我们来看这段代码
.section .text.exc_vec3
NESTED(except_vec3, 0, sp)
.set noat
.set noreorder
1:
mfc0 k1, CP0_CAUSE
la k0, exception_handlers
andi k1, 0x7c
addu k0, k1
lw k0, (k0)
nop
jr k0
nop
END(except_vec3)
.set at
首先它指定了输出段的名称
.section .text.exc_vec3
然后可以看到他用多个语句去分析 Cause,Cause 的具体设置,我异常处理那篇博客写了
mfc0 k1, CP0_CAUSE
andi k1, 0x7c
然后利用异常码作为索引去跳转
la k0, exception_handlers
addu k0, k1
lw k0, (k0)
nop
jr k0
五、TLB 缺失中断
5.1 总体概览
这种异常会发生在 TLB 缺失的时候,流程图如下:
### 5.2 TLB 结构
首先我们需要弄懂 tlb 的结构,计组认为的 TLB,是长这样的:
也就是说,TLB 是一个全相连的 cache,既然是全相连,就不由 index 段了。我们用虚拟地址的前 22 位作为 TAG,并行的比较 64 个TLB 的 line,如果 TAG 相等,就说明找到了,反之,这说明没有找到。
不过这个模型还是有些粗糙的,很多细节并没有说明白。
在操作系统指导书里提到,tlb 构建了一个映射关系,我简化一下,就是 V P N → P P N VPN\\space \\rightarrow\\space PPN VPN → PPN 。当然这是对的了,但是这种说法我就弄成了每个 VPN 都会对应一个 PPN,但是其实这种映射关系只有 64 对。而且叫映射似乎就是一下就射过去了,而不是一个并行的比较过程。
其次就是,我们没有了解具体硬件发生了啥,比如 VPN 是怎样被检索的,被检索到的 PPN 放到了哪里,tlb 缺失以后具体怎么填补。都是没有的。这其实跟协处理器有很大关系。
在了解协处理器之前,我们先来看一下 tlb 的表项,他比计组版本要复杂一些,我们以 MOS 中 64MB (也就是共有 2 14 2^14 214 页 )的物理内存为例
我们来说明一下这些差别:
- 朴素版的 PPN 只有 14 位,是因为物理页框号可以最少用 14 位表示,但是真实版的 PPN 也与 VPN 相同,是 22 位。可能是考虑到不同电脑上内存不同吧,这估计也是 mips_detect_memory() 这个函数的设置。
- 朴素版一个 entry 是 36 位,而真实版一个 entry 是 64 位。这是因为真实版的标志位更多,所以需要的位数就更多
- 朴素版没有 ASID 段,而真实版有。ASID(address space identifier)应该是用于区分不同进程的一个标识符,因为操作系统可以同时运行多个进程,要是不用 ASID 的话,只要进程一切换,那么 TLB 里的所有内容都需要失效(因为进程切换就以为着虚拟地址和物理地址的映射关系切换),而这样是低效的,因为每次 TLB 中的内容清空,就意味着会发生 64 次的冷缺失。
- 朴素版没有物理地址权限标志位(N,D,V,G),而真实版有。这四个标志位的解释见下表
标志位 | 解释 |
---|---|
N(Non-cachable) | 当该位置高时,后续的物理地址访存将不通过 cache |
D(Dirty) | 事实上是可写位。当该位置高时,该地址可写;否则任何写操作都将引发 TLB 异常。 |
V(Valid) | 如果该位为低,则任何访问该地址的操作都将引发 TLB 异常。 |
G(Global) | 如果该位置高,那么允许不同的虚拟地址映射到相同的物理地址,可能类似于进程级别的共享 |
总结起来就是真实版的 tlb 建立了一个这样的映射 < V P N , A S I D > → < P P N , N , D , V , G > <VPN,ASID>\\space\\rightarrow\\space<PPN,N,D,V,G> <VPN,ASID> → <PPN,N,D,V,G> 。
然后我们来解决下一个问题,就是 tlb 怎么用的问题。这是一个我之前忽略的点,因为其实我对于 tlb 的定位并不清楚,我本以为它就好像是一个 cache,是对于程序员是透明的,我就在编程的时候写虚拟地址,然后就有硬件(MMU)拿着这个地址去问 tlb,tb再做出相关反应,这一切都是我不需要了解的,但是实际上 tlb 的各种操作,都是需要软件协作的。之所以有这个错误认知,是因为似乎在 X86 架构下确实是由硬件干的,但是由于我们的 MIPS 架构,也就是 RISC 架构,所以似乎交由软件负责效率更高一些。
如果 tlb 是程序员可见的,那么我们必然要管理它,那么我们就需要思考怎样管理它?我们管理它的方式就是设置了专门的寄存器和专门的指令。指令用于读或者写 tlb 中的内容,而寄存器则用于作为 CPU 和 tlb 之间沟通的媒介,就好像我们需要用 hi 和 lo 寄存器与乘除单元沟通一样。这些寄存器,都位于 CP0 中
在协处理器里面与 tlb 有关的寄存器如下表:
寄存器 | 编号 | 作用 |
---|---|---|
EntryHi | 10 | 保存某个 tlb 表项的高 32 位,任何对 tlb 的读写,都需要通过 EntryHi 和 EntryLo |
EntryLo | 2 | 保存某个 tlb 表项的低 32 位 |
Index | 0 | 决定索引号为某个值的 tlb 表项被读或者写 |
Random | 1 | 提供一个随机的索引号用于 tlb 的读写 |
这里再说一下各个寄存器的域
-
EntryHi,EntryLo 的域与 tlb 表项完全相同
-
Index 的域:
-
Random 的域:
与 tlb 相关的指令
指令 | 作用 |
---|---|
tlbr | 以 Index 寄存器中的值为索引,读出 TLB 中对应的表项到 EntryHi 与 EntryLo。 |
tlbwi | 以 Index 寄存器中的值为索引,将此时 EntryHi 与 EntryLo 的值写到索引指定的 TLB 表项中。 |
tlbwr | 将 EntryHi 与 EntryLo 的数据随机写到一个 TLB 表项中(此处使用 Random 寄存器来“随机”指定表项,Random 寄存器本质上是一个不停运行的循环计数器) |
tlbp | tlb probe。用于查看 tlb 是否可以转换虚拟地址(即命中与否)根据 EntryHi 中的 Key(包含 VPN 与 ASID),查找 TLB 中与之对应的表项。如果命中,并将表项的索引存入 Index 寄存器。若未找到匹配项,则 Index 最高位被置 1。 |
5.3 do_refill
那么当引发异常以后,我们的操作系统干了什么?可以很容易看到哈,找到异常向量组,发现处理这类异常的函数是 handle_tlb()
,然后再把通用的部分忽略,发现实现功能的是 do_refill
这个函数,我们看一下
.extern tlbra
.set noreorder
NESTED(do_refill,0 , sp)
.extern mCONTEXT
//this "1" is important
1:
lw k1, mCONTEXT
and k1, 0xfffff000
mfc0 k0, CP0_BADVADDR
srl k0, 20
and k0, 0xfffffffc
addu k0, k1
lw k1, 0(k0)
nop
move t0, k1
and t0, 0x0200
beqz t0, NOPAGE
nop
and k1, 0xfffff000
mfc0 k0, CP0_BADVADDR
srl k0, 10
and k0, 0xfffffffc
and k0, 0x00000fff
addu k0, k1
or k0, 0x80000000
lw k1, 0(k0)
nop
move t0, k1
and t0, 0x0200
beqz t0, NOPAGE
nop
move k0, k1
and k0, 0x1
beqz k0, NoCOW
nop
and k1, 0xfffffbff
NoCOW:
mtc0 k1, CP0_ENTRYLO0
nop
tlbwr
j 2f
nop
NOPAGE:
mfc0 a0, CP0_BADVADDR
lw a1, mCONTEXT
nop
sw ra, tlbra
jal pageout
nop
lw ra, tlbra
nop
j 1b
2: nop
jr ra
nop
END(do_refill)
首先我们需要找到引发异常的地址的页目录项,这个地址被存在了 CP0_BADVADDR
。此时的页目录为 mCONTEXT
。
lw k1, mCONTEXT # k1 存着当前用户进程页目录的地址
and k1, 0xfffff000 # k1 的后12位偏移量被抹去,其实应该本来就没有
mfc0 k0, CP0_BADVADDR # k0 存着引发异常的虚拟地址
srl k0, 20 # 取出 k0 的一级页目录号并 * 4,这是因为一个页目录是 4 字节
and k0, 0xfffffffc # 抹去 k0 后 2 位,对齐
addu k0, k1 # 页目录基地址加偏移量
lw k1, 0(k0) # k1 现在存着对应的页目录项
nop
当我们拿到这个页目录项以后,要看这个页目录项是否有效
move t0, k1 # t0 存着页目录项
and t0, 0x0200 # 0x200 是 PTE_V,所以进行与运算,如果该位有效,则 t0 非 0
beqz t0, NOPAGE # 如果是 0 ,无效,那么跳转到 NOPAGE
nop
我们来看 NOPAGE 的操作
NOPAGE:
mfc0 a0, CP0_BADVADDR # 把 a0 存成出现异常的虚拟地址
lw a1, mCONTEXT # 把 a1 存成当前页目录地址
nop
sw ra, tlbra # 把当前的栈指针保存一下,
# 这么做是因为又要调用新的函数了,ra 马上被覆盖了。tlbra是块内存指定空间
jal pageout # 调用 pageout 这个C函数
nop
我们来看一下 pageout
void pageout(int va, int context)
...
if ((r = page_alloc(&p)) < 0)
panic ("page alloc error!");
p->pp_ref++;
page_insert((Pde *)context, p, VA2PFN(va), PTE_R);
printf("pageout:\\t@@@___0x%x___@@@ ins a page \\n", va);
可以看出这个函数就是就是分配一个空闲物理页面,然后把对应关系建立起来。这里才是用到页表结构的地方。也就是说,这个函数是体现我们用页表管理映射关系的地方。但是这里还没完,因为这个其实对应的是虚拟页面没有在物理内存中被映射的情况。解决了这个问题,还有把映射好的物理页号填到 TLB 中的工作。
出来这个函数以后,就是一些收尾工作
lw ra, tlbra # 把栈指针恢复了
nop
j 1b # 跳回一开始重新来一遍
2: nop
jr ra # do_refill 执行完成
nop
那么如果页目录项有效呢?我们会继续执行,检索第二级页表
and k1, 0xfffff000 # k1 原来是页目录项,抹掉后 12 位权限位
mfc0 k0, CP0_BADVADDR # k0 存着异常的地址
srl k0, 10 # 取出 k0 的二级页表号并 * 4,这是因为一个页目录是 4 字节
and k0, 0xfffffffc # 抹去 k0 后 2 位,对齐
and k0, 0x00000fff # 前面的一级页目录号去掉
addu k0, k1 # 基地址加偏移量
or k0, 0x80000000 # 物理地址转换成虚拟地址
lw k1, 0(k0) # 把对应的页表项存到 k1
nop
然后接着对页表项进行权限检查
move t0, k1
and t0, 0x0200
beqz t0, NOPAGE
nop
然后下面开展重填操作(如果都处理好了)
move k0, k1 # k0 k1 存着万事具备的页表项
and k0, 0x1 # 看 k0 的最低位,是 PTE_COW copy on write
beqz k0, NoCOW # 如果没有,就不需要特殊处理了
nop
and k1, 0xfffffbff # 有的话,把 PTE_R 位置 0 ,相当于限制写权限
NoCOW:
mtc0 k1, CP0_ENTRYLO0 # 把 k1 装到 EntryLow 中
nop
tlbwr # 随便找个 tlb 项用 EntryLow 覆盖掉
j 2f
综上,能看见页表的是操作系统,而不是硬件。操作系统在 TLB 缺失异常的时候,给 TLB 提供的内容都是经过页表系统管理的,所以 TLB 的所有内容都是经过页表系统管理的,不得不说真是巧妙啊。
5.5 COW 的处理
在这里的 do_refill
会有一个对于页表项权限位 PTE_COW
的判断。但是由于我们还没有介绍写时复制机制,所以这里只需要了解一下,如果一个页面被标记为 PTW_COW
了,那么我们就会去掉他的写权限,也就是说,如果这个页面被写了,就会触发 handle_mod
。那么是不是所有的 COW
都会被去掉写权限呢?我们说是的,因为这些页面被标记为 COW
的时候要么没在 TLB 中,要么在 TLB 中被失效了(在 page_insert 中),所以这些页面都是被使用都需要先经过 TLB,所以一定会发生缺失,那么就一定会被去掉写权限。
六、时钟中断
6.1 总体概览
这个异常其实虽然叫做时钟中断,但是其实现的功能其实是进程切换。这个流程也是一个极其复杂的流程,所以需要有很好的理解。总体的流程如下:
6.2 恢复另一个现场
这是跟其他异常最大的区别。就是其他异常都是在操作系统运行完异常处理程序以后,就会返回那个引发异常的进程。但是时钟中断不是,它不会返回发生异常的那个进程,而是重新挑选一个进程去运行。
这就提出了一个很严峻的问题,就是 TIMESTACK
不够用了,因为对于其他异常,我们会把用户进程的现场存储在内核栈上,在干完事情以后再从内核栈上把这个现场恢复了。但是对于进程调度,这个就不行了,因为栈上存储的还是原来进程的现场,所以即使在某个函数里把现场调整成了新进程的现场,但是一个 ret_from_exception
就又给恢复了。正是因为如此,在流程图中可以看到,这个流程里是没有 ret_from_exception
的。不仅如此,我们还需要把被替换掉的进程上下文保存在进程控制块中,这是因为不然等这个进程再次被调用的时候,又怎么知道当时的现场是啥呢?这件事也可以这样理解,就是我们恢复现场的来源变成了不再是从 TIMESTACK
上,而是从进程控制块里。
6.3 handle_int
先是在栈上保存用户寄存器的值,都是调用 SAVE_ALL。这个函数有两个功能,一个是选择一个内核栈(是 KERNEL_SP
还是 TIMESTACK
)。另一个是在栈上报存寄存器。在时钟中断中,我们选择的是 TIMESTACK
。
SAVE_ALL
然后是禁用一切异常,这是通过将 STATUS
的最低位清 0 设置的
CLI
然后就进入了特殊环节,每个异常处理程序具体会变得存在差异,我们首先检验是不是时钟中断,利用的是 STATUS
和 CAUSE
寄存器的值,这里可能是 handle_int
要单独处理的原因,因为时钟中断是一个外部中断,所以在处理的时候要检测中断掩码,而其他的很多异常是不需要检测中断掩码的。
# 上面所作的一切都是为了检验是不是时钟中断
mfc0 t0, CP0_CAUSE
mfc0 t2, CP0_STATUS
and t0, t2
andi t1, t0, STATUSF_IP4
当检测通过之后,我们需要进行真正的处理。首先是要响应时钟的信号(可能是为了让这个信号停止吧,计组中也出现过)
sb zero, 0xb5000110
然后跳到 C 的调度函数中
j sched_yield
最后由恢复了统一的格式,即所有异常都需要恢复用户寄存器的值,并且将控制权重新归还给用户进程,但是这个函数从来不会用到,因为前面跳 sched_yield
连 jal
都没用。
j ret_from_exception
6.4 env_yield
这个函数实现的功能是进行进程调度,即选择一个进程去运行。根据函数的名字,其实是当前进程进行一次谦让,然后如果谦让成功就会挑选另一个进程去运行,这大概就是调度方式。
这个函数还是一个非常困难的函数的,首先我们需要知道一些 C 的语法知识。对于一个静态局部变量,只会初始化一次,并且其值不会在函数外部发生变化,所以这三个数只有在第一次执行 env_yield
的时候是这三个值,之后执行的时候不会赋值。
static int count = 0; // remaining time slices of current env
static int point = 0; // current env_sched_list index
static struct Env *e = NULL; // the current env
count
表示当前进程还剩多少个时间片,point
表示当前用的是哪一个调度链表,e
是当前进程,那么为啥不用 curenv
。我觉得是因为 e
在这个函数里还有 “遍历” 的功能,而且在其他函数里还设置了这个,所以实在是没必要。
我们首先要明确要挑一个进程的条件,有三个:
- 当前的时间片为0
- 第一次调度,要挑一个
- 当前进程的状态不再是可运行的
对应了下面的三个条件
if (count == 0 || e == NULL || e->env_status != ENV_RUNNABLE)
这里提一嘴 MOS 中的三个状态,ENV_RUNNABLE
对应的是当前运行和就绪的进程,ENV_NOT_RUNNABLE
指的是被阻塞的进程,即缺少一定条件的进程,ENV_FREE
指的是空闲的进程,它们应该被插入到 free_list
中。
我们首先要把这个要被替换掉的进程从当前的队列中移除掉,然后根据它的状态考虑要不要把他插入到另一个队列的队尾
if (e != NULL)
LIST_REMOVE(e, env_sched_link);
if (e->env_status != ENV_FREE)
LIST_INSERT_TAIL(&env_sched_list[1 - point], e, env_sched_link);
然后考虑挑出下一个要运行的进程,方法是遍历链表
while (1)
while (LIST_EMPTY(&env_sched_list[point]))
point = 1 - point;
e = LIST_FIRST(&env_sched_list[point]);
if (e->env_status == ENV_FREE)
LIST_REMOVE(e, env_sched_link);
else if (e->env_status == ENV_NOT_RUNNABLE)
LIST_REMOVE(e, env_sched_link);
LIST_INSERT_TAIL(&env_sched_list[1 - point], e, env_sched_link);
else
count = e->env_pri;
break;
挑出后就可以运行了
count--;
e->env_runs++;
env_run(e);
这里有个细节是无论换不换进程,都会进行一个 env_run
的操作。我一开始觉得很没必要,因为只有换进程才有执行env_run的必要。如果不换进程,env_yield
会正常返回,那么就会由 ret_from_exception
恢复现场。但是我发现呵呵,time_irq
里调用 env_yield
用的是 j
而不是 jal
,也就是说 MOS 就没把 env_yield
当作一个可以返回的函数用。
最后看一下整体的布局:
void sched_yield(void)
static int count = 0; // remaining time slices of current env
static int point = 0; // current env_sched_list index
static struct Env *e = NULL;
/* hint:
* 1. if (count==0), insert `Kotlin 协程Flow 流异常处理 ( 收集元素异常处理 | 使用 try...catch 代码块捕获处理异常 | 发射元素时异常处理 | 使用 Flow#catch 函数捕获处理异常 )
文章目录
一、Flow 流异常处理
在 Flow 流 的
- 构建器代码 : flow , flowOf , asFlow ;
- 发射元素 : emit 发射元素 ;
- 收集元素 : collect 收集元素 ;
- 各种运算符代码 : 过渡操作符 , 限长操作符 , 末端操作符 等 ;
中 , 如果运行时 , 抛出异常 , 可以使用
- trycatch(e: Exception) 代码块 收集元素时捕获异常
- Flow#catch 函数 发射元素时捕获异常
处理异常 ;
二、收集元素异常处理
1、收集元素异常代码示例
异常代码示例 : 如果收集的元素 it <= 1
, 则检查通过 , 否则当 it > 1
时 会报异常 ;
package kim.hsl.coroutine
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
class MainActivity : AppCompatActivity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
runBlocking
flowEmit().collect
println("收集元素 $it")
check(it <= 1)
"抛出异常 $it <= 1"
suspend fun flowEmit() = flow<Int>
// 以 100 ms 的间隔发射元素
for (i in 0..5)
emit(i)
println("发射元素 $i")
执行结果 : 当 it > 1
时 会报异常 Caused by: java.lang.IllegalStateException: 抛出异常 2 <= 1
;
21:51:03.014 System.out kim.hsl.coroutine I 收集元素 0
21:51:03.015 System.out kim.hsl.coroutine I 发射元素 0
21:51:03.015 System.out kim.hsl.coroutine I 收集元素 1
21:51:03.015 System.out kim.hsl.coroutine I 发射元素 1
21:51:03.015 System.out kim.hsl.coroutine I 收集元素 2
--------- beginning of crash
21:51:03.021 Andro...time kim.hsl.coroutine D Shutting down VM
21:51:03.030 Andro...time kim.hsl.coroutine E FATAL EXCEPTION: main
Process: kim.hsl.coroutine, PID: 6476
java.lang.RuntimeException: Unable to start activity ComponentInfokim.hsl.coroutine/kim.hsl.coroutine.MainActivity: java.lang.IllegalStateException: 抛出异常 2 <= 1
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2951)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3086)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1816)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6718)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Caused by: java.lang.IllegalStateException: 抛出异常 2 <= 1
at kim.hsl.coroutine.MainActivity$onCreate$1$invokeSuspend$$inlined$collect$1.emit(Collect.kt:134)
at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:77)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:59)
at kim.hsl.coroutine.MainActivity$flowEmit$2.invokeSuspend(MainActivity.kt:27)
at kim.hsl.coroutine.MainActivity$flowEmit$2.invoke(Unknown Source:8)
at kim.hsl.coroutine.MainActivity$flowEmit$2.invoke(Unknown Source:4)
at kotlinx.coroutines.flow.SafeFlow.collectSafely(Builders.kt:61)
at kotlinx.coroutines.flow.AbstractFlow.collect(Flow.kt:212)
at kim.hsl.coroutine.MainActivity$onCreate$1.invokeSuspend(MainActivity.kt:32)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:87)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:61)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:40)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source:1)
at kim.hsl.coroutine.MainActivity.onCreate(MainActivity.kt:14)
at android.app.Activity.performCreate(Activity.java:7144)
at android.app.Activity.performCreate(Activity.java:7135)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2931)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3086)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1816)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6718)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
21:51:03.066 Process kim.hsl.coroutine I Sending signal. PID: 6476 SIG: 9
---------------------------- PROCESS ENDED (6476) for package kim.hsl.coroutine ----------------------------
2、收集元素捕获异常代码示例
代码示例 : 在 收集元素 时 , 使用 try…catch 代码块捕获异常 ;
package kim.hsl.coroutine
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
class MainActivity : AppCompatActivity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
runBlocking
try
flowEmit().collect
println("收集元素 $it")
check(it <= 1)
"抛出异常 $it <= 1"
catch (e: Exception)
println("捕获到了异常 $e.message")
suspend fun flowEmit() = flow<Int>
// 以 100 ms 的间隔发射元素
for (i in 0..5)
emit(i)
println("发射元素 $i")
执行结果 :
21:57:22.932 System.out kim.hsl.coroutine I 收集元素 0
21:57:22.932 System.out kim.hsl.coroutine I 发射元素 0
21:57:22.933 System.out kim.hsl.coroutine I 收集元素 1
21:57:22.933 System.out kim.hsl.coroutine I 发射元素 1
21:57:22.933 System.out kim.hsl.coroutine I 收集元素 2
21:57:22.934 System.out kim.hsl.coroutine I 捕获到了异常 抛出异常 2 <= 1
三、发射元素异常处理
1、发射元素异常代码示例
代码示例 :
package kim.hsl.coroutine
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runBlocking
import java.io.IOException
class MainActivity : AppCompatActivity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
runBlocking
flowEmit().collect
println("收集元素 $it")
suspend fun flowEmit() = flow<Int>
emit(0)
throw IOException("IO 异常")
.flowOn(Dispatchers.IO)
执行结果 :
22:19:59.361 System.out kim.hsl.coroutine I 收集元素 0
22:19:59.368 Andro...time kim.hsl.coroutine D Shutting down VM
22:19:59.374 Andro...time kim.hsl.coroutine E FATAL EXCEPTION: main
Process: kim.hsl.coroutine, PID: 11490
java.lang.RuntimeException: Unable to start activity ComponentInfokim.hsl.coroutine/kim.hsl.coroutine.MainActivity: java.io.IOException: IO 异常
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2951)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3086)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1816)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:以上是关于操作系统-异常处理流的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin 协程Flow 流异常处理 ( 收集元素异常处理 | 使用 try...catch 代码块捕获处理异常 | 发射元素时异常处理 | 使用 Flow#catch 函数捕获处理异常 )
Kotlin 协程Flow 流异常处理 ( 收集元素异常处理 | 使用 try...catch 代码块捕获处理异常 | 发射元素时异常处理 | 使用 Flow#catch 函数捕获处理异常 )
2018-2019-1 20165317 《信息安全系统设计基础》第七周学习总结