异常控制流 : 信号与 非本地跳转 ECF: Signals & Nonlocal Jumps 罗世通元培学院 2018 年年 12 月 6 日 1
为什什么需要信号? Simple Shell shellex.c Foreground job child process execve shell command fork waitpid Background job child process execve zombie (as long as its parent process is running) shell command fork 需要 一种消息机制, 后台执 行行的 子进程结束后, 通知 父进程, 及时将其回收 否则只要 父进程不不结束, 子进程就会成为僵 尸进程驻留留在内存 里里浪费资源 2
定义 Linux 信号 : 是 一条 小消息, 通知进程系统中发 生了了某种类型的事件 是 一种更更 高层的软件形式异常, 允许进程和内核中断其他进程 ECF 异步 (Async) 中断 (Interrupts) 信号 (Signals) 陷阱 (Traps) 1. 异步的, 外 生的异常, 不不是由进程 自身某条指令导致的 2. 由进程处理理, 与其它类型的异常不不同 ( 系统内核处理理 ) 同步 (Async) 故障 (Faults) 终 止 (Aborts) 可以由内核发出, 也可以由其他某个进程发出 每个信号与 一个整数相对应 (1-30) SIGINT (2) SIGKILL (9) SIGSEGV (11) SIGALRM (14) SIGCHLD (17) SIGTSTP (14) SIGFPE (8) 信号消息只携带信号的类型这 一信息 3
Final 2014
发送信号 内核位每个进程维护了了 一个 pending 位向量量, 只要该进程被传送了了 一个类型为 k 的信号, 那么向量量的第 k 位就会被设置为 1. 当 一个进程接收了了类型 k 的信号, 那么向量量的第 k 位就会被设置为 0. 因此, 只能知道是否有未被接收的某种类型的信号, 无法知道有多少个信号 在进程接收某种信号前, 多次发送某种类型的信号与发送 一次的效果是相同的 系统调 用作 用参数特别说明返回值说明 pid_t getpgrp(void); int setpgid(pid_t pid, pid_t pgid); 返回当前进程所属的进程组 ID 将进程 pid 的进程组改为 pgid - - pid=0, 使 用当前进程 pid; pgid=0, 使 用 pid 指定的进程 pid 作为 pgid. - int kill(pid_t pid, int sig); 向进程 / 进程组发送信号 sig pid=0, 以当前进程所属进程组所有进程为 目标 ; pid<0, 以进程组 (-pid) 为 目标. - unsigned int alarm(unsigned int secs); 在 secs 秒后向 自身发送 SIGALRM - 取消还没有到时间的闹钟并返回其秒数 5
Final 2015
Final 2014
人为发送信号的 方法 /bin/kill -X PID 键盘 : Ctrl-C(SIGINT) Ctrl-Z(SIGTSTP) int kill(pid_t pid, int sig); 8
接收信号 When: 内核把进程 p 从内核模式切换到 用户模式时 ( 上下 文切换或者从系统调 用返回 ) How: 内核检查进程 p 未被阻塞的待处理理信号集合 (pending & ~blocked), 如果集合 非空, 则选择某个信号 ( 通常是最 小的信号 ), 强制 p 接收 What: 触发进程采取某些 行行为 信号预定义的默认 行行为 : 终 止 (SIGKILL, SIGINT) 停 止 (SIGTSTP, SIGSTOP) 忽略略 (SIGCHLD) 进程通过注册信号 handler 以 自定义 行行为覆盖默认 行行为,SIGSTOP, SIGKILL 的默认 行行为不不能被修改 最后, 如果可能, 控制传递回 p 逻辑控制流的下 一条指令 9
阻塞信号 接收信号 : 内核检查进程 p 未被阻塞的待处理理信号集合 (pending & ~blocked), 如果集合 非空, 则选择某个信号 ( 通常是最 小的信号 ), 强制 p 接收 修改阻塞信号的条件即修改 blocked 位向量量 相关的系统调 用 : handler_t *signal(int signum, handler_t *handler); /* 出错返回 SIG_ERR*/ void sigprocmask(int how, const sigset_t *set, sigset_t *oldset); int how: SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK void sigemptyset(sigset_t *set); void sigfillset(sigset_t *set); void sigaddset(sigset_t *set, int signum); void sigdelset(sigset_t *set, int signum); int sigismember(const sigset_t *set, int signum); 隐式阻塞信号 : 内核默认阻塞当前正在处理理的信号类型的待处理理信号 10
安全的信号处理理 必要性 : 主程序和信号处理理程序并发运 行行 Main process SIGINT handler I1: jerrysbalance -= 50; SIGINT Ih: print(jerrysbal+tomsbal) I2: tomsbalance += 50; Inconsistency 主程序处理理 一个 转账 事务 : 从 Jerry 的账户转 50 元到 Tom 的账户, 该事务被分为 2 条指令 I1 I2. I1 结束后 I2 开始前, 主程序被 SIGINT 打断, 进 入处理理程序, 处理理程序输出所有 人账户余额的总和, 此时的数据是不不 一致的 (Inconsistent) 11
安全的信号处理理 必要性 : 主程序和信号处理理程序并发运 行行 对策 : G0. 处理理程序尽可能简单, 做少错少, 可以设置全局标志 立即返回, 让主程序去做主要的事情 G1. 在处理理程序中只调 用异步信号安全的函数 (Async-Signal-Safe) 异步信号安全函数 : (1) 可重 入 ( 只访问局部变量量 ) 的函数. (2) 不不能被信号处理理程序中断的函数 G2. 保存和恢复 errno 许多 Linux 异步信号安全函数都会在出错返回时设置全局变量量 errno. G3. 阻塞所有信号, 保护对全局共享数据的访问 如果处理理程序和主程序需要访问同 一个全局数据结构, 应该在访问前 一刻阻塞所有信号, 保证当前程序能够不不被打断地完成对该数据的全部操作 G4. 用 volatile 声明全局变量量 避免编译器器优化时把这个全局变量量 一直放在寄存器器中 G5. 用 sig_atomic_t 声明标志 标志可 用来标记已收到某种信号, 主程序通过判断标志的变化执 行行相关的操作 (G0) 使 用 sig_atomic_t 关键字修饰全局变量量可以保证对这 一变量量的单次读 / 写是不不可中断的 12
同步流避免并发错误 Foreground job child process execve shell command fork waitpid Foreground job (Explicitly Waiting for Signals) child process execve shell command fork wait for SIGCHLD 13
同步流避免并发错误 while (!pid) ; child process execve no SIGCHLD SIGCHLD Handler sets pid=0 loop breaked CPU 空转 浪费资源 shell command fork pid=fork() check if pid==0? yes while (!pid) sleep(1); shell child process command fork pid=fork() execve sleep SIGCHLD SIGCHLD Handler sets pid=0 loop breaked no check if pid==0? yes 太慢 14
同步流避免并发错误 while (!pid) pause(); shell child process block SIGCHLD fork pid=fork() execve unblock pause SIGCHLD no SIGCHLD Handler sets pid=0 SIGCHLD has been handled (pending[17] set to zero) just before pause() is called yes check if pid==0? 潜在冲突 while (!pid) sigsuspend(&prev); shell child process block SIGCHLD fork pid=fork() execve sigsuspend(prev) SIGCHLD no yes check if pid==0? SIGCHLD will be handled right after sigsuspend is called and pid will be set to 0 Note that SIGCHLD is not masked by prev 15
同步流避免并发错误 man pause Pause is made obsolete by sigsuspend
非本地跳转 Nonlocal Jumps setjmp / longjmp int setjmp(jmp_buf j); 标记跳转 目的地将标记时的上下 文 ( 寄存器器 PC 栈指针) 存 入 j (jump buffer) 调 用 一次返回两次 : 第 一次是标记时 ( 返回 0), 第 二次是跳转回来时, 返回 一个 自定义的整型 i 函数值不不能赋值给 一个变量量, 但是可以 用在 if switch 语句句中 void longjmp(jmp_buf j, int i); 从 j 恢复上下 文 ( 寄存器器 栈指针 ), 将 %rax 设置为 i 将 PC 设置为 j 中储存的 PC 值 限制 : 只能跳转回正在执 行行的过程中 f1 f2 f3 f1 f2 (returned) f3 17
Final 2016
谢谢