-
基本概念
-
控制流
a0, a1, ..., an-1
其中,ai代表指令Ii的地址。ai->ai+1的过渡称为控制转移,这样的控制转移队列称为控制流
-
ECF(Exceptional Control Flow, 异常控制流)
现代os通过使得控制流发生突变,来对特殊情况做出反应,称为异常控制流
-
ECF发生在计算机系统的各个层次
硬件层 --- 硬件检测到的事件会触发控制转移到异常处理程序 操作系统层 --- 内核上下文切换将控制从一个用户进程转向另一个用户进程 应用层 --- 一个进程向另一个进程发**信号**,接收进程会将控制转向信号处理程序
-
异常 --- 硬件和操作系统交界 进程和信号 --- 应用和操作系统交界 非本地跳转(例如try/catch/throw) --- 应用层
-
-
异常
-
处理器的状态变化称为事件
(1) 事件可能和当前指令相关
eg. 缺页、除0
(2) 事件可能和当前指令无关
eg. 定时器信号、IO请求完成
-
异常处理的原理
(1) 系统为每种可能出现的异常分配一个异常号
(2) 在系统启动时,系统初始化一个异常表:异常表的索引是异常号,对应的是异常处理程序的地址
(3) 异常处理程序运行在内核态
-
异常的类别
(1) 中断
1° IO设备(网络适配器、磁盘控制器、定时器),向处理器芯片的一个管脚发信号触发中断
2° 唯一一种异步类型的异常
(2) 陷阱
1° 有意的异常,目标是用户程序向内核请求服务,陷阱提供了这个接口
2° 执行
syscall n
指令,请求某个内核服务(例如读文件read、创建新进程fork、加载一个新程序execve、终止当前进程exit)
3° 系统调用的过程类似函数调用。但是函数调用在用户模式下,系统调用在内核模式下
(3) 故障
1° 执行故障处理程序,如果能修复就重新执行,修复不了就返回内核的abort例程终止程序
2° 典型的故障异常是缺页异常
(4) 终止
1° 典型的终止异常是硬件错误
2° 终止处理程序会直接返回内核的abort例程终止程序
(5) 总结
类别 原因 同步or异步 返回行为 中断 IO设备的信号 异步 总是返回到下一条指令 陷阱 有意进行的系统调用 同步 总是返回到下一条指令 故障 潜在可恢复的错误 同步 要么返回到当前指令重新执行,要么abort终止应用程序 终止 不可恢复的错误 同步 abort终止应用程序
-
-
进程
-
基本概念
(1) 定义
一个执行中程序的实例。
系统中的每个程序都是运行在某个进程的上下文中
(2) 上下文(context)
由程序正确运行所需的状态组成。
包括:
1° 存储器中程序的代码和数据 2° 栈 3° 通用目的寄存器中的内容 4° 程序计数器 5° 环境变量 6° 打开文件的描述符集合
(3) 进程提供的抽象
1° 一个独立的逻辑控制流
---- 提供了一个假象,让人们觉得程序在独占处理器
2° 一个私有的地址空间
---- 提供了一个假象,让人们觉得程序在独占存储器系统
-
逻辑控制流
(1) 进程轮流使用处理器,然后被抢占
A ------ B ----- C -- A -------
但是每个进程看上去都像是在独占处理器
(2) 并发进程
任何逻辑流在时间上和其他逻辑流有重叠的进程之间称为并发进程
eg.
A 和 B A 和 C 但是 B 和 C 就不是并发进程
(3) 时间片
一个进程在执行它的控制流的一部分的每一时间段叫做时间片
-
私有地址空间
(1) 一个进程为每个程序提供它自己的私有地址空间,一般而言,和这个空间中某个地址相关联的存储器字节不可被读写
(2) 对于n位地址的机器,地址空间是2^n个可能地址的集合
(3) 每个地址空间的结构类似
顶部1/4预留给内核,底部3/4预留给用户程序,包括通常的文本、数据、堆、栈。
-
用户模式和内核模式
(1) 内核模式
一个运行在内核模式的进程,可以
1° 执行指令集中的任何指令
2° 访问任何存储器的位置
(2) 用户模式
1° 不允许执行特权指令
2° 不允许直接使用地址空间中的内核区的代码和数据 —> 必须通过系统调用接口的方式
(3) 初始时应用程序都在用户模式,进程从用户模式到内核模式的唯一方法是通过异常
-
上下文切换
(1) 上下文是内核重新启动一个被抢占进程所需的状态,由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、各种内核数据结构(页表、进程表、文件表等)
页表 --- 描述地址空间 进程表 --- 包含有关当前进程信息 文件表 --- 包含进程已打开文件的信息
(2) 调度
内核中的调度器决定抢占当前进程,并重新开始一个先前被抢占的进程
(3) 上下文切换
工作:
1° 保存当前进程的上下文
2° 恢复某个先前被抢占进程的上下文
3° 将控制传递给这个新恢复的进程
典型的发生时刻:
1° 系统调用
2° 阻塞
3° 定时器中断
(4) 一般而言,cache不能和中断/上下文切换这样的ECF很好的交互,cache中的数据往往在ECF之后被污染,难以存放有效数据
-
-
系统调用和错误处理
-
man syscalls
查看完整的系统调用列表
-
标准C库提供了一组针对常用系统调用的包装函数,一般直接用这种包装函数。系统调用和它们的包装函数可以互换的称为系统级函数
-
当Unix系统级函数遇到错误时,会典型的返回-1,并设置全局整数变量errno来表示错误类型
-
Unix风格的错误处理包装函数(自己写的)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <errno.h> #include <string.h> pid_t Fork(void) { pid_t pid; if (pid = fork() < 0) { unix_error("Fork error"); } return pid; } void unix_error(char* msg) { fprintf(stderr, "%s: %s\n", msg, strerror(errno)); exit(0); }
在错误处理包装函数Fork()中,既完成了和fork()相同的功能,同时记录错误类型,并exit程序。
-
-
进程控制
-
系统调用函数可以靠
man xxx
的方式获取必须包含的include文件
-
获取进程pid
(1) 每个进程都有一个唯一的正数PID
(2) 相关系统调用
#include <unistd.h> #include <sys/types.h> pid_t getpid(void) --- 返回调用进程的pid pid_t getppid(void) --- 返回调用进程父进程的pid
-
创建和终止进程
(1) 每个进程一定处于以下三种状态之一
1° 运行
在CPU上执行或者等待被调度
2° 暂停
进程的执行被挂起
运行 --- 收到 SIGSTOP/SIGTSTP/SIDTTIN 信号 --- 暂停 --- 收到 SIGCONT 信号 --- 运行
信号是一种软件中断
3° 终止
进程永远的停止了。有三种原因:
I. 收到一个信号,这个信号的默认行为是终止进程
II. 从主程序返回
III. 调用 exit() 函数
(2) 相关系统调用
#include <stdlib.h> void exit(int status); --- 终止进程,并以status状态退出(在主程序中return status也可以设置退出状态) --------- #include <unistd.h> #include <sys/types.h> pid_t fork(void); --- 父进程创建新的运行子进程
(3) 关于fork系统调用
1° fork函数会被调用1次,但是会返回2次:一次是在父进程中返回,一次是在子进程中返回
2° 出错时fork函数的返回值是-1,不出错时子进程中的返回值为0,父进程中的返回值为子进程的PID,以此来区分父子进程
示例 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <errno.h> #include <string.h> void unix_error(char* msg) { fprintf(stderr, "%s: %s\n", msg, strerror(errno)); exit(0); } pid_t Fork(void) { pid_t pid; if (pid = fork() < 0) { unix_error("Fork error"); } return pid; } 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); return 0; }
3° 父子进程最大的区别在于PID不同
4° 子进程得到与父进程用户级虚拟地址空间相同的一份拷贝(但是独立的),包括文本、数据、bss段、堆、用户栈、打开的文件描述符(子进程可以读写父进程打开的任意文件)。它们拥有相同(fork刚调用完)但独立(从此以后空间私有)的地址空间
5° 父子进程是并发执行的独立进程
6° 画进程图的方式可以方便理解fork函数
int main() { int x = 1; if (fork() == 0) { printf("f1: x = %d\n", ++x); } printf("f2: x = %d\n", --x); exit(0); } 子进程 ------------x = 1---> f1, x=2 ----> f2, x=1 ----> | |fork() | ---------父进程--------------x = 1---> f2, x=1 ---->
-
回收子进程
(1) 当进程终止时,内核并不是立刻清除,而是保持终止状态,直到被它的父进程回收
(2) 父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,此时开始子进程才不复存在
(3) 僵尸进程
已经终止但是未被回收的进程
(4) 若父进程没有回收它的僵尸子进程就终止了,内核会安排init进程(PID恒等于1)来回收僵尸子进程
(5) 等待子进程终止或暂停(然后将子进程从系统中去除)
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int* status, int options);
(6) 检查已回收子进程的退出状态
P519 有几个宏,例如
WIFEXITED(status); WIFSIGNALED(status);
(7) 示例
#include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <wait.h> #include <stdio.h> #include <errno.h> #define N 2 int main() { int status, i; pid_t pid[N+1], retpid; for (int i = 0; i < N; i++) { if ((pid[i] = fork()) == 0) { // child process exit(100 + i); } } for (int i = 0; i < N; i++) { printf("%d\n", pid[i]); } // parent process collects N zombie child processes in order i = 0; while ((retpid = waitpid(pid[i++], &status, 0)) > 0) { // judge if a process exited by exit(status) or return if (WIFEXITED(status)) { printf("child %d terminated normally with exit status %d\n", retpid, WEXITSTATUS(status)); } else { printf("child %d terminated abnormally\n", retpid); } } exit(0); }
-
让进程休眠
#include <unistd.h> unsigned int sleep(unsigned int secs); // 休眠一段secs秒,直到休眠结束或是被信号中断提前返回;返回剩余要休眠的秒数 int pause(void); // 让调用函数休眠,直到被信号唤醒
-
加载并运行程序
(1)
#include <unistd.h> int execve(char* filename, char* []argv, char* []envp); filename: 可执行目标文件名 argv: 参数列表数组,以null结尾,argv[0]是文件名 envp: 环境变量数组,以null结尾,形式是"name=value"
几个操作环境变量数组的系统调用
#include <stdlib.h> char* getenv(const char* name); int setenv(const char* name, const char* newvalue, int overwrite); void unsetenv(const char* name);
(2) 程序与进程
程序:代码和数据的集合。可以作为目标模块存在于磁盘上,也可以作为段存在于地址空间。
进程:执行中程序的一个特殊实例。 —> 程序总是运行在某个进程的上下文中
fork: 在新的子进程中,运行相同的程序。 – 调用一次,返回两次
execve: 在当前进程的上下文中,运行一个新的程序,覆盖当前进程的地址空间,具有相同的PID,并且继承了调用函数的所有打开的文件描述符。 – 调用一次,成功了就不返回
(3) Unix shell 的原理
while (1) { read_user_input(); // 读取用户输入 ifBackGround, cmd, argv, env = parse_input(); // 解析用户输入 // 如果是内置shell命令(例如pwd这种),立刻解释这个命令 if (built_in_cmd(cmd)) { do_built_in_cmd(cmd); continue; } pid = fork(); // fork()出一个子进程,用于执行非内置的可执行文件 // 子进程中调用execve将地址空间完全替换成新程序 if (pid == 0) { execve(argv[0], argv, env); exit(0); } // 父进程根据用户输入的是否在后台执行的请求,决定是否等待子进程返回再进行下一轮循环 if (!ifBackGround) { waitpid(pid, &status, 0); } }
-
-
信号
-
低级的硬件异常是由内核异常处理程序处理的,通常对用户进程不可见。信号提供了一种机制,向用户进程通知这些异常的发生。
eg.
某个进程除零错误 ---> 内核给它发送 SIGFPE 信号
-
Linux系统上支持30种不同类型的信号,它们有号码,名字,默认行为,相应事件(P529)
示例
号码 名字 默认行为 相应事件 9 SIGKILL 终止 杀死程序
-
步骤
(1) 发送信号
1° 内核通过更新目的进程上下文的某个状态,将信号发送给目的进程
2° 发送信号的可能原因
I. 内核检测到系统事件(eg. 除零错误、子进程终止)
II. 一个进程调用了kill函数,显式要求内核发送信号给目的进程
3° 进程组
I. 每个进程都属于一个进程组
II. 默认地,一个子进程和它的父进程同属于一个进程组
III. 改变进程组
#include <unistd.h> pid_t setpgid(pid_t pid, pid_t pgid);
4° 发送信号的方式
I. 用kill程序发送信号
unix> kill .9 12213 // 发送信号9(对应着SIGKILL)给PID为12213的进程 unix> kill .9 .12213 // 发送信号9(对应着SIGKILL)给PGID为12213的进程组的每个进程
II. 从键盘发送信号
ctrl-c ---- SIGINT, 终止前台作业 ctrl-z ---- SIGTSTP, 暂停前台作业
III. 用kill函数发送信号
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); // 发送sig信号给pid进程,如果pid小于0,发送给abs(pid)的进程组
IV. 用 alarm 函数发送信号
#include <unistd.h> unsigned int alarm(unsigned int secs); // 安排内核在secs秒内,发送一个SIGALRM信号给调用进程
示例
#include <unistd.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> void handler(int sig) { static int beeps = 0; printf("beep\n"); if (++beeps < 5) { alarm(1); } else { printf("boom\n"); exit(0); } } int main() { signal(SIGALRM, &handler); alarm(1); while(1) { ; } exit(0); }
(2) 接收信号
1° 待处理信号和阻塞信号
一个只发出而没有被接收的信号称为待处理信号,任何时刻一种类型的待处理信号只有一个,如果一种类型已经有一个待处理信号了,那么同一类型的其他信号会被直接丢弃。
一个进程可以选择性阻塞某种类型信号的接收,此时这种类型的信号可以发送但是不会被接收。
2° 原理
内核为每个进程在pending位向量中维护着待处理信号集合,在blocked位向量中维护着被阻塞信号集合。
每次只要一个类型为k的信号被传送,内核就检查一下blocked对应的位,如果没有被阻塞就set pending的第k个位;只要一个类型为k的信号被接收,内核就会在pending中reset第k个位
3° 每个信号类型都有一个预定义的默认行为 P529
I. 进程终止
II. 进程终止并转储存储器
III. 进程暂停直到被SIGCONT信号重启
IV. 进程忽略该信号
4° 改变和信号相关联的行为 – signal函数
#include <signal.h> typedef void handler_t(int) handler_t* signal(int signum, handler_t* handler); // 若成功则返回前次信号处理程序的函数指针
I. 如果handler是SIG_IGN,那么忽略signum的信号
II. 如果handler是SIG_DFL,那么signum的信号恢复默认行为
III. 否则,handler是用户定义的函数指针——称为信号处理程序
-
信号处理的问题
(1) 待处理信号被阻塞
一个k信号正在被handler处理,同样的k信号到达,只能在待处理信号集合中等待
(2) 待处理信号不会排队等待
一个k信号在待处理集合中,同样后来的k信号再到达时,直接丢弃
(3) 系统调用可以被中断
类似read/write/accept一类的慢速系统调用,在某些系统中(例如Solaris)一旦被中断且handler返回以后,慢速系统调用不再继续而是直接返回错误
-
可移植的信号处理
(1) 想要解决的问题:
中断后的慢速系统调用重启or放弃,在不同的系统上表现不一致
(2) 解决办法:POSIX标准定义的sigaction函数
#include <signal.h> int sigaction(int signum, struct sigaction* act, struct sigaction* oldact);
一般使用的是它的Wrapper函数Signal
handler_t *Signal(int signum, handler_t *handler) { struct sigaction action, old_action; action.sa_handler = handler; sigemptyset(&action.sa_mask); /* Block sigs of type being handled */ action.sa_flags = SA_RESTART; /* Restart syscalls if possible */ if (sigaction(signum, &action, &old_action) < 0) { unix_error("Signal error"); } return (old_action.sa_handler); }
Signal函数的语义是:
I. 只有这个处理程序当前正在处理的那种类型的信号被阻塞
II. 信号不会排队,直接丢弃
III. 只要可能,被中断的系统调用会自动重启
IV. 一旦设置了信号处理程序就会一直保持,直到Signal带着handler参数为SIG_IGN或者SIG_DFL被调用
-
显式地阻塞信号
#include <signal.h> int sigpromask(int how, const sigset_t* set, sigset_t* oldset); int sigemptyset(sigset_t* set); int sigfillset(sigset_t* set); int sigaddset(sigset_t* set, int signum); int sigdelset(sigset_t* set, int signum); int sigismember(const sigset_t* set, int signum);
-
-
非本地跳转
-
作用
将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列
-
系统调用
#include <setjmp.h> int setjmp(jmp_buf env); int sigsetjmp(sigjmp_buf env, int savesigs); void longjmp(jmp_buf env, int retval); void siglongjmp(sigjmp_buf env, int retval);
(1) setjmp和sigsetjmp函数在env缓冲区中保存当前栈的内容,返回0
(2) longjmp和siglongjmp会从env缓冲区中恢复栈的内容,然后触发从最近一次初始化env的setjmp调用的返回,然后从setjmp返回,并带有非0的返回值retval
(3) 关系
1° setjmp函数只被调用一次,但返回多次——一次是第一次调用setjmp,后面是每个相应的longjmp
2° longjmp函数只被调用一次,从不返回
-
非本地跳转的应用
(1) 从深层嵌套的函数调用中立刻返回,而不是费力解开各层栈
#include <setjmp.h> #include <stdio.h> #include <stdlib.h> jmp_buf buf; int error1 = 0; int error2 = 1; void foo(void), bar(void); int main() { int rc; rc = setjmp(buf); if (rc == 0) { foo(); } else if (rc == 1) { printf("Detected an error1 condition in foo\n"); } else if (rc == 2) { printf("Detected an error2 condition in foo\n"); } else { printf("Unknown error condition in foo\n"); } exit(0); } /* Deeply nested function foo */ void foo(void) { if (error1) { longjmp(buf, 1); } bar(); } void bar(void) { if (error2) { longjmp(buf, 2); } }
(2) 中断发生时,使一个信号处理程序分支到一个特殊位置,而不是返回到被信号中断了的指令的位置
下面这个示例程序的效果是按下ctrl-c时程序重新到sigsetjmp指令处
#include <stdlib.h> #include <stdio.h> #include <setjmp.h> #include <signal.h> #include <unistd.h> sigjmp_buf buf; void handler(int sig) { siglongjmp(buf, SIGINT); } int main() { signal(SIGINT, &handler); if (!sigsetjmp(buf, SIGINT)) { printf("starting\n"); } else { printf("restarting\n"); } while(1) { sleep(1); printf("processing...\n"); } exit(0); }
-
C++和Java中提供的异常机制是更高层次的
catch 类似于 setjmp 的封装
throw 类似于 longjmp 的封装
-
chapter08_异常控制流
猜你喜欢
转载自blog.csdn.net/captxb/article/details/103166626
今日推荐
周排行