2017-2018-1 20155302 第十三周作业
重新精学第八章异常控制流以及系统级I/O相关知识
数据流:只能以事先规定好的顺序被读取一次的数据的一个序列。
控制流:控制转移序列叫做处理器的控制流。
异常控制流:现代系统通过使控制流发生突变做出反应的突变。
教材内容精学及回顾
8.1异常
异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。
异常就是控制流中的突变,用来响应处理器状态中的某些变化。
系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号,其中一些号码是由处理器的设计者分配的,其他号码是由操作系统内核的设计者分配的。
异常号是到异常表中的索引,异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里。
异常分为一下四类:中断(interrupt),陷阱(trap),故障(fault)和终止(abort)。
1.中断
中断是异步发生,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成,从这个意义上它是异步的。
硬件中断的异常处理程序通常称为中断处理程序(interrupt handle)
I/O设备通过向处理器芯片的一个引脚发信号,并将异常号放到系统总线上,以触发中断。
在当前指令执行完后,处理器注意到中断引脚的电压变化,从系统总线读取异常号,调用适当的中断处理程序。
当处理程序完成后,它将控制返回给下一条本来要执行的指令
2.陷阱和系统调用
陷阱是有意的异常,是执行一个指令的结果。也会返回到下一跳本来要执行的指令。
陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用
用户程序经常需要向内核请求服务。
-读文件(read)
-创建进程(fork)
-新的程序(execve)
-终止当前进程(exit)
为了运行对这些内核服务的受控访问,处理器提供了一条特殊的syscall n的指令
系统调用是运行在内核模式下,而普通调用是用户模式下。
3.故障
故障由错误引起,可能被故障处理程序修正。
如果能被修正,返回引起故障的指令。
否则返回abort例程,进行终结。
4.终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM和SRAM被损坏。
终止处理程序从不将控制返回给应用程序。返回一个abort例程。
C程序可用syscall函数来直接调用任何系统调用。我们将系统调用与他们相关联的包装函数称为系统级函数。
系统级函数写的hello world:
int main()
{
write(1,"hello,world\n",13);
exit(0);
}
汇编写的hello world:
string:
"hello world\n"
main:
movl $4,%eax
movl $1,%ebx
movl $String,%ecx
movl $len,%edx
int $0x80
movl $1,%eax
movl $0,%ebx
int $0x80
思考问题:
系统级函数编写的C语言程序如何转换成汇编程序???
这就与第三章的内容相关了,简单来复习一下转换方法
举例实验测试:
截图:
系统级代码:
int g(int x)
{
return x+5;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(10)+1;
}
gcc -S -o 1.s 1.c -m32
用指令编写为汇编代码
汇编代码:
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $5, %eax
popl %ebp
ret
f:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call g
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $10, (%esp)
call f
addl $1, %eax
leave
ret
通过这个例子,总结一下函数调用的过程:
进入函数:
1.当前栈基地址压栈(当前栈基地址实际上是前一个函数的栈基地址)
调用其他函数:
1.参数从右到左进栈
2.下一条指令地址进栈
退出函数:
1.栈顶esp归位,回到本函数的ebp
2.基地址回退到上一个函数的基地址
3.eip退回到上一个函数即将要执行的那条语句的地址上
8.2进程
进程和程序:
程序是一堆代码和数据;进程是执行中程序的一个具体实例。程序总是运行在某个进程的上下文中。例:fork函数在新的子进程中运行相同的程序;execve函数在当前进程上下文中加载运行一个新的程序,它会覆盖当前进程的地址空间,但并没有创建新的进程。
·
用户模式和内核模式 :
处理器通过模式位来控制进程权限。内核模式下,进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器位置。用户模式中的进程不允许执行特权指令,如停止处理器、改变模式位,或者发起一个读操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据。运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷人系统调用这样的异常。
·
上下文切换
操作系统内核通过上下文切换来实现多任务。
思考问题
三个进程的起始和结束时间如下图,哪些互为并发????
AB和BC互相并发,原因是他们各自的执行时重叠的,一个进程在另一个进程的结束前开始,相反的A与C就不是并发因为A在C开始前就已经结束。
8.3系统调用错误处理
当Unix系统级函数遇到错误时,他们典型地返回-1,并设置全局变量errno来表示什么出错了。
if((pid=fork()<0){
fprintf(stderr,"fork error: %s\n", strerror(errno));
exit(0);
}
strerror 函数返回一个文本串,描述了个某个errno值相关联的错误。
但这样使得代码繁琐,简单一层包装:
if((pid = fork()) < 0)
unix_error("fork error");
void unix_error(char * msg){
fprintf(stderr,"%s,%s\n",msg,strerror(errno));
exit(0);
}
使用错误包装处理函数:
pid_t Fork(void){
pid_t pid;
if((pid = fork()) < 0){
unix_error("fork error");
}
return pid;
}
思考问题
一些IO系统调用执行时,如 read 等待输入期间,如果收到一个信号,系统将中断read, 转而执行信号处理函数. 当信号处理返回后, 系统遇到了一个问题: 是重新开始这个系统调用, 还是让系统调用失败????
早期UNIX系统的做法是, 中断系统调用,并让系统调用失败, 比如read返回 -1, 同时设置 errno 为EINTR中断了的系统调用是没有完成的调用,它的失败是临时性的,如果再次调用则可能成功,这并不是真正的失败,所以要对这种情况进行处理,处理的方式为:
again:
if ((n = read(fd, buf, BUFFSIZE)) < 0) {
if (errno == EINTR)
goto again; /* just an interrupted system call */
/* handle other errors */
}
·
……
while ((r = read (fd, buf, len)) < 0 && errno == EINTR) /*do
nothing*/ ;
……
8.4进程控制
获取进程ID:
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);//获取当前进程
pid_t getppid(void);//获取父进程
进程的三种主要状态:
1.运行 要么再CPU上运行,要么等待且最终被CPU执行;
2.停止 进程的执行被挂起,且不会被调度;
3.终止 进程永久停止
父进程通过调用fork函数创建一个新的运行子进程
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
返回:子进程返回0,父进程返回子进程的PID,如果出错,返回-1;新创建的子进程几乎但不完全与父进程相同。
fork()函数会第一次调用,返回两次,一次在父进程,一次在子进程。
返回值用来明确是在父进程还是在子进程中执行。
一次fork():
#include "csapp.h"
int main()
{
Fork();
printf("hello\n");
exit (0);
}
·
两次fork():
#include "csapp.h"
int main()
{
Fork();
Fork();
printf("hello\n");
exit (0);
}
·
三次fork():
#include "csapp.h"
int main()
{
Fork();
Fork();
printf("hello\n");
exit (0);
}
回收子进程:
当进程终止时,还是保存在内核中的,等待被其父进程回收;
当父进程回收子进程时,内核将子进程的退出状态传递给父进程,然后抛弃子进程;(要求父进程回收为了获取子进程退出状态,内核会抛弃子进程,维护系统资源);终止却未被回收的子进程是僵尸进程;
如果父进程没有回收其子进程就终止,那么由init进程回收;
waitpid()函数:pid_t waitpid(pid_t pid,int *status,int options);
waitpid函数有点复杂。默认(option=0)时,waitpid挂起调用进程的执行,知道它的等待集合中的一个子进程终止,如果等待集合的一个子进程在调用时刻就已经终止,那么waitpid立即返回。在这两种情况下,waitpid返回导致waitpid返回的已终止子进程的PID,并且将这个已终止的子进程从系统中去除。
参数pid:
pid > 0 : 等待集合就一个单独的子进程,就是此pid;
pid = -1:所有子进程;
Unix进程组
参数options:
WNOHANG 立即返回,不等待;
WUNTRACED 挂起调用进程,直到等待集合中一个进程被终止或者停止,返回相关pid;
思考问题
1.自我编写程序探索父子进程关系
代码:
#include"csapp.h"
int main()
{
pid_t pid;
int x=1;
pid=Fork();
if(pid==0){
printf("child:x=%d\n",++x);
exit(0);
}
printf("parents:x=%d\n",--x);
exit(0);
}
第8行创建子进程,子进程由于继承了父进程的存储空间等,因此也从第8行开始执行。由于它返回的pid为0,因此执行第9行的child块。对于父进程,执行第14行的parent块。并且两者存储是独立的,因此x值互不影响。并且他们是并发执行,共享文件(在同一屏幕上打印)。
测试结果:
这是书中一道练习题,其中父子进程有相同的代码段,所以有可能父子进程执行的指令集合是相关的,此处的子进程执行了两个printf()语句,fork返回后执行第六行的printf(),然后从if语句中出来,执行第7行printf()语句,子进程产生的输出为p1:x=2;p2:x=1,父进程因为只执行了第7行的printf()所以p2:x=0。
8.5信号
Unix中的发送信号
1、 每个进程属于一个进程组,函数getpgrp()获得当前进程的进程组
2、 子进程和父进程属于同一个进程组。Setpgid()设置自己或其它进程的进程组。
常见的信号有kill,如kill -9 12345。
还有alarm,这个略复杂,uint alarm(uint secs),这个函数安排内核在secs后发送一个SIGALRM信号给调用进程。如果secs是零,不会调用新的闹钟。
用kill函数发送信号:
int kill(pid_t pid,int sig);
pid > 0,发送给指定进程, pid < 0,给进程组每个进程
用alarm函数发送信号:
unsigned int alarm(unsigned int secs);
alarm函数和sleep函数由相似之处,被打断时都会返回剩余秒数
对alarm的调用都会取消正在等待的alarm,并返回秒数;没有真在等待的就返回0;
(理解与注意!!!):进程可以使用signal函数修改信号的默认处理行为; SIGSTOP和SIGKILL是不可忽略的的; 当处理程序处理完时,控制通常会返回给下一个指令;但在某些系统中,被中断的系统掉用,会立即返回一个错误。
sigaction包装函数:Signal
此函数信号处理语义如下:
当前处理信号被屏蔽
信号不会排队(不会有超过1个的相同信号在排队)
只要有可能,被中断的系统调用会重启;
一旦被设置了处理函数,就一直会存在,除非将handler参数设置为SIG_DEF或者SIG_IGN;
阻塞和取消阻塞信号:
#include <signal.h>
/* 改变blocked向量的值,若oldset!=null,会用来保存以前blocked向量的值 */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/* 初始化set为空集 */
int sigemptyset(sigset_t *set);
/* 初始化set全为1,每个信号都填入blocked向量 */
int sigfillset(sigset *set);
/* 添加、删除signum到set */
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
/* set中对应signum是否置1 */
int sigismember(const sigset_t *set, int signum);
思考问题
设计代码研究测试信号的发送接收以及处理。
signal.c:
#include "csapp.h"
void snooze(unsigned int sec)
{
unsigned int left_time = sleep(sec);
printf("Sleep for %u of %ds\n",sec-left_time, sec);
}
void handler(int sig)
{
return;
}
int main(int argc, char **argv)
{
if(argc != 2)
{
printf("argment error\n");
exit(0);
}
if(signal(SIGINT, handler) == SIG_ERR)
unix_error("signal error");
snooze(atoi(argv[1]));
return 0;
}
当执行到3s时(sleep(5) 执行了3s),ctrl+c中断,此时sleep返回,并继续向下执行。 结果:Sleep for 3 of 5s 信号处理问题 1. 待处理信号可以被阻塞 2.待处理信号不会排队。同一待处理信号 只能有一个,其他到来的 丢弃 3. 系统可以被中断。如 read, wait, accept 等 慢速系统调用,有些系统可以在中断后 继续执行,有些不可以,而是返回一错误条件,并将errno设置为EINTR
signal2.c:
#include "csapp.h"
void handler(int sig)
{
pid_t pid;
while((pid = waitpid(-1, NULL, 0)) > 0)
printf("reap pid:%d\n",pid);
if(errno != ECHILD)
unix_error("wait error");
sleep(2);
return;
}
int main()
{
int i;
int n;
pid_t pid;
char buf[MAXLINE];
if(signal(SIGCHLD, handler) == SIG_ERR)
unix_error("signal error");
unix_error("signal error");
for(i = 0; i < 3; i++)
{
pid = Fork();
if(pid == 0)
{
printf("child procss pid:%d\n",(int)getpid());
sleep(1);
exit(0);
}
}
// n = read(STDIN_FILENO, buf, sizeof(buf));
while(n = read(STDIN_FILENO, buf, sizeof(buf)) < 0)
if(errno != EINTR)
unix_error("read error");
printf("%s\n",buf);
return 0;
}
if((pid = waitpid(-1, NULL, 0)) > 0) printf("reap pid:%d\n",pid); 对比 while((pid = waitpid(-1, NULL, 0)) > 0) printf("reap pid:%d\n",pid); 前者 因为相同的待处理信号,只能有一个进入待处理信号集,不排队,因此会被丢弃。而后者 每次处理SIGCHILD信号时,能回收尽可能多的 僵死进程。
n = read(STDIN_FILENO, buf, sizeof(buf)); 对比 while(n = read(STDIN_FILENO, buf, sizeof(buf)) < 0) read属于慢速系统调用。 在有些系统中 被信号中断后,不能重启,而是返回一错误条件,并将errno设置为EINTR。 安全起见,应 手动设置 自启动程序。
8.6非本地跳转
setjmp setjmp保存当前程序的堆栈上下文环境(stack context),注意,这个保存的堆栈上下文环境仅在调用setjmp的函数内有效,如果调用setjmp的函数返回了,这个保存的堆栈上下文环境就失效了。调用setjmp的直接返回值为0。
longjmp longjmp将会恢复由setjmp保存的程序堆栈上下文,即程序从调用setjmp处重新开始执行,不过此时的setjmp的返回值将是由longjmp指定的值。注意longjmp不能指定0为返回值,即使指定了0,longjmp也会使setjmp返回1。
典型的调用过程如下:
jmp_buf jmp_env;
ret = setjmp(jmp_env);
if (ret == 0) {
......
if (some_condition)
longjmp(jmp_env, 1); //longjmp 的调用可以在其他的函数中
......
if (some_condition2)
longjmp(jmp_env, 2);
......
}
else if (ret == 1)
{
//process the env return from longjmp(jmp_env, 1);
}
else if (ret == 2)
{
//process the env return from longjmp(jmp_env, 2);
}
else
{
// why come here ?
}
家庭作业学习
8.9
AB 不并发
AC 并发
AD 并发
BC 并发
BD 并发
CD 并发
8.10
A. 调用一次,返回两次: fork
B. 调用一次,从不返回: execve, longjmp
C. 调用一次,返回一次或者多次: setjmp
8.11
4行
8.12
8行
8.13
->x=2
->x=4->x=3
8.14
主进程只打印一行。
主进程的直接子进程会打印一行,子进程的子进程又打印一行。
所以是3行。
8.15
这里的子进程不是exit,而是return,说明两个子进程都要到回到main函数去打印那里的hello。所以是5行。
8.16
输出counter = 2,因为全局变量也是复制的,而不是共享的。
8.17
满足
Hello--->1->Bye
\ \
\--->0---->2-->Bye
这种拓扑即可。
8.18
画一下进程图就可以知道。
所以ACE是可能的。
8.19
总共会输出2^n行。
8.20
int main(int argc, char* args[])
{
execve("/bin/ls", args, environ); //没有错误处理,注意环境变量
return 0;
}
8.21
abc或者bac。c肯定在a和b之后。
8.22
int mysystem(char *command)
{
int status;
char *argv[4];
char *a0 = "sh";
char *a1 = "-c";
if( fork()==0 ) /*子进程*/
{
argv[0] = a0;
argv[1] = a1;
argv[2] = command;
argv[3] = NULL;
execve("/bin/sh", args, environ);
return -1; //执行异常
}
else{ /*父进程*/
if( wait(&status) > 0)
{
if(WIFEXITED(status) != 0)
return WEXITSTATUS(status);
else return status;
}
else return -1; //wait异常
}
}
8.23
一个可能的原因是,在第一个信号发给父进程之后,父进程进入handler,并且阻塞了SIGUSR2,第二个信号依然可以发送,然而,之后的3个信号便会被抛弃了。因为是连续发送,所以很可能是没等上下文切换,这5个信号就同时发送了。所以只有2个信号被接收。
8.24
#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);
}
}
/* The only normal termination is if there are no more children */
if (errno != ECHILD)
unix_error("waitpid error");
exit(0);
}
8.25
fgets的定义如下:
char fgets(char buf, int bufsize, FILE stream);
参数:
buf: 字符型指针,指向用来存储所得数据的地址。
bufsize: 整型数据,指明buf指向的字符数组的大小。
*stream: 文件结构体指针,将要读取的文件流。
这个应该是用alarm发送信号给自己,然后在信号处理程序里面做文章。
显然,在tfgets里一开始需要调用fgets。然而,因为五秒时间到了,fgets还没有返回,所以我们必须在处理程序里直接跳转到某个地方进行tfgets的NULL返回。这就需要用到非本地跳转。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
sigjmp_buf env;
void tfgets_handler(int sig)
{
signal(SIGALRM, SIG_DFL);
siglongjmp(env, 1);
}
char *tfgets(char *buf, int bufsize, FILE *stream)
{
static const int TimeLimitSecs = 5;
signal(SIGALRM, tfgets_handler)
alarm(TimeLimitSecs);
int rc = sigsetjmp(env, 1);
if(rc == 0) return fgets(buf, bufsize, stream);
else return NULL; //alarm,time out
}
8.26
难度太大不会了,JID还必须要管理所有job...那JID和pid如何互相查找呢??和hash类似?还是使用枚举找pid?..不会了。
蓝墨云中CH8习题错题汇总:
2 ( 多选题 | 1 分)
Linux信号处理说法正确的是(ABDEF)
:
A .
可以用signal()处理信号
B .
一个信号最多只能被接收一次
C .
kill(1)用来杀死进程
D .
kill(1)用来发送信号
E .
可以通过键盘发送信号
F .
可以用sigaction()处理信号
解析:开始少选了A,这道题属于课本内容细节题,书中8.5有详细的说明,当时没有细心看。
3 ( 多选题 | 1 分)
有关exec系列函数,下面说法正确的是(CE)
:
A .
可以用char[][] 来传递argv
B .
进程调用了exec系列函数后,pid会变
C .
进程调用了exec系列函数后,代码会改变。
D .
system()和exec系列等价。
E .
exec系列函数中带e的要传入环境变量参数
F .
exec系列函数中带v的要传入环境变量参数
解析:多选了A选项,不能用char[][] 来传递argv,结尾的0(null)无法处理;system=fork+exec+wait;
7 ( 多选题 | 1 分)
关于代码 int main(){} 说法正确的是(ACE)
:
A .
返回值是0
B .
返回值不确定
C .
会调用exit(0)
D .
返回值大于0
E .
上面代码运行完,在命令行中运行echo $? 的值是0
解析:少选了C选项,main中不调用exit,会补上exit(0)
9 ( 单选题 | 1 分)
Unix/Linux中通过调用( D )可以获取子进程PID。
:
A .
getpid()
B .
getppid()
C .
getcpid()
D .
fork()
解析:错选成了A,概念不清晰,getpid – 获取当前进程的PID;而fork在父进程中返回子进程的PID,在子进程中fork返回0,也可通过这一点来判断程序是在子进程还是父进程中执行的!
28 ( 单选题 | 1 分)
Linux中,信号(Signal)是一种(C)异常控制流。
:
A .
硬件层
B .
操作系统层
C .
用户层
D .
网络层
解析:P501看书不够仔细。操作系统层是内核通过上下文切换将控制从一个用户进程转移到另一个用户进程。硬件层是将事件转移到异常处理程序中。