2018-11-29

## 异常控制流

异常控制流存在于系统的每个层级,最底层的机制称为**异常(Exception)**,用以改变控制流以响应系统事件,通常是由**硬件**和**操作系统**共同实现的。更高层次的异常控制流包括**进程切换(Process Context Switch)**、**信号(Signal)**和**非本地跳转(Nonlocal Jumps)**。进程切换是由硬件计时器和操作系统共同实现的,信号则只是操作系统层面的概念,非本地跳转就已经是在 C 运行时库中实现。

## 异常 Exception

这里的异常指的是把控制交给系统内核来响应某些事件(例如处理器状态的变化),其中内核是操作系统常驻内存的一部分,而这类事件包括除以零、数学运算溢出、页错误、I/O 请求完成或用户按下了 ctrl+c 等等系统级别的事件。

具体的过程可以用下图表示:

![img](https://wdxtub.com/images/14613541138958.jpg)

系统会通过异常表(Exception Table)来确定跳转的位置,每种事件都有对应的唯一的异常编号,发生对应异常时就会调用对应的异常处理代码

### 异步异常(中断)

异步异常(Asynchronous Exception)称之为中断(Interrupt),是由处理器外面发生的事情引起的。对于执行程序来说,这种“中断”的发生完全是异步的,因为不知道什么时候会发生。CPU对其的响应也完全是被动的,但是可以屏蔽掉[1]。这种情况下:

- 需要设置处理器的中断指针(interrupt pin)

- 处理完成后会返回之前控制流中的『下一条』指令

比较常见的中断有两种:计时器中断和 I/O 中断。计时器中断是由计时器芯片每隔几毫秒触发的,内核用计时器终端来从用户程序手上拿回控制权。I/O 中断类型比较多样,比方说键盘输入了 ctrl-c,网络中一个包接收完毕,都会触发这样的中断。

### 同步异常

同步异常(Synchronous Exception)是因为执行某条指令所导致的事件,分为陷阱(Trap)、故障(Fault)和终止(Abort)三种情况。

| 类型 | 原因            | 行为            | 示例                |

| ---- | ---------------- | ---------------- | ------------------- |

| 陷阱 | 有意的异常      | 返回到下一条指令 | 系统调用,断点      |

| 故障 | 潜在可恢复的错误 | 返回到当前指令  | 页故障(page faults) |

| 终止 | 不可恢复的错误  | 终止当前程序    | 非法指令            |

这里需要注意三种不同类型的处理方式,比方说陷阱和中断一样,会返回执行『下一条』指令;而故障会重新执行之前触发事件的指令;终止则是直接退出当前的程序。

### 系统调用示例

系统调用类似函数调用,在 x86-64 系统中,每个系统调用都有一个唯一的 ID,如

| 编号 | 名称    | 描述          |

| ---- | -------- | -------------- |

| 0    | `read`  | 读取文件      |

| 1    | `write`  | 写入文件      |

| 2    | `open`  | 打开文件      |

| 3    | `close`  | 关闭文件      |

| 4    | `stat`  | 获取文件信息  |

| 57  | `fork`  | 创建进程      |

| 59  | `execve` | 执行一个程序  |

| 60  | `_exit`  | 关闭进程      |

| 62  | `kill`  | 向进程发送信号 |

对应的示意图是:

![img](https://wdxtub.com/images/14613688255926.jpg)

### 故障示例

以 Page Fault 为例,来说明 Fault 的机制。Page Fault 发生的条件是:

- 用户写入内存位置

- 但该位置目前还不在内存中

比如:

```

int a[1000];

main()

{

    a[500] = 13;

}

```

那么系统会通过 Page Fault 把对应的部分载入到内存中,然后重新执行赋值语句:

![img](https://wdxtub.com/images/14613689402121.jpg)

但是如果代码改为这样:

```

int a[1000];

main()

{

    a[5000] = 13;

}

```

也就是引用非法地址的时候,整个流程就会变成:

![img](https://wdxtub.com/images/14613690660319.jpg)

具体来说会像用户进程发送 `SIGSEGV` 信号,用户进程会以 segmentation fault 的标记退出。

从上面我们就可以看到异常的具体实现是依靠在用户代码和内核代码间切换而实现的,是非常底层的机制。

## 进程

进程是程序(指令和数据)的运行实例。进程给每个应用提供了两个关键的抽象:一是**逻辑控制流**,二是**私有地址空间**。

逻辑控制流通过称为**上下文切换**(context switching)的内核机制让每个程序都感觉自己在独占处理器。

私有地址空间则是通过称为**虚拟内存**(virtual memory)的机制让每个程序都感觉自己在独占内存。这样的抽象使得具体的进程不需要操心处理器和内存的相关适宜,也保证了在不同情况下运行同样的程序能得到相同的结果。

计算机会同时运行多个进程,有前台应用,也后台任务

### 进程切换 Process Context Switch

这么多进程,具体是如何工作的呢?我们来看看下面的示意图:

![img](https://wdxtub.com/images/14613707308133.jpg)

左边是单进程的模型:内存中保存着进程所需的各种信息,因为该进程独占 CPU,所以并不需要保存寄存器值。

在右边的单核多进程模型中,虚线部分是正在执行的进程:因为可能会切换到其他进程,所以内存中需要另一块区域来保存当前的寄存器值,以便下次执行的时候进行恢复(也就是所谓的上下文切换)。整个过程中,CPU 交替执行不同的进程,虚拟内存系统会负责管理地址空间,而没有执行的进程的寄存器值会被保存在内存中。切换到另一个进程的时候,会载入已保存的对应于将要执行的进程的寄存器值。

而现代处理器一般有多个核心,所以可以真正同时执行多个进程。这些进程会共享主存以及一部分缓存,具体的调度是由内核控制的,示意图如下:

![img](https://wdxtub.com/images/14613708880333.jpg)

切换进程时,内核会负责具体的调度,如下图所示

![img](https://wdxtub.com/images/14613717282590.jpg)

### 进程控制 Process Control

**系统调用的错误处理**

在遇到错误的时候,Linux 系统级函数通常会返回 -1 并且设置 `errno` 这个全局变量来表示错误的原因。使用的时候记住两个规则:

1. 对于每个系统调用都应该检查返回值

2. 当然有一些系统调用的返回值为 void,在这里就不适用

例如,对于 `fork()` 函数,我们应该这么写:

```

if ((pid = fork()) < 0) {

    fprintf(stderr, "fork error: %s\n", strerror(errno));

    exit(0);

}

```

我们可以把整个 `fork()` 包装起来,就可以自带错误处理,比如

```

pid_t Fork(void)

{

    pid_t pid;

    if ((pid = fork()) < 0)

        unix_error("Fork error");

    return pid;

}

```

调用的时候直接使用 `pid = Fork();` 即可

**获取进程信息**

我们可以用下面两个函数获取进程的相关信息:

- `pid_t getpid(void)` - 返回当前进程的 PID

- `pid_t getppid(void)` - 返回当前进程的父进程的 PID

进程主要有三个主要状态:

- 运行 Running

  - 正在被执行、正在等待执行或者最终将会被执行

- 停止 Stopped

  - 执行被挂起,在进一步通知前不会计划执行

- 终止 Terminated

  - 进程被永久停止

**终止进程**

在下面三种情况时,进程会被终止:

1. 接收到一个终止信号

2. 返回到 `main`

3. 调用了 `exit` 函数

`exit` 函数会被调用一次,但从不返回,具体的函数原型是

```

// 以 status 状态终止进程,0 表示正常结束,非零则是出现了错误

void exit(int status)

```

**创建进程**

调用 `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

```

fork被当前函数调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的区域结构的原样副本;当两个进程中任一个后来进行写操作时,写时复制机制会创建新页面,从而保持了私有地址空间的概念

有以下几点需要注意:

- 调用一次,但是会有两个返回值

- 并行执行,不能预计父进程和子进程的执行顺序

- 拥有自己独立的地址空间(也就是变量都是独立的),除此之外其他都相同

- 在父进程和子进程中 `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);

}

```

对应的进程图为

![img](https://wdxtub.com/images/14625029984869.jpg)

### 加载

子进程载入其他的程序,就需要使用 `execve` 函数

1、子进程通过execve系统调用启动加载器,加载器**删除**子进程现有的虚拟内存段;

2、**创建**一组新的代码、数据、堆和栈段,新的栈和堆段被初始化为0;

3、通过将虚拟内存空间中的页**映射**到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容;

4、最后加载器**跳转**到_start地址,最终调用main函数

### 回收子进程

即使主进程已经终止,子进程也还在消耗系统资源,我们称之为『僵尸』。为了『打僵尸』,就可以采用『收割』(Reaping) 的方法。父进程利用 `wait` 或 `waitpid` 回收已终止的子进程,然后给系统提供相关信息,kernel 就会把 zombie child process 给删除。

如果父进程不回收子进程的话,通常来说会被 `init` 进程(pid == 1)回收,所以一般不必显式回收。但是在长期运行的进程中,就需要显式回收(例如 shell 和 server)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容