2017-2018-1 20155227 《信息安全系统设计基础》第十四周学习总结
找出全书你认为学得最差的一章,深入重新学习一下,要求(期末占5分):
•总结新的收获
•给你的结对学习搭档讲解或请教,并获取反馈
•参考上面的学习总结模板,把学习过程通过博客(随笔)发表,博客标题“学号 《信息安全系统设计基础》第十四周学习总结”,博客(随笔)要通过作业提交,截至时间本周日 23:59。
本书有很多章的内容都很难懂,但自我感觉学得最差的是第八章内容。
教材学习内容总结
第8章 异常控制流
现代系统通过使
控制流
发生突变对这些情况做出反应。我们称这种突变为
异常控制流( Exceptional Control Flow,ECF)
异常控制流发生在系统的各个层次。
理解
ECF
很重要
- 理解
将帮助你理解重要的系统概念。ECF
- 理解
将帮助你理解应用程序如何与操作系统交互ECF
- 通过
或者陷阱(trap)
的系统调用(system call)
形式,向操作系统请求服务。ECF
- 通过
- 理解
将帮助你编写有趣的应用程序ECF
- 理解
将帮助你理解并发ECF
- 理解
将帮助你理解软件异常如何工作。ECF
8.1 异常
异常是异常控制流的一种,一部分由
硬件
实现,一部分由
操作系统
实现。
异常(exception)就是
控制流
的突变,用来响应处理器状态的某些变化。
- 状态变化又叫做
事件(event)
- 事件可能与当前执行指令有关
- 存储器缺页,算数溢出
- 除
- 也可能与当前执行指令无关
-
I/O请求
- 定时器产生信号
-
- 事件可能与当前执行指令有关
- 通过异常表(exception table) 的跳转表,进行一个间接过程调用,到专门设计处理这种事件的操作系统子程序
(异常处理程序(exception handler))
- 异常处理完成后,根据事件类型,会有
三种情况
- 返回当前指令,即发生事件时的指令。
- 返回没有异常,所执行的下一条指令
- 终止被
的程序中断
8.1.1 异常处理
- 为每个
分配了一个非负的异常
异常号(exception number)
- 一些号码由处理器设计者分配
- 其他号码由操作系统内核的设计者分配。
- 系统启动时,操作系统分配和初始化一张称为
的跳转表。异常表
- 条目k包含异常k的处理程序的地址。
-
的地址放在叫异常表
异常表基址寄存器的特殊CPU寄存器中。)
-
类似异常
,不过有以下不同过程调用
-
,跳转到处理程序前,处理器将返回地址压入栈中。对于异常,返回地址是当前,或下一跳指令。过程调用
- 处理器会把额外的处理器状态压入栈中。
- 如果控制一个用户程序到内核,那么所有这些项目会被压入内核栈中,而是
。用户栈
- 异常处理程序运行在
下,这意味他们对所有系统资源有完整访问权限。内核模式
-
8.1.2 异常的类别
异常分为一下四类:
中断(interrupt),陷阱(trap)
,
故障(fault)
和
终止(abort)
。
前者可以叫
异步中断/异
常或
外中断
,后三个可以叫
同步中断/异常
- 中断
-
是中断
发生,是来自处理器外部的异步
的信号的结果。硬件中断不是由任何一条专门的指令造成,从这个意义上它是异步的。I/O设备
- 硬件中断的
程序通常称为异常处理
)中断处理程序(interrupt handle
-
通过向处理器芯片的一个引```脚发信号,并将异常号放到系统总线上,以触发中断。I/O设备
- 在当前指令执行完后,处理器注意到中断引脚的电压变化,从系统总线读取异常号,调用适当的中断处理程序。
- 当处理程序完成后,它将控制返回给下一条本来要执行的指令。
-
-
- 剩下的异常类型(陷阱,故障,终止)是同步发生,执行当前指令的结果。我们把这类指令叫做
.故障指令(faulting instruction)
-
和陷阱
系统调用
-
是有意的异常,是执行一个指令的结果。也会返回到下一跳本来要执行的指令。陷阱
-
最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做陷阱
系统调用
- 用户程序经常需要向
请求服务。内核
- 1.
读文件(read)
- 2.
创建进程(fork)
- 3.新的程序
(execve)
- 4.终止当前进程(
exit)
- 1.
- 为了运行对这些内核服务的受控访问,处理器提供了一条特殊的
的指令syscall n
-
是运行在系统调用
下,而普通调用是内核模式
下。用户模式
- 用户程序经常需要向
-
- 故障
-
由错误引起,可能被故障处理程序修正。故障
- 如果能被
,返回引起故障的指令。修正
- 否则返回
例程,进行终结。abort
- 如果能被
-
- 终止
- 终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如
和DRAM
被损坏。SRAM
- 终止处理程序从不将控制返回给应用程序。返回一个
例程。abort
- 终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如
8.1.3 Linux/IA32 系统中的异常
- 有高达
种不同的异常256
-
由0~31
架构师定义的异常,对任何Intel
系统都一样。IA32
-
对应操作系统定义的中断和陷阱。23~255
-
故障和终止Linux/IA32
-
- 除法错误
- 一般保护故障缺页
- 机器检查
-
系统调用Linux/IA32
-
-
- 在
系统中,系统调用是通过一条称为IA32
的陷阱指令完成,其中int n
可能是n
表IA32异常
个条目中任何一个索引,历史中,系统调用是通过异常256
提供的。128(0x80)
-
可用C程序
来直接调用任何系统调用syscall函数
- 实际上没必要这么做
-
提供了一套方便的C库
。这些包装函数将参数打包到一起,以适当的包装函数
陷入内核,然后将系统调用号
的返回状态传递回系统调用
。调用函数
- 我们将系统调用与他们相关联的包装函数称为系统级函数。
研究程序如何使用
int指令
直接调用
Linux
系统调用是很有趣的。所有到
Linux
系统调用的参数都是通过通用寄存器而不是栈传递。
惯例
-
%eax
包含系统调用号
-
包含六个任意的参数。%ebx,%ecx,%edx,%esi,%edi,%ebp
-
不能使用,进入内核模式后,内核会覆盖它。%esp
- 系统级函数写的
hello world
int main()
{
write(1,"hello,world
",13);
exit(0);
}
- 汇编写的
hello world
string:
"hello world
"
main:
movl $4,%eax
movl $1,%ebx
movl $String,%ecx
movl $len,%edx
int $0x80
movl $1,%eax
movl $0,%ebx
int $0x80
8.2 进程
-
是允许操作系统提供进程的概念的基本构造快,进程是计算机科学中最深刻,最成功的概念之一。异常
-
,觉得我们的程序是系统中唯一运行着的程序。我们的程序好像独占处理器和存储器。假象
- 这些
都是通过进程概念提供给我们的。假象
-
-
经典定义:一个执行中的程序实例.进程
- 系统中每个程序都是运行某个进程的上下文中的。
-
是由程序正确运行所需的状态组成。上下文
- 这个状态包括存储器中的代码和数据,它的
,栈
,通用目的寄存器
,程序计数器
等。环境变量
-
- 系统中每个程序都是运行某个进程的上下文中的。
-
提供的进程
假象
- 一个独立的
。逻辑控制流
- 一个私有的
。地址空间
- 一个独立的
8.2.1 逻辑控制流
-
值的序列叫做PC
,或者简称逻辑控制流
逻辑流
8.2.2 并发流
-
也有不同的形式。逻辑流
-
,进程,信号处理程序,线程和异常处理程序
进程都是逻辑流的例子。Java
-
- 一个逻辑流的执行在执行上与另一个流重叠,称为
,这两个流被称为并发流
。并发地运行
- 更准确地说,
。流X和Y互相并发
- 更准确地说,
- 多个流并发执行的一般现象称为
。并发
- 一个进程和其他进程轮流执行的概念称为
。多任务
- 一个进程执行它的控制流的一部分的每一时间段叫做
。时间片
- 因此,多任务 又叫
时间分片
- 一个进程和其他进程轮流执行的概念称为
- 并发的思想与流运行的处理器核数与计算机数无关。
- 如果两个流在时间上重叠,即使运行在同一处理器,也是并发。
-
是并发流的一个真子集。并行流
- 两个流并发地运行在不同的处理器核或者计算机上,我们称为
。并行流
- 它们并行地运行,且并行地执行
- 两个流并发地运行在不同的处理器核或者计算机上,我们称为
8.2.3 私有地址空间
进程
为个程序好像独占了
系统地址空间
。
- 一个进程为每个程序提供它自己的
。私有地址空间
- 不同系统一般都用
的结构。相同
8.2.4 用户模式和内核模式
处理器提供一种机制,限制一个应用程序可以执行的指令以及它可以访问的地址空间范围。这就是
用户模式和内核模式
。
- 处理器通过控制寄存器中的一个模式位来提供这个功能。
- 该寄存器描述了进程当前享有的特权。
- 设置了模式位后,进程就运行在
中(有时也叫超级用户模式)内核模式
-
下的进程可以执行指令集的任何指令,访问系统所有存储器的位置。内核模式
-
- 没有设置模式位时,进程运行在用户模式。
-
不允许程序执行特权指令。用户模式
- 比如停止处理器,改变模式位,发起一个
。I/O操作
- 比如停止处理器,改变模式位,发起一个
- 不允许
的进程直接引用地址空间的内核区代码和数据。用户模式
- 任何尝试都会导致保护故障。
- 用户通过系统调用间接访问内核代码和数据。
-
- 设置了模式位后,进程就运行在
- 进程从用户模式转变位内核模式的方法
- 通过中断,故障,陷入系统调用这样的异常。
- 在异常处理程序中会进入内核模式。退出后,又返回用户模式。
- 该寄存器描述了进程当前享有的特权。
-
提供一种聪明的机制,叫Linux
。/proc文件系统
- 允许用户模式访问
结构的内容。内核数据
-
文件将许多内核数据结构输出为一个用户程序可以读的文本文件的/proc
。层次结构
- 如
CPU类型(/proc/cpuinfo)
- 特殊进程使用的
存储器段(‘/proc//maps’)
- 如
- 2.6 版本引入
引入Linux内核
。/sys文件系统
- 输出关于
的额外的底层信息。系统总线和设备
- 输出关于
- 允许用户模式访问
8.2.5 上下文切换
操作系统内核使用一种称为上下文切换的 较高层次 的异常控制流来实现多任务。
- 上下文切换机制建立在之前讨论的较低层次异常机制上的。
内核为每个进程维护一个上下文。
-
就是重新启动一个被抢占的进程所需的状态。上下文
- 由一些
组成对象的值
- 通用目的寄存器
- 浮点寄存器
- 程序计数器(
)PC
- 用户栈
- 状态寄存器
- 内核栈
- 各种内核数据结构
- 描绘地址空间的
页表
- 包含当前进城信息的
进程表
- 进程已打开文件信息的
文件表
- 描绘地址空间的
- 由一些
- 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定叫做
,由内核中称为调度(shedule)
的代码处理的。调度器(scheduler)
- 当内核选择一个新的进程运行时,我们就说内核调度了这个进程。
- 当调度进程时,使用一种上下文切换的机制来控制转移到新的进程
- 保存当前进程的
上下文
- 恢复某个先前被抢占的进程被保存的
上下文
- 将控制传递给这个新恢复的进程
- 保存当前进程的
- 什么时候会发生上下文切换
- 内核代表用户执行系统调用。
- 如果系统调用因为某个事件阻塞,那么内核可以让当前进程休眠,切换另一个进程。
- 或者可以用
,显式请求让调用进程休眠。sleep系统调用
- 即使系统调用没有阻塞,内核可以决定执行
上下文切换
- 中断也可能引发
。上下文切换
- 所有系统都有某种产生周期性定时器中断的机制,典型为
,或1ms
。10ms
- 每次定时器中断,内核就能判断当前进程运行了足够长的时间,切换新的进程。
- 所有系统都有某种产生周期性定时器中断的机制,典型为
- 内核代表用户执行系统调用。
8.3 系统调用错误处理
- 当
遇到错误时,他们典型地返回Unix系统级函数
,并设置全局变量-1
来表示什么出错了。errno
if((pid=fork()<0){
fprintf(stderr,"fork error: %s
", strerror(errno));
exit(0);
}
-
函数返回一个文本串,描述了个某个strerror
值相关联的错误。errno
8.4 进程控制
8.4.1 获取进程ID
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
-
是每个进程唯一的正数。PID
-
返回调用进程的getpid()
,PID
返回它的父进程的getppid()
。PID
- 返回一个类型
的值,在pid_t
系统下在Linux
被定义为type.h
int
8.4.2 创建和终止进程
进程总是处于下面
三种状态
-
。进程要么在运行
中执行,要么等待执行,最终被内核调度。CPU
-
。进程的执行被停止
,且不会被调度。挂起
- 收到S
,进程就会停止。IGSTOP,SIGTSTP,SIDTTIN或者SIGTTOU信号
- 直到收到一个
,在这个时刻,进程再次开始运行。SIGCONT信号
- 信号是一种
的形式。软件中断
- 收到S
-
。进程永远停止。终止
- 收到一个信号。信号默认行为是终止进程。
- 从主程序返回
- 调用
exit函数
-
以exit函数
退出状态来终止进程(另一种设置方式在status
)main中return
-
子进程
父进程通过调用
fork函数
创建一个新的运行
子进程
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
返回:子进程返回0,父进程返回子进程的PID,如果出错,返回-1;
新创建的
子进程
几乎但不完全与
父进程
相同。
-
得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝。子进程
- 包括文本,
,堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。数据和bss段
- 意味着当父进程调用
时,子进程可以读写父进程中打开的任何文件。fork
- 父进程和新创建的子进程之间最大的区别在于有不同的
。PID
- 包括文本,
-
会第一次调用,返回两次,一次在fork()函数
,一次在父进程
。子进程
- 返回值用来明确是在父进程还是在子进程中执行。
- 调用一次,返回两次
- 对于具有多个
的需要仔细推敲了fork实例
- 并发执行
-
和父进程
是并发运行的独立进程。子进程
-
可能以任意方式觉得执行他们的顺序。内核
- 不能对不同进程中指令的交替执行做任何假设。
-
- 相同但是独立的地址空间
- 在刚调用时,几乎什么都是相同的。
- 但是它们都有自己的私人空间,之后对x的改变是相互独立的。
- 共享文件
-
和父进程
都把他们的输出显示在屏幕上。子进程
-
继承了子进程
所有父进程
。打开的文件
-
8.4.3 回收子进程
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终结的状态,知道被它的父进程
回收(reap)
。
当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。
一个终止了但还未被回收的进程叫做
僵死进程
如果父进程没有回收,而终止了,那么内核安排
init
进程来回收它们。
-
的init进程
,在系统初始化时由内核创建的。PID位1
- 长时间运行的程序,如
,服务器,总是应该回收他们的僵死子进程shell
一个进程可以通过调用
waitpid函数
来等待它的子进程终止或停止
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid ,int *status, int options);
返回:如果成功,则为子进程的PID,如果WNOHANG,则为0,如果其他错误,则为-1.
waitpid函数有点复杂。
默认(option=0)时
,
waitpid
挂起调用进程的执行,知道它的等待集合中的一个子进程终止,如果等待集合的一个子进程在调用时刻就已经终止,那么
waitpid
立即返回。在这两种情况下,
waitpid
返回导致
waitpid
返回的已终止子进程的
PID
,并且将这个已终止的子进程从系统中去除。
-
判断等待集合的成员
等待集合的成员通过参数
确定pid
- 如果
,那么等待集合就是一个独立的子进程,它的进程pid>0
等于ID
PID
- 如果
,那么等待集合就是由父进程所有的子进程组成的。pid=-1
-
还支持其他类型的等待集合,包括waitpid函数
组等,不做讨论。UNIX进程
- 如果
-
修改默认行为
可以通过将
设置为常量options
和WHOHANG
的各种组合,修改默认行为。WUNTRACED
- WHOHANG: 如果等待集合中的任何子进程都还没有终止,那么立即返回(返回值为0)
- 默认的行为返回已终止的子进程。
- 当你要检查已终止和被停止的子进程,这个选项会有用。
- WUNTRACED:挂起调用进程的执行,知道等待集合中的一个进程变为已终结或被停止。
- 返回的
为导致的已终止或被停止的子进程PID
·PID
- 默认的行为是挂起调用进程,直到有子进程终止。
- 返回的
- WHOHANG|WUNTRACED: 立即返回,如果等待集合中没有任何子进程被停止或已终止,那么
- WHOHANG: 如果等待集合中的任何子进程都还没有终止,那么立即返回(返回值为0)
检查已回收子进程的退出状态
- 如果
参数是非空的,那么status
就会在waitpid
参数中放上关于导致返回的子进程的状态信息。status
头文件定义解释wait.h
参数的几个宏(函数宏):status
- WIFEXITED(status) : 如果子进程通过调用
或者一个返回exit
正常终止,就返回真。(return)
- WEXITSATUS(status): 返回一个正常终止的子进程的退出状态。只有在WIFEXITED定义为真是,才会定义这个状态。
- WIFSIGNALED(status): 如果子进程是因为一个未被捕获的信号终止的,那么就返回真
- WTERMSIG(status): 返回导致子进程终止的信号的数目,只有在WIFSIGNALED返回真时,才会定义这个状态。
- WIFSTOPPED(status): 如果引起返回的子进程当前是被停止的,那么就返回真。
- WSTOPSIG(status): 取得引发子进程暂停的信号代码,只有在
为真,才定义这个状态。WIFSTOPPED
- WIFEXITED(status) : 如果子进程通过调用
-
错误条件
- 调用进程没有子进程,那么
返回waitpid
,并且设置-1
为errno
。ECHILD
- 如果
函数被一个信号中断,那么它返回waitpid
,并且设置-1
为errno
。EINTR
- 调用进程没有子进程,那么
-
wait 函数
wait函数是
waitpid函数
的简单版本:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
调用
wait(&status)
等价于调用
waitpid(-1,&status,0)
。
-
实例,按顺序回收waitpid
僵死进程
8.4.4 让进程休眠
-
将一个进程挂起一段指定时间sleep函数
#include <unistd.h>
unsigned int sleep (unsigned int secs);
返回:还要休眠的描述
-
让调用进程休眠,知道该进程收到一个信号pause
#include<unistd.h>
int pause(void);
8.4.5 加载并运行一个程序
execve函数在当前进程的上下文中加载并运行了一个新程序。
#include <unistd.h>
int execve(const char *filename,const char *argv[],const char *envp[]);
execve函数加载并运行可执行目标文件
filename
,且带参数
argv
和环境变量列表
envp
。
只有当出现错误时,
execve
才会返回到调用程序
-
参数列表数据结构表示*argv[]
- 指向一个以
结尾的指针数组。null
- 每个指针指向一个参数串。
- 一般来说,
是可执行目标文件的名字。argv[0]
- 一般来说,
-
环境列表数据结构表示类似*envp[]
- 指向一个以
结尾的指针数组。null
- 每个指针指向一个环境变量串。
- 每个串都是形如
的 键值对KEY=VALUE
- 每个串都是形如
在
execve
加载
filename
以后,调用7.9节的启动代码,启动代码设置用户栈。并将控制传递给新程序的主函数。
- 主函数有如下原型
int main(int argc,char **argv,char **envp);
int main(int argc,char *argv[],char *envp[]);
- 当开始执行时,用户栈如图。
-
: 命令行参数个数argc
-
: 命令行指针数组的地址argv
-
: 环境变量指正数组的地址envp
Unix提供一下几个函数来操作环境数组。
- getenv
#include<stdlib.h>
char *getenv(const char *name);
//getenv函数在环境变量搜索字符串“name=value"。如果找到了,它就返回一个指向value的指针,否则返回NUL
- setenv和unsetenv
#include<stdlib.h>
int setenv(const char *name,const char *newvalue,int overwrite);
//成功返回0,错误返回-1
void unsetenv(const char *name);
//如果环境数组包含一个形如"name=oldvalue"的字符串,那么unsetenv会删除它
//,而setenv会用newvalue代替oldvalue,但是只有在overwirte非零时才会这样。
//如果name不存在,那么setenv就把”name=newvalue"添加进指针数组。
fork与execve区别
-
:在新的子进程运行相同的程序。fork
- 新进程是父进程的复制品。
-
:在当前进程的上下文加载并运行一个新的程序。execve
- 覆盖当前进程的地址空间。
- 但没有创建新进程。
- 新的程序仍然有相同的
,并且继承了调用PID
时已打开的所有文件描述。execve函数
8.4.6 利用fork和execve运行程序
Unix shell
和
Web
服务器 这样的程序大量使用
fork
和
execve
函数。
shell是一种交互型的应用级程序,代表用户运行其他程序。
- 最早的
是shell
程序。sh
- 后面出现了
。csh,tcsh,ksh,bash
-
执行一系列shell
read/evaluate
-
:读取来自用户的命令。read
-
:解析命令,并代表用户执行程序。evaluate
-
- 对字符串的处理。考虑各种
。trick
- 通过判断命令结尾是否有& - 来决定
是否shell
。即是否后台运行。waitpid
- 输出一个
,等待接收命令。>
- 调用
对命令运算。eval
-
解析以空格分割的命令行参数,并将分割后的值丢入parseline
中。argv
- 如果末尾是
,则返回&
。表示后台运行1
- 如果末尾是
-
判断一下是否存在这样的指令。builtin_command
- 如果
,那么等待程序结束,bg=0
才会继续执行。shell
-
具体代码就不贴了。parseline
注意这个简单的
shell
是有缺陷的,因为它并不回收它的后台子进程。修改这个缺陷,就必须使用信号。
8.5 信号
研究一种更高层次的软件形式的异常, 也是一种软件中断,称为
Unix
信号,它允许进程中断其他进程。
一个信号就是一条小消息,它通知进程系统中发生一个某种类型的事件。
Linux系统支持
30多种
信号。
每种信号类型对应于某种系统事件
- 底层的信号。
- 当底层发生硬件异常,信号通知 用户进程 发生了这些异常。
-
:发送除以0
信号。SIGILL
- 非法存储器引用:发送
信号SIGSEGV
-
- 较高层次的软件事件
- 键入
:发送ctrl+c
信号SIGINT
- 一个进程可以发送给另一个进程
信号强制终止它。SIGKILL
- 子进程终止或者停止,内核会发送一个
信号给父进程。SIGCHLD
- 键入
- 当底层发生硬件异常,信号通知 用户进程 发生了这些异常。
8.5.1 信号术语
传送一个信号到目的进程有
两个步骤
。
-
: 内核通过更新目的进程上下文的某个状态,就说发送一个信号给目的进程。发送信号
- 发送信号有
两个原因
- 内核检测到一个系统事件。比如被零除错误,或者子进程终止。
- 一个进程调用了
。显示要求进程发送信号给目的进程。kill函数
- 一个进程可以发信号给它自己。
- 接收信号: 当目的进程 被内核强迫以某种方式对信号的发送做出反应。目的进程就接收了信号。
- 进程可以忽略这个信号,终止。
- 或者通过一个称为
的用户层函数捕获这个信号。信号处理程序(signal handler)
一个只发出而没有被接收的信号叫做
待处理信号(pending signal)
- 一种类型至多只有一个
。待处理信号
- 如果一个进程有一个类型为
的待处理信号。k
- 那么接下来发送到这个进程类型为
的信号都会被简单的丢弃。k
- 如果一个进程有一个类型为
一个进程可以有选择性地阻塞接收某种信号
- 它任然可以被发送。但是产生的待处理信号不会被接收。
8.5.2 发送信号
Unix系统 提供大量向进程发送信号的机制。所有这些机制都是
基于进程组(process group)
。
-
进程组
- 每个进程都属于一个
。进程组
- 由一个正整数进程组
来标示ID
-
返回当前进程的getpgrp()函数
:进程组ID
-
- 由一个正整数进程组
- 每个进程都属于一个
#include<unistd.h>
pid_t getpgrp(void);
- 默认,一个子进程和它的父进程同属于一个进程组
- 一个进程可以通过setpgid()来改变自己或者其他进程的进程组。
#include<unistd.h>
int setpgid(pid_t pid,pid_t pgid);
如果pid是0 ,那么使用当前进程的pid。
如果pgid是0,那么使用指定的pid作为pgid(即pgid=pid)。
例如:进程15213调用setpgid(0,0)
那么进程15213会 创建/加入进程组15213.
- 用
程序发送信号/bin/kill
-
可以向另外的进程发送任意的信号。/bin/kill
- 比如
-
unix>/bin/kill -9 15213
发送信号9(SIGKILL)给进程15213。
- 一个为负的PID会导致信号被发送到进程组PID中的每个进程。
unix>/bin/kill -9 -15213
发送信号9(SIGKILL)给进程组
15213
中的每个进程。
- 用
的原因是,有些/bin/kill
有自己的Unix shell
kill命令
- 从键盘发送信号
:对一个命令行求值而创建的进程。作业(job)
- 在任何时候至多只有一个前台作业和0个或多个后台作业
- 前台作业就是需要等待的
- 后台作业就是不需要等待的
- 键入unix>ls|sort
- 创建一个两个进程组成的前台作业。
- 两个进程通过Unix管道链接。
- shell为每个作业创建了一个独立的进程组。
- 进程组ID取自作业中父进程中的一个。
在键盘输入ctrl-c 会发送一个SIGINT信号到外壳。外壳捕获该信号。然后发送SIGINT信号到这个前台进程组的每个进程。在默认情况下,结果是终止前台作业
类似,输入ctrl-z会发送一个SIGTSTP信号到外壳,外壳捕获这个信号,并发送SIGTSTP信号给前台进程组的每个进程,在默认情况,结果是停止(挂起)前台作业(还是僵死的)
- 在任何时候至多只有一个前台作业和0个或多个后台作业
- 用
发送信号kill函数
- 进程通过调用
发送信号给其他进程,类似于kill函数
bin/kill
- 进程通过调用
int kill(pid_t pid, int sig);
- pid>0,发送信号sig给进程pid
- pid<0,发送信号sig给进程组abs(pid)
- 事例:kill(pid,SIGKILL)
- 用
发送信号alarm函数
进程可以通过调用
alarm函数
向它自己
SIGALRM信号
。
#include<unistd.h>
unsigned int alarm(unsigned int secs);
返回:前一次闹钟剩余的秒数。
alarm函数安排内核在
secs
秒内发送一个
SIGALRM信号
给调用进程
- 如果
那么不会调度闹钟,当然不会发送secs=0
。SIGALRM信号
- 在任何情况,对
的调用会取消alarm
的闹钟,并且会返回被取消的闹钟还剩余多少秒结束。如果没有待处理(pending)
的话,pending
返回0
一个例子:
输出
unix> ./alarm
BEEP
BEEP
BEEP
BEEP
BEEP
BOOM!
//handler是一个自己定义的信号处理程序,通过signal函数捆绑。
8.5.3 接收信号
信号的处理时机是在从内核态切换到用户态时,会执行
do_signal()函数
来处理信号
当内核从一个异常处理程序返回,准备将控制传递给
进程p
时,它会检查进程p的未被阻塞的
待处理信号的集合(pening&~blocked)
。
- 如果这个集合为空,内核将控制传递到p的逻辑控制流的下一条指令。
- 如果非空,内核选择集合中某个
,并且强制p接收k。收到这个信号会触发进程某些行为。一旦进程完成行为,传递到p的逻辑控制流的下一条指令。信号k(通常是最小的k)
- 每个信号类型都有一个预定义的默认类型,以下几种.
- 进程终止
- 进程终止并转储存器(
)dump core
- 进程停止直到被
信号重启SIGCONT
- 进程忽略该信号
- 进程可以通过使用
修改和信号相关联的默认行为。signal函数
-
是不能被修改的例外。SIGSTOP,SIGKILL
-
- 每个信号类型都有一个预定义的默认类型,以下几种.
#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
-
通过下列三种方式之一改变和信号signal函数
相关联的行为。signum
- 如果
是handler
,那么忽略类型为SIG_IGN
的信号signum
- 如果
是handler
,那么类型为SIG_DFL
的信号恢复为默认行为。signum
- 否则,
就是用户定义的函数地址,这个函数称为信号处理程序handler
- 只要进程接收到一个类型为
的信号,就会调用signum
。handler
- 设置信号处理程序:把函数传递给
改变信号的默认行为。signal
- 调用信号处理程序,叫捕获信号
- 执行信号处理程序,叫处理信号
- 只要进程接收到一个类型为
- 如果
- 当处理程序执行它的
语句后,控制通常传递回控制流中进程被信号接收中断位置处的指令。return
8.5.4 信号处理问题
当一个程序要捕获多个信号时,一些细微的问题就产生了。
- 待处理信号
被阻塞
-
信号处理程序通常会阻塞 当前处理程序正在处理 的类型的待处理信号。Unix
-
- 待处理信号(被抛弃了)不会排队等待
- 当有两个同类型信号都是待处理信号时,有一个会
。被抛弃
- 关键思想:存在一个待处理的信号k仅仅表明至少一个一个信号k到达过。
- 当有两个同类型信号都是待处理信号时,有一个会
- 系统调用可以被中断(在某些
会出现)unix系统
- 像
这样的系统调用潜在的阻塞一段较长的时间,称为慢速系统调用。read,wait和accept
- 当处理程序捕获一个信号,被中断的慢速系统调用在信号处理程序返回后将不在继续,而是立即返回给用户一个错误条件,并将
设置为errno
。EINTR
- 当处理程序捕获一个信号,被中断的慢速系统调用在信号处理程序返回后将不在继续,而是立即返回给用户一个错误条件,并将
- 像
用一个后台
回收僵死子进程
的程序,前台读入做例子
- 1.初始简单利用接收
回收,一次调用只回收一个。SIGCHLD信号
- 在调用的过程中,又有信号发送过来,但是被阻塞了。之后又被直接抛弃。
- 如果不处理被阻塞和不会排队等待的问题。会有信号被抛弃。
- 重要教训:不可以用信号对其他进程中发送的事件计数
-
handle1-code
- 2.一次调用尽可能的多回收,保证在回收过程中,没有遗漏的信号。
-
handle2-code
-
- 3.还存在一个问题,在前台中,某些unix系统(
)的Solaris系统
被中断后不会自动重启,需要手动重启,read
一般会自动重启。Linux
- 之前
模块read
code
- 之前
- 现在改为如果是
手动重启。errno==EINTR
- 或者使用
包装函数标准。Signal
8.5.5 可移植的信号处理
不同系统之间,信号处理语义的差异(比如一个被中断的慢速系统调用是重启,还是永久放弃)是
Unix信号系统
的一个缺陷。
为了处理这个问题,
Posix
标准定义了
sigaction函数
,它允许
与Linux
和
Solaris
这样与
Posix
兼容的系统上的用户,明确指明他们想要的信号处理语义。
#include<signal.h>
int sigaction(int signum,stuct sigaction *act,struct sigaction *oldcat);
//若成功则为1,出错则为-1。
sigaction函数应用不广泛,它要求用户设置多个结构条目。
一个更简洁的方式,是定义一个包装函数,称为
Signal
,它调用
sigaction
。
- 它的调用方式与
的调用方式一样。signal函数
-
包装函数设置了一个信号处理程序,其信号处理语义如下(设置标准):Signal
- 只有这个处理程序当前正在处理的那种类型被阻塞。
- 和所有信号实现一样,信号不会排队等待。
- 只要可能,被中断的系统调用会自动重启
- 一旦设置了信号处理程序,它就会一直保持,直到
带着Signal
参数为handler
被调用。SIG_IGN或者SIG_DFL
- 在某些比较老的
,信号处理程序被使用一次后,又回到默认行为。Unix系统
- 在某些比较老的
8.5.6 显示地阻塞和取消阻塞信号
通过sigprocmask函数来操作。
#include<signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
-
改变当前已阻塞信号的集合(8.5.1节描述的blocked位向量)。sigprocmask函数
- 具体行为依赖how值
-
:添加SIG_BLOCK
中的信号到set
中。blocked
-
: 从SIG_UNBLOCK
删除blocked
中的信号。set
-
:SIG_SETMASK
。blocked=set
-
- 如果
非空,oldset
位向量以前的值会保存到block
中。oldset
- 具体行为依赖how值
还有以下函数操作
set
集合
#include<signal.h>
int sigemptyset(sigset_t *set);
//置空
int sigfillset(sigset_t *set);
//每个信号全部填入
int sigaddset(sigset_t *set,int signum);
//添加
int sigdelset(sigset_t *set,int signum);
//删除
//成功输出0,出错输出-1
int sigismember(const sigset_t *set,int signum);
//判断
//若signum是set的成员,输出1,不是输出0,出错输出-1。
8.5.7 同步流以避免讨厌的并发错误
如何编写读写相同存储位置的并发流程序的问题,困扰着数代计算机科学家。
- 流可能交错 的数量是与指令数 量呈指数关系
- 有些交错会产生正确结果,有些可能不会。
所谓
同步流
就是。以某种方式同步并发流,从而得到 最大的可行
交错的集合
,每个
交错集合
都能得到正确的结果。
如果发生以下情况,会出现同步错误。
- 父进程执行
,内核调度新创建的子进程运行,而不是父进程。fork函数
- 在父进程再次运行前,子进程已经终止,变成僵死进程,需要内核一个
信号给父进程SIGCHLD
- 父进程处理信号,调用
deletejob.
- 调用
。addjob
显然
deletejob
必须在
addjob
之后,不然添加进去的
job
永久存在。这就是同步错误。
这是一个称为竞争(
race
)的经典同步错误的示例。
-
中的main
和处理程序中调用deletejob之间存在竞争。addjob
- 必须
赢得进展,结果才是正确的,否则就是错误的。但是addjob
不一定能赢,所以有可能错误。即为同步错误。addjob
- 因为内核的调度问题,这种错误十分难以被发现。难以调试。
8.6 非本地跳转
C语言提供一种用户级
异常控制流形式
,称为非
本地跳转(nonlocal jump)
。
- 它将控制直接从一个函数转移到另一个当前正在执行的函数。不需要经过正常的调用-返回序列。
- 非本地跳转是通过
和setjmp
来提供。longjmp函数
#include<setjmp.h>
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env,int savesigs);//信号处理程序使用
//参数savesigs若为非0则代表搁置的信号集合也会一块保存
-
在setjmp函数
保存当前调用环境,以供后面env缓冲区
使用,longjmp
并返回0
- 调用环境包括程序计数器,栈指针,通用目的寄存器。
8.7 操作进程的工具
-
:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。STRACE(痕迹)
-
,能得到一个更干净,不带有大量共享库相关的输出的轨迹。用-static编译
-
-
: 列出当前系统的进程(包括僵死进程)PS(Processes Status)
-
(因为我们关注峰值的几个程序,所以叫TOP
打印当前进程使用的信息。TOP):
-
: 查看进程的内存映像信息PMAP(rePort Memory map of A Process)
-
:一个虚拟文件系统,以/proc
文本格式输出大量内核数据结构。ASCII
- 用户程序可以读取这些内容。
- 比如,输入"
,观察cat /proc/loadavg
上当前的平均负载。Linux系统
8.8 小结
-
发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。异常控制流(ECF)
- 在硬件层,异常是处理器中的事件出发的控制流中的突变。控制流传递给一个异常处理程序,该处理程序进行一些处理,然后返回控制被中断的控制流。
- 有
的异常:四种不同类型
。中断,故障,终止和陷阱
- 定时器芯片或磁盘控制器,设置了处理器芯片上的中断引脚时,中断会异步发生。返回到
Inext
- 一条指令的执行可能导致故障和终止同时出现。
- 故障可能返回调用指令。
- 终止不将控制返回。
- 陷阱用于系统调用。结束后,返回
Inext
- 定时器芯片或磁盘控制器,设置了处理器芯片上的中断引脚时,中断会异步发生。返回到
- 有
- 在操作系统层,内核用
提供进程的基本概念。进程给应用两个重要抽象:ECF
-
逻辑控制流
-
私有地址空间
-
- 在操作系统和应用程序接口处,有子进程,和信号。
- 最后,C语言的非本地跳转 完成应用程序层面的异常处理。
第八章课后家庭作业
8.9
进程对 | 是否并发 |
---|---|
AB | NO |
AC | YES |
AD | YES |
BC | YES |
BD | YES |
CD | YES |
8.10
A. 调用一次,返回两次:
fork
B. 调用一次,从不返回:
execve, longjmp
C. 调用一次,返回一次或者多次:
setjmp
8.11
4行
8.12
8行
8.13
->x=2
->x=4->x=3
满足这种拓扑即可。
8.14
主进程只打印一行。
主进程的直接子进程会打印一行,子进程的子进程又打印一行。
所以是3行。
8.15
这里的子进程不是exit,而是return,说明两个子进程都要到回到main函数去打印那里的hello。所以是5行。
8.16
输出counter = 2,因为全局变量也是复制的,而不是共享的。
8.17
满足
Hello--->1->Bye
--->0---->2-->Bye
这种拓扑即可。
8.18
画一下进程图就可以知道。
所以ACE是可能的。
8.19
总共会输出2^n行。
8.20
int main
(int argc, char args[])
{ execve
("/bin/ls", args, environ);
//没有错误处理,注意环境变量
return0;
}
8.21
abc或bac,c在ab之后。
8.22
int mysystem(char *command)
{
int status;
char *argv[4];
char *a0 = "sh";
char *a1 = "-c";
if( fork()==0 ) /*子进程*/
{
argv[0] = a0;
argv[1] = a1;
argv[2] = command;
argv[3] = NULL;
execve("/bin/sh", args, environ);
return -1; //执行异常
}
else{ /*父进程*/
if( wait(&status) > 0)
{
if(WIFEXITED(status) != 0)
return WEXITSTATUS(status);
else return status;
}
else return -1; //wait异常
}
}
8.23
一个可能的原因是,在第一个信号发给父进程之后,父进程进入handler,并且阻塞了SIGUSR2,第二个信号依然可以发送,然而,之后的3
个信号便会被抛弃了。因为是连续发送,所以很可能是没等上下文切换,这5个信号就同时发送了。所以只有2个信号被接收。
8.24
#include "csapp.h"
#define N 2
int main()
{
int status, i;
pid_t pid;
char errorInfo[128];
/* Parent creates N children */
for
(i=0;i<N;i++)
if
((pid = Fork()) == 0)
/* Child */
exit(100+i);
/* Parent reaps N children in no particular order */
while((pid = waitpid(-1, &status, 0)) > 0) {
if(WIFEXITED(status))
printf("child %d terminated normally with exit status=%d
", pid, WEXITSTATUS(status));
else if(WIFSIGNALED(status))
{ printf("child %d terminated by signal %d: ",pid, WTERMSIG(status) );
psignal(WTERMSIG(status), errorInfo); //psignal会打印sig的信息
}
}
if(errno != ECHILD)
unix_error("waitpid error");
exit(0);
}
8.25
fgets的定义如下:
char *fgets(char *buf, int bufsize, FILE *stream);
参数:
*buf: 字符型指针,指向用来存储所得数据的地址。
bufsize: 整型数据,指明buf指向的字符数组的大小。
*stream: 文件结构体指针,将要读取的文件流。
这个应该是用alarm发送信号给自己,然后在信号处理程序里面做文章。
显然,在tfgets里一开始需要调用fgets。然而,因为五秒时间到了,fgets还没有返回,所以我们必须在处理程序里直接跳转到某个地方进行tfgets的NULL返回。这就需要用到非本地跳转。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
sigjmp_buf env;
void tfgets_handler(int sig)
{
signal(SIGALRM, SIG_DFL);
siglongjmp(env, 1);
}
char *tfgets(char *buf, int bufsize, FILE *stream)
{
static const int TimeLimitSecs = 5;
signal(SIGALRM, tfgets_handler)
alarm(TimeLimitSecs);
int rc = sigsetjmp(env, 1);
if(rc == 0) return fgets(buf, bufsize, stream);
else return NULL; //alarm,time out
}
8.26
1)需要定义一个struct结构体数组,JID是1,那么就是数组的第一个。
结构体中有pid和状态。状态分空,运行,停止等等。
难点是JID和pid的互相查找,有点像hash。
如果不用hash,就是枚举数组寻找pid。所以,结构体数组大小要先设定(最多运行的jobs数目),不能太大,否则会增加复杂度。
2)可以写一个addjob和deletejob函数,以pid为参数。
当然,对于后台运行的进程,要注意竞争。
3)需要写handler函数,当子进程运行完毕或终止,需要用handler函数来回收。
还需要写一些handler来实现发送消息的功能,比如ctrl-z和ctrl-c。
4)jobs就是列出数组中状态不为空的进程。
可以写一个searchJID(pid_t pid)函数,根据某个pid来查询JID。
5)fg和bg倒是比较简单,给子进程发送那些消息,让那些消息重新execve。
教材学习中的问题及解决
以下是我和同伴互相解决对方的疑问:
- 问题1: 如何消除竞争?
- 问题1解决:可以在
之前,阻塞fork
,在调用SIGCHLD信号
后取消阻塞。addjob
- 注意,子进程继承了阻塞,我们要小心地接触子进程中的阻塞。
- 消除竞争的原则就是,让该赢得竞争的对象在任何情况下都能赢。
- 问题2: 如何使用非本地跳转来规避正常的调用/返回栈规则。
- 问题2解决: 非本地跳转通过setjmp和longjmp函数来提供。
上周考试错题总结
无
结对及互评
点评模板:
- 博客中值得学习的或问题:
- xxx
- xxx
- ...
- 代码中值得学习的或问题:
- xxx
- xxx
- ...
- 其他
本周结对学习情况
-[20155318](http://www.cnblogs.com/lxy1997/)
- 结对照片
- 结对学习内容
- 教材第八章内容
-
- ...
其他(感悟、思考等,可选)
通过这一章的学习,进一步加深了对异常控制流的了解。
代码托管
(statistics.sh脚本的运行结果截图)
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 |
---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 |
第一周 | 133/133 | 1/1 | 8/8 |
第三周 | 159/292 | 1/3 | 10/18 |
第五周 | 121/413 | 1/5 | 10/28 |
第七周 | 835/3005 | 2/7 | 10/38 |
第八周 | 1702/4777 | 1/8 | 10/48 |
第九周 | 1664/6441 | 3/11 | 10/58 |
第十一周 | 300/6741 | 3/14 | 10/68 |
第十三周 | 743/7484 | 2/16 | 10/78 |
第十四周 | 345/7829 | 1/17 | 10/88 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。
参考:软件工程软件的估计为什么这么难,软件工程 估计方法
- 计划学习时间:15小时
- 实际学习时间:10小时
- 改进情况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)
参考资料
- 《深入理解计算机系统V3》学习指导
- ...