深入理解计算机系统(四):异常控制流
文章目录
一、异常
在系统启动的时候,操作系统分配和初始化一张称为异常表
的跳转表,系统中每种可能出现的异常都有一个唯一的非负整数的异常号
,这些异常号就组成了这个异常表。当处理器检测到了发生了一个事件的时候,会在异常表中找到对应异常的异常号,随后处理器触发异常,通过异常号转到响应的处理程序,完成异常的处理。
异常表的起始地址存放在异常表基址寄存器
中,异常号相当于是到异常表中的索引,二者共同确定某个异常处理程序的地址。
小朋友,你是否有很多问号哈哈哈?异常到底有哪几类?
类别 | 原因 | 异步/同步 | 返回行为 |
---|---|---|---|
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
异常的分类
1.中断
中断是来自处理器外部的I/O设备的信号的结果。
2.陷阱
陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
用户程序经常向内核请求服务,比如读文件、创建新进程等等,为了允许对这些内核服务的受控的访问,处理器提供了特殊的syscall n
指令,当用户程序像请求服务
n
n
n的时候,就可以执行这个指令。
3.故障
故障可能被故障处理程序修正。如果能够修正,那么会将控制返回到引起故障的指令,重新执行;否则返回内核中的abort例程,终止该程序。
4.终止
顾名思义,这是由不可恢复的错误造成的结果,不会返回。处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。
二、进程
关于进程的一些基本介绍可以看看鹅厂进击OS系列(二):进程三连。
这里对于进程空间做一点补充,地址空间底部是保留给用户程序的,代码段通常是从地址0x400000开始的,地址空间顶部保留给内核。
进程控制(Unix提供的系统调用)
1.获取进程Id:getpid
getpid
函数返回调用者或者父进程的PID,每个线程都有唯一的正数进程ID,我们叫做PID。
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
pid_t
类型在Linux系统上的types.h
中定义为int
类型。
2.创建和终止进程
进程终止一般有三种情况,首先是收到一个信号(该信号的默认行为是终止进程),其次是从主程序返回,最后是调用exit
函数。
#include <stdlib.h>
void exit(int status);
父进程通过调用fork
函数创建新的运行的子进程。
#include <sys/types.h>
#include <unistd.h>
//子进程返回0,父进程返回子进程的PID,如果出错,则为-1
pid_t fork(void);
子进程得到与父进程虚拟地址空间相同的(但是独立)一份副本,子进程还获得与父进程任何打开文件描述符相同的副本。两个进程的地址空间是相同的,每个进程有相同的用户栈、堆什么的,但是父进程和子进程是独立的,各自有各自的私有地址空间,父进程和子进程对某一个变量的任何改变都是独立的。此外二者的区别还在于PID不同。
fork函数只被调用一次,但是却返回两次:一次是在父进程中,一次是在新创建的子进程中。父进程中,fork返回子进程的pid,在子进程中返回0。但是!子进程的PID总是为非0,所以返回值就提供一个明确的方法来分辨程序在父进程还是在子进程中执行。
3.回收子进程
当一个进程由于某种原因终止的时候,内核并不是立即把它从系统中清除,而是处于一种已终止的状态里,直到被它的父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,该进程才不存在。一个终止了但是没有被回收的进程称为僵死进程。
4.让进程休眠
sleep函数可以实现将一个进程挂起一段指定时间。
#include <unistd.h>
//如果请求的时间到了,那么返回0,否则返回还需要休眠的秒数
unsigned int sleep(unsigned int secs);
5.加载并运行程序
#include <unistd.h>
//如果成功,则不返回,如果错误,则返回-1
int execve(cosnt char *filename,const char *argv[],consit char *envp[]);
加载并运行filename,argv是参数,envp是环境变量,当出现错误的时候,execve才会返回到调用程序,成功的话是不返回任何东西的。
argv[0]指的是文件的名字,环境变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如name=value
的键值对。
三、信号
信号是一种更高层的软件形式的异常,运行进程和内核终端其他进程。
信号术语
传送一个信号到目的进程由两个不同步骤组成:
- 发送信号。内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。发信号有2种原因:1)进程调用
kill
函数;2)内核检测到系统事件,如子进程终止。 - 接收信号。当目的进程被迫对信号的发送作出反应时,他就接受了信号。进程可以忽略信号,终止或者通过执行一个称为信号处理程序的用户层函数捕获这个信号。
发出去但是没被接受的信号叫做待处理信号,一个类型最多只有一个待处理信号,如果一个进程已经有了类型为 k k k的带春丽信号,那么接下来再给这个进程发类型为 k k k的信号都会被简单丢弃。
发送信号
1.进程组
每个进程只属于一个进程组,进程组用正整数ID来标识。子进程和其父进程默认属于一个进程组,可以通过int setgpid(pid_t pid,pid_t pgid)
改变自己或者其它进程的进程组,将pid的进程组改为pgid。
#include <unistd.h>
//返回调用进程的进程组ID
pid_t getpgrp(void);
2./bin/kill程序发送信号
linux> /bin/kill -9 15213
上面这命令意思是发送信号9(SIGKILL)给进程15213,如果PID是个负的,那么会导致信号发送到进程组PID中的每个进程。
3.从键盘发送信号
linux> ls|sort
上面这命令会创建两个进程(二者通过Unix管道连接),一个进程运行ls,一个进程运行sort。
4.kill函数发送
kill函数发送信号可以给自己也可以给其他进程。如果pid大于0,那么该函数发送sig给进程pid,如果pid为0,那么发送sig给进程所在进程组的每个进程,包括调用进程自己,如果pid小于0,那么发送信号sig给进程组|pid|中的每个进程。
#include <sys/types.h>
#include <signal.h>
//若成功返回0,错误为-1
int kill(pid_t pid,int sig);
5.alarm函数
调用该函数给自己发送SIGALRM信号。
#include <unistd.h>
//前一次闹钟剩余的描述,若以前没有设定闹钟则返回0
unsigned int alarm(unsigned int secs);
接收信号
收到某个信号,会做相关联的默认行为,比如说我们收到了SIGKILL
信号的默认欣慰就是终止接收进程。signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
- 如果handler是SIG_IGN,那么忽略类型为signum的信号。
- 如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为。
- 否则,handler就是用户定义的函数的地址,这个函数就叫做信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序。
#include <signal.h>
typedef void (*sighandler_t)(int);
//若成功则为指向前次处理程序的指针,若出错则为SIG_ERR
sighandler_t signal(int signum,sighandler_t handler);
未分类
1.像异常处理程序、进程、信号处理程序、线程和Java进程等等都是逻辑流,一个逻辑流的执行在时间上和另一个流重叠,就叫做并发流。多个流并发执行的现象叫做并发,一个进程和其它进程轮流运行的概念叫做多任务,一个进程执行它的控制流的一部分的每一时间段叫做时间片,因此,多任务也叫做时间分片。
2.并发流和流运行的处理器核数或者计算机数无关。如果两个流在时间上重叠,那么是并发的,即使是运行在同一个处理器上。如果两个流并发的运行在不同的处理器核或者是计算机上,那么就是并行流。