异常控制流

现代系统通过使控制流发生突变来对这些情况做出反应。一般而言,我们把这些突变称为异常控制流(Exceptional Control Flow, ECF)。
作为程序员,理解EFC很重要,这有很多原因:

  • 理解EFC将帮助你理解重要的系统概念
  • 理解EFC将帮助你理解应用程序时如何与操作系统交互的
  • 理解EFC将帮助你编写有趣的新应用程序
  • 理解EFC将帮助你理解并发
  • 理解EFC将帮助你理解软件异常如何工作

    8.1异常

  • 异常(exception)* 就是控制流中的突变,用来相应处理器状态中的某种变化。在处理器中,状态被编码为不同的位和信号。状态变化称为事件(event)
    异常的剖析
    在任何情况下,当处理器检测到有事件发生时,他就会通过一张叫做异常表(exception table) 的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序(exception handler))。当异常处理程序完成处理后,根据引起异常的事件的类型,将会发生以下3种情况的一种:
  1. 处理程序将控制返回给当前指令Icuur,即当前事件发生时正在执行的指令。
  2. 处理程序将控制返回给Inext,如果没有发生异常将会执行的下一条指令。
  3. 处理程序终止被中断的程序。

    异常处理

    在运行时(当系统执行某个程序时),处理器检测到发生了一个事件,并且确定了相对的异常号k。随后,处理器触发异常,方法是执行间接过程调用,通过异常表的表目k,转到相应的处理程序。
    异常表

    异常的类别

    异常的类别
    总结:
  4. 中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。
  5. 陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
  6. 故障由错误情况引起,它可能能够被故障处理程序修正。
  7. 终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。

    Linux/x86-64 异常与系统调用

    异常

    系统调用


    在x86-64系统上,系统调用是通过一条称为syscall的陷阱指令提供的,其参数通过寄存器来传递。
    例如一个hello world
    1
    2
    3
    4
    5
    int main()
    {
    write(1, "hello, world\n",13);
    _exit(0);
    }

下面是汇编版本,直接使用syscall指令来调用write和exit系统调用

1
2
3
4
5
6
7
8
9
10
11
12
main:
//First call write(1, "hello, world\n",13);
movq $1, %rax //write is system call 1
movq $1, %rdi //Arg1: stdout has descriptor 1
movq $string, %rsi //Arg2: hello, world string
movq &len, %rdx //Arg3: string length
syscall //Make the system call

//Next, call _exit(0)
movq $60, %rax //exit is system call 60
movq &0, %rdi //Arg1: exit status is 0
syscall //Make the system call

进程

进程的经典定义就是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中(context)。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

每次用户通过向shell输入一个可执行目标文件的名字,运行程序时shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,然后在这个新进程的上下文中运行它们自己的代码或者其它的应用程序。

进程提供给应用程序的关键抽象:

  • 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占的使用处理器。
  • 一个私有的地址空间,他提供一个假象,好像我们的程序独占地使用内存系统。

    进程控制

    获取进程 ID

    每个进程都有一个唯一的正数(非零)进程 ID(PID)。getpid函数返回调用进程的 PID。getppid函数返回它的父进程的PID(创建调用进程的进程)。
    1
    2
    3
    4
    5
    #include <sys/types.h>
    #include <unistd.h>

    pid_t getpid(void);
    pid_t getppid(void);

创建和终止进程

父进程通过调用fork函数创建一个新的运行的子进程

1
2
3
4
5
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
返回:子进程返回0,父进程返回子进程的PID,如果出错则为-1

父进程和子进程之间最大的区别在于它们有不同的PID。

一个使用fork创建子进程的父进程的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
pid_t pid;
int x = 1;

pid = Fork();
if(pid == 0){
printf("child : x=%d\n", ++x);
exit(0);
}

/*Parent */
printf("parent: x=%d\n", --x);
exit(0);
}

当在Unix系统上运行这个程序时,我们得到下面结果:

1
2
3
linux> ./fork
parent: x=0
child: x=2

这说明了fork:

  • 调用一次返回两次。fork函数被父进程调用一次,但是却返回两次——一次是返回到父进程,一次是返回到新创建的子进程。
  • 并发执行。父进程和子进程是并发运行的独立进程。内核能够以任意方式交替执行它们的逻辑控制流中的指令。在我们的系统上运行这个程序时,父进程先完成它的printf语句,然后是子进程。然而,在另一个系统上可能正好相反。
  • 相同但是独立的地址空间。父进程和子进程对x所做的任何改变都是独立的,不会反应在另一个进程的内存中。这就是为什么当父进程和子进程调用他们各自的printf语句时,它们中的变量x会有不同的值。
  • 共享文件

    加载并运行程序

    execv函数在当前进程的上下文中加载并运行一个程序。
    1
    2
    3
    4
    5
    #include<unistd.h>

    int execcve(const char *filename, const char *argv[],
    const char *envp[]);
    如果成功,则不返回,如果错误则返回-1

argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数字符串。envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如”name=value”的名字-值对。

进程是执行中程序的一个具体的实例;程序总是运行在某个进程的上下文中。fork函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。execve函数在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间,但没有创建一个新的进程。新的程序仍然有相同的pid,并且继承调用execve函数时已打开的所有文件描述符。

-------------本文结束感谢您的阅读-------------
+ +