20145312 《信息安全系统设计基础》第11周学习总结
20145312 《信息安全系统设计基础》第11周学习总结
教材学习内容总结
第8章 异常控制流
- 控制转移序列称为控制流。
- 从一条指令到下一条指令称为转移控制。
- 异常控制流:现代操作系统通过使控制流发生突变来对系统状态做出反应,这些突变称为异常控制流。
- 异常控制流ECF:即这些突变。
-ECF是操作系统用来实现I/O、进程和虚拟存器的基本机制
-应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务
-ECF是计算机系统中实现并发的基本机制
-软件异常机制——C++和Java有try,catch,和throw,C中非本地跳转是setjmp和longjmp
8.1 异常
- 异常是异常控制流的一种形式,一部分由由硬件实现,一部分由操作系统实现。由于系统的不同而有所不同。
- 异常就是控制流的突变。
异常处理
- 系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。
- 处理器:被零除、缺页、存储器访问违例、断点以及算术溢出
- 操作系统:系统调用和来自外部I/O设备的信号
异常的类别
- 分为四种:中断、陷阱、故障和终止。
(1)中断
-异步发生
-来自处理器外部的I/O设备的信号的结果
-将控制返回给下一条指令
(2)陷阱和系统调用
-陷阱是有意的异常
-是执行一条指令的结果
-最重要的用途:在用户和内核间提供一个像过程一样的接口,叫系统调用
(3)故障
-由错误状况引起,可能能够被故障处理程序修正
-故障发生时,处理器将控制转移给故障处理程序,如果能够修正,返回引起故障的指令,重新执行指令,否则返回abort例程,终止
(4)终止
-是不可恢复的致命错误造成的结果
-通常是一些硬件错误
-终止示例:将控制返回abort例程,如图:
Linux系统中的异常
(1)Linux故障和终止
-除法错误:异常号:0,通常报告为“浮点异常”
-一般保护故障:异常号:13,Linux不会尝试回复这类故障,通常报告为“段故障”
-缺页:异常号:14,是会重新执行产生故障的指令的一个异常事例
-机器检查:异常号:18,从不返回控制给应用程序
(2)Linux系统调用
-每个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量
-将系统调用和与他们相关的包装函数称为系统级函数
8.2 进程
- 在操作系统层:逻辑控制流,私有地址空间,多任务,并发,并行,上下文,上下文切换,调度。
- 进程就是一个执行中的程序实例。系统中的每个程序都是运行在某个进程的上下文中的。
- 进程提供给应用程序的关键抽象:
a)一个独立的逻辑控制流 ;b)一个私有的地址空间
逻辑控制流
- 程序计数器(PC)的值的序列叫做逻辑控制流,简称逻辑流
- 每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程
并发流
- 一个逻辑流的执行在时间上与另一个流重叠,称为并发流
并发:多个流并发的执行的一般现象
多任务:一个进程和其他进程轮流运行
时间片:一个进程执行它的控制流的一部分的每一段时间
私有地址空间
- 一个进程为每个程序提供它自己的私有地址空间。
- 运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常。
- 进程地址空间,如图:
用户模式和内核模式
- 当设置了模式位时,进程就运行在内核模式中
- 没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令。
- 进程从用户模式变位内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。
- /proc文件系统允许用户模式进程访问内核数据结构的内容
上下文切换
- 上下文切换:操作系统内核使用叫上下文切换的异常控制流来实现多任务。
- 上下文切换机制:
保存当前进程的上下文;
恢复某个先前被抢占的进程被保存的上下文;
将控制传递给这个新恢复的进程
- 调度:内核中的调度器实现调度。
- 当内核代表用户执行上下文切换时,可能会发生上下文切换。
- 如果系统调用发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,如read系统调用,或者sleep会显示地请求让调用进程休眠。一般,即使系统调用没有阻塞,内核亦可以决定上下文切换,而不是将控制返回给调用进程。
8.3 系统调用错误处理
- 定义错误报告函数,简化代码
- 错误处理包装函数
8.4 进程控制
获取进程ID
- 每个进程都有一个唯一的正数的进程ID。
- getpid函数返回调用进程的PID,getppid函数返回它的父进程的PID。上面两个函数返回一个同类型为pid_t的整数值,在linux系统中,它在types.h中被定义为int。
创建和终止进程
(1)进程总处于三种状态
运行:进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
停止:程序的执行被挂起,,且不会被调度。
终止:进程用永远停止了。
(2)终止原因:
收到一个信号,默认行为是终止进程
从主进程返回
调用exit函数
(3)父进程通过调用fork函数创建一个新的运行的子进程。
(4)子进程和父进程的异同:
异:有不同的PID
同:用户级虚拟地址空间,包括:文本、数据和bss段、堆以及用户栈。任何打开文件描述符,子进程可以读写父进程中打开的任何文件。
(5)fork函数: 因为父进程的PID总是非零的,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
- fork函数的特点:
调用一次,返回两次
并发执行
相同的但是独立的地址空间
共享文件
回收子进程
- 当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个终止了但还未被回收的进程称为僵死进程。
- 一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
//返回:若成功,返回子进程的PID;若WNOHANG,返回0;若其他错误,返回-1。
- 默认地,当option=0时,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。
(1)判定等待集合的成员
- 有参数pid来确定的:
pid>0:等待集合是一个单独的子进程,进程ID等于pid。
pid=-1:等待结合就是由父进程所有的子进程组成的。
(2)修改默认行为
- 通过options设置:
WNOHANG:默认行为是挂起调用进程。
WUNTRACED:默认行为是只返回已终止的子进程。
WNOHANG|WUNTRACED:立即返回,如果等待集合中没有任何子进程被停止或者已终止,那么返回值为0,或者返回值等于那个被停止或者已经终止的子进程的PID。
(3)检查已回收子进程的退出状态
- wait.h头文件定义了解释status参数的几个宏:
WIFEXITED:如果子进程通过调用exit或者一个返回正常终止,就返回真;
WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回真时,才会定义这个状态。
(4)错误条件
若调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD;
若waitpid函数被一个信号中断,那么返回-1,并设置errno为EINTR
wait函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
//返回:若成功,返回子进程的PID;若错误,返回-1。
调用wait(&status)等价于调用waitpid(-1.&status,0)
让进程休眠
(1)sleep函数:将进程挂起一段指定的时间
#include <unistd.h>
unsigned int sleep(unsigned int secs);
//返回:还要休眠的秒数
如果请求的时间量已经到了,返回0,否则返回还剩下的要休眠的秒数。
(2)pause函数:让调用函数休眠,直到该进程收到一个信号。
#include <unistd.h>
int pause(void);
//返回:总是-1
加载并运行程序
(1)execve函数:在当前进程的上下文中加载并运行一个新程序。
#include <unistd.h>
int execve(const char *filename,const char *argv[],const char *envp[]);
//返回:若成功,则不返回,若错误,返回-1
filename:可执行目标文件
argv:带参数列表
envp:环境变量列表
- 特点:execve调用一次从不返回
(2)getenv函数:在环境数组中搜素字符串“name =VALUE”,若找到了,就返回一个指向value的指针,否则它就返回NULL。
#include <stdlib.h>
char *getenv(const char *name);
//返回:存在,返回指向name的指针,若无匹配的,为NULL
利用fork和execve运行程序
- 外壳是一个交互型的应用级程序,它代表用户运行其他程序。
- 外壳执行一系统的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解释命令行,并代表用户运行程序。
- eval函数:对外壳命令行求值
- parseline函数:解析外壳的一个输入
8.5 信号
- Unix信号:更高层的软件形式的异常,允许进程中断其他进程
- P505:30种不同类型的信号表
信号术语
- 发送信号:1)内核检测到一个系统事件 2)一个进程调用了kill函数
- 接收信号:当目的进程被内核强迫以某种方式对发送的信号作出反应时,目的进程就接收了信号
- 待处理信号:一个只发出而没有被接收的信号
发送信号
(1)进程组
- 每个进程都只属于一个进程组
- 一个进程可以通过使用setpigd函数来改变自己或者其他进程的进程组
(2)用/bin/kill程序发送信号 - 一个为负的PID会导致信号被发送到进程组PID中的每个进程中
(3)从键盘发送信号
(4)用kill函数发送信号
(5)用alarm函数发送信号
接收信号
- 如果集合是非空的,那么内核选择集合中的某个信号k,并且强制p接收信号k
- signal函数可以改变和信号signum相关联的行为
信号处理问题
- 捕获多个信号时的问题:
待处理信号被阻塞
待处理信号不会排队等待
系统调用可以被中断
可移植的信号处理
- 不同系统之间,信号处理语义有差异
- sigaction函数明确地指定它们想要的信号处理语义
显式地阻塞和取消阻塞
- 应用程序可以使用sigprocmask函数显式地阻塞和取消阻塞选择的信号
- sigprocmask函数改变当前已阻塞信号的集合
代码调试中的问题和解决过程
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\n");
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中。
运行结果如下:
exec2
- 它与exec1的区别就在于exevp函数的第一个参数,exec1传的是ls,exec2直接用的arglist[0],不过由定义可得这两个等价,所以运行结果是相同的。
运行结果如下:
exec3
- 此程序指定了环境变量,然后依然执行了ls -l指令,成功后没有返回,所以最后一句话不会输出,结果依然相同。
- 相关代码
#include <stdio.h>
#include <unistd.h>
int main()
{
char *arglist[3];
char*myenv[3];
myenv[0] = "PATH=:/bin:";
myenv[1] = NULL;
arglist[0] = "ls";
arglist[1] = "-l";
arglist[2] = 0 ;
printf("* * * About to exec ls -l\n");
execlp("ls", "ls", "-l", NULL);
printf("* * * ls is done. bye\n");
}
- 函数说明:execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。如果用常数0来表示一个空指针,则必须将它强制转换为一个字符指针,否则将它解释为整形参数,如果一个整形数的长度与char * 的长度不同,那么exec函数的实际参数就将出错。如果函数调用成功,进程自己的执行代码就会变成加载程序的代码,execlp()后边的代码也就不会执行了.
- 返回值:如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno 中。也就是说,这个代码指定了环境变量,然后依然执行了ls -l指令,成功后没有返回,所以最后一句话不会输出。运行结果同exec1.
运行结果如下:
forkdemo1
- 使用fork创建子进程的父进程,调用fork一次,打印两个输出行
- 相关代码
#include "csapp.h"
int main()
{
pid_t pid;
int x = 1;
pid = Fork(); //line:ecf:forkreturn//父进程和子进程中x的值都为1
if (pid == 0) { /* Child */
printf("child : x=%d\n", ++x); //子进程增加并输出它的x的拷贝
exit(0);
}
/* Parent */
printf("parent: x=%d\n", --x); //父进程减少和输出它的x的拷贝
exit(0);
}
运行结果如下:
forkdemo3
- 父进程返回调用进程子进程的PID,不为0,输出 i am the parent……
- 相关代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int fork_rv;
printf("Before: my pid is %d\n", getpid());
fork_rv = fork();
if ( fork_rv == -1 )
perror("fork");
else if ( fork_rv == 0 ){
printf("I am the child. my pid=%d\n", getpid());
exit(0);
}
else{
printf("I am the parent. my child is %d\n", fork_rv);
exit(0);
}
return 0;
}
运行结果如下:
forkdemo4
- getpid返回调用进程的PID
- getppid返回它的父进程的PID
- 相关代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int fork_rv;
printf("Before: my pid is %d\n", getpid());
fork_rv = fork(); //fork产生子进程
if ( fork_rv == -1 )
perror("fork");
else if ( fork_rv == 0 ){
printf("I am the child. my pid=%d\n", getpid());
printf("parent pid= %d, my pid=%d\n", getppid(), getpid());
exit(0);
}
else{
printf("I am the parent. my child is %d\n", fork_rv);
sleep(10);
exit(0);
}
return 0;
}
运行结果如下:
forkgdb
- 父进程和子进程这两个线程是并发运行的独立进程,一个在休眠时另一个在执行,两个相互独立不干扰。
- 相关代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int gi=0;
int main()
{
int li=0;
static int si=0;
int i=0;
pid_t pid = fork();
if(pid == -1){
exit(-1);
}
else if(pid == 0){
for(i=0; i<5; i++){
printf("child li:%d\n", li++);
sleep(1);
printf("child gi:%d\n", gi++);
printf("child si:%d\n", si++);
}
exit(0);
}
else{
for(i=0; i<5; i++){
printf("parent li:%d\n", li++);
printf("parent gi:%d\n", gi++);
sleep(1);
printf("parent si:%d\n", si++);
}
exit(0);
}
return 0;
}
运行结果如下:
psh1
- 代码分析:这个代码就相当于你输入要执行的指令,回车表示输入结束,然后输入的每个参数对应到函数中,再调用对应的指令。
运行结果如下:
psh2
- 比起psh1来,多了循环判断,不退出的话就会一直要你输入指令。
运行结果如下:
testbuf1:
- 效果是先输出hello,然后换行。之后不退出。
运行结果如下:
testbuf2
- 效果同上
- 由此可知:fflush(stdout)的效果和换行符\n是一样的。
testbuf3
- 代码分析:将内容格式化输出到标准错误、输出流中。
运行结果如下:
testpid
- 代码分析:输出当前进程pid和当前进程的父进程的pid。
运行结果如下:
testsystem
- 代码分析:system()——执行shell命令,也就是向dos发送一条指令。这里是后面可以跟两个参数,然后向dos发送这两个命令,分别执行。如下图,输入ls和dir两个指令后,可以看到分别执行了。
运行结果如下:
waitdemo1
- 代码分析:如果有子进程,则终止子进程,成功返回子进程pid。
运行结果如下:
waitdemo2
- 代码分析:这个比起waitdemo1来就是多了一个子进程的状态区分,把状态拆分成三块,exit,sig和core。
运行结果如下:
运行结果如下:
本周代码托管截图
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 200/200 | 2/2 | 20/20 | |
第二周 | 200/400 | 2/4 | 18/38 | |
第三周 | 100/500 | 1/5 | 10/48 | |
第四周 | 250/750 | 1/6 | 10/58 | |
第五周 | 100/850 | 1/7 | 10/68 | |
第六周 | 100/950 | 1/8 | 12/80 | |
第七周 | 200/1150 | 1/9 | 12/92 | |
第八周 | 124/1274 | 2/11 | 10/102 | |
第九周 | 205/1479 | 2/13 | 5/107 | |
第十周 | 646/2125 | 2/15 | 9/116 | |
第十一周 | 421/2546 | 2/17 | 12/128 |