08 异常控制流

异常控制流

本章的核心内容:

  1. 了解什么是异常控制流(ECF)
  2. 了解系统中哪些功能是基于ECF实现的

什么是控制流?

程序计数器中指令的序列叫控制流

什么是突变?

指令是相邻的,我们说是“平滑的”
如果不相邻,就叫突变
引起突变的可能有跳转、调用、返回等指令,这些都是必要的突变
也有一些突变是系统状态的变化导致的

什么是异常控制流(ECF)?

这些指令的突变叫做异常控制流

异常控制流会发生在计算机系统的哪些层次?

硬件层:事件触发异常,控制流转移到异常处理程序
操作系统层:上下文切换让控制流在进程间切换
应用层:进程接受信号,控制流转移信号处理程序

了解ECF的好处?

  1. ECF是实现I/O,进程和虚拟内存的基本机制
  2. 理解程序和操作系统的交互,通过“陷阱”或者“系统调用”的ECF
  3. 编写shell程序,通过“上下文切换”的ECF
  4. 理解并发,ECF是实现并发的基本机制,如被中断的异常处理程序,时间上重叠的进程和线程,被中断后的信号处理程序
  5. 理解try、catch是怎么一回事,通过非本地跳转的ECF

异常

什么是异常?

ECF的一种形式,用来反映处理器状态的变化。
属于硬件层,但也有一部分由操作系统实现

什么是事件?比如?

处理器状态的变化称为事件
比如:虚拟内存缺页、算术溢出、除零

事件发生时如何处理?

由处理器来检测事件的发生
通过异常表跳转到异常处理程序

异常表

完成处理后,有3中选择:

  1. 返回到事件发生时正在执行的指令
  2. 返回到事件发生时的下一条指令
  3. 程序终止

异常和过程调用有什么区别?

  1. 异常有选择的把当前指令或者吓一跳指令压入栈
  2. 会把一些额外的处理器状态压入栈
  3. 如果控制流从用户程序转移到内核,那么栈是内核栈,而不是用户栈
  4. 异常处理程序运行在内核模式(见后文)

异常的类别?

中断(interrupt)、陷阱(trap)、故障(fault)、终止(abort)

异常

中断

中断是异步发生的,是来自处理器外部I/O设备的信号的结果
所谓异步,是指并非由指令造成,而是任意时间的外部因素

流程:
中断

陷阱和系统调用

陷阱是有意的异常(故意的跳转),是指令执行的结果
譬如:执行”syscall n”指令会导致一个到陷阱处理程序的跳转
由此可见,陷阱的一个重要用户的让用户程序调用内核函数,叫做系统调用
如:读文件、创建进程、加载进程、终止进程

陷阱

什么是errno?

Linux中系统调用的错误都存储于errno中,errno由操作系统维护,存储就近发生的错误,即下一次的错误码会覆盖掉上一次的错误。
Linux中的strerror(errno)可以返回某个errno值的文本描述

故障

故障是由指令错误引起的
故障处理程序能处理,则重新执行指令,否则,返回到内核中的abort历程

陷阱

终止

终止由不可恢复的致命错误导致,一般是硬件错误
譬如DRAM、SRAM位损坏

陷阱

进程

异常控制流是操作系统内核得以提供进程概念的基本构造块

什么是进程?

进程就是执行中的一个程序实例

什么是上下文?

上下文是一个进程运行所需要的各种状态
譬如:内存中的代码和数据、栈、寄存器、程序计数器、环境变量、打开文件描述符集合

一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
用户级上下文: 正文、数据、用户堆栈以及共享存储区;
寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。

进程提供给应用程序的两个抽象是?

  1. 一个独立的逻辑控制流
  2. 一个私用的虚拟地址空间

什么是逻辑控制流?

一个进程运行所独有的PC序列叫做逻辑流

逻辑流

什么是并发流?多任务?

两个在时间上有重叠的逻辑流称为并发流
从宏观上看,进程间的轮流执行叫多任务

什么是用户模式和内核模式?

为了进一步完善进程的概念,处理器需要提供一种机制,来限制进程能够执行的指令和能够访问的地址空间
处理器通过某个寄存器中的一个模式位为来提供这种机制,也就是用户模式和内核模式的概念
用户模式:无法执行特权指令(停止处理器,修改位模式),也无法访问内核地址空间中代码和数据(只能通过系统调用)
内核模式:能够执行所有和指令和访问机型所有的地址空间

进入内核模式的方式有哪些?

唯一的方式就是通过中断、陷阱这样的异常

linux的/proc文件系统有什么用?

提供一个后门,让进程在用户模式下安全的访问内核数据结构的内容(譬如CPU类型,内存段)

什么是调度?

内核决定在某时刻执行一个新的进程叫做调度

什么是上下文切换?

属于操作系统层的异常控制流,基于硬件层的异常控制流之上(所以上下文切换也是发生在内核模式),用来实现多任务

  1. 保存当前进程上下文
  2. 恢复另一个进程的上下文
  3. 控制转移

上下文切换

引起上下文切换的情况?

  1. 系统调用(陷阱):系统调用因为等待某个事件而发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程;比如read文件阻塞被动休眠,再比如sleep主动休眠。
  2. 中断:所有的计算机都会有某种周期性的定时器中断机制,中断后进行上下文切换

系统调用错误处理

什么是错误包装函数?

对系统调用进行一次包装,包装函数中检查错误

为什么要使用错误包装函数?

系统调用出错会设置errno值
如果每次都去检查错误,代码会变得臃肿难懂
使用错误包装函数能使代码变得简洁

进程控制

每一个进程都有一个进程id(pid)

进程的三种状态是?

  • 运行:正在CPU上执行或者等待被内核调度
  • 停止:进程被挂起,无法被调度,等待被唤醒
  • 终止:进程死掉了

有哪些因素会导致进程终止?

  1. 收到一个终止进程的信号
  2. 从主程序返回
  3. 调用exit函数

fork函数具有什么功能?

创建一个新的运行的子进程

fork后的子进程和父进程有什么关联和区别?

最大的区别就是PID不同

  1. 调用fork函数会有两次返回,父进程中返回子进程的PID,子进程中返回0
  2. 并发执行,新创建的子进程和父进程会并发的执行
  3. 相同但是独立的虚拟地址空间
  4. 共享文件,子进程可以读写父进程中打开的共享文件

fork进程图

子进程终止如何处理?

终止的进程不会立即在系统中消失,而是等待被父进程回收
如果父进程先终止,内核会安排init进程称为孤儿进程的“养父”
init进程pid为1,是所有进程的祖先

回收僵尸进程的相关函数?

1
2
pid_t waitpid(pid_t pid, int *statusp, int options);
pid_t waitpid(int *statusp); //等价于pid_t waitpid(-1, &statusp, 0);

等待一个集合中的子进程终止并回收
如果没有子进程,则返回-1,设置errno为ECHILD
如果被信号中断,则返回-1,设置errno为EINTR

  • pid:等待集
    1. pid > 0:等待集是编号为pid的进程
    2. pid = -1:等待集是父进程所有的子进程
  • options:修改函数行为
    1. 默认(0):挂机调用进程,直到等待集中的一个子进程终止
    2. WNOHANG:立即返回(都没终止返回0)
    3. WUNTRACED:挂机调用进程,直到等待集中的一个子进程终止或停止
    4. WCONTINUED:挂机调用进程,直到等待集中的一个子进程终止或者一个停止的子进程重新执行
  • statusp:返回状态
    1. WIFEXITED(status):如果子进程正常结束,它就返回真;否则返回假。
    2. WEXITSTATUS(status):如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。
    3. WIFSIGNALED(status):如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。
    4. WTERMSIG(status):如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。
    5. WIFSTOPPED(status):如果当前子进程被暂停了,则返回真;否则返回假。
    6. WSTOPSIG(status):如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。

进程休眠的相关函数?

1
2
unsigned int sleep(unsigned int secs);    //等待时间到或者被信号中断
int pause(void); //一致挂起知道被信号中断

execve函数具有什么样的功能?

在当前的进程的上下文中加载并运行一个新程序

1
int execve(const char *filename, const char *argv[], const char *envp[])
  • filename:可执行的文件名
  • argv[]:参数列表
  • envp[]:环境变量列表

fork进程图

程序和进程有什么区别?

程序运行在进程的上下文中

案例:结合fork和execve实现一个简单的shell

信号

什么是信号?和异常的区别?

信号是应用层的异常,信号是进程上下文中的某个状态,对应一种底层事件
底层事件是处理器通知到内核,由内核异常处理程序处理的,这些用户进程是看不到的
信号是操作系统提供给进程的一种机制,让进程可以知道发生了这些异常事件

信号

发送信号和接受信号的表现形式是什么?

  • 发送信号:内核更新了进程上下文中的某个状态
    1. 检测到一个系统事件
    2. 调用了kill函数
  • 接受信号:目的进程被内核强迫以某种方式处理信号,忽略、终止、或者执行信号处理程序

发个发出了但未接收的信号是待处理信号
每种类型的待处理信号只有一个,多于的呗丢弃
信号被阻塞,意味着信号可以被发送,但是不会被接收。

什么是进程组?

每个进程都只属于一个进程中
默认情况下,一个子进程继承父进程的进程组

1
2
pid_t getpgrp(void)                 //获取当前进程的进程组id
int setpgid(pid_t pid, pid_t pgid) //将进程pid的进程组改为pgid

什么是作业

linux的一条命令行就是一个“作业”
同时只能有一个前台作业和多个后台作业

信号

具体的发送信号的方式有哪些?

1
2
3
linux>/bin/kill -9 (-)15213           //发送信号给进程(组)
int kill(pid_t pid, int sig)
unsigned int alarm(unsigned int secs) //定时发送SIGALRM信号

接受信号时机是什么时候?以及如何处理?

接受信号的时机是从内核模式切换到用户模式时(从系统调用返回或者上下文切换),去检查待处理且未阻塞的信号集合,选择其中的某个信号k
指定下面的某个行为:

  1. 终止
  2. 终止并转储内存
  3. 挂起,等待重启
  4. 忽略
  5. 信号处理程序
    默认行为可以修改

如何修改信号的默认行为?

1
2
3
4
5
6
/**
* handler = SIG_IGN:忽略
* handler = SIG_DFL:恢复默认
* handler = 函数指针:信号处理程序
*/
sighandler_t signal(int signum, sighandler_t handler)

信号处理程序

具体的阻塞信号的方法?

  • 隐式阻塞机制:正在被处理的信号默认被阻塞
  • 显式阻塞机制:
1
2
3
4
5
6
7
8
9
10
11
12
/**
* set 操作集
* oldset 以往的阻塞集
* how = SIG_BLOCK:把操作集添加到阻塞集中
* how = SIG_UNBLOCK:把操作集从阻塞集中删除
* how = SETMASK:阻塞集=操作集
*/
int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset );
int sigemptyset(sigset_t *set)//清空操作集
int sigemptyset(sigset_t *set)//用所有信号填充操作集
int sigaddset(sigset_t *set, int signum)//添加
int sigdelset(sigset_t *set, int signum)//删除

编写信号处理函数的难点是什么?

  1. 处理程序和主程序共享全局变量
  2. 接受信号的规则有违直觉
  3. 不同的系统由不同的信号处理语义

编写信号处理函数保守规则是什么?

  1. 处理程序尽可能简单
  2. 使用安全的函数
  3. 保存和恢复errno
  4. 阻塞其他的信号,保护对共享变量的访问
  5. 用volatile声明,立刻刷新主存

信号的接口规则会导致怎样的意外错误?

如果用信号处理程序来处理排队问题
由于信号的非排队机制会直接导致严重的错误

信号处理函数的系统兼容性问题是什么?

  1. signal语义不同:有的是一次性的,每次都需要去重新修改
  2. 有些系统调用可能会被信号处理中断,因此需要手动的去重启

如何解决这些兼容性问题?

使用signal的包装函数

什么情况下需要等待信号被接收?

比如,维护唯一的前台作业

如何显示的等待信号?

1
int sigsuspend(const sigset_t *mask)//用mask暂时替换当前的阻塞集合,挂起并等待信号的接受

非本地跳转

什么是非本地跳转?

非本地跳转是一种用户级的异常控制流形式
可以从某一个函数的某处直接转移到另一个函数的某处,不需要进过栈进出

1
2
int setjmp(jmp_buf env)                   //在env缓存区保存当前的调用环境
int sigsetjmp(sigjmp_buf env, int retval) //从env中恢复调用环境,从setjmp返回,返回值是retval

非本地跳转有哪些重要的应用?

操作进程的工具

STRACE:打印出一个正在运行的进程和子进程调用的每个系统调用的轨迹
PS:列出当前所有的进程(包括僵尸进程)
TOP:打出当前进程资源使用信息
PMAP:显示进程的内存映射

本文标题:08 异常控制流

文章作者:Sun

发布时间:2019年07月18日 - 21:07

最后更新:2019年07月25日 - 14:07

原始链接:https://sunyi720.github.io/2019/07/18/系统原理/08 异常控制流/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。