20145227《信息安全系统设计基础》第十一周学习总结

20145227《信息安全系统设计基础》第十一周学习总结

第八章 异常控制流

  • 控制转移序列称为控制流。
  • 从从一条指令到下一条指令称为转移控制。
  • 异常控制流:现代操作系统通过使控制流发生突变来对系统状态做出反应,这些突变称为异常控制流。
  • 异常控制流ECF:即这些突变。
1.ECF是操作系统用来实现I/O、进程和虚拟存器的基本机制
2.应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务
3.ECF是计算机系统中实现并发的基本机制
4.软件异常机制——C++和Java有try,catch,和throw,C中非本地跳转是setjmp和longjmp

异常

  • 异常是异常控制流的一种形式,一部分由由硬件实现,一部分由操作系统实现。由于系统的不同而有所不同。
  • 异常就是控制流的突变

1.异常处理

  • 异常号:系统为每种类型的异常分配的唯一的非负整数。
  • 异常表:系统启动时操作系统就会初始化一张条转变,使得条目k包含异常k的处理程序的地址。
  • 关系:异常号是到异常表中的索引,异常表的起始地址放在异常表基址寄存器。
  • 异常类似于过程调用,区别在:
1.处理器压入栈的返回地址,是当前指令地址或者下一条指令地址。
2.处理器也把一些额外的处理器状态压到栈里
3.如果控制一个用户程序到内核,所有项目都压到内核栈里。
4.异常处理程序运行在内核模式下,对所有的系统资源都有完全的访问权限。

2.异常的类别

  • 分为四种:中断、陷阱、故障和终止。

(1)中断

  • 异步发生
  • 来自处理器外部的I/O设备的信号的结果
  • 将控制返回给下一条指令

(2)陷阱和系统调用

  • 陷阱是有意的异常
  • 是执行一条指令的结果
  • 最重要的用途:在用户和内核间提供一个像过程一样的接口,叫系统调用

(3)故障

  • 由错误状况引起,可能能够被故障处理程序修正
  • 故障发生时,处理器将控制转移给故障处理程序,如果能够修正,返回引起故障的指令,重新执行指令,否则返回abort例程,终止

(4)终止

  • 是不可恢复的致命错误造成的结果
  • 通常是一些硬件错误
  • 终止示例:将控制返回abort例程,如图:

3.Linux/IA32系统中的异常

高达256种异常类型,如图:

(1)Linux/IA32故障和终止

  • 除法错误/浮点异常:异常0,选择中止程序
  • 一般保护故障/段故障:异常13,选择中止程序
  • 缺页:异常14,重新执行产生故障的指令
  • 机器检查:异常18,不返回控制给应用程序

(2)Linux/IA32系统调用

  • 系统调用示例,如图9所示:

进程

  • 在操作系统层:逻辑控制流,私有地址空间,多任务,并发,并行,上下文,上下文切换,调度。

  • 进程就是一个执行中的程序实例。系统中的每个程序都是运行在某个进程的上下文中的。

  • 进程提供给应用程序的关键抽象:a)一个独立的逻辑控制流 ;b)一个私有的地址空间

1.逻辑控制流

  • 一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令,这个PC值的序列就叫做逻辑控制流,或者简称逻辑流

  • 进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。对于运行在改程序上下文的其他程序,它看上去在独占的使用处理器。

2.并发流

  • 并发流:并发流一个逻辑流的执行在时间上与另一个流重叠
  • 并发:多个流并发执行的一般现象称为并发。
  • 多任务:多个进程并发叫做多任务。
  • 并行:并发流在不同的cpu或计算机上。

3.私有地址空间

  • 一个进程为每个程序提供它自己的私有地址空间。
  • 运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常。
  • 进程地址空间,如图11所示

4.用户模式和内核模式

  • 需要限制一个应用可以执行的指令以及可访问的地址空间范围来实现进程抽象,通过特定控制寄存器的一个模式位来提供这种机制。设置了模式位时,进程运行在内核模式中,进程可以执行任何指令和访问任何存储器位置。没设置模式位时,进程运行在用户模式中,进程不允许执行特权指令和访问地址空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据。

  • 用户程序的进程初始是在用户模式中的,必须通过中断、故障或陷入系统调用这样的异常来变为内核模式。

  • Linux的聪明机制——/proc文件系统,包含内核数据结构的内容的可读形式,运行用户模式进程访问。

5.上下文切换

  • 上下文切换:操作系统内核使用叫上下文切换的异常控制流来实现多任务。

  • 上下文切换机制:
    (1)保存当前进程的上下文;
    (2)恢复某个先前被抢占的进程被保存的上下文;
    (3)将控制传递给这个新恢复的进程

  • 调度:内核中的调度器实现调度。

  • 当内核代表用户执行上下文切换时,可能会发生上下文切换。

  • 如果系统调用发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,如read系统调用,或者sleep会显示地请求让调用进程休眠。一般,即使系统调用没有阻塞,内核亦可以决定上下文切换,而不是将控制返回给调用进程。

系统调用错误处理

  • 在Linux中,可以使用 man syscalls 查看全部系统调用的列表。
  • 系统级函数遇到错误时,通常返回-1,并设置全局变量 errno 。

进程控制

1.获取进程ID

    #include <sys/types.h>
    #include <unistd.h>
	pid_t getpid(void);	/*返回调用进程的PID*/
	pid_t getppid(void);	/*返回它的父进程的PID(创建调用进程的进程)*/

2.创建和终止进程

  • 进程的三种状态——运行、停止和终止。
  • 运行:要么在CPU上执行,要么在等待被执行,且最终被内核调度。
  • 停止:进程的执行被挂起,且不会被调度。收到 SIGSTOP 、 SIGTSTP 、 SIDTTIN 、 SIGTTOU 信号,进程停止,收到 SIGCONT 信号,进程再次开始运行。
  • 终止:永远停止。原因可能是:收到终止进程的信号,从主程序返回,调用 exit 函数。

创建进程

  • 父进程通过调用fork创建一个新的运行子进程:父进程与子进程有相同(但是独立的)地址空间,有相同的文件藐视符集合。

  • fork函数定义如下:

    #include <sys/types.h>
    #include <unistd.h>    
    pid_t fork(void);

  • fork 函数调用一次,返回两次;父子进程是并发运行的,不能假设它们的执行顺序;两个进程的初始地址空间相同,但是是相互独立的;它们还共享打开的文件。因为有相同的程序代码,所以如果调用 fork 三次,就会有八个进程。

3.回收子进程

  • 当一个进程终止时,内核并不立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。

  • 僵死进程:一个终止了但是还未被回收的进程称为僵死进程。父进程回收终止的子进程时,内核将子进程退出状态传给父进程,然后抛弃该进程。如果回收前父进程已经终止,那么僵死进程由 init 进程回收。

  • 一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。

  • waitpid函数的定义如下:

    #include <sys/types.h>
    #include <sys/wait.h>
    
    pid_t waitpid(pid_t pid, int *status, int options);

(1)判断等待集合的成员

  • 等待集合的成员由参数pid来确定:
       如果pid>0:等待集合是一个单独子进程,进程ID等于pid
       如果pid=-1:等待集合是由父进程所有的子进程组成

(2)修改默认行为

  • 将options设置为常量WNOHANG和WUNTRACED的各种组合,修改默认行为:

(3)检查已回收子进程的退出状态——status

(4)让进程休眠

  • sleep函数使一个进程挂起一段指定的时间。
    #include <unistd.h>    
    unsigned int sleep(unsigned int secs);

  • pause函数让调用函数休眠
    #include <unistd.h>    
    int pause(void);

(5)加载并运行程序

  • filename:可执行目标文件

  • argv:参数列表

  • envp:环境列表

信号

  • 在操作系统和应用程序之间:进程之间传送信号
  • 一种更高层次的软件形式的异常,称为unix信号,它允许进程中断其他进程。

1.信号术语

(1)发送信号

  • 发送信号:/bin/kill , kill函数,键盘,alarm函数

  • 用kill函数发送信号:发送SIGKILL信号

  • 用alarm函数发送信号:发送SOGALARM信号

(2)接收信号

  • 进程可以通过使用signal函数来修改和信号相关的默认行为。唯一的例外是SIGSTOP和SIGKILL,它们的默认行为不能被修改。

2.发送信号

(1)进程组

  • 每个进程都只属于一个进程组。
  • 进程组是由一个正整数进程组ID来标识的。
  • getpgrp函数返回当前进程组id
  • setpgid函数修改自己或其他进程组

(2)用/bin/kill程序发送信号

  • /bin/kill程序可以向另外的进程发送任意的信号,比如 /bin/kill -9 15213 即为:发送信号9给进程15213

(3)从键盘发送信号

  • 在任何时刻,至多只有一个前台作业和0个或多个后台作业。外壳为每个作业创建一个独立的进程组,一个作业对应一个进程组。

(4)用kill函数发送信号

  • 发送SIGKILL信号

(5)用alarm函数发送信号

  • 发送SOGALARM信号

3.接收信号

  • 当内核从一个异常处理程序返回,准备将控制传递给进程P时,他会检查进程P的未被阻塞的处理信号的集合。如果这个集合为空,那么内核将控制传递到P的逻辑控制流中的下一条指令;如果集合是非空的,那么内核选择集合中的某个信号K(通常是最小的K0,并且强制P接收信号K。收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回P的逻辑控制流中的下一条指令。

每个信号类型都有一个预定的默认行为:

  • 进程终止

  • 进程终止并转储存储器

  • 进程停止直到被SIGCONT型号重启

  • 进程忽略该信号

4.信号处理问题

当一个程序要捕获多个信号时,一些细微的问题就产生了。

  • 待处理信号被阻塞。Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。

  • 待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为K的信号传送到一个目的进程,而由于目的进程当前正在执行信号K的处理程序,所以信号K时阻塞的,那么第二和信号就简单地被简单的丢弃,他不会排队等待。

  • 系统调用可以被中断。像read、wait和accept这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误的条件,并将errno设置为EINTR。

5.可移植的信号处理

Signal包装函数设置的信号处理程序的信号处理语义:

  • 只有这个处理程序当前正在处理的那种类型的信号被阻塞

  • 和所有信号实现一样,信号不会排队等候

  • 只要有可能,被中断的系统调用会自动重启。

  • 一旦设置了信号处理程序,它就会一直保持,知道signal带着handler参数为SIG_IGN或者SIG_DFL被调用。

6.显式地阻塞和取消阻塞信号

7.同步流以避免讨厌的并发错误

  • 以某种方式同步并交流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。

非本地跳转

  • c语言中,用户级的异常控制流形式,通过setjmp和longjmp函数提供。

  • setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0.

  • longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。

       setjmp函数只被调用一次,但返回多次;
       longjmp函数被调用一次,但从不返回。

操作进程的工具

Linux系统提供了大量的监控和操作进程的有用工具:

  • STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。对于好奇的的工具。用-StatiC编译你的程序,能传到一个更干净的、不带学生而言,这是一个令人着迷有大量与共享库相关的输出的轨迹。

  • PS:列出当前系统中的进程(包括僵死进程)

  • TOP:打印出关于当前进程资源使用的信息。

  • PMAP:显示进程的存储器映射。proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数数据结构的内容,用户程序可 cat 2 / proc / load avg” , 观察在Linux系统上的平均负载。

代码的调试和分析

exec1

  • 代码如下:
#include <stdio.h>
#include <unistd.h>

int main()
{
    char    *arglist[3];

    arglist[0] = "ls";
    arglist[1] = "-l";
    arglist[2] = 0 ;//NULL
    printf("* * * About to exec ls -l
");
    execvp( "ls" , arglist );
    printf("* * * ls is done. bye");

    return 0;
}

  • 可以看到这个代码中用了execvp函数。

  • 表头文件:#include<unistd.h>

  • 定义函数:int execvp(const char file ,char const argv []);

  • execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。

  • 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。

  • 运行结果如下:

  • 从运行结果可以看到,exevp函数调用成功没有返回,所以没有打印出“* * * ls is done. bye”这句话。

exec2

  • 它与exec1的区别就在于exevp函数的第一个参数,exec1传的是ls,exec2直接用的arglist[0],不过由定义可得这两个等价,所以运行结果是相同的。

exec3

  • 这个代码里使用了execlp函数,用法如下:

  • 头文件:#include<unistd.h>

  • 定义函数:int execlp(const char * file,const char * arg,....);

  • 函数说明:execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。如果用常数0来表示一个空指针,则必须将它强制转换为一个字符指针,否则将它解释为整形参数,如果一个整形数的长度与char * 的长度不同,那么exec函数的实际参数就将出错。如果函数调用成功,进程自己的执行代码就会变成加载程序的代码,execlp()后边的代码也就不会执行了.

  • 返回值:如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno 中。也就是说,这个代码指定了环境变量,然后依然执行了ls -l指令,成功后没有返回,所以最后一句话不会输出。运行结果同exec1.

forkdemo1

  • 代码分析:这个代码先是打印进程pid,然后调用fork函数生成子进程,休眠一秒后再次打印进程id,这时父进程打印子进程pid,子进程返回0.
  • 运行结果如下:

forkdemo2

  • 代码分析:这个代码调用两次fork,一共产生四个子进程,所以会打印四个aftre输出。

  • 结果如图:

forkdemo3

  • 代码分析:fork产生子进程,父进程返回子进程pid,不为0,所以输出父进程的那句话,子进程返回0,所以会输出子进程那句话。
  • 结果如下:

forkdemo4

  • 代码分析:先打印进程pid,然后fork创建子进程,父进程返回子进程pid,所以输出parent一句,休眠十秒;子进程返回0,所以输出child与之后一句。

  • 运行结果如下:

forkgdb

  • 代码分析:这个的主要区别是在,父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰。
  • 运行结果:

psh1

  • 代码分析:这个代码就相当于你输入要执行的指令,回车表示输入结束,然后输入的每个参数对应到函数中,再调用对应的指令。
  • 运行结果:

psh2

  • 比起psh1来,多了循环判断,不退出的话就会一直要你输入指令。
  • 运行结果如下:

testbuf1:

  • 效果是先输出hello,然后换行。之后不退出。
  • 运行结果:

testbuf2

  • 效果同上。
  • 由此可知:fflush(stdout)的效果和换行符 是一样的。

testbuf3

  • 代码分析:将内容格式化输出到标准错误、输出流中。
  • 结果如图:

testpid

  • 代码分析:输出当前进程pid和当前进程的父进程的pid。
  • 运行结果:

testsystem

  • 代码分析:system()——执行shell命令,也就是向dos发送一条指令。这里是后面可以跟两个参数,然后向dos发送这两个命令,分别执行。如下图,输入ls和dir两个指令后,可以看到分别执行了。
  • 运行结果:

waitdemo1

  • 代码分析:如果有子进程,则终止子进程,成功返回子进程pid。
  • 运行结果:

waitdemo2

  • 代码分析:这个比起waitdemo1来就是多了一个子进程的状态区分,把状态拆分成三块,exit,sig和core。
  • 运行结果如下:

本周代码托管链接

https://git.oschina.net/20145227/IS-Design-20145227/tree/master/ch11

本周代码总数

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 0 2/2 20/20
第二周 100/100 1/3 20/40
第三周 200/300 1/4 22/62
第五周 200/500 1/5 22/84
第六周 274/774 1/6 22/106
第七周 127/901 2/8 22/128
第八周 50/951 2/10 22/150
第九周 418/1369 2/12 22/172
第十周 485/1854 2/14 22/194
第十一周 628/2482 3/17 32/226

参考资料

原文地址:https://www.cnblogs.com/m3182218/p/6103224.html