2017-2018-1 20155217 《信息安全系统设计基础》第十四周学习总结
教材学习内容总结
8.1 异常
-
从处理器运行开始到结束,程序计数器假设一个序列的值
a0a1......an-1
,这个控制转义序列叫做处理器的控制流
。
-
异常,就是控制流中的突变,用来响应处理器状态中的某些变化。
-
状态的变化称为事件,在任何情况下,当处理器检测到有事件发生时,会通过一张叫做异常表的跳转表,进行一个间接过程调用到专门处理程序——异常处理程序。当异常处理程序完成之后,根据引起引起异常的事件类型,会发生以下三种情况之一:
- 处理程序将控制返回给当前指令,即事件发生之时正在执行的指令。
- 处理程序将控制返回给如果没有异常将会执行的下一条指令。
- 处理程序终止被中断的程序。
异常处理
- 系统中可能的每种异常都被分配了唯一一个非负整数的异常号,异常表中的条目k中包含异常k的处理程序地址。异常表的起始地址存放在一个叫做异常表基址寄存器的特殊寄存器中。
- 异常类和过程调用的不同之处:
- 返回地址是当前地址或者下一条指令
- 处理器也会把额外的处理器状态压回栈中,在处理程序返回时,重新开始被中断的程序会需要这些状态。
- 如果控制从一个用户程序转移到内核,那么所有项目都会被压到内核栈中而不是用户栈。
- 异常处理程序运行在内核模式下,意味着他们对所有的系统资源拥有完全的访问权限。
异常的类别
中断
- 中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。因此是异步的。硬件中断的异常处理程序通常称为中断处理程序。
- 其余异常类型都是同步发生的,是执行当前指令的结果。这一类指令称为
故障指令
。
陷阱
- 陷阱是有意的异常,最重要的用途是在用户程序和内核之间提供一个向过程一样的接口,叫做系统调用。
- 为了允许内核服务的受控访问,使用
“syscall n”
指令,跳转到一个异常处理程序的陷阱,处理程序对参数解码并调用适当的内核程序。
故障
- 故障由错误情况引起,可能能够被故障处理程序修正。故障发生时,处理器将控制转移给故障处理程序,若能修正,则将控制返回到引起故障的指令,重新执行;若不能修正,处理程序返回abort例程,终止引起故障的应用程序。
终止
- 终止是不可恢复的致命错误造成的结果,通常是硬件错误。终止处理程序将控制直接返回给abort例程,直接终止该应用程序。
Linux/A32系统中的异常
Linux/A32故障和终止
- 除法错误(异常0):应用试图除以0,或者除法指令的结果对于目标操作数过大。
- 一般故障保护(异常13):通常因为一个程序引用一个未定义的虚拟存储区域,或者试图写一个只读文本段。
- 缺页(异常14):处理程序将磁盘上虚拟存储器相应页面映射到物理存储器的一个页面,然后重新开始执行这条指令。
- 机器检查(异常18):在导致故障的指令执行中检测到致命的硬件错误。
Linux/A32系统调用
- 每个系统调用都对应着唯一的整数号,对应于一个到内核中跳转表的偏移量。
- IA32系统调用是通过一条称为 int n 的陷阱指令提供的。
- C程序通过syscall函数可以直接调用任何系统调用。
- 所有Linux系统调用都是通过
通用寄存器
而不是栈传递的,%eax
包含系统调用号,%ebx、%ecx、%edx、esi%、%edi和%ebp
包含最多6
个参数。栈指针%esp不能使用,因为当进入内核模式时,内核
会覆盖它。
进程
- 进程,就是一个执行中的程序的实例,系统中的每个程序都是定义在运行在某个进程的上下文中的。
异常
是允许操作系统提供进程的概念所需要的基的本构造块。
逻辑控制流
- PC的值唯一地对应于包含在程序可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做
逻辑控制流
。
- 进程轮流使用处理器,每个进程执行流的一部分,然后被抢占(暂时挂起)。
并发流
- 一个逻辑流的执行
在时间上
与另一个流重叠,称为并发流。 - 一个进程和其他进程
轮流运行
的概念称为多任务
,一个进程执行它的控制流的一部分时间称为时间片
。
私有地址空间
-
一个进程为每个程序提供他自己的私有地址空间,一般而言,和这个空间中某个地址相关联的存储器字节不能被其他程序读或写。
-
地址空间底部是保留给用户程序的,
顶部
保留给保留给内核
,用来存放内核在代表进程执行时的指令。
用户模式和内核模式
- 处理器通过某个控制寄存器中的一个模式位来提供这种功能。
- 内核模式:设置模式位,进程可以执行指令集中的任何指令,并且访问系统中的任何存储器位置。
- 用户模式:不设置模式位,进程不允许执行特权指令,也不允许直接引用地址空间中内核区的代码和数据。
- 初始模式是用户模式,进入内核模式的唯一方法是通过诸如
中断、故障或者陷入系统调用
这样的异常。 - /proc文件系统,允许用户模式进程访问内核数据结构的内容。
上下文切换
- 上下文:内核重新启动一个被抢占的进程所需的状态。包括
描绘地址空间的页表
、包含有关当前进程信息的进程表
,以及半酣进程已打开文件的信息的文件表
。
- 调度:进程执行过程中,内核决定抢占当前进程并开始一个先前被抢占的进程,由内核中的调度器完成。
- 内核为每个进程维持一个上下文。并通过一种称为上下文切换的较高层形式的异常控制流来实现多任务上下文切换:
- 保存当前进程的上下文;
- 恢复某个先前被抢占的进程被保存的上下文;
- 将控制传递给新恢复的进程。
8.3系统调用错误处理
- Unix系统级函数遇到错误时,会典型地
返回-1
,并设置全局变量errno
来表示出错内容。 - 通过使用
错误处理包装函数
,可以进一步简化代码。
8.4进程控制
获取进程ID
- 每个进程有一个唯一的非零正数进程ID(PID)。
pid_t getpid(void); /*返回调用进程的PID*/
pid_t getppid(void); /*返回它的父进程的PID*/
创建和终止进程
进程总是处于以下三种状态之一:
-
运行。在CPU上执行,或者等待被执行且最终会被内核调度。
-
停止。进程的执行被挂起,且不会被调度。(与信号有关)
-
终止。进程永远停止。进程终止的原因:
- 收到一个信号,默认行为是终止程序.
- 从主程序返回.
- 调用exit函数。
- exit函数以status退出状态来终止进程。
void exit(int ststus)
- 父进程通过调用
fork函数
创建一个新的运行子进程。
pid_t fork(void); /*子进程返回0,父进程返回子进程的pid,出错则返回-1。*/
fork函数:
- 调用一次,返回两次。一次返回到父进程,一次返回到新创建的子进程。
- 并发执行,父进程和子进程是并发运行的独立程序。内核能够以任意方式交替执行他们的逻辑控制流中的指令。
- 相同但是独立的地址空间,父进程和子进程
地址空间
都相同,但对于变量所做的改变都是独立的。 - 共享文件,子进程继承了父进程所有打开的文件。
回收子进程
- 当一个进程由于某种原因终止,内核不会将他马上清除,而是将进程保持在已终止的状态中,直到被他的父进程回收。
- 系统通过调用
waitpid函数
来等待它的子进程终止或停止。
pid_t waitpid(pid_t pid,int *status,int options);
- 判定等待集合的成员:pid>0,则等待集合为单独的进程;pid=-1,等待集合是由父进程所有的子进程组成的。
- 错误条件:如果调用进程没有子进程,那么waitpid返回-1,并设置errno为ECHILD,如果函数被一个信号中断,那么返回-1,并设置errno为EINTR。
- wait函数
pid_t wait(int *status);
调用wait(&status)等价于调用waitpid(-1,&status,0)
。
让进程休眠
- sleep函数将一个进程挂起一段指定的时间。
unsigned int sleep(unsigned int secs); /*返回还要休眠的秒数*/
- pause函数让调用函数休眠,直到进程收到一个信号。
int pause(void); /*总是返回-1*/
加载并运行程序
- execve函数在当前进程的上下文中加载并运行一个新程序。
int execve(const char*filename,const char *argv[],const char *envp);
/*成功则不返回,错误返回-1*/
利用fork和execve运行程序
- 外壳:一个交互型的应用级程序,代表用户运行其他程序,执行一系列的读/求值步骤,然后终止。读步骤读取来自于
命令行
,求值步骤解析命令行,并代表用户运行程序。
8.5信号
- 信号:一条小消息,通知进程系统中发生了一个某种类型的事件。
信号术语
- 发送信号:内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。
- 接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。通过
信号处理程序
捕获信号。
发送信号
- 每个进程都只属于一个进程组,由一个正整数进程组ID来标识。
getpgrp函数
返回当前进程的进程组ID。pid_t getpgrp(void);
setpgrp函数
改变自己或者其他进程的进程组。int setpgrp(pid_t pid,pid_t pgid);
用/bin/kill
程序发送信号
- /bin/kill程序可以向另外的进程发送任意的信号。一个为负的PID会导致信号被发送到进程组PID中的每个进程。
从键盘发送信号
- Unix外壳使用
作业
这个抽象概念来表示为对一个命令行求值而创建的进程。int kill(pid_t pid,int sig);
用kill函数
发送信号
- 进程通过调用kill函数发送信号给其他进程(包括自己)。
- 如果
pid大于0
,则发送信号sig给进程pid;若小于0
,则发送信号给sig给进程组abs(pid)中的每个进程.
用alarm函数
发送信号
- 进程通过调用alarm函数向他自己发送SIGALRM信号.
unsigned int alarm(unsigned int secs); /*返回前一次闹钟剩余的秒数,若没有则返回0*/
接收信号
- 内核从一个异常处理的程序返回,准备将控制传递给p时,会检查p未被阻塞的待处理信号的集合,若集合为空,则转到p逻辑控制流中的
下一条指令
;若集合不为空,则内核选择
集合中的某个信号k,并且强制p接受
信号k,触发
进程的某种行为,完成后转到
p逻辑控制流中的下一条指令。
k的默认行为有:
- 进程终止。
- 进程终止并转储存储器。
- 进程停止直到被SIGCONT信号重启。
- 进程忽略该信号。
信号处理问题
一个程序捕获多个信号时产生的问题:
- 待处理信号被阻塞。
- 待处理信号不会排队等待。
- 系统调用可以被中断。
- 不可以用信号来对其他进程中发生的事件计数。
可移植的信号处理
- 为了处理不同系统之间信号处理语义的差异,通过
sigaction函数
使不同系统上兼容的用户明确指定想要的信号处理语义。int sigaction(int signum,struct sigaction *act,struct sigaction *oldact);
显示地阻塞和取消阻塞信号
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
- sigprocmask函数改变当前已阻塞信号的集合,具体行为依赖
how的值
。 - SIG_BLOCK:添加set中的信号到blocked中。
- SIG_UNBLOCK:从blocked中删除set中的信号。
- SIG_SETMASK:blocked=set。
同步流以避免并发错误
- 以某种方式同步并发流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。
8.6非本地跳转
-
用户级异常控制流形式,称为
非本地跳
转,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要
经过正常的调用返回序列。 -
举例:
-
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:一个虚拟文件系统,以ASCII格式输出大量内核数据结构内容。
代码调试
exec
exec1
- exec1.c中execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件
- 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中
exec2
- 它与exec1的区别就在于exevp函数的第一个参数,exec1传的是ls,exec2直接用的arglist[0],不过由定义可得这两个等价,所以运行结果是相同的。
exec3
- 这个代码里使用了execlp函数,execlp()会从PATH环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。
- exec3指定了环境变量,然后依然执行了ls -l指令,成功后没有返回,所以运行结果同exec1。
fork
forkdemo1
- 打印进程pid,然后调用fork函数生成子进程,休眠一秒后再次打印进程id,这时父进程打印子进程pid,子进程返回0。
forkdemo2
- 调用两次fork,一共产生四个子进程,所以会打印四个aftre输出.
forkdemo3
- fork产生子进程,父进程返回子进程pid,不为0,所以输出父进程的那句话,子进程返回0,所以会输出子进程那句话。
forkdemo4
- 先打印进程pid,然后fork创建子进程,父进程返回子进程pid,所以输出parent一句,休眠十秒;子进程返回0,所以输出child与之后一句。
forkgdb
- 父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰。
课后部分习题
- 考虑三个具有下述起始和结束时间的进程:
进程 | 起始时间 | 结束时间 |
---|---|---|
A | 0 | 2 |
B | 1 | 4 |
C | 3 | 5 |
对于每对进程,指出他们是否是并发运行的:
进程对 | 是否? |
---|---|
AB | 是 |
AC | 否 |
BC | 是 |
解析:进程AB、BC它们各自的执行是重叠的,也就是一个进程在另一个进程结束前开始,所以是并发运行。对于进程AC,它们的执行没有重叠;A在C开始之前就结束了,因此AC是不并发的。
- 考虑下面的程序:
A.子进程的输出是什么?
答:printfl:x=2 printf2: x=1
解析:子进程执行了两个printf语句。在fork返回之后,它执行了第8行的printf然后它从if语句中出来,执行了第9行的printf语句。
B.父进程的输出是什么?
答:printf2: x=0
解析:父进程只执行了第9行的printf语句。
- 列出下列程序所有可能的输出序列:
答:有四个这样的序列:acbc, bcac, abcc和bacco。
解析:父进程打印b,然后是c。子进程打印a,然后是c。因此,任何满足b}c和a -- c的拓扑排序都是可能的输出序列。
- 考虑下面的程序:
A.下面这个程序会产生多少个输出?
答:会产生6个输出行。
B.这些输出行的一种可能顺序是什么?
解析:输出行的顺序根据系统不同而不同,取决于内核如何交替执行父子进程的指令。一般而言,满足下图的任意拓扑排序都是合法的顺序:
所以,当我们在系统上运行这个程序时,会得到下面的输出:unix> ./waitprob1 Hello 0 1 Bye 2 Bye
在这种情况下,父进程首先运行,在第6行打印Hello,在第8行打印0。
对wait的调用会阻塞,因为子进程还没有终止,所以内核执行一个上下文切换,并将控制传递给子进程,子进程在第8行打印1,在第15行打印bye,然后在第16行终止,退出状态为2。
在子进程终止后,父进程继续,在第12行打印子进程的退出状态,在第15行打印bye。
- 编写一个sleep的包装函数,叫做snooze,带有下面的接口:
unsigned int snooze(unsigned int secs);
除了snooze函数会打印一条信息来描述进程实际休眠多长时间外,它和sleep函数的行为完全一样:sleep for 4 of 5 secs
答:代码截图如下:
- 编写一个叫做snooze的程序,有一个命令行参数,用这个参数调用之前练习的snooze函数,然后终止。编写程序,使得用户可以通过在键盘上输入ctrl-c中断snooze函数,比如:
nuix> ./snooze 5
Slept for 3 of 5 secs. User hits ctrl-c after 3
unix>