CSAPP(8)Exception Control Flow


nonlocal jump:jumps that violate the usual call/return stack discipline

Exceptions

在这里插入图片描述
当在执行 I c u r r I_{curr} Icurr指令发生Exception时(可能由于 I c u r r I_{curr} Icurr引起,也可能是外部引起),会进入Exception handler,当处理Exception被解决后会有三种方式:

  • 返回之前的 I c u r r I_{curr} Icurr执行
  • 执行下一条指令 I n e x t I_{next} Inext
  • 当前程序被handler抛弃,abort

Exception table

由processor和os kernel的 设计人员指定了一些exception number,大家把各种异常归类到exception table里,而这个table的首地址存在CPU里的exception table base register里,当开机时会初始化exception table,里面的每一行对应一个exception number,而里面存放的数据则是用于处理异常的代码的地址.下面分别是静态图和运行时的示例
在这里插入图片描述
在这里插入图片描述

Exception vs Procedure Call

  • 对于ProcedureCall而言,processor会把返回地址压栈,对于Exception而言,会根据ExceptionNumber不同把当前指令或者下一条指令压栈
  • 为了返回后继续制定,processor也会把一些参数压栈
  • 当从user转向kernel时,上面的压栈都是在kernel栈上
  • 处理异常的程序处于kernel mode,拥有所有权限

当handler处理完之后可能会返回,那么就执行return from interrupt指令来恢复原状,同时也返回了user mode.也能不返回了…

classes of exceptions

classcauseasync/syncreturn behavior
InterruptSignal from IO deviceAsync I n e x t I_{next} Inext
TrapIntentional exceptionSync I n e x t I_{next} Inext
FaultPotentially reconverable errorSync I c u r r I_{curr} Icurr or abort
AbortNonrecoverable errorSyncabort

对于Interrupt而言,当processor执行完一条指令时观察下interrupt pin的状态,然后从system bus中读取exception number,然后执行handler,执行完返回.
下面是一段有System Call的C代码,以及其汇编代码,注意汇编中int $0x80是用于系统调用,而%eax一般用于指定System Call的编码,而%ebx,%ecx,%edx,%esi,%edi,%ebp用于参数传递:

int main(){
	write(1,"hello world\n",13);
	exit(0);
.section .data
string:
	.ascii "hello,world\n"
string_end:
	.equ len,string_end - string

.section .text
.globe main
main:
	// First,call write
	movl $4,%eax			//System call number 4
	movl $1,%ebx			//stdout has descriptor 1
	movl $string,%ecx		//Hello world
	movl $len,%edx			//String length
	int $0x80				//System call code
	// Next,call exit
	movl $1,%eax			//System call number 0
	movl $0,%ebx			//argument is 0
	int $0x80				//System call code

Processes

CPU

在这里插入图片描述
如上图所示,processor被三个process公用,其PC的指向是会跳来跳去的,但是对于logical control flow而言,认为是顺序的.
对于两个process如果时间上有重叠,则是concurrent,如果同时在两个不同的core上运行,则是parallel

Main Memory

运行时内存分配如下图
在这里插入图片描述

Context Switches

对于block的system call而言,会发生context switch,但是对于noblock的system call而言,也可能发生context switch,这个由kernel的心情决定
在发生异常时(包括context switch),缓存会失效,这时称为cache pollution

System Call Error Handling

一般而言,system call的函数会通过返回-1来表示失败,并通过errno来具体说明.为了避免遗漏对调用结果的检查,也为了不让这些检查代码扰乱了正常的业务处理,书中提出了一种封装方案

//原始方式调用
if((pid=fork())<0){
	fprintf(stderr,"fork error:%s\n",strerror(errno));
	exit(0);
}
//改进版,先定义个帮助函数
void unix_error(char *msg){
	fprintf(stderr,"%s:%s\n",msg,strerror(errno));
	exit(0);
}
//使用帮助函数
if((pid=fork())<0)
	unix_error("fork error");
//终极版,为系统调用创建wrapper(使用大写开头)
pid_t Fork(void){
	pid_t pid;
	if((pid=fork())<0)
		unit_error("Fork error");
	return pid;
}
//使用wrapper
pid=Fork();

Process Control

getpid

获取自己的pid

getppid

获取parent的pid

exit

退出该进程,可传入参数status
当child process结束后进入zombie状态,等待parent process的reap,如果parent结束时仍然没有reap,那么kernel就会安排init这个process(PID=1)来做这件事

fork

#include "caspp.h"
int main(){
	pid_t pid;
	int x=1;
	pid=Fork();	//这里使用了上面的Fork
	if(0==pid){
		printf("child:x=%d\n",++x);
		exit(0);
	}
	printf("parent:x=%d\n",--x);
	exit(0);
}

上面的代码执行后结果如下.

parent:x=0
child:x=2

注意在Fork后有两个x,parent和child对x的修改彼此互不影响

waitpid

等待指定process结束

pid_t waitpid(pid_t pid,int *status,int options);

当参数pid的值为-1时表示等待自己的所有子进程结束,当大于0时表示等待指定子进程结束.
其中options可以明确当子进程未中止时父进程的状态
status用于表示子进程是由于什么原因中止

#include "csapp.h"
#define N 2
int main(){
	int status ,i;
	pid_t pid;
	for(i=0;i<N;i++)
		if((pid=Fork())==0)
			exit(100+i);
	while((pid=waitpid(-1,&status,0))>0){
		if(WIFEXITED(status)){
			printf("child %d terminated normally with exit status=%d\n",
				pid,WEXITSTATUS(status));)
		}else{
			printf("child %d terminated abnormally\n",pid);
		}
	}
	/*the only normal termination is if there are no more children*/
	if(errno!=ECHILD)
		unix_error("error");
	exit(0);
}

上面是用循环来等待所有的子进程结束,当所有子进程都结束后执行waitpid会发生错误,从而导致errno的值为ECHILD

sleep

sleep函数的返回是还需要继续睡眠的时间,返回0当然是表示时间到了,但是有的时候进程会收到一些signal,那么sleep也会返回,这个时候返回值是还需要睡多长时间.

pause

一直睡直到收到signal

execve

执行指定的文件(永远不会返回),签名如下

int execve(const char *filename,const char *argv[],const char *envp[]);

其中argv[0]一般是executable object file name,后面是参数,而envp则是"KEY=VALUE"格式.argv和envp都是用null来表示结束
然后会按照下面形式调用main函数.

int main(int argc,char *argv[],char *envp[]);

调用时栈结构如下
在这里插入图片描述

getenv & setenv & unsetenv

这三个函数用于获取/设置/取消设置上面提到的envp

#include <stdlib.h>
char *getenv(const char *name);//return:ptr to name if exists,NULL if no match

//return:0 on success,-1 on error
int setenv(const char *name,const char *newvalue,int overrite);

void unsetenv(const char *name);

Signals

signal is a high-level software from exceptional control flow,that allows processes and the kernel to interrupt other processes.

NumberNamedefault actioncorrosponding event
1SIGHUPterminateterminal line hangup
2SIGINTterminateinterrupt from keyboard(Ctrl-c)
3SIGQUITterminatequit from keyboard
4SIGILLterminateillegal instruction
5SIGTRAPterminate & dump coretrace trap
6SIGABRTterminate & dump coreabort signal from abort function
7SIGBUSterminatebus error
8SIGFPEterminate & dump corefloating point exception
9SIGKILLterminatekill program
10SIGUSR1terminateuser define signal
11SIGSEGVterminateinvalid memory reference(seg falut)
12SIGUSR2terminateuser define signal
13SIGPIPEterminatewrote to a pipe with no reader
14SIGALRMterminatetimer signal from alarm function
15SIGTERMterminatesoftware termination signal
16SIGSTKELTterminatestack fault on coprocessor
17SIGCHLDignorea child process has stopped or terminated
18SIGCONTignorecontinue process if stopped
19SIGSTOPstop until next SIGCONTstop signal not from terminal
20SIGTSTPstop until next SIGCONTstop signal from terminal(Ctrl-z)
21SIGTTINstop until next SIGCONTbackground process read from terminal
22SIGTTOUstop until next SIGCONTbackgroup process wrote to terminal
23SIGURGignoreurgent condition on socket
24SIGXCPUterminatecpu time limit exceeded
25SIGXFSZterminatefile size limit exceeded
26SIGVTALRMterminalvirtual timer expored
27SIGPROFterminalprofiling timer expired
28SIGWINCHignorewindow size changed
29SIGIOterminateIO now possible on a discriptor
30SIGPWRterminatepower failure

terminology

signal的处理分为两步:

  1. sending a signal
    是指kernel把process指定位置设置标记.这一般由两种原因造成:既可能是外部system event(例如除以0),也可以是process发起指令(例如kill)
  2. receiving a signal
    是指process相应signal,包括忽略,中止和调用signal handler来catch几种方法

当进行了第一步而没有第二步时,这个signal处于pending signal状态.如果处于这种状态则再收到signal的时候会丢弃新的signal(no queue),另外process还可以block特定的signal
对于第二步而言逻辑大致如下:接收signal后调用signal handler,当siganl handler执行return后从中断的指令的下一条继续执行
在这里插入图片描述
下面具体说下这两个阶段的处理

sending a signal

每一个process都归属于一个process group,unix提供了一些原语来发送signal

//return process group id of calling process
pid_t getpgrp(void);

//return 0 on success,-1 on error
int setpgid(pid_t pid,pid_t pgid);
setpgid(0,0);//使用当前process的pid创建process group并把自己加进去
unix>kill -9 12345		#杀死pid为12345的process
unix>kill -9 -12345		#杀死pid为12345的process group里所有process

下面是C中一些发送signal的函数

//和上面的kill一样,pid可正可负
int kill(pid_t pid,int sig);
//给自己发送alarm,需要先通过singal来设置handler
unsigned int alarm(unsigned int secs);

receiving signals

根据default action有以下几种

  • 中止
  • 中止并dump
  • 挂起
  • 忽略
    除了SIGSTOPSIGKILL,其他的signal的相应方式可以通过下面函数来设置
#include <signal.h>
typedef void (*sighandle_t)(int);
//return previous handler if OK,SIG_ERR on error
sighandle_t signal(int signum,sighandler_t handler);

另外系统提供了SIG_IGNSIG_DFL两个handler来表示忽略和重置成default action
关于receiving signal还有一些特殊情况需要说明:
pending,当一个process正在执行signal handler时,不会再处理新收到的同类型signal,而是将其保持pending状态
discard,当一个process已经在pending时,又收到了同类型signal,那么就会丢弃
interrupt,对于一些会block的系统调用(例如read,write,accept)被称为slow system call,这些调用会被signal中断
下面是一些block signal的函数

# include <signal.h>
//return 0 if OK,-1 on error
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set,int signum);
int sigdelset(sigset_t *set,int signum);

//return 1 if member,0 if not,-1 on error
int sigismember(const sigset_t *set,int signum);

其中sigprocmask的how参数可以是以下三种取值:

  • SIG_BLOCK
    添加一种block,相当于blocked=blcoked | set
  • SIG_UNBLOCK
    去掉一种block,相当于blocked=blocked & ~set
  • SIG_SETMASK
    设置block(忽略当前),相当于blocked=set

concurrency

文中举了下面的例子将signal使用时会遇到的并发问题

void handler(int sig){
	pid_t pid;
	while((pid=waitpid(-1,NULL,0))>0)
		deletejob(pid);//reap a zombie child
	if(errno!=ECHILD)
		unix_error("waitpid error");
}
int main(int argc,char **argv){
	int pid;
	Signal(SIGCHLD,handler);
	initjobs();
	while(1){
		if((pid=Fork())==0){
			Execve("/bin/date",argv,NULL);
		}
		addjob(pid);
	}
	exit(0);
}

初看上去上面代码没有问题,其逻辑就是parent创建子进程后把子进程加入addjob,然后通过handler来reap子进程(就是deletejob).但是书中提到了一种特殊场景会触发bug:在parent创建child后(此时还未执行addjob),kernel先调度了子进程,并且子进程执行完毕,从而给parent process设置了SIGCHLD标记.然后kernel再调度到parent process进程,由于kernel观察到SIGCHLD标记,于是先执行了handler,而这个时候addjob还未执行,所以deletejob不能如预期那样删除子进程.当handler执行完毕后又执行了addjob函数,此时addjob加入了一个zombie!.正确的main函数如下(handler无需更改):

int main(int argc,char **argv){
	int pid;
	sigset_t mask;
	Signal(SIGCHLD,handler);
	initjobs();
	while(1){
		Sigemptyset($mask);
		Sigaddset(&mask,SIGCHLD);
		Sigprocmask(SIG_BLOCK,&mask,NULL);//block SIGCHLD
		
		if((pid=Fork())==0){
			Sigprocmask(SIG_UNBLOCK,&mask,NULL);//由于子进程继承了父进程的SIG_BLOCK,此处先还原
			Execve("/bin/date",argv,NULL);
		}
		addjob(pid);
		Sigprocmask(SIG_UNBLOCK,&mask,NULL);//确保addjob完成后才会调用handler
	}

为了方便复现这种由于kernel先调度parent还是先调度child引发的问题,书中提供了一个Fork函数,这个函数在执行后会随机的睡一会儿,从而确保child和parent都有可能先被调用

Nonlocal Jumps

longjmp

#include <setjmp.h>

//return 0 from setjmp,nonzero from longjmps
int setjmp(jmp_buf env);

//never returns
void longjmp(jmp_buf env,int retval);

从使用上来说和goto的用处很像,差别是goto只能在函数内调整,而longjmp可以跨函数调整.setjmp相当于用来标记的label(先对于goto而言会做一些保存指针等操作从而以后可以恢复),然后调用longjmp时相对于执行了goto语句并恢复之前的保存.可以参见下面的例子

#include "csapp.h"
jmp_buf buf;
int error1=0;
int error2=1;
void foo(void);
int main(){
	int rc;
	rc=setjmp(buf);
	if(0==rc)
		foo();
	else if(1==rc)
		printf("detected an error1 in foo\n");
	else if(2==rc)
		printf("detected an error2 in foo\n");
	else
		printf("unknow error in foo\n");
	exit(0);
}
void foo(void){
	if(error1)
		longjmp(buf,1);
	bar();
}
void bar(void){
	if(error2)
		longjmp(buf,2);
}

siglongjmp

#include <setjmp.h>

//return 0 from setjmp,nonzero from longjmps
int sigsetjmp(sigjmp_buf env,int savesigs);

//never returns
void siglongjmp(sigjmp_buf env,int retval);

与上面的longjmp用法类似,差别是调用goto函数(此时是siglongjmp)是位于一个signal handler内.下面是一个接收signal后完成软重启功能的示例

#include "csapp.h"
sigjmp_buf buf;
void handler(int sig){
	siglongjmp(buf,1);
}
int main(){
	Signal(SIGINT,handler);
	if(!sigsetjmp(buf,1))
		printf("starting\n");
	else
		printf("restarting\n");
	while(1){
		Sleep(1);
		printf("processing...\n");
	}
	exit(0);
}

Tools for Manipulating Processes

strace
ps
top
pmap
/proc

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值