wywwzjj's Blog.

CSAPP 异常控制流 笔记

字数统计: 1.4k阅读时长: 5 min
2019/10/28 Share

异常

异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。

进程

关键抽象:

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

逻辑控制流

并发流

私有地址空间

用户模式和内核模式

上下文切换

系统调用错误处理

参见 error.h,这里想说的还是对错误返回处理的封装。

进程控制

程序员角度,可认为进程总是处于三种状态之一:

  • 运行:进程要么在 CPU 上执行,要么在等待被执行且最终会被内核调度。

  • 停止:进程的执行被挂起(suspended),且不会被调度,直到收到 SIGCONT 信号再次运行。

  • 终止:进程永远地停止了。

    三种原因可使得进程停止:

    • 收到一个信号,该信号的默认行为是终止进程。
    • 从主程序返回。
    • 调用 exit 函数。

获取进程 ID

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

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

创建和终止进程

1
2
3
4
#inlcude <stdlib.h>

pid_t fork(void);
void exit(int status);

回收子进程

当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递个父进程,然后抛弃已终止进程,从此时开始,该进程才不存在。

一个终止了但还未被回收的进程成为僵死进程(zombie)。

僵死进程已经终止了,但内核仍保留着它的某些状态直到父进程回收它为止。

一个进程可通过调用 waitpid() 来等待它的子进程终止或者停止。

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

pid_t waitpid(pid_t pid, int* statusp, int options);

(TODO:深入整理)

让进程休眠

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

unsigned int sleep(unsigned int secs);

int pause(void); // 休眠至进程接收到一个信号

加载并运行程序

execve 函数在当前进程的上下文加载并运行一个新程序。

而 fork 函数则是在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。

1
2
3
#include <unistd.h>

int execve(const char* filename, const char** argv, const char** envp);

与 fork 一次调用两次返回不同,execve 调用一次并从不返回。

信号

一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的时间。在 Linux 上支持了 30 中不同类型的信号。每个信号类型都对应于某种系统事件。

低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。

信号提供了一种机制,通知余户进程发生了这些异常。

发送

内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。

发送信号可以有如下两种原因:

  • 内核检测到一个系统事件,比如除零错误或者子进程终止。
  • 一个进程调用了 kill 函数,显示地要求内核发送一个信号给目的进程。

一个进程可以发送信号给它自己。

Unix 系统发送信号的机制

  • 进程组

    每个进程都只属于一个进程组,进程组是由一个正整数进程组 ID 来标识的。

    默认一个子进程和它父进程同属于一个进程组。

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

    pid_t getpgrp(void); // 返回调用进程的进程组 ID

    int setpgid(pid_t pid, pid_t pgid); // 设置进程组成功返回 0,否则为 -1
  • /bin/kill 程序

    1
    2
    kill -9 1023	# 杀掉 1023 进程
    kill -9 -1023 # 杀掉 1023 进程组的每个进程
  • 从键盘发送

    CTRL + C / Z :终止 / 挂起

  • 用 kill 函数

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

    int kill(pid_t pid, int sig);
  • 用 alarm 函数发送

    进程可通过调用 alarm 函数向自己发送 SIGALRM 信号,网络编程中可拿来处理超时。

    1
    2
    3
    #include <unistd.h>

    unsigned int alarm(unsigned int secs);

接收

当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个信号处理函数的用户层函数捕获这个信号。

处理

操作进程的工具

  • strace

    打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。

    这是一个超级牛逼的工具,比如你想跟进 PHP 内核底层实现,这就能收获大量信息。

  • ps

    列出当前系统中的进程(包括僵尸进程)。

  • top

    打印出关于当前进程资源使用的信息。

  • pmap

    显示进程的内存映射。

  • /proc

    一个虚拟文件系统,以 ASCII 文本格式输出大量内核数据结构的内容(从这也能感受到 Linux 文件的重要性),用户程序可以读取这些内容。

    (TODO:补充详细结构及其作用)

CATALOG
  1. 1. 异常
  2. 2. 进程
    1. 2.1. 逻辑控制流
    2. 2.2. 并发流
    3. 2.3. 私有地址空间
    4. 2.4. 用户模式和内核模式
    5. 2.5. 上下文切换
  3. 3. 系统调用错误处理
  4. 4. 进程控制
    1. 4.1. 获取进程 ID
    2. 4.2. 创建和终止进程
    3. 4.3. 回收子进程
    4. 4.4. 让进程休眠
    5. 4.5. 加载并运行程序
  5. 5. 信号
    1. 5.1. 发送
    2. 5.2. 接收
    3. 5.3. 处理
  6. 6. 操作进程的工具