CSAPP(8)Exception Control Flow

文章目录

  • Exceptions
    • Exception table
    • Exception vs Procedure Call
    • classes of exceptions
  • Processes
    • CPU
    • Main Memory
    • Context Switches
  • System Call Error Handling
  • Process Control
    • getpid
    • getppid
    • exit
    • fork
    • waitpid
    • sleep
    • pause
    • execve
    • getenv & setenv & unsetenv
  • Signals
    • terminology
    • sending a signal
    • receiving signals
    • concurrency
  • Nonlocal Jumps
    • longjmp
    • siglongjmp
  • Tools for Manipulating Processes

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

class cause async/sync return behavior
Interrupt Signal from IO device Async I n e x t I_{next} Inext
Trap Intentional exception Sync I n e x t I_{next} Inext
Fault Potentially reconverable error Sync I c u r r I_{curr} Icurr or abort
Abort Nonrecoverable error Sync abort

对于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

CSAPP(8)Exception Control Flow_第1张图片
如上图所示,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[]);

调用时栈结构如下
CSAPP(8)Exception Control Flow_第2张图片

getenv & setenv & unsetenv

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

#include 
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.

Number Name default action corrosponding event
1 SIGHUP terminate terminal line hangup
2 SIGINT terminate interrupt from keyboard(Ctrl-c)
3 SIGQUIT terminate quit from keyboard
4 SIGILL terminate illegal instruction
5 SIGTRAP terminate & dump core trace trap
6 SIGABRT terminate & dump core abort signal from abort function
7 SIGBUS terminate bus error
8 SIGFPE terminate & dump core floating point exception
9 SIGKILL terminate kill program
10 SIGUSR1 terminate user define signal
11 SIGSEGV terminate invalid memory reference(seg falut)
12 SIGUSR2 terminate user define signal
13 SIGPIPE terminate wrote to a pipe with no reader
14 SIGALRM terminate timer signal from alarm function
15 SIGTERM terminate software termination signal
16 SIGSTKELT terminate stack fault on coprocessor
17 SIGCHLD ignore a child process has stopped or terminated
18 SIGCONT ignore continue process if stopped
19 SIGSTOP stop until next SIGCONT stop signal not from terminal
20 SIGTSTP stop until next SIGCONT stop signal from terminal(Ctrl-z)
21 SIGTTIN stop until next SIGCONT background process read from terminal
22 SIGTTOU stop until next SIGCONT backgroup process wrote to terminal
23 SIGURG ignore urgent condition on socket
24 SIGXCPU terminate cpu time limit exceeded
25 SIGXFSZ terminate file size limit exceeded
26 SIGVTALRM terminal virtual timer expored
27 SIGPROF terminal profiling timer expired
28 SIGWINCH ignore window size changed
29 SIGIO terminate IO now possible on a discriptor
30 SIGPWR terminate power 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后从中断的指令的下一条继续执行
CSAPP(8)Exception Control Flow_第3张图片
下面具体说下这两个阶段的处理

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 
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 
//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 

//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 

//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

你可能感兴趣的:(底层知识,signal,exception,longjmp,process,group,ECF)