CMU 15-213 Intro to Computer Systems Lecture 14
课程主页:http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/schedule.html
课程资料:https://github.com/EugeneLiu/translationCSAPP
课程视频:https://www.bilibili.com/video/av31289365/
这一讲介绍了异常。
异常控制流
控制流
- 处理器只做一件事:
- 从启动到关闭,CPU只需读取并执行(解释)一系列指令,一次一条命令
- 此序列是CPU的控制流
改变控制流
- 到目前为止,有两种用于更改控制流的机制:
- 跳跃和分支
- call和return
对程序状态的变化做出反应
- 对于有用的系统而言不够:难以对系统状态的变化做出反应
- 数据来自磁盘或网络适配器
- 除以零的指令
- 用户在键盘上按Ctrl+C
- 系统计时器到期
- 系统需要“异常控制流”的机制
异常控制流
- 存在于计算机系统的各个级别
- 低级机制
- 1.异常
- 响应系统事件而改变控制流(即系统状态改变)
- 结合使用硬件和OS软件来实现
- 1.异常
- 更高级别的机制
- 2.进程上下文切换
- 由OS软件和硬件计时器实现
- 3.信号
- 由OS软件实现
- 4.非本地跳转:setjmp()和longjmp()
- 由C运行库实现
- 2.进程上下文切换
异常
异常
- 一个异常是响应某些事件(即处理器状态更改)将控制权转移到OS内核。
- 内核是操作系统的内存驻留部分
- 事件示例:除以0,算术溢出,页面错误,$\text{I/O}$请求完成,键入Ctrl+C
- 处理完引起异常的事件的类型,会发生以下三种情况
- 处理程序将控制返回给当前指令$I_{\text {curr} }$,即当事件发生时正在执行的指令。
- 处理程序将控制返同给 $I_{\text {next } },$ 如果没有发生异常将会执行的下一条指令。
- 处理程序终止被中断的程序。
异常表
- 每种类型的事件都有一个唯一的异常号k
- k=异常表的索引(也称为中断向量)
- 每次发生异常k时都调用程序k
异步异常(中断)
- 由处理器外部的事件引起
- 通过设置处理器的中断引脚来指示
- 处理程序返回到“下一条”指令
- 例子:
- 定时器中断
- 每隔几毫秒,一个外部计时器芯片就会触发一个中断
- 内核用来从用户程序取回控制权
- 来自外部设备的$\text{I/O}$中断
- 在键盘上按Ctrl+C
- 来自网络的数据包到达
- 磁盘中的数据到达
- 定时器中断
同步异常
- 由执行指令引起
- 陷阱
- 有意的异常
- 示例:系统调用,断点陷阱,特殊说明
- 将控制权返回“下一条”指令
- 故障
- 无意但可能可以恢复
- 示例:页面错误(可恢复),保护错误(不可恢复),浮点异常
- 重新执行产生故障的“当前”指令或中止
- 终止
- 意外和无法恢复
- 示例:非法指令,奇偶校验错误,机器检查
- 中止当前程序
- 陷阱
系统调用
- 每个x86-64系统调用都有一个唯一的ID号
- 例子:
进程
进程
- 定义:进程是正在运行的程序的实例。
- 计算机科学中最深刻的想法之一。
- 与“程序”或“处理器”不同。
- 进程为每个程序提供了两个关键的抽象:
- 逻辑控制流程
- 每个程序似乎都专用于CPU。
- 由称为上下文切换的内核机制提供。
- 专用地址空间
- 每个程序似乎都专用于主存储器。
- 由称为虚拟内存的内核机制提供。
- 逻辑控制流程
多进程
- 计算机同时运行多个进程
- 一个或多个用户的应用程序
- Web浏览器,电子邮件客户端,编辑器……
- 后台任务
- 监视网络和$\text{I/O}$设备
- 一个或多个用户的应用程序
进程并发
- 每个进程都是一个逻辑控制流。
- 如果两个进程的时间重叠,则两个进程将同时运行(并发)。
- 否则,它们是顺序的。
- 例子(运行在单核上):
- 并发:$A \& B, A \& C$
- 顺序:$B\& C$
用户视角下的并发
- 并发进程的控制流在时间上实际上是不相交的
- 但是,我们可以将并发进程视为彼此并行运行
用户模式和内核模式
为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常是用某个控制寄存將中的一个模式位(mode bit)来提供这种功能的,当设置了该模式,进程就运行在内核模式中,该模式下执行指令集中的任何指令;当没有设置模式位时,进程就运行用户模式中,该模式下不允许执行特权指令。
上下文切换
- 进程由共享的内存驻留OS代码块(称为内核)管理
- 重要提示:内核不是一个独立的进程,而是作为某些现有进程的一部分运行的。
- 控制流通过上下文切换从一个进程传递到另一个进程
进程控制
系统调用错误处理
遇到错误时,Linux系统级函数通常返回-1并设置全局变量errno以指示原因。
硬性规定:
- 必须检查每个系统级函数的返回状态
- 唯一的例外是少数返回void的函数
例子:
if ((pid = fork()) < 0) { fprintf(stderr, "fork error: %s\n", strerror(errno)); exit(0); }
错误报告函数
使用错误报告函数可以稍微简化一下:
void unix_error(char *msg) /* Unix-style error */ { fprintf(stderr, "%s: %s\n", msg, strerror(errno)); exit(0); }
调用的代码为:
if ((pid = fork()) < 0) unix_error("fork error");
错误包装处理函数
我们使用Stevens风格的错误处理包装器进一步简化了我们提供的代码:
pid_t Fork(void) { pid_t pid; if ((pid = fork()) < 0) unix_error("Fork error"); return pid; }
调用的代码为:
pid = Fork();
获得进程ID
- pid_t getpid(void)
- 返回当前进程的PID
- pid_t getppid(void)
- 返回父进程的PID
备注:pid_t在Linux系统上的types.h中被定义为int
创建和终止进程
- 从程序员的角度来看,我们可以认为进程处于以下三种状态之一
- 运行
- 进程正在执行中或正在等待执行,并且最终将由内核调度(即选择执行)
- 停止
- 进程执行被暂停,并且只有在另行通知之前才会调度
- 终止
- 进程永久停止
终止进程
- 进程由于以下三个原因之一而终止:
- 接收到其默认操作将终止的信号
- 从main函数返回
- 调用exit函数
- void exit(int status)
- 以status退出状态来终止进程
- 约定:正常返回状态为0,出错时为非零
- 显式设置退出状态的另一种方法是从主进程返回整数值
- exit只会被调用一次,但永远不会返回。
创建进程
- 父进程通过调用fork创建一个新的正在运行的子进程
- int fork (void)
- 返回0给子进程,子PID返回给父进程
- 子进程几乎与父进程相同:
- 子进程获得父进程虚拟地址空间的相同(但独立)副本。
- 子进程获得父进程打开文件描述符的相同副本。
- 子进程的PID与父进程的PID不同。
- fork很有趣(并且经常令人困惑),因为它被调用了一次,但是返回两次。
fork例子
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);
}
linux> ./fork
parent: x=0
child : x=2
- 调用一次,返回两次
- 并发执行
- 无法预测父进程和子进程的执行顺序
- 相同但独立的地址空间
- 当fork在父进程和子进程中返回时,x的值为1
- x的后续更改是独立的
- 共享的打开文件
- 父进程和子进程双方的标准输出相同
使用过程图建模fork
- 流程图是捕获并发程序中语句的半序的有用工具:
- 每个顶点都是一条语句的执行
- a->b表示a在b之前发生
- 边可以用变量的当前值标记
- printf顶点可以用输出标记
- 每个图都以没有入边的顶点开始
- 图的任何拓扑排序都对应于可行的全序排列。
- 所有边从左到右指向的顶点的全序排列。
例1
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);
}
原始图:
排序后的图:
例2
void fork2()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("Bye\n");
}
原始图:
例3
void fork4()
{
printf("L0\n");
if (fork() != 0) {
printf("L1\n");
if (fork() != 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
原始图:
例4
void fork5()
{
printf("L0\n");
if (fork() == 0) {
printf("L1\n");
if (fork() == 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
原始图如下:
回收子进程
- 理念
- 当进程终止时,它仍然消耗系统资源
- 示例:退出状态,各种OS表
- 被称为“僵死进程”
- 当进程终止时,它仍然消耗系统资源
- 回收
- 由父进程对终止的子进程执行(使用wait或waitpid)
- 父进程被给予退出状态信息
- 内核然后删除“僵死进程”
- 如果父进程不回收怎么办?
- 如果任何父进程在没有回收子进程的情况下终止,那么内核会安排init进程(pid == 1)成为孤儿进程的养父,然后进行回收操作。
- 因此,长期运行的进程中会进行显式回收,否则会消耗系统的内存资源。
- 例如,shells和服务器
例1
void fork7() {
if (fork() == 0) {
/* Child */
printf("Terminating Child, PID = %d\n", getpid());
exit(0);
} else {
printf("Running Parent, PID = %d\n", getpid());
while (1)
; /* Infinite loop */
}
}
运行结果
linux> ./forks 7 &
[1] 6639
Running Parent, PID = 6639
Terminating Child, PID = 6640
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6639 ttyp9 00:00:03 forks
6640 ttyp9 00:00:00 forks <defunct>
6641 ttyp9 00:00:00 ps
linux> kill 6639
[1] Terminated
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6642 ttyp9 00:00:00 ps
- ps将子进程显示为“已终止”(即“僵死进程”)
- 杀死父进程可以让子进程被init回收
例2
void fork8()
{
if (fork() == 0) {
/* Child */
printf("Running Child, PID = %d\n",
getpid());
while (1)
; /* Infinite loop */
} else {
printf("Terminating Parent, PID = %d\n",
getpid());
exit(0);
}
}
运行结果
linux> ./forks 8
Terminating Parent, PID = 6675
Running Child, PID = 6676
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6676 ttyp9 00:00:06 forks
6677 ttyp9 00:00:00 ps
linux> kill 6676
linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6678 ttyp9 00:00:00 ps
- 即使父进程已终止,子进程仍处于活动状态
- 必须明确杀死子进程,否则将无限期运行
wait:与子进程同步
- 父进程通过调用wait为子进程回收
- int wait (int * child_status)
- 暂停当前进程,直到其子进程之一终止
- 返回值是终止的子进程的pid
- 如果child_status != NULL,则它将指向的整数将设置为一个值,该值指示子进程终止的原因和退出状态:
- 使用wait.h中定义的宏进行检查
- WIFEXITED,WEXITSTATUS,WIFSIGNALED,WTERMSIG,WIFSTOPPED,WSTOPSIG,WIFCONTINUED
- 使用wait.h中定义的宏进行检查
例1
void fork9() {
int child_status;
if (fork() == 0) {
printf("HC: hello from child\n");
exit(0);
} else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
}
进程图如下:
waitpid:等待特定进程
- pid_t waitpid(pid_t pid, int &status, int options)
- 挂起当前进程,直到特定进程终止
- 各种选项(请参阅教科书)
execve:加载和执行程序
- $\text{int execve(char }\star \text{filename, char }\star \text{argv[], char }\star\text{envp[])}$
- 在当前进程中加载并运行:
- 可执行文件filename
- 可以是以#!interpreter开头的目标文件或脚本文件(例如,#!/bin/bash)
- 带有参数列表argv
- 按照约定argv [0]==filename
- 以及环境变量列表envp
- “名称=值”字符串(例如USER=droh)
- getenv,putenv,printenv
- 可执行文件filename
- 覆盖代码,数据和堆栈
- 保留PID,打开的文件以及信号上下文
- 调用一次,永不返回
- 除非有错误
新程序开始时的栈结构
execve例子
在子进程执行$\text{“/bin/ls –lt /usr/include”}$
当前使用环境:
总结
- 异常
- 需要非标准控制流程的事件
- 由外部(中断)或内部(陷阱和故障)生成
- 进程
- 在任何给定时间,系统都有多个活动的进程
- 虽然一次只能在一个内核上执行一个
- 每个进程似乎都可以完全控制处理器+专用内存空间
- 复制进程
- 调用fork
- 调用一次,返回两次
- 结束进程
- 调用exit
- 一次调用,无return
- 回收并等待进程
- 调用wait或waitpid
- 加载并运行程序
- 调用execve(或变体)
- 一次调用,(通常)无返回
http://www.doraemonzzz.com/2020/06/23/CMU%2015-213%20Intro%20to%20Computer%20Systems%20Lecture%2014/
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Doraemonzzz!
评论
ValineLivere