20135210程涵——信息安全系统设计基础第十一周学习总结
第8章 异常控制流
8.1 异常
异常是ECF的一种,一部分由硬件实现,一部分由操作系统实现。就是位于硬件和操作系统之间的ECF。
异常可以分为四类:中断(interrupt),陷阱(trap),故障(fault),终止(abort)。
中断——来自处理器外部的I/O设备的信号的结果。
中断处理程序——异步异常——由处理器外部I/O设备中的事件产生的。同步异常是执行一条指令的直接产物。
中断通过向处理器芯片上的一个引脚发信号(高低电平),并将异常号放在系统总线上,以触发中断,很清楚。
其中断处理程序完成后直接返回给下一条指令。
陷阱——同步异常——完成处理后也返回到下一条指令。
陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
从程序员的角度来看,系统调用和普通的函数调用是一样的。
但是它们的实现是非常不同的。普通函数在用户模式,系统调用在内核模式。
syscall n指令,执行这条指令,会导致一个到异常处理程序的陷阱,就是跳到一个异常处理程序
这个程序会对参数解码,并调用适当的内核程序,比如read,fork,execve,exit。
故障——同步异常——对应的处理程序叫故障处理程序。
如果处理程序可以修正这个错误(缺页异常),那么就返回到引起故障的指令,如果不能修正,就返回到内核的abort例程。
终止——同步异常——对应的处理程序叫做终止处理程序,终止处理程序从不将控制返回,它会将控制返回给内核的abort例程。
Linux/A32系统调用
每个系统调用都对应着唯一的整数号,对应于一个到内核中跳转表的偏移量。
IA32系统调用是通过一条称为 int n 的陷阱指令提供的。
C程序通过syscall函数可以直接调用任何系统调用。
所有Linux系统调用都是通过通用寄存器而不是栈传递的,%eax包含系统调用号,%ebx、%ecx、%edx、esi%、%edi和%ebp包含最多6个参数。栈指针%esp不能使用,因为当进入内核模式时,内核会覆盖它。
8.2 进程
-
上下文是由程序正确运行所需的状态组成的。这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
-
上下文是一份集合,它的每个组成部分都真实存在,但这些组成部分不是放在一起的,这个集合的概念是概念上,将这些组成部分合在一起,我们称这个集合是上下文。
-
当我们说在上下文中执行一个程序,其实是说,这些组成部分都分配安排好了,放在那里了,然后开始执行这些组成部分中的存储器中的代码。
进程提供给应用程序的关键抽象:
一个独立的逻辑控制流,它提供了一个假象,好像我们的程序独占的使用处理器。 一个私有的地址空间,它提供了一个假象,好像我们的程序独占的使用存储器系统。 上下文是集合,进程其实是这个集合下实际一条一条执行代码的过程。进程包含上下文以及执行的过程。
PC值的序列称为逻辑控制流。
处理的实际的PC序列称为物理控制流,物理控制流总包含多个逻辑流。
进程是程序和内核之间的屏障,进程是应用程序的抽象,它将内核和硬件踢掉,给应用程序假象。内核和硬件提供了进程这种功能,应用程序都是一个一个的进程,应用程序不知道内核和硬件,只知道进程。
应用程序没有时间和空间的概念,它只知道进程,但进程自己知道,但进程不让应用程序知道。
异常处理程序、进程、信号处理程序、线程、java进程都是逻辑流的例子。
-
多个流并发的执行的一般现象称为并发。
-
一个进程和其他进程轮流运行的概念称为多任务。
一个进程执行它的控制流的一部分的每一个时间段叫做时间片。因此多任务也叫时间分片。
-
两个流并发的运行在不同的处理器核或者计算机上,我们称为并行流。
并行流一定是并发流,并发流是“老大”。
-
一个进程为每个程序提供它自己的私有地址空间,这个空间中的某个地址相关联的那个存储器字节,一般而言,是不能被其他进程读或者写的,从这个意义上说,这个地址空间是私有的。
x86 linux地址空间顶部是保留给内核的。这部分也包含了
代码
数据
栈
但是,这部分是内核在代表进程执行指令时才会使用的,比如系统调用时,使用的就是这部分空间。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定就叫做调度,是由内核中称为调度器的代码处理的。
上下文切换:
保存当前进程的上下文。
恢复某个先前被抢占的进程被保存的上下文。
将控制传递给这个新恢复的进程。
8.3 系统调用错误
当UNIX系统级函数遇到错误时,它们典型地会返回-1,并设置全局整数变量errno来表示什么出错了。
8.4 进程控制
Unix提供了大量从c程序中操作进程的系统调用。
从程序员的角度,我们可以认为进程总处于下面三种状态之一:
运行——在cpu上运行,或者,等待运行且最终会运行(会被内核调度)
停止——进程被挂起(也就是被其他的进程抢占了),且不会被调度,但可以被信号唤醒
终止——进程被永远的停止了,受到终止信号,或者从主程序返回,或者调用exit函数。
fork函数:
-
调用一次,返回两次。父进程和子进程是独立的进程,并发执行的。一般而言,作为程序员,我们决不能对不同的进程中指令的交替执行做任何假设。相同的但是独立的地址空间。共享文件。
-
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中。直到被它的父进程回收。
-
当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程。然后抛弃已终止的进程。
一个终止了但是还未被回收的进程称为僵死进程。
- 如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排另外一个进程来回收它们,这个进程是init。其pid为1。其在系统初始化时由内核创建。
一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
这个函数有三个参数:
pid
status
options。
-
options=0时,调用这个函数的进程挂起,也就是这个进程处于函数中什么也不做,等待着,等待什么呢,等待其子进程终止,如果终止了一个,那么函数就返回了,返回的,就是终止的子进程的pid,并且将这个子进程从系统中除去。
-
等待的子进程有哪些呢?这点由pid决定,pid=-1,那么就是所有的子进程,pid大于0,那么就是一个子进程,当前pid表示的那个子进程。
-
options=WNOHANG时,如果没有终止的子进程,那么函数立即返回,返回0。options=WUNTRACED时,和options=0类似,但这里还检测停止的子进程。options=0只检测终止的子进程。且,本options不会将子进程从系统中除去。options=WNOHANG|WNUNTRACED时,立即返回,返回值要么是停止或者终止的pid,要么是0。
-
如果调用进程没有子进程,那么waitpid返回-1,并设置errno为ECHLILD;如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR。
-
status是一个指向int类型的指针,waitpid返回后,其会有相应的值,但这个值的编码貌似比较复杂,c标准库提供了一些宏来处理它。WIFEXITED(status)等。
wait函数等价于waitpid(-1, &status, 0)。
execve函数在当前进程的上下文中加载并运行一个新程序。fork一次调用两次返回,execve调用一次,从不返回。
8.5 信号
Unix信号是一种更高层次的软件形式的异常。
linux支持30种不同类型的异常。
每种信号类型都对应于某种系统事件。底层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。
发送信号——内核通过更新目的进程上下文中的某个状态,告诉目的进程,有一个信号来了。 接受信号——当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。 进程可以忽略、终止、捕获。
发送信号的方式
/bin/kill
键盘发送信号
kill函数
alarm函数。
接收信号的默认行为
进程终止 进程终止并转储存储器 进程停止直到被SIGCONT信号重启 进程忽略该信号。
但接受信号的行为可以不是默认行为,通过设置信号处理程序。
当一个程序要捕获多个信号时,一些细微的问题就产生了。
-
待处理信号被阻塞——Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。待处理信号不会排队等待——任意类型至多只有一个待处理信号。系统调用可以被中断——像read、wait、accept这样的系统调用潜在的会阻塞进程一段较长的时间,称为慢速系统调用。
-
父进程有一套自己的信号处理程序,放在那里,然后父进程用了一个read函数,这个函数会执行一段时间,在执行的这段时间里,来了一个信号,所以控制转移到了信号处理程序,也就是说控制从read函数里跳到了信号处理程序。当处理程序处理完毕了,其返回,在有的系统中,返回了,但read函数的调用不再继续了,而是直接返回给用户一个错误。当然在其他的系统中,比如linux,还是会返回到read函数继续执行。
Signal包装函数设置的信号处理程序的信号处理语义:
1. 只有这个处理程序当前正在处理的那种类型的信号被阻塞。
2. 和所有信号实现一样,信号不会排队等候。
3. 只要有可能,被中断的系统调用会自动重启。
4. 一旦设置了信号处理程序,它就会一直保持,知道signal带着handler参数为SIG_ IGN或者SIG_DFL被调用。
同步流以避免讨厌的并发错误
1. 一般而言,流可能交错的数量是与指令的数量呈指数关系的。
2. 以某种方式同步并交流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。
3.如何编写读写相同存储位置的并发流程序的问题,困扰着数代计算机科学家。比如,竞争问题。
8.6 非本地跳转
c语言提供一种用户级异常控制流形式——非本地跳转。
-
异常控制流,现在想想这个名词——很贴切,控制,就是控制,正常的控制移动是顺序的,线性控制流不正常的就是控制跳走了,就是异常的,控制跳走了,跳到哪里去了?调到异常处理程序中去了,控制怎么跳走的?很多方法,比如系统调用——系统调用看起来起码还是顺序的,比如read,就停在read函数那里,等read函数执行完,事实上控制已经不再main中了,而是通过read函数跳到了内核。还比如信号,进程收到信号,就触发控制跳到信号处理程序中了。
现在讲的非本地跳转,也是如此。控制跳走了。
用户级异常控制流形式,称为非本地跳转,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。
setjmp函数在env缓冲区中保存当前调用环境,供后面longjmp使用,并返回0。调用环境包括程序计数器、栈指针和通用目的寄存器。
int setjmp(jmp_buf env); int sigsetjmp(setjmp_buf env,int savesigs);
longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。
void longjmp(jmp_buf env,int retval); void siglongjmp(sigjmp_buf env,int retval);
8.7 操作进程的工具
STRACE
ps
top
pmap
/proc