异常
异常是异常控制流的一种,一部分由硬件实现,一部分由操作系统实现。
异常(exception)就是控制流的突变,用来响应处理器状态的某些变化。
状态变化又叫做事件(event)。
事件可能与当前执行指令有关 :
- 存储器缺页,算数溢出
- 除0
也可能与当前执行指令无关 :
- I/O请求
- 定时器产生信号
通过异常表(exception table)的跳转表,进行一个间接过程调用,到专门设计处理这种事件的操作系统子程序(异常处理程序(exception handler))
异常处理完成后,根据事件类型,会有三种情况:
- 返回当前指令,即发生事件时的指令。
- 返回没有异常,所执行的下一条指令
- 终止被中断的程序
异常的分类
“异常” 按处理方式分为中断、故障、自陷和终止四类
-
中断:
1.中断是异步发生,是来自处理器外部的I/O设备的信号的结果。
2.硬件中断不是由任何一条专门的指令造成,从这个意义上它是异步的。
(1)硬件中断的异常处理程序通常称为中断处理程序(interrupt handle)
(2)I/O设备通过向处理器芯片的一个引脚发信号,并将异常号放到系统总线上,以触发中断。
(3)在当前指令执行完后,处理器注意到中断引脚的电压变化,从系统总线读取异常号,调用适当的 中断程序。
(4)当处理程序完成后,它将控制返回给下一条本来要执行的指令。 -
故障(fault) :执行指令引起的异常事件,如溢出、非法指令、缺页、
访问越权等。“断点”为发生故障指令的地址。 -
自陷(Trap) :预先安排的事件(“埋地雷”),如单步跟踪、断点、
系统调用 (执行访管指令) 等。是一种自愿中断。“断点”为自陷指令下条指令地址。 -
终止(Abort) :硬故障事件,此时机器将“终止”,调出中断服务程
序来重启操作系统。“断点”是任意的。
“断点”:异常处理结束后回到原来被“中断”的程序执行时的起始指令
进程
-
异常是允许操作系统提供进程的概念的基本构造块,进程是计算机科学中最深刻,最成功的概念之一。
-
假象,觉得我们的程序是系统中唯一运行着的程序。我们的程序好像独占处理器和存储器。
-
这些假象都是通过进程概念提供给我们的。
-进程经典定义:一个执行中的程序实例. -
系统中每个程序都是运行某个进程的上下文中的。
-
上下文是由程序正确运行所需的状态组成。
-
这个状态包括存储器中的代码和数据,它的栈,通用目的寄存器,程序计数器,环境变量等。
-
进程提供的假象 :
-
一个独立的逻辑控制流。
-
一个私有的地址空间。
-
用户模式和内核模式
处理器提供一种机制,限制一个应用程序可以执行的指令以及它可以访问的地址空间范围。这就是用户模式和内核模式。
处理器通过控制寄存器中的一个模式位来提供这个功能。
1.该寄存器描述了进程当前享有的特权。
(1)设置了模式位后,进程就运行在内核模式中(有时也叫超级用户模式);内核模式下的进程可以执行指令集的任何指令,访问系统所有存储器的位置。
(2)没有设置模式位时,进程运行在用户模式。 用户模式不允许程序执行特权指令。 比如停止处理器,改变模式位,发起一个I/O操作。不允许用户模式的进程直接引用地址空间的内核区代码和数据。任何尝试都会导致保护故障。用户通过系统调用间接访问内核代码和数据。
2.进程从用户模式转变位内核模式的方法
(1)通过中断,故障,陷入系统调用这样的异常。
(2)在异常处理程序中会进入内核模式。退出后,又返回用户模式。
Linux提供一种聪明的机制,叫/proc文件系统。
1.允许用户模式访问内核数据结构的内容。
2./proc文件将许多内核数据结构输出为一个用户程序可以读的文本文件的层次结构。
进程的上下文切换
获取进程
我们可以用下面两个函数获取进程的相关信息:
pid_t getpid(void) - 返回当前进程的 PID
pid_t getppid(void) - 返回当前进程的父进程的 PID
我们可以认为,进程有三个主要状态:
- 运行 Running
正在被执行、正在等待执行或者最终将会被执行 - 停止 Stopped
执行被挂起,在进一步通知前不会计划执行 - 终止 Terminated
进程被永久停止
终止进程
调用 fork 来创造新进程。这个函数很有趣,执行一次,但是会返回两次,具体的函数原型为
// 对于子进程,返回 0
// 对于父进程,返回子进程的 PID
int fork(void)
子进程几乎和父进程一模一样,会有相同且独立的虚拟地址空间,也会得到父进程已经打开的文件描述符(file descriptor)。比较明显的不同之处就是进程 PID 了。
看一个简单的例子:
int main()
{
pid_t pid;
int x = 1;
pid = Fork();
if (pid == 0)
{ // Child
printf("I'm the child! x = %d\n", ++x);
exit(0);
}
// Parent
printf("I'm the parent! x = %d\n", --x);
exit(0);
}
输出是
linux> ./forkdemo
I'm the parent! x = 0
I'm the child! x = 2
有以下几点需要注意:
- 调用一次,但是会有两个返回值
- 并行执行,不能预计父进程和子进程的执行顺序
- 拥有自己独立的地址空间(也就是变量都是独立的),除此之外其他都相同
- 在父进程和子进程中 stdout 是一样的(都会发送到标准输出)
进程图
进程图是一个很好的帮助我们理解进程执行的工具:
- 每个节点代表一条执行的语句
- a -> b 表示 a 在 b 前面执行
- 边可以用当前变量的值来标记
- printf 节点可以用输出来进行标记
- 每个图由一个入度为 0 的节点作为起始
对于进程图来说,只要满足拓扑排序,就是可能的输出。我们还是用刚才的例子来简单示意一下:
int main()
{
pid_t pid;
int x = 1;
pid = Fork();
if (pid == 0)
{ // Child
printf("child! x = %d\n", --x);
exit(0);
}
// Parent
printf("parent! x = %d\n", x);
exit(0);
}
对应的进程图为:
回收子进程
即使主进程已经终止,子进程也还在消耗系统资源,我们称之为『僵尸』。为了『打僵尸』,就可以采用『收割』(Reaping) 的方法。父进程利用 wait 或 waitpid 回收已终止的子进程,然后给系统提供相关信息,kernel 就会把 zombie child process 给删除。
如果父进程不回收子进程的话,通常来说会被 init 进程(pid == 1)回收,所以一般不必显式回收。但是在长期运行的进程中,就需要显式回收(例如 shell 和 server)。
如果想在子进程载入其他的程序,就需要使用 execve 函数,具体可以查看对应的 man page,这里不再深入。
fork函数简单举例
void fork0()
{
if (fork() == 0) {
printf("Hello from child\n");
}
else {
printf("Hello from parent\n");
}
}
void fork1()
{
int x = 1;
pid_t pid = fork();
if (pid == 0) {
printf("Child has x = %d\n", ++x);
}
else {
printf("Parent has x = %d\n", --x);
}
printf("Bye from process %d with x = %d\n", getpid(), x);
}
void fork17()
{
if (fork() == 0) {
printf("Child: pid=%d pgrp=%d\n",
getpid(), getpgrp());
}
else {
printf("Parent: pid=%d pgrp=%d\n",
getpid(), getpgrp());
}
while(1);
}