异常是什么?
从我们的代码运行机制来讲,所有事情的发生不见得都是事前有所准备的,这就和我们的生活一样,总会有各种各样的意外产生,所谓异常,在计算机底层中,也意味着这样一件事情。
当我们运行一段程序代码时,如果所有指令在执行过程中都是顺着程序指令一条一条,最多只通过跳转/调用/返回这一系列可控的指令而运转,那么毫无疑问这样的运行结果是我们所期待的模样。但是程序运行时并不是独立的,它在使用CPU或者系统内存时不可能做到只有它一个部门在运转,它可能还需要调用系统底层的函数或者传递一些信号等等,在这些交互中就可能出现一些意料之外的事情,或者需要正在运行的程序作出一些改变,比如说暂停下当前的工作,这些都是异常。
异常是异常控制流(ECF)的一种形式,由硬件和操作系统共同实现。什么又是异常控制流?它是一种能够在非正常顺序指令(跳转/调用/返回等都是正常指令转移)间产生突变转移变化的事件。
常用以下图例来说明异常:
常常这样来描述一次异常事件的过程,当程序执行到一条指令的时候,系统记录下这条指令或者其下一条指令的地址,进而调用异常,紧接着由异常处理机制进行相关处理,再判定是否将控制权返回到将要执行的指令的地址继续刚刚的进程。
异常可以分为四类,分别是中断,陷阱,故障和终止。
中断是处理器外部I/O设备的信号产生的结果,硬件中断不是由任何一条专门的指令造成的。执行完后它总是返回到下一条指令。
陷阱是有意的异常,它最重要的用途实在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。当用户程序想要请求服务n时,可以通过“syscall n”(n指代不同系统调用的编号)指令产生陷阱。它总是返回到下一条指令。
故障本身又分可修复和不可修复,它由错误引起,如果在调用故障处理程序后该故障被修正了,则重新执行当前指令(被故障中止的指令),如“缺页异常“/“除法错误”/“一般保护故障”(后二者不可恢复)等。如果不能被修正,则终止。
终止是不可恢复的致命错误造成的,通常是一些硬件错误,如“DRAM或者SRAM位被损坏时发生的奇偶错误”/“机器检查”等,它会直接终止进程,而不会返回。
其中中断异常是外部的,陷阱/故障和终止都是执行当前指令的后果。
进程就是一个正在执行中的程序。它拥有一个独立的逻辑控制流和一个私有的地址空间。
什么叫做逻辑控制流?它实际上是对于序列指令的称呼(实质上一个进程从启动到结束仅且唯一拥有一个逻辑控制流即一套指令执行序列)。
这个时候要强调一下并发流的概念,当不同的逻辑流在执行时间上重叠的时候(中途进行进程切换),就被称为并发流。
那么什么又叫做私有地址空间呢?这实质上就是一种通用的地址空间的结构。每一个进程都有一段属于自己的数据以及代码的存储空间,这些地方的地址被存放在一个统一标准化的地址空间中,且这个地址空间自身的地址都是相同的!这实质上是虚拟内存起到的作用,这里暂不细说。
至于用户模式和内核模式也不消多说,只知道进程运行在权限较低的用户模式,而系统则处于权限极高的内核模式就可。一般的异常调用以及信号传送处理等都是在这两个模式之间进行切换。
上下文切换是由操作系统内核使用的高级异常控制流,用来实现多任务,也就是实现多个进程并发执行的操作。它在实际运用中有很多优点,对于系统来说非常有用。
简单来说,我们在运行一个程序文件的时候,首先经过fork函数创建一个新的进程,再由execve函数在该进程中加载并运行可执行目标文件。
调用fork函数就能在当前进程中创建一个子进程,子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本,包括代码和数据段,堆,共享哭以及用户栈(甚至缓冲区),能够执行本次fork之后的所有指令。子进程与父进程最大的区别便在于它们有不同的编号PID,而且它只进行一次调用却能返回两次,如果是在父进程中则返回值为子进程的PID,如果在子进程中则返回0。
且有进程组的概念,当子进程被创建出来时自动归于以父进程为名的进程组名下,这样一来只要杀死进程组的进程名,就可以杀死这个进程组中所有进程。可通过getgrop返回当前进程的进程组ID,可通过setgrop改变自己或其它进程的进程组。
二者是并发执行的,拥有相同但是相互独立的地址空间,并且拥有一些共享文件。关于实例部分请参看实验代码之fork。
可用getpid返回当前进程PID,可用getppid返回改进程父进程的PID。常用pid_t数据类型定义PID,实质上一般是int型。
fork是创建,相应的就是exit函数的终止。当然,终止进程并不只有 (1)exit()/_exit()系列函数 可以实现,它还可通过 (2)收到一个默认行为是终止的信号, (3)从主程序返回(return())。
当子进程没能在父进程运行完之前结束,它就会变成孤儿,直到运行结束之后被init进程回收。但是,当子进程一直处在僵死状态时,如在一个循环中一直没能结束,就会消耗系统的内存资源!面对这种情况,系统设置了 wait() 和 waitpid() 两个函数用来给父进程调用对子进程进行等待回收。
waitpid(pid,*statusp,options),其中pid>0则表示特定某一子进程,=-1表示等待此父进程下所有子进程结束,statusp地址非空时用来返回其子进程status,为下段宏定义值,options为0时默认先挂起父进程等待子进程结束,可修改。该函数返回结束子进程的PID。
这样一来,如果子进程没有结束,父进程就也不会结束,从而引起程序员的警觉。(在实例中发现当父进程结束之后便难以再查觉到子进程的状态,此时只能通过 ps 命令查看当前所有进程并且相应地调用 kill 命令区杀死进程)
通过系列宏定义可以知晓相关子进程的状态信息:
WIFEXITED(status):如果子进程正常结束则返回真。
WEXITSTATUS(status):取得子进程exit()返回的结束状态,先用WIFEXITED 来判断为真才能使用此宏。
WIFSIGNALED(status):如果子进程是因为信号而结束则此宏值为真 。
WTERMSIG(status):取得子进程因信号而中止的信号代码,先用WIFSIGNALED 来判断为真才能使用此宏。
WIFSTOPPED(status):如果子进程处于暂停执行情况则此宏值为真。
WSTOPSIG(status):取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED 来判断为真才能使用此宏。
WIFCONTINUED(status):如果子进程收到SIGCONT信号重新启动,则返回真。
sleep函数可以使当前进程挂起一段指定的时间,这个时间在Linux系统上以秒/s为单位,在windows系统上以毫秒/ms为单位。
除此之外,pause函数也可让进程进入休眠状态,而它需要等待一个特定的信号才能退出休眠状态。
信号实质上也是一种异常,它被用来通知进程在系统中发生的事件,或者被用来传递消息。Linux系统上支持三十种信号:
传送一个信号分为两个步骤:①发送信号;②接收信号。
每一个信号类型都有以下几种默认行为:
在这种默认情形下,除了SIGSTOP和SIGKILL信号之外,所有的信号类型都可通过signal(signum,handler)(signum是信号编号)函数修改其默认行为,其中handler有几种情形:
阻塞信号及相关非本地跳转暂时不讲,异常暂告一段落。