20145218 《信息安全系统设计基础》第十一周学习总结
20145218 《信息安全系统设计基础》第十一周学习总结
教材学习内容总结
8.1 异常
- 从处理器运行开始到结束,程序计数器假设一个序列的值a0a1......an-1,这个控制转义序列叫做处理器的控制流。
- 异常,就是控制流中的突变,用来响应处理器状态中的某些变化。
- 状态的变化称为事件,在任何情况下,当处理器检测到有事件发生时,会通过一张叫做异常表的跳转表,进行一个间接过程调用到专门处理程序——异常处理程序。当异常处理程序完成之后,根据引起引起异常的事件类型,会发生以下三种情况之一:
- 处理程序将控制返回给当前指令,即事件发生之时正在执行的指令。
- 处理程序将控制返回给如果没有异常将会执行的下一条指令。
- 处理程序终止被中断的程序。
异常处理
- 系统中可能的每种异常都被分配了唯一一个非负整数的异常号,异常表中的条目k中包含异常k的处理程序地址。异常表的起始地址存放在一个叫做异常表基址寄存器的特殊寄存器中。
- 异常类和过程调用的不同之处:
- 返回地址是当前地址或者下一条指令
- 处理器也会把额外的处理器状态压回栈中,在处理程序返回时,重新开始被中断的程序会需要这些状态。
- 如果控制从一个用户程序转移到内核,那么所有项目都会被压到内核栈中而不是用户栈。
- 异常处理程序运行在内核模式下,意味着他们对所有的系统资源拥有完全的访问权限。
异常的类别
1.中断
- 中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。因此是异步的。硬件中断的异常处理程序通常称为中断处理程序。
- 其余异常类型都是同步发生的,是执行当前指令的结果。这一类指令称为故障指令。
2.陷阱
- 陷阱是有意的异常,最重要的用途是在用户程序和内核之间提供一个向过程一样的接口,叫做系统调用。
- 为了允许内核服务的受控访问,使用“syscall n”指令,跳转到一个异常处理程序的陷阱,处理程序对参数解码并调用适当的内核程序。
3.故障
- 故障由错误情况引起,可能能够被故障处理程序修正。故障发生时,处理器将控制转移给故障处理程序,若能修正,则将控制返回到引起故障的指令,重新执行;若不能修正,处理程序返回abort例程,终止引起故障的应用程序。
4.终止
- 终止是不可恢复的致命错误造成的结果,通常是硬件错误。终止处理程序将控制直接返回给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文件系统,允许用户模式进程访问内核数据结构的内容。
- 上下文切换
- 上下文:内核重新启动一个被抢占的进程所需的状态。包括描绘地址空间的页表、包含有关当前进程信息的进程表,以及半酣进程已打开文件的信息的文件表。
- 调度:进程执行过程中,内核决定抢占当前进程并开始一个先前被抢占的进程,由内核中的调度器完成。
- 内核为每个进程维持一个上下文。并通过一种称为上下文切换的较高层形式的异常控制流来实现多任务上下文切换:
1.保存当前进程的上下文;
2.恢复某个先前被抢占的进程被保存的上下文;
3.将控制传递给新恢复的进程。
8.3系统调用错误处理
- Unix系统级函数遇到错误时,会典型地返回-1,并设置全局变量errno来表示出错内容。
- 通过使用错误处理包装函数,可以进一步简化代码。
8.4进程控制
获取进程ID
- 每个进程有一个唯一的非零正数进程ID(PID)。
pid_t getpid(void); /*返回调用进程的PID*/
pid_t getppid(void); /*返回它的父进程的PID*/
创建和终止进程
进程总是处于以下三种状态之一:
-
运行。在CPU上执行,或者等待被执行且最终会被内核调度。
-
停止。进程的执行被挂起,且不会被调度。(与信号有关)
-
终止。进程永远停止。进程终止的原因:1)收到一个信号,默认行为是终止程序2)从主程序返回3)
调用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,等待集合是由父进程所有的子进程组成的。
-
修改默认行为:可以通过将options设置为常量,修改默认行为。
-
检查已回收子进程的退出状态:如果status参数是非空的,则status参数会放上关于导致返回的子进程的状态信息。
- 错误条件:如果调用进程没有子进程,那么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
- 父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰。
psh
- psh1
- 相当于输入要执行的指令,回车表示输入结束,然后输入的每个参数对应到函数中,再调用对应的指令。
- psh2
- 多了循环判断,按Ctrl+c退出。
testbuf
- testbuf1
- 输出hello,换行
- testbuf2
- 效果与testbuf1相同。
- testbuf3
- 将内容格式化输出到标准错误、输出流中。
testpid
- 输出当前进程pid和当前进程的父进程的pid。
testsystem
wait
1.waitdemo1
- 如果有子进程,则终止子进程,成功返回子进程pid。
2.waitdemo2
- 比waitdemo1多了一个子进程的状态区分,把状态拆分成三块,exit,sig和core。
env
1.environ.c
- getenv函数是获得环境变量值的函数,参数是环境变量名name,例如”HOME”或者”PATH”。如果环境变量存在,那么getenv函数会返回环境变量值,即value的首地址;如果环境变量不存在,那么getenv函数返回NULL。
- setenv功能说明:查询或显示环境变量。表头文件 #include<stdlib.h>
- environvar.c
- 每个程序都有一个环境表,它是一个字符指针数组,其中每个指针包含一个以NULL结尾的C字符串的地址。全局变量environ则包含了该指针数组的地址。
本周代码托管截图
代码托管链接:https://git.oschina.net/senlinmilelu/IS20145218
其他(感悟、思考等,可选)
本章的内容较多,但是都是针对之前章节学习中有所涉及而不能解答的地方展开,比如:处理器怎样执行程序、怎样处理错误、怎样控制进程甚至怎样灵活地跳转等等;同时,也和其他课程有序衔接了起来(比如和操作系统中的进程控制部分就有相似之处)。所以读起来不时会有豁然开朗的感觉,知识永无止境,在我们有条件学习的时候更应该好好把握。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 100/100 | 1/2 | 20/20 | |
第二周 | 92/192 | 1/3 | 18/38 | |
第三周 | 195/387 | 1/4 | 22/60 | |
第四周 | 180/567 | 0/4 | 30/90 | |
第五周 | 120/687 | 1/5 | 20/20 | |
第六周 | 130/817 | 1/6 | 18/38 | |
第七周 | 550/1367 | 1/7 | 22/60 | |
第八周 | 0/1367 | 2/9 | 30/90 | |
第九周 | 60/1427 | 2/11 | 20/20 | |
第十周 | 514/1941 | 2/13 | 18/38 | |
第十一周 | 1856/3797 | 2/15 | 22/60 |