2017-2018-1 20155232 《信息安全系统设计基础》第十四周学习总结
2017-2018-1 20155232 《信息安全系统设计基础》第十四周学习总结
1.第八章教材内容总结
异常
进程
系统调用错误处理
进程控制
信号
非本地跳转
操作进程的工具
2.课本练习题
3.家庭作业
4.ch08错题整理以及分析
5.总结
找出全书你认为学得最差的一章,深入重新学习一下,要求(期末占5分):
- 总结新的收获
- 给你的结对学习搭档讲解或请教,并获取反馈
- 参考上面的学习总结模板,把学习过程通过博客(随笔)发表。
我认为我第八章异常控制流这里学习的很不好,很多内容不是很理解原理。异常控制流-ECF理解ECF将帮助我们理解重要的系统概念。更能帮助理解并发。
第八章教材学习内容总结
异常
- 异常是异常控制流的一种形式,由硬件和操作系统实现。简单来说,就是控制流中的突变。
- 出现异常的处理方式:
1.处理器检测到有异常发生
2.通过异常表,进行间接过程调用,到达异常处理程序
3.完成处理后:①返回给当前指令②返回给下一条指令③终止
1、异常处理
- 异常号:系统为每种类型的异常分配的唯一的非负整数。
- 异常表:系统启动时操作系统就会初始化一张条转变,使得条目k包含异常k的处理程序的地址。
- 异常号是到异常表中的索引,异常表的起始地址放在异常表基址寄存器。
- 异常类似于过程调用,区别在:
1.处理器压入栈的返回地址,是当前指令地址或者下一条指令地址。
2.处理器也把一些额外的处理器状态压到栈里
3.如果控制一个用户程序到内核,所有项目都压到内核栈里。
4.异常处理程序运行在内核模式下,对所有的系统资源都有完全的访问权限。
2、异常的类别
-
故障指令:执行当前指令导致异常(陷阱、故障、终止)
-
中断处理程序:硬件中断的异常处理程序(中断)
-
异常的类别如下图。异步异常时有处理器外部的I/O设备中的事件产生的,同步异常时执行一条指令的直接产物
-
陷阱是有意的异常,是执行一条指令的结果,最重要的用途——系统调用
-
故障是由错误状况引起,可能能够被故障处理程序修正。结果要么重新执行指令(就是返回当前指令地址),要么终止。典型示例:缺页异常
3、Linux/IA32系统中的异常
-
系统中的异常列表如下
-
每一个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量
-
所有的到Linux系统调用的参数都是通过寄存器传递的。惯例如下:
%eax:包含系统调用号
%ebx,%ecx,%edx,%esi,%edi,%ebp:包含最多六个任意参数
%esp:栈指针,不能使用
进程
- 进程的经典定义:一个执行中的程序的实例。
- 系统中的每个程序都是运行在某个进程的上下文中的。
- 上下文:由程序正确运行所需的状态组成的。
- 进程提供给应用程序的关键抽象:
一个独立的逻辑控制流:独占的使用处理器
一个私有的地址空间:独占的使用存储器系统
1、逻辑控制流
- 一系列的程序计数器PC的值,分别唯一的对应于包含子啊程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令,这个PC值的序列就叫做逻辑控制流。
- 进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。但是进程可以向每个程序提供一种假象,好像它在独占的使用处理器。
- 逻辑流示例:异常处理程序、进程、信号处理程序、线程、Java进程
2、并发流
- 一个逻辑流的执行在时间上与另一个流重叠。(与是否在同一处理器无关)
- 两个流并发的运行在不同的处理机核或者计算机上。
- 并行流并行的运行,并行的执行。
3、私有地址空间
- 进程为程序提供的假象,好像它独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读写的。
4、用户模式和内核模式
-
用户模式和内核模式的区别就在于用户的权限上,权限指的是对系统资源使用的权限。
-
具体的区别是有无模式位,有的话就是内核模式,可以执行指令集中的所有指令,访问系统中任何存储器位置;没有就是用户模式。
-
进程从用户模式变为内核模式的唯一方法是通过异常——中断,故障,或者陷入系统调用。
-
Linux的聪明机制——/proc文件系统,将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。
5、上下文切换 -
操作系统内核使用上下文切换这种较高层形式的异常控制流来实现多任务。上下文切换机制建立在较底层异常机制之上。
-
上下文:内核重新启动一个被抢占的进程所需的状态。由一些对象的值组成:
通用目的寄存器
浮点寄存器
程序计数器
用户栈
状态寄存器
内核栈
内核数据结构:页表、进程表、文件表
- 上下文切换机制:
1.保存当前进程的上下文
2.恢复某个先前被抢占的进程被保存的上下文
3.将控制传递给这个新恢复的进程。
- 可能发生上下文切换的原因:
内核代表用户执行系统调用时
中断
系统调用错误处理
- 系统会使用错误处理包装函数,系统级函数是小写,他们的包装函数名大写,包装函数调用基本函数,有任何问题就终止,如果没有问题和基本函数是一样的。
进程控制
1、获取进程ID
- 每个进程都有一个唯一的正数进程ID(PID)。
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回调用进程的PID
pid_t getppid(void);返回父进程的PID(创建调用进程的进程)
2、创建和终止进程
- 进程总是处于下面三种状态之一:运行;停止:被挂起且不会被调度;终止:永远停止
- 终止的原因:
1.收到信号,默认行为为终止进程
2.从主程序返回
3.调用exit函数
- 父进程通过调用fork函数来创建一个新的运行子进程。fork函数定义如下:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
- fork函数只被调用一次,但是会返回两次:父进程返回子进程的PID,子进程返回0.如果失败返回-1
- 调用fork函数n次,产生2的n次方个进程。
- 终止进程用exit函数:
#include <stdlib.h>
void exit(int status);
- exit函数以status退出状态来终止进程
3、回收子进程
- 进程终止后还要被父进程回收,否则处于僵死状态。
- 如果父进程没有来得及回收,内核会安排init进程来回收他们。init进程的PID为1.
- 一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。waitpid函数的定义如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
- 成功返回子进程PID,如果WNOHANG,返回0,其他错误返回-1.
- 判断等待集合的成员——pid:
pid>0:等待集合是一个单独子进程,进程ID等于pid
pid=-1:等待集合是由父进程所有的子进程组成
其他
- 修改默认行为——options:
- 检查已回收子进程的退出状态——status,在wait.h头文件中定义了解释status参数的几个宏:
WIFEXITED:如果子进程通过调用exit或一个返回正常终止,就返回真
WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态
WIFSIGNALED:如果子进程是因为一个未被捕获的信号终止的,那么返回真
WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSIGNALED返回为真时才定义这个状态
WIFSTOPPED:如果引起返回的子进程当前是被停止的,那么返回真
WSTOPSIG:返回引起子进程停止的信号的数量。只有在WIFSTOPPED返回为真时才定义这个状态
- 错误条件:如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。
如果waitpid被一个信号中断,那么他返回-1,并且设置errno为EINTR。 - wait函数是waitpid函数的简单版本,wait(&status)等价于waitpid(-1,&status,0).成功返回子进程pid,出错返回-1:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
4.让进程休眠
- sleep函数使一个进程挂起一段指定的时间。定义如下:
#include <unistd.h>
signed int sleep(unsigned int secs);
返回值是剩下还要休眠的秒数,如果到了返回0.
- pause函数让调用函数休眠,直到该进程收到一个信号:
#include <unistd.h>
int pause(void);
5、加载并运行程序——execve函数
- execve函数调用一次,从不返回:
#include <unistd.h>
int execve(const char *filename, const char *argv[], const char *envp[]);
成功不返回,失败返回-1.
- getnev函数在环境数组中搜寻字符串"name=value",如果找到了就返回一个指向value的指针,否则返回null:
#include <stdlib.h>
char *getenv(const char *name);
若存在则为指向name的指针,无匹配是null
- setenv和unsetenv函数:如果环境数组包含"name=oldvalue"的字符串,unsetenv会删除它,setenv会用newvalue代替oldvalue,只有在overwrite非零时成立。
如果name不存在,setenv会将"name=newvalue"写进数组。
#include <stdlib.h>
int setenv(const char *name, const char *newvalue, int overwrite);
若成功返回0,错误返回-1
void unsetenv(const char *name);
无返回值
- fork函数和execve函数的区别
fork函数是创建新的子进程,是父进程的复制体,在新的子进程中运行相同的程序,父进程和子进程有相同的文件表,但是不同的PID
execve函数在当前进程的上下文中加载并运行一个新的程序,会覆盖当前进程的地址空间,但是没有创建一个新进程,有相同的PID,继承文件描述符。
信号
1、信号术语
- 传递一个信号到目的进程的两个步骤:发送信号和接收信号。
- 发送信号的原因:
1.内核检测到一个系统事件
2.一个进程调用了kill函数,显式的要求内核发送一个信号给目的进程。
- 一个进程可以发送信号给它自己。
- 接收信号:
1.忽略
2.终止
3.执行信号处理程序,捕获信号
- 待处理信号:
只发出没有被接收的信号
任何时刻,一种类型至多只会有一个待处理信号,多的会被直接丢弃
一个进程可以选择性的阻塞接受某种信号,被
阻塞仍可以被发送,但是不会被接收
一个待处理信号最多只能被接收一次。
pending:待处理信号集合
blocked:被阻塞信号集合。
2、发送信号——基于进程组
- 进程组:
每个进程都只属于一个进程组。
进程组ID:正整数
一个子进程和他的父进程属于同一进程组。
查看进程组id:getpgrp
修改进程组:setpgid
- /bin/kill程序可以向另外的进程发送任意的信号,格式是:
/bin/kill -n m
n是信号,m是进程或进程组
当n>0时,发送信号n到进程m
当n<0时,使信号|n|发送到进程组m中的所有进程。
- 进程通过调用kill函数发送信号给其他进程。
- 进程可以通过调用alarm函数向它自己发送SIGALRM信号
include <unistd.h>
unsigned int alarm(unsigned int secs);
- 返回前一次闹钟剩余的秒数,若没有返回0
非本地跳转
- c语言中,用户级的异常控制流形式,通过setjmp和longjmp函数提供。
- setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0.
- longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval
- 注:setjmp函数只被调用一次,但返回多次;longjmp函数被调用一次,但从不返回。
操作进程的工具
- STRACE:打印一个正在运行的程序和他的子程序调用的每个系统调用的痕迹
- PS:列出当前系统中的进程,包括僵死进程
- TOP:打印出关于当前进程资源使用的信息
- PMAP:显示进程的存储器映射
课本练习题
- 课本练习8.2中代码如下:
子进程执行了两个printf语句,在fork返回后,子进程产生输出
p1:x=2
p2:x=1
而父进程只执行了第七个printf
p2:x=0
进程图
- 课本练习题8.3中代码如下:
验证所有可能的输出序列:
序列acbc,abcc,bacc都是可能的序列,因为对应的拓扑序列如下图,通过运行验证出来结果:
- 课本练习题8.4验证程序会输出多少行,可能序列是什么?
观察代码中printf的个数有6个,就可以确定输出行数,所以会打印6行。可能序列对应拓扑图:
- 练习题8.5
解:
unsigned int snooze(unsigned int secs) {
unsigned int rc = sleep(secs);
printf("sleep for %d secs.\n",secs-rc,secs);
return rc;
}
- 练习8.6
代码如下:
#include"csapp.h"
int main(int argc,char *argv[],char *envp[])
{
int i;
printf("command-line arguments:\n");
for(i=0;argv[i] != NULL;i++)
printf("\n");
printf("evironment variables:\n");
for(i=0;envp[i]!=NULL;i++)
printf(" envp[%2d]: %s\n",i,envp[i]);
exit(0);
}
-
8.7 编写一个叫做snooze的程序,有一个命令行参数,用这个参数调用习题8.5中的snooze函数,然后终止。编写程序,使得用户可以通过在键盘上输入Ctrl-c中断snooze函数
-
解:只要休眠进程收到一个未被忽略的信号,sleep函数就会提前返回。但是,因为收到一个SIGINT信号的默认行为就是终止进程,我们必须设置一个SIGINT处理程序来允许sleep函数返回。处理程序简单的捕获SIGNAL,并将控制返回给sleep函数,该函数会立即返回。
#include "csapp.h"
void handler(sig) {
return;
}
unsigned int snooze(unsigned int secs) { unsigned int rc=sleep(secs); printf("Slept for %d of %d secs\n",secs-rc,secs);
return rc;
}
int main(int argc,char **argv) { if(argc!=2) { fprintf(stderr,"usage:%s<secs>\n",argv[0]);
exit(0); } if(signal(SIGINT,handler)==SIG_ERR) unix_error("signal error"); (void)snooze(atoi(argv[1]));
exit(0);
}
家庭作业
- 习题8.9
解:
-
习题8.10
-
习题8.11
这个程序会输出多少个hello?
#include"csapp.h"
int main()
{
Fork();
Fork();
printf("hello\n");
exit(0);
}
-
运行结果
一共运行了四个hello。
-
8.12
这个程序会输出几个hello?
#include"csapp.h"
void doit()
{
Fork();
Fork();
printf("hello\n");
return;
}
int main()
{
doit();
printf("hello\n");
exit(0);
}
-
运行结果
输出8个hello,原因如下:
-
8.13
下面程序的一种可能输出是什么?
#include"csapp.h"
int main() {
int x=3;
if(Fork() !=0)
printf("x=%d\n",++x);
printf("x=%d",--x);
exit(0);
}
- 运行结果:
注意,父进程和子进程不分享x,他们拥有自己的x。
- 8.14
下面程序会输出多少个hello?
#include"csapp.h"
void doit()
{
if(Fork()==0) {
Fork();
printf("hello\n");
exit(0);
}
return;
}
int main()
{
doit();
printf("hello\n");
exit(0);
}
-
运行结果
会运行出3行hello:
-
8.15下面程序 会输出几个hello?
-
运行结果:这里的子进程不是exit,而是return,说明两个子进程都要到回到main函数去打印那里的hello。所以是5行。
-
8.16运行下面程序输出是什么?
#include"csapp.h"
int counter =1;
int main()
{
if(Fork()==0) {
counter--;
exit(0);
}
else {
Wait(NULL);
printf("counter=%d\n",++counter);
}
exit(0);
}
- 运行结果:
输出counter = 2,因为全局变量也是复制的,而不是共享的。
- 8.18
下面程序可能输出什么?
#include"csapp.h"
void end(void)
{
printf("2");
fflush(stdout);
}
int main()
{
if(Fork()==0)
atexit(end);
if(Fork()==0) {
printf("0");
fflush(stdout);
}
else {
printf("1");
fflush(stdout);
}
exit(0);
}
A.112002 B.211020 C.102120 D.122001 E.100212
-
运行结果:
-
画出拓扑图如下:2必须在0、1后面,因此排除B和D。ACE都可能。
-
8.19下面的函数会打印多少行输出?
void foo(int n)
{
int i;
for(i=0;i<n;i++)
Fork();
printf("hello\n");
exit(0);
}
- 解:总共会输出2^n行。
- 8.20
使用execve编写一个叫做myls的程序,该程序的行为和/bin/ls程序的一样。 - 先使用
/bin/ls
- 代码入下:只要使用execve。
// myls.c
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[])
{
execve("/bin/ls", argv, environ);
exit(0);
}
运行:
- 8.21 下面程序的输出是什么?
#include"csapp.h"
int main()
{
if(fork()==0) {
printf("a");
fflush(stdout);
exit(0);
}
else{
printf("b");
fflush(stdout);
waitpid(-1,NULL,0);
}
printf("c");
fflush(stdout);
exit(0);
}
-
运行结果:
-
abc或者bac。c肯定在a和b之后。
-
8.22编写Unix system函数你自己的版本,
int mysystem(char *comman);
-
先使用man命令查看system的帮助文档:
-
system()函数调用
/bin/sh来执行参数指定的命令,/bin/sh 一般是一个软连接,指向某个具体的shell,比如bash, -
-c选项是告诉shell从字符串command中读取命令
-
为了更好的理解system()函数返回值,需要了解其执行过程,实际上system()函数执行了三步操作:
1.fork一个子进程;
2.在子进程中调用exec函数去执行command;
3.在父进程中调用wait去等待子进程结束。
- 对于fork失败,system()函数返回-1。 如果exec执行成功,也即command顺利执行完毕,则返回command通过exit或return返回的值。
- 实现代码如下:
// mysystem.c
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
extern int erron;
extern char **environ;
extern int EINTP;
int mysystem(char *command)
{
pid_t pid;
int status;
if (command == NULL)
return -1;
if ((pid = fork()) == -1)
return -1;
if (pid == 0)
{
char *argv[4];
argv[0] == "sh";
argv[1] == "-c";
argv[2] == command;
argv[3] == NULL;
execve("bin/sh", argv, environ);
exit(-1); // control should never come here
}
while (1)
{
if (waitpid(pid, &status, 0) == -1)
{
if (errno != EINTR)
exit(-1);
}
else
{
if (WIFEXITED(status))
return WEXITSTATUS(status);
else
return status;
}
}
}
- 8.23
-
解:一个可能的原因是,在第一个信号发给父进程之后,父进程进入handler,并且阻塞了SIGUSR2,第二个信号依然可以发送,然而,之后的3个信号便会被抛弃了。因为是连续发送,所以很可能是没等上下文切换,这5个信号就同时发送了。所以只有2个信号被接收。
-
8.24 修改8-18中程序,使满足
-
1.每个子进程在试图写一个只读文本段中的位置时,会异常终止
-
2.父进程打印和下面所示相同
child 1255 terminated by signal 11:segmentation faults
child 12254 terminnated by signal 11:segmentation faults
- 解:参考psignal的man页:
- 代码如下:
#include "csapp.h"
#define N 2
int main()
{
int status, i;
pid_t pid;
char errorInfo[128];
/* Parent creates N children */
for(i=0;i<N;i++)
if ((pid = Fork()) == 0) /* Child */
exit(100+i);
/* Parent reaps N children in no particular order */
while ((pid = waitpid(-1, &status, 0)) > 0) {
if (WIFEXITED(status))
printf("child %d terminated normally with exit status=%d\n",
pid, WEXITSTATUS(status));
else if(WIFSIGNALED(status))
{
//为什么只在写只读文本时才报错?下面的程序对所有异常退出都会提示
printf("child %d terminated by signal %d: ",
pid, WTERMSIG(status) );
psignal(WTERMSIG(status), errorInfo); //psignal会打印sig的信息
}
}
/* The only normal termination is if there are no more children */
if (errno != ECHILD)
unix_error("waitpid error");
exit(0);
}
- 8.25 编写一个fgets函数版本叫做tfgets,5秒后会超时,如果用户不在5秒内键入一行,tfgets返回NULL。
- 解:
fgets的定义如下:
char *fgets(char *buf, int bufsize, FILE *stream);
- 参数:
*buf: 字符型指针,指向用来存储所得数据的地址。
bufsize: 整型数据,指明buf指向的字符数组的大小。
*stream: 文件结构体指针,将要读取的文件流。
显然,在tfgets里一开始需要调用fgets。然而,因为五秒时间到了,fgets还没有返回,所以我们必须在处理程序里直接跳转到某个地方进行tfgets的NULL返回。这就需要用到非本地跳转。
#include <stdio.h>
#include "csapp.h"
sigjmp_buf buf;
void handler(int sig) {
/* jump */
siglongjmp(buf, 1);
}
char* tfgets(char* s, int size, FILE* stream) {
char* result;
if (!sigsetjmp(buf, 1)) {
alarm(5);
if (signal(SIGALRM, handler) == SIG_ERR)
unix_error("set alarm handler error");
return fgets(s, size, stream);
} else {
/* run out of time */
return NULL;
}
}
#define LEN 100
int main(int argc, char* argv[]) {
char buf[LEN];
char* input = tfgets(buf, LEN, stdin);
if (input == NULL) {
printf("nothing input: NULL\n");
} else {
printf("%s", input);
}
return 0;
}
运行结果
用户不在5秒内键入一行,tfgets返回NULL。
ch08错题整理以及分析
- 3.有关exec系列函数,下面说法正确的是()
A . 可以用char[][] 来传递argv
B . 进程调用了exec系列函数后,pid会变
C . 进程调用了exec系列函数后,代码会改变。
D . system()和exec系列等价。
E . exec系列函数中带e的要传入环境变量参数
F . exec系列函数中带v的要传入环境变量参数
正确答案: C E
你的答案: A C E
分析:不能用char[][] 来传递argv,结尾的0(null)无法处理;system=fork+exec+wait;
- 7.关于代码 int main(){} 说法正确的是()
A . 返回值是0
B . 返回值不确定
C . 会调用exit(0)
D . 返回值大于0
E . 上面代码运行完,在命令行中运行echo $? 的值是0
正确答案: A C E
你的答案: A E
分析main中不调用exit,会补上exit(0)
- 9.Unix/Linux中通过调用( )可以获取子进程PID。
A . getpid()
B . getppid()
C . getcpid()
D . fork()
正确答案: D
你的答案: A
分析课本p514,在父进程中,fork返回子进程的pid
- 10.进程调度由内核中的( )处理完成
A . 上下文切换
B . 中断服务程序
C . 调度器
D . 异常处理程序
正确答案: C
你的答案: B
分析课本p511内核可以将决定抢占当前进程,并重新开始了一个先前被抢占了的进程,决策叫调度,由内核中调度器处理。
- 18.有关异常,下面说法正确的是()
A . 系统中的异常由异常名唯一确定
B . 异常表中存放的是异常处理程序
C . 异常表的起始地址存放在异常表基址寄存器中
D . 异常处理程序运行在内核模式下
正确答案: C D
你的答案: A C D
分析课本p503,系统中每种类型的异常都分配了一个唯一的饿非负整数异常号。
- 19.进程上下文包括程序的( )
A . 代码和数据
B . 栈
C . 通用寄存器中的内容
D . 程序计数器
E . 环境变量
F . 打开的文件描述符的集合
正确答案: A B C D E F
你的答案: B C D F
分析课本p508,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,栈,通用目的寄存器的内容,程序计数器,环境变量,以及打开文件描述符的集合。
总结
异常控制流发生在计算机系统的各个层次。理解ECF将帮助我们理解重要的系统概念,理解应用程序是如何与操作系统交互的,更有助于理解并发,所以异常这部分的学习也是很重要的。再重新学习这章内容和分析错题,课后习题后,对异常控制流这部分的内容掌握更加熟悉,对于一些函数的应用也更加熟练了。