异常是什么
异常就是控制流中的突变(也就是正常的程序指令流被打乱了),但是请记住,异常不代表一定就有问题
异常有多种形式,由软件和硬件共同实现
- 异常的种类
- 中断是异步的!比如键盘不知道什么时候会输入,网卡不知道什么时候数据会到达
- 陷阱,比如应用程序的read指令,称为陷阱是因为是让用户主动触发的。在应用程序上看着实际上和普通函数没有什么不同,但是系统调用在内核中执行,并调用的内核栈。系统调用其实就是一种内核服务,应用想要访问内核服务,就要通过这种方式
- 故障,是一种意料之外的异常,但是并不意味不可修复,如缺页
- 终止,一些致命的错误,比如除0
异常控制流的处理方式
它和过程调用很类似,因为也是一种指令的结构控制形式
当异常发生时,其实也是执行一些指令(当然这些指令已经是在其他位置的指令,指令流不是顺序的了),这些会破坏当前处理器的状态,比如寄存器的值,所以这些值需要被保存在某个地方,根据层级的不同,可能在内核栈上,可能在用户层的堆上等,当异常处理完毕后,恢复异常发生前的处理器状态。
但是有一些区别,异常的返回地址可能当前指令,也可能是下一条指令
异常控制流的实现
异常控制流有很多的层次,以下按层次从低到高依次书写。但是要记住,异常控制流中,当控制流改变过后又回到源控制流的时,需要保证当前上下文的状态和之前一样(寄存器等),所以在切换之前需要把当时的上下文保存在某个地方(根据层级的不同,而不同,可能在内核栈上,也可能在堆上)
硬件与内核之间的异常流控制(x86-64 系统中定义的异常)
x86-64系统中定义的异常 是 CPU 运行过程中 意外发生 的错误,通常由 硬件和软件错误 触发,会导致 CPU 进入异常处理程序。这些由CPU赋予异常号
这也是异常控制流能被支持的脊柱
异常号的展示
异常号 | 描述 | 异常类别 | 详细解释 |
---|---|---|---|
0 | 除法错误(Divide Error) | 故障(Fault) | 除数为 0 或结果溢出时触发,通常会导致 SIGFPE 信号 |
6 | 无效操作码(Invalid Opcode) | 故障(Fault) | CPU 试图执行一个无效指令,如执行不支持的指令 |
13 | 一般保护错误(General Protection Fault) | 故障(Fault) | 访问受保护的内存地址,如访问内核地址或非法访问某些寄存器 |
14 | 缺页异常(Page Fault) | 故障(Fault) | 访问的内存页未映射,操作系统可能会加载该页,或者终止进程 |
18 | 机器检查(Machine Check) | 终止(Abort) | 硬件故障或严重的系统错误,例如 CPU 内部错误 |
32-255 | 操作系统定义的异常 | 中断或陷阱(Interrupt/Trap) | 操作系统使用这些编号定义自己的异常处理,如系统调用 |
32 | 任务切换(Task Switch) | 中断 | 可能用于任务切换或调度 |
33 | 调试陷阱(Debug Trap) | 陷阱 | 进入调试模式,通常在 gdb 调试时触发 |
128 | 系统调用(Syscall) | 陷阱 | 旧的 int 0x80 方式的系统调用 |
x86-64系统中的异常实现方式
在系统启动时有一张异常表的跳转表被操作系统分配和初始化
通过异常表的跳转表,转移到一个专门设计用来处理这类事件的操作系统子程序
异常表其实是一张跳转表,跳转到异常处理程序,它是一块连续的内存块,数组,只需要基地址和异常号就能跳转到异常处理程序的入口上。
检测到异常的过程是当执行完当前指令后,会发现中断引脚的电压变高,然后从系统总线读取异常号。然后跳入异常处理程序中,
内核与进程之间的异常流控制(系统调用,syscall)
系统调用是基于syscall的异常号 128 ,的基础上实现的,这个系统调用又对应于一个单独的系统调用跳转表
系统调用是 用户进程主动请求 操作系统提供的服务,通常用于 文件操作、进程管理、内存管理等。
系统调用的返回值为负,表明发生了错误,可以使用errno来进行查看
系统调用是 用户进程主动请求 操作系统提供的服务,通常用于 文件操作、进程管理、内存管理等。
系统调用会导致进程的上下文切换,陷入内核态
编号 | 名称 | 描述 | 详细解释 |
---|---|---|---|
0 | read | 读取文件 | 从文件描述符读取数据到用户空间 |
1 | write | 写文件 | 将用户数据写入文件描述符 |
2 | open | 打开文件 | 以指定模式打开文件 |
3 | close | 关闭文件 | 关闭打开的文件描述符 |
4 | stat | 获取文件信息 | 读取文件的元数据(如大小、权限等) |
9 | mmap | 将文件映射到内存 | 用于内存映射 I/O,允许进程访问文件内容 |
12 | brk | 调整堆内存大小 | 用于扩展或收缩进程的堆空间 |
32 | dup2 | 复制文件描述符 | 复制一个文件描述符,通常用于重定向 |
33 | pause | 让进程挂起直到信号到达 | 进程挂起,直到接收到信号 |
37 | alarm | 设定定时器 | 让进程在 N 秒后接收 SIGALRM 信号 |
39 | getpid | 获取进程 ID | 返回当前进程的 PID |
57 | fork | 创建子进程 | 复制当前进程,创建新进程 |
59 | execve | 执行程序 | 运行一个新程序,替换当前进程映像 |
60 | _exit | 终止进程 | 立即终止当前进程 |
61 | wait4 | 等待子进程结束 | 等待子进程终止,回收资源 |
62 | kill | 发送信号到进程 | 终止或控制另一个进程 |
在c语言中,read,write,print都是包装函数
本质上都是通过syscall进行包装的结果,
printf("hello, world!")
write(1, "hello, world!", 13); // 1 输出流 13 个字节
进程之间的异常控制流(信号)
进程与进程之间的异常控制流主要是通过信号来进行实现的,,
信号的实现方式是:
struct task_struct {
...
sigset_t blocked; // 进程当前屏蔽的信号集(位向量)
...
};
每个进程的进程信息中都有一个位向量集合,每一位代表一个信号是否被接收到。当这个信号被多次触发,而进程还在处理上个信号的时候,多次触发会被丢弃(因为位只有0 和 1 的区别,无法计数)。
信号是一种异步通知机制,这就好像内核无法知道你什么时候敲键盘一样。
基于以上俩个原则,不可以使用信号处理函数来处理计数操作,同时信号处理函数共享与进程一样的数据,可以理解为并发访问,要么就加锁,要么就不要有临界区。或者说这个信号处理函数是可重入的(不要有临界资源)
当然在执行信号处理函数的时候,进程的控制流已经被打乱了,在执行之前需要保存上下文,当信号处理函数return的时候,返回到上次应该执行的那条指令下。
系统调用是可以被中断的,像read、write、accept这种,慢速系统调用被中断时,在信号处理程序返回时,将不再继续,而是返回一个errno: EINTR
正因为系统调用是可以被中断,返回值可能是错误的,比如不可重入的系统调用localtime,如果被中断,那么它的值将是NULL,如果不处理错误,应用层就会挂掉了。
进程内的异常控制流(try,catch)
应用层的异常控制流是通过非本地跳转来实现的
c语言, 非本地跳转(nonlocal jump),setjmp 保存当前调用环境,longjmp 恢复环境
c++和java中的try catch 是 c语言这俩个函数的更结构化的版本
具体详细没有再了解过