深入理解计算机系统(六) —— 异常控制流

参考书籍《深入理解计算机系统》

1 异常

异常使得操作系统能够提供进程的概念。

1.1 基本概念

  • 控制流:从处理机上电开始,直到处理器断电,程序计数器有一个序列的值:a0,a1……,a(k)。其中a(n)对于某个相应指令I(n)的地址,从a(n)到a(n+1)的过渡称为控制转移,这样的控制转移序列就叫处理器的控制流。
  • 异常控制流(ECF):现代系统通过使控制流发生突变来对系统状态的变化(如:硬件定时器产生的信号、数据包到达网络适配器等情况)做出反应,我们称这些突变为ECF。
  • 异常:是异常控制流的一种形式,指的是控制流中的突变,用来响应处理器状态中的某些变化,由硬件与操作系统共同实现,下图为异常实现的基本思想。
    在这里插入图片描述
  • 事件:在处理器中,状态被编码为不同的位和信号,状态变化被称为事件,如:算术溢出、系统定时器信号或I/O请求完成等。
  • 异常处理程序:当处理器检测有事件发生,将通过异常表进行一个间接过程调用到异常处理程序,异常处理程序完成后将根据引起异常事件的类型发生以下情况:
    1.处理程序将控制返回给当前指令(即事件发生时正在执行的指令)
    2.处理程序将控制返回给下一条指令
    3.处理程序终止被中断的程序
  • 系统调用与普通函数调用:系统调用与普通函数调用实现不同,普通函数是在用户模式中,可执行的指令类型会被限制,只能访问与调用函数相同的栈;系统调用运行在内核模式,允许系统调用执行命令,并访问定义在内核中的栈。

1.2 异常处理

  • 步骤1:系统中可能出现的每种类型的异常都分配了一个唯一的非负整数的异常号,异常号由处理器的设计者和操作系统的设计者定义,下图是Inter系统定义的一些异常示例;
    在这里插入图片描述

  • 步骤2:系统启动时(重启或上电),操作系统分配和初始化异常表,使得表目包含对应异常号的处理程序的地址,其中异常表的起始地址放在一个叫异常表基寄存器的特殊CPU寄存器里;

  • 步骤3:运行时,处理器检测到发生了事件,并确定了相应的异常号,随后处理器触发异常(执行间接过程调用);

异常与过程调用的不同之处:
1.过程调用跳转到处理程序之前,处理器会将返回地址压到栈中;异常根据其类型返回地址不是固定的;
2.处理器也会把额外的处理器状态压到栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态;
3.控制从用户程序转移到内核,所有这些项目都被压到内核中,而不是用户栈;
4.异常处理程序运行在内核模式下,即对所有的系统资源都有完全的访问权限。

1.3 异常的类别

异常分为四大类:中断、陷阱、故障和终止。
在这里插入图片描述

  • 中断:由中断处理程序处理,中断是异步发生的,是处理器外部I/O设备信号的结果(如:中断管脚的电平变化),中断执行完将返回到下一条指令。
  • 陷阱:是执行一条指令的结果,常有意为之,陷阱处理程序将控制返回到下一条指令。用途是在用户程序和内核之间提供一个像过程一样的接口(系统调用)。
  • 故障:由错误情况引起,如果能被故障处理程序修正,控制将返回到引起故障的指令重新执行,否则将返回到内核的abort例程终止应用程序。
  • 终止:是不可恢复的致命错误造成的结果,典型的是一些硬件错误,如:DRAM或SRAM位被损坏,终止处理程序从不把控制返回给用户程序,常见的是返回给abort例程。

2 进程

进程给每个程序提供了一种在独占使用处理器的假象。

2.1 基本概念

  • 进程:一个正在执行中的应用程序的实例,系统中的每个程序都是运行在某个进程的上下文(context)中的,应用程序还能够创建新进程,即进程与应用程序不是1对1的概念。

  • 进程组:每个进程都只属于一个进程组,进程组由一个正整数进程组ID来标识,getpgrp函数返回当前进程的进程组ID。

  • 调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这一过程叫调度,是由内核中的调度器处理的。

  • 上下文:是由程序正确运行所需要的状态组成的,这个状态包括:存储器中的程序代码和数据、它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

  • 上下文切换:是一种较高级形式得异常控制流,基于较底层的异常机制实现。在内核调度一个新的进程后,使用上下文切换机制来将控制转移到新的进程,上下文切换的主要作用有:
    1.保存当前进程的上下文;
    2.恢复某个先前被抢占进程所保存的上下文;
    3.将控制传递给新恢复的进程。
    在这里插入图片描述

  • 逻辑控制流:即PC(程序计数器)值,PC值唯一对应于被包含于程序的一条指令。
    如下图,假设是一个运行三个进程的系统,处理器的一个物理控制流被分成了三个逻辑流,每个进程一个,每个竖直方向上的列表示一个进程的逻辑流的一部分,即下图表示的是A先运行一会,然后B运行一会,然后C运行一会,A接着运行,最后C接着运行。
    每个逻辑流与其他逻辑流相对独立,除非他们之间有进程间通信(IPC)
    逻辑流在时间上有重叠的进程称为并发进程,如A和B、A和C是并发运行的,但B和C不是并发运行的。
    在这里插入图片描述

  • 私有地址空间:每个私有地址空间关联的存储器的内容一般不同,但是有相同的结构,如下图:
    在这里插入图片描述

  • 用户模式和内核模式:处理器用某个控制寄存器中的一个方式位来限制一个应用可以执行的指令和可以访问的地址空间范围,该寄存器描述了进程当前享有的权力,当方式位被设置时,进程运行在内核模式,否则运行在用户模式。
    内核模式下的进程可以执行指令集中的任何指令,并且可以访问任何存储器位置;
    用户模式下的进程不被允许执行特权指令,也不允许直接引用地址空间中内核区的代码和数据。

  • 进程的状态:从开发者的角度,可认为进程总处于下面三种状态:
    1.运行:要么在CPU上执行,要么在等待被执行且会被调度;
    2.暂停:进程被挂起且不会被调度。当收到SIGSTOP、SIGTSTP、SIDTTIN或SIGTTOU信号时,进程就暂停,直到收到SIGCONT信号,该进程再次开始运行;
    3.终止:进程永远的停止了,一般是如下三种情况:收到一个信号,该信号默认行为是终止程序;从主程序返回;调用exit函数。

2.2 进程控制

  • 获取进程ID:每个进程都有一个唯一的正数进程ID(PID),getpid函数返回调用进程的PID,getppid返回其父进程的PID。
  • 创建进程:父进程通过fork函数创建一个新的子进程,子进程拷贝父进程用户级虚拟地址空间,包括:文本、数据、bss段、堆以及用户栈,子进程可以读写父进程中打开的任何文件。
fork函数的特性:
1.调用一次,返回两次:一次是返回到父进程,一次是返回到子进程。在父进程中fork返回子进程的PID,在子进程中返回02.并发执行:父进程和子进程是并发运行的独立进程;
3.相同但是相互独立的地址空间;
4.共享文件:子进程继承了父进程所有的打开文件。

在这里插入图片描述
在这里插入图片描述

  • 回收子进程:一个进程由于某种原因终止时,内核并不是立即从系统中清除它,而且使进程保持在终止状态,直到被它的父进程回收。相关函数是waitpid,调用后返回子进程的pid,如果没有子进程则返回-1。
//示例:
//WNOHANG:非阻塞的调用此函数
int iStatus = 0;
pid_t pid = waitpid(-1, &iStatus, WNOHANG);
if (WIFEXITED(iStatus))
{
	std::cout << " pid: " << pid << " exit normal, status = " << WEXITSTATUS(iStatus) << std::endl;
}
//iStatus:子进程状态信息,wait.h文件包含了解析此参数定义相关的宏:
//返回一个正常终止的子进程的退出状态,只有WIFEXITED返回为真时,才会定义这个状态
# define WEXITSTATUS(status)	__WEXITSTATUS (status)
//返回引起子进程终止的信号,只有在WIFSIGNALED返回真时,才定定义这个状态
# define WTERMSIG(status)	__WTERMSIG (status)
//返回引起子进程暂停的信号,只有在WIFSTOPPED返回真时,才定定义这个状态
# define WSTOPSIG(status)	__WSTOPSIG (status)
//如果子进程正常终止就返回真,即是调用exit或主程序return
# define WIFEXITED(status)	__WIFEXITED (status)
//如果是因为一个未被捕获的信号导致子进程终止,则返回真(见3 信号)
# define WIFSIGNALED(status)	__WIFSIGNALED (status)
//如果引起返回的子进程当前是暂停的,那么就返回真
# define WIFSTOPPED(status)	__WIFSTOPPED (status)
# ifdef __WIFCONTINUED
#  define WIFCONTINUED(status)	__WIFCONTINUED (status)
# endif
  • 加载并允许程序execve函数加载并运行可执行目标文件,且带参数列表rgv和环境变量列表envp,此函数调用一次从不返回。

3 信号(signal)

一个信号就是一条消息,它负责通知进程某种类型的事件已经在系统中发生了。
在这里插入图片描述

3.1 基本概念

  • 发送信号:内核通过更新目的进程上下文中的某个状态,发生一个信号给目的进程。
    发生信号的原因有:
    1.内核检测到一个系统事件,如:除零错误或子进程终止;
    2.一个进程调用了kill函数,显示的要求内核发生一个信号给目的进程(包括它自己)。
    给进程发送信号的几种方法:
    1.键盘:键盘上输入ctrl + c,发送SIGINT信号到shell,shell捕获该信号然后发送到前台进程组中的每个进程,默认情况是终止前台作业。类似的ctrl + z会发送一个SIGTSTP信号;
    2.kill函数:进程可以通过调用kill函数发送任意信号给其他进程(包括它自己);
    3.alarm函数:进程可以调用alarm函数向它自己发送SIGALRM信号,起到定时中断的作用;
    4.kill程序:可以像另外的进程发送任意信号;
//示例1:kill函数
kill(20244, SIGINT);

//示例2:alarm函数
signal(SIGALRM, handlefun);
alarm(1);//1s后发送SIGALRM信号

//示例3:发送SIGKILL给Test程序,使其终止
//终端运行
$ ps -ef | grep Test
**      20244  20163  0 11:13 pts/3    00:00:00 ./Test
**      20286  20251  0 11:13 pts/4    00:00:00 grep --color=auto Test
$ kill -9 20244

  • 接收信号:内核从异常处理程序返回准备将控制返回给进程p时,会检测未被阻塞的待处理信号集合,如果此集合非空,内核将选择某个信号k(通常是最小的),并且强制p接收信号k,收到信号将会触发进程完成某种行为。
    每个信号类型都有一个预定义的默认行为,为进程终止、进程终止并转储存储器(dump core)、进程暂停直到SIGCONT信号重启、忽略该信号之一。
    signal函数可以改变信号相关联的行为:
//示例:设置SIGCHLD信号的关联行为
//忽略信号
signal(SIGCHLD, SIG_IGN);
//恢复默认行为
signal(SIGCHLD, SIG_DFL);
//执行全局函数handleChld
signal(SIGCHLD, handleChld);
//执行静态成员函数Manager::handleInter
signal(SIGCHLD,Manager::handleInter);
  • 待处理信号:一个只被发出而没有被接收的信号叫做待处理信号,在任何时刻,一个进程一种类型至多只会有一个待处理信号,如:一个进程与一个类型位k的待处理信号,那么接下来发送到这个进程的类型为k的信号都不会排队等待,而是简单的被丢弃。
    一个进程可以有选择性的阻塞接收某种信号,当某种信号被阻塞时,它仍可以被发射,但产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。
    一个待处理信号最多只能被接收一次,内核为每个进程在pending位向量中维护待处理信号集合,而在blocked位向量中维护被阻塞的信号集合。
  • 信号处理:一个程序要捕捉多个信号时,会产生如下问题:
    1.待处理信号被阻塞:Unix信号典型处理会阻塞当前处理程序正在处理的待处理信号类型,如:A进程捕捉了一个SIGINT信号,且正在执行相关处理程序(本质为函数),那么此时另一个SIGINT信号传递到这个进程时,会变成待处理的但是不会被接收,直到处理程序返回;
    2.待处理信号不会排队等待:任意类型至多只有一个待处理信号,多个同类型的信号同时到达只会保留一个,其他的会被丢弃,如SIGCHLD类型的待处理信号,仅表示至少有一个子进程退出;
    3.系统调用可以被中断:像read、write、accpt这些系统调用潜在的会阻塞进程一段时间,称之为慢速系统调用,在某些系统中,当处理程序捕捉一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误条件,并将errno置为EINTR。
  • 可移植的信号处理:不同系统之间,信号处理语义有差异,如:一个中断慢速系统调用是否重启或永久放弃。sigaction函数可以要求用户设置一个结构的项目(entry),调用方式与signal一样。
  • 显示的阻塞信号sigprocmask函数可以显示的阻塞和取消阻塞选择的信号,常用于同步父子进程,原型为extern int sigprocmask (int __how, const sigset_t *__restrict __set, sigset_t *__restrict __oset) __THROW;具体行为设定:
    SIG_BLOCK:添加set中的信号到blocked中(blocked = blocked | set)
    SIG_UNBLOCK:从blocked中删除set中的信号(blocked = blocked & ~set)
    SIG_SETMASK:blocked = set
    如果不同步父子进程,可能在执行addjob之前就执行deletejob,考虑如下情况:
    1.父进程执行fork函数,并且内核调度新创建的子进程代替父进程运行;
    2.在父进程可以再次运行之前,子进程会终止,并变成一个僵死进程,使得内核传递一个SIGCHLD信号给父进程;
    3.当父进程再次可以运行,但又在执行之前,内核注意到待处理得SIGCHLD信号,并通过在父进程中运行处理程序接收这个信号;
    4.处理程序回收终止的子进程,并调用deletejob,这个函数什么也不会做,因为父进程还没有把该子进程添加到列表中;
    5.在处理程序运行完毕后,内核运行父进程,父进程从fork返回,通过调用addjob错误的把(不存在的)子进程添加到列表中。

4 非本地跳转

将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用 - 返回序列,非本地跳转通过setjmplongjmp函数来实现。其重要应用有:
1.允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的;
2.使一个信号处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令的位置。

5 操作进程的工具

  • strace:打印一个程序和它的子进程调用的每个系统调用的轨迹。
  • ps:列出系统中当前的进程(包括僵死进程)。
  • top:打印出关于当前进程资源使用的信息。
  • kill:发送一个信号给进程。
  • /proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数据结构的内容。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值