当在执行 I c u r r I_{curr} Icurr指令发生Exception时(可能由于 I c u r r I_{curr} Icurr引起,也可能是外部引起),会进入Exception handler,当处理Exception被解决后会有三种方式:
由processor和os kernel的 设计人员指定了一些exception number,大家把各种异常归类到exception table里,而这个table的首地址存在CPU里的exception table base register里,当开机时会初始化exception table,里面的每一行对应一个exception number,而里面存放的数据则是用于处理异常的代码的地址.下面分别是静态图和运行时的示例
当handler处理完之后可能会返回,那么就执行return from interrupt指令来恢复原状,同时也返回了user mode.也能不返回了…
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
如上图所示,processor被三个process公用,其PC的指向是会跳来跳去的,但是对于logical control flow而言,认为是顺序的.
对于两个process如果时间上有重叠,则是concurrent,如果同时在两个不同的core上运行,则是parallel
运行时内存分配如下图
对于block的system call而言,会发生context switch,但是对于noblock的system call而言,也可能发生context switch,这个由kernel的心情决定
在发生异常时(包括context switch),缓存会失效,这时称为cache pollution
一般而言,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();
获取自己的pid
获取parent的pid
退出该进程,可传入参数status
当child process结束后进入zombie状态,等待parent process的reap,如果parent结束时仍然没有reap,那么kernel就会安排init这个process(PID=1)来做这件事
#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的修改彼此互不影响
等待指定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函数的返回是还需要继续睡眠的时间,返回0当然是表示时间到了,但是有的时候进程会收到一些signal,那么sleep也会返回,这个时候返回值是还需要睡多长时间.
一直睡直到收到signal
执行指定的文件(永远不会返回),签名如下
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[]);
这三个函数用于获取/设置/取消设置上面提到的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);
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 |
signal的处理分为两步:
当进行了第一步而没有第二步时,这个signal处于pending signal状态.如果处于这种状态则再收到signal的时候会丢弃新的signal(no queue),另外process还可以block特定的signal
对于第二步而言逻辑大致如下:接收signal后调用signal handler,当siganl handler执行return后从中断的指令的下一条继续执行
下面具体说下这两个阶段的处理
每一个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);
根据default action有以下几种
#include
typedef void (*sighandle_t)(int);
//return previous handler if OK,SIG_ERR on error
sighandle_t signal(int signum,sighandler_t handler);
另外系统提供了SIG_IGN和SIG_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参数可以是以下三种取值:
文中举了下面的例子将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都有可能先被调用
#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);
}
#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);
}
strace
ps
top
pmap
/proc