异常
控制流突变,用来响应处理器的某些变化。处理器中,状态编码为不同的位和信号,状态变化称为事件,处理器检测到有事件发生时,他会通过一张叫异常表的跳转表,进行间接调用。
系统中的每个异常都有一个异常号,当系统启动时,操作系统分配和初始化一张称为异常表的跳转表,当处理器检测到一个事件发生时,处理器触发异常,通过异常表目k转到相应的处理程序。
异常类别
信号
- 发送信号。内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。
- 接受信号。当目的进程被内核强迫以某种方式对信号的发送做出反应时,他就接受了信号。
fork后子进程继承父进程信号处理方式,但是exec后信号处理方式消失。
当执行一个程序时,所有信号的状态都是系统默认或忽略,通常所有信号都被设置为他们的默认动作,除非调用exec的进程忽略该信号,确切的来说,exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号状态不变(一个进程原先要捕捉信号,当其执行一个新程序后,就不能再捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序中无意义)。
一旦安装了信号函数,它便一直安装着。posix保证信号在其信号处理函数运行期间总是阻塞的。如果一个信号再被阻塞期间产生了一次或多次,那么该信号被解阻塞后通常只递交一次,也就是UNIX默认不排队。
实际执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
实时信号
其值在[SIGRTMIN,SIGRTMAX]。如果需要实时行为,必须再安装信号处理程序时给sigaction指定SA_SIGINFO。Posix要求至少提供RTSIG_MAX中实时信号,改制的最小值是8。
- 信号是排队的。同一信号产生几次就递交几次。它以先进先出的方式排队。非实时信号产生多次可能只递交一次。
- 当有多个SIGRTMIN、SIGTMAX之间解阻塞的信号排队时,较小的信号值大于较大的信号值递交。
- 当某个非实时信号递交时,他传递给信号处理程序的唯一参数是信号值。实时信号比其他信号携带更多的信息。
非实时信号
一个发出而没有被接受的信号叫做待处理信号。一个待处理信号最多只能被接受一次,内核为每个进程在pending位向量中维护着待处理信号的集合。在blocked位向量中维护着被阻塞的信号集合。只要传送了类为k的信号,就会在pending中设置第k个位,接受了类型为k的信号,内核就会清除pending第k位。pending中k只有一位,所以每种类型信号最多有一个未处理信号。
如SIGALRM、SIGINT、SIGKILL等。
中断的系统调用
早期UNIX系统的一个特性是:如果在进程执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。
慢系统调用(slow system call):此术语适用于那些可能永远阻塞的系统调用。永远阻塞的系统调用是指调用有可能永远无法返回,多数网络支持函数都属于这一类。如:若没有客户连接到服务器上,那么服务器的accept调用就没有返回的保证。
- 在读某些类型的文件时,如果数据并不存在则可能会使调用者永远阻塞(管道、终端设备以及网络设备)。
- 在写这些类型的文件时,如果不能立即接受这些数据,则也可能会使调用者永远阻塞。
- 打开文件,在某种条件发生之前也可能会使调用者阻塞(例如,打开终端设备,它要等待直到所连接的调制解调器回答了电话)。
- pause(按照定义,它使调用进程睡眠直至捕捉到一个信号)和wait。
- 某种ioctl操作。
- 某些进程间通信函数
当阻塞于慢系统调用的一个进程捕捉到某个信号且响应信号处理函数返回时,系统调用可能返回一个EINTR,有些内核能重启被中断的系统调用,有些不能(即便设置了SA_RESTART)。
自动再起动的系统调用包括:ioctl、 read、readv、write、writev、wait和waitpid。正如前述,其中前五个函数只有对低速设备进行操作时才会被信号中断。而 wait和waitpid在捕捉到信号时总是被中断。
connect返回EINTR时不能再次调用,否则会返回一个错误。此时必须调用select等待完成连接。
信号集
不同的信号编号可能超过一个整型量所包含的位数,所以一般不能用整型量中的一位代表一种信号,也就是不能用一个整型量代表一个信号集。
所用应用程序在使用信号集前,要对该信号集调用sigemptyset或sigfillset一次, 因为c编译程序将不赋初值的外部变量和静态变量都初始化为0,而这是否与给定系统上的信号集的实现相对应并不清楚。
#include <signal.h> int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set); int sigdelset(sigset_t *set); //以上四个函数若成功返回0,若失败返回-1 int sigismember(sigset_t *set);//若真:返回1,若假:返回0
信号屏蔽
一个进程的信号屏蔽字规定了当前阻塞而不能地送给该进程的信号集
int sigprocmask(int how,const sigset_t *set,sigset_t *oset) //成功:0出错:-1,错误原因存于error中
- set非空,how(决定函数的操作方式):
SIG_BLOCK:该进程新的信号屏蔽字是当前信号屏蔽字和set指向信号集的并集,set包含了希望阻塞的附加信号。不能阻塞SIGKILL和SIGSTOP
SIG_UNBLOCK:该进程新的信号屏蔽字是当前信号屏蔽字和set所指的信号集补集的交集,set包含了希望解除阻塞的信号
SIG_SETMASK:该进程新的信号屏蔽字是set所指的值。
- oset:非空,当前进程的信号屏蔽字通过oset返回。
signal
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); //signum:一个int类型的参数(即接收到的信号代码), //handler:信号处理函数,可取一下两个特殊值:① SIG_IGN 屏蔽该信号 ② SIG_DFL 恢复默认行为
当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数
但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。执行后信号注册函数signal_hander_fun失效,对SIGINT信号的处理回到操作系统的默认处理方式,当应用进程再次收到SIGINT信号时,会按操作系统默认的处理方式进行处理(即不再执行signal_hander_fun处理函数)
#define SIG_ERR (void (*)())-1 #define SIG_DEL (void (*)())0 #define SIG_IGN (void (*)())1
函数原型
void (*signal(int signo,void (*func)(int))) (int) typedef void Sigfuc(int);//这里可以看成一个返回值 //再对signal函数进行简化就是这样的了 Sigfunc *signal(int,Sigfuc *);
sigaction
每个信号都有与之关联的处置,称为信号的行为(action),可以通过sigaction设定一个信号的行为。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //成功返回0,失败返回-1,并设置errno
- signum:要操作的信号
- act :要设置的对信号的新处理方式
- oldact :原来对信号的处理方式
struct sigaction { void (*sa_handler)(int);/*信号处理函数指针,与single一样,如果指定某个信号的行为为默认或忽略该信号, 则sa_handler设置为SIG_DFL或SIG_IGN,并不设置SA_SIGINFO*/ sigset_t sa_mask;//在调用该信号捕捉函数前,这一信号集要加到新进程的信号屏蔽字中,仅当从信号捕捉函数返回时再将进程的 //信号屏蔽字恢复为原先值,该信号处理函数被调用时,os建立的信号屏蔽字包括正在被递送的信号,因此可保证 //在处理一个给定的信号时,如果这种信号再次发生,那么他会阻塞到对前一个信号的处理结束为止 int sa_flags; //信号处理方式,如果指定新的信号安装机制,处理函数被调用的时候,不但可以得到信号编号, //而且可以获悉被调用的原因以及产生问题的上下文的相关信息,函数指针的三个参数含义为 void (*sa_sigaction)(int iSignNum, siginfo_t* pSignInfo, void *);//一个代替的信号处理程序,在sigaction结构中使用了 //SA_SIGINFO标志时,使用该信号处理程序,对于sa_sigaction和sa_handler两者实现可能使用同一存储区,所以应用只能使用一次这两个字段 void (*sa_restorer)(void); //网上找的资料说明都是说(保留,占时无用) }; siginfo_t { int si_signo; /* 信号值,对所有信号有意义 */ int si_errno; /* errno 值,对所有信号有意义 ,if nonzero ,errno value form <errno.h>*/ int si_code; /* 信号产生的原因,对所有信号有意义 SI_ASYNCIO-信号由某个异步I/O请求完成, 这些异步I/O请求是posix的aio_XXX,SI_MESGQ-信号在有个消息被放置到空消息队列中产生, SI_QUEUE-信号由sigqueue函数发出,SI_TIMER-信号由使用timer_settime函数设置的某个定时器 产生,SU_USER-信号由kill发出,如果信号由其他事件产生,这里设置成不同于这里所列的值 但是si_value只有si_code的值设置成这里所列的值时才有意义*/ pid_t si_pid; /* 发送信号的进程ID */ uid_t si_uid; /* 发送信号进程的真实用户ID */ int si_status; /* 对出状态,对SIGCHLD 有意义 exit value or signal number*/ void *si_addr; /* 触发fault的内存地址,对SIGILL,SIGFPE,SIGSEGV,SIGBUS 信号有意义 */ int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */ clock_t si_utime; /* 用户消耗的时间,对SIGCHLD有意义 */ clock_t si_stime; /* 内核消耗的时间,对SIGCHLD有意义 */ union sigval si_value; /* 信号值,对所有实时有意义,是一个联合数据结构,可以为一个整数(由si_int标示,也可以为一个指针,由si_ptr标示) */ int si_int; /* POSIX.1b signal */ void *si_ptr; /* POSIX.1b signal */ int si_overrun; /* Timer overrun count; POSIX.1b timers */ int si_timerid; /* Timer ID; POSIX.1b timers */ long si_band; /* 对SIGPOLL信号有意义 */ int si_fd; /* 对SIGPOLL信号有意义 */ short si_addr_lsb; /* Least significant bit of address(since kernel 2.6.32) */ }; /* 1.若信号是SIGCHLD,则设置si_pid,si_status和si_uid字段 2.若信号是SIGBUS,SIGILL,SIGFPE或SIGSEGV,则si_addr包含造成故障的根源地址,该地址可能并不正确 3.si_errno包含错误编号,他对应造成信号产生条件,并由实现定义 */ union sigval { int sival_int; void *sival_ptr; }; //在递送信号时sigval_int传递一个整数或sival_ptr传递一个指针
1.通常按下列方式调用信号处理程序
void handler(int signo);
2.但是如果设置了SA_SIGINFO标志,那么按下列方式调用
void sa_action(int signo,siginfo_t *info,void *context); /* context是无类型指针,可被强制转换为ucontext_t结构类型,该结构表示信号传递时进程的上下文,至少包含以下字段 */ ucontext_t *un_link;//pointer to context resumed when this context returns sigset_t uc_sigmask;//signal blocked when this context is active stack_t uc_stack;//stack used by this context mcontext_t uc_mcontext;//machine-specific representation of saved context //uc_stack描述了当前上下文使用的栈,至少包括 void *ss_sp;//stack base or pointer size_t ss_size;//stack size int ss_flags;//flags
SA_RESTART标志是可选的,如果设置,相应信号中断的系统调用将由内核自动重启。如果信号不是SIGALRM且SA_RSTART有定义,我们就设置该标志(因为SIGFALRM目的在于:通常是为I/O操作超时设置,这时我们希望受阻塞的系统调用被该信号中断掉,并且定义了SA_INTERRUPT,如果定义了该标志,在被捕获的信号是SIGALRM时设置它)。
其它一些操作
#include <signal.h> int sigpending(sigset_t *set);//返回调用进程中阻塞信号不能递送的,而且也一定是未决的 //成功返回0失败返回-1 #include <setjmp.h> int sigsetjmp(sigjmp_buf env,int savemask); //若直接调用返回0,若从siglongjmp调用返回,返回非0 //savemask保存了当前进程的屏蔽字,调用siglongjmo时,如果带非0 savemask的sigsetjmp调用已经保存了env,则siglongjmp //从中恢复保存的信号屏蔽字 void siglongjmp(sigjmp_buf env,int val);
如果希望对一个信号解除阻塞,然后调用pause以等待以前被阻塞的信号发生,则:
sigset_t newmask,oldmask; sigemptyset(&newmask); sigaddset(&newmask,SIGINT); /*block SIGINT and save current signal mask*/ if(sigpromask(SIG_BLOCK,&newmask,&oldmask)<0) err_sys("SIG_BLOCK error"); /*critical region of code*/ /*restore signal mask,which unblocks SIGINT*/ if(sigpromask(SIG_SETMASK,&oldmask,nullptr)<0)//$$ err_sys("SIG_SETMASK error"); /*windows is open*/ pause();//wait for signal to occur//$$ /*continue processing*/
在两个$$之间有时间窗口,如果在信号阻塞时,产生了信号,那么信号的递送在解除阻塞时,该信号好像发生在接触对SIGINT阻塞和pause之间,如果发生这种情况或在解除阻塞和pause之间确实发生了信号,那么就会产生问题,因为再也看不见该信号,从某种意义上来讲,在此时间窗口发生了信号丢失,就是的pause永远阻塞,这也是不可靠信号机制的另一个问题
#include <signal.h> int sigsuspend(const sigset_t *sigmask); //他返回给调用者,并总是返回-1并将errno设置为EINTR,没有成功返回值, //在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起,如果捕捉到一个信号并从该信号处理程序 //中返回,sigsuspend返回并且该进程的信号屏蔽字设置为调用sigsuspend之前的
sigwait
同步的等待一个异步事件。使用了信号但没有涉及信号处理程序。
#include <signal.h> int sigwait( const sigset t* set, int* sig );
调用sigwait前,阻塞某个信号集,由set指定。sigwait一直阻塞到这些信号中有一个或多个待处理,这时它返回其中一个信号。该信号值通过sig指针存放。sigwait 成功时返回0一旦sigwait 正确返回,我们就可以对接收到的信号做处理了。
如果我们使用了sigwait,就不应该再为信号设置信号处理函数了。这是因为当程序接收到信号时,二者中只能有一个起作用。
- SIGHUP:当挂起进程的控制终端时,SIGHUP将被触发,对于没有控制终端的网络后台程序而言,通常利用SIGHUP来强制服务器重读配置文件。strace跟踪调试时系统调用收到和信号。
- SIGPIPE:默认情况下,往一个读端关闭的管逍或socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程。引起SIGPIPE信号的写操作将设置ermo为EPIPE。我们可以使用send的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号。在这种情况下,我们应该使用send的数反锁的ermo值来判断管道或者socket连接的读端是否已经关闭。此外,我们也可以利用I/O复用系统调用来检测管道和sockcer连接的读端是否已经关闭。以poll为例,当管道的读端关闭时,写端文件描述符上的PLLHUP事件将被触发:当socket连接被对方关团时,socket 上的POLLRDHUP事件将被触发。
- SIGURG:使用此信号接收外带数据
- SIGKILL和SIGSTOP:不能被捕获。
SIGEV_THREAD
当使用aio_read或aio_write时初始化一台异步设备读或写时,程序员指定一个struct aiocb,其中包含了一个struct sigevent成员,接受struct sigevent的其他函数包括timer_create(他创造一个进程范围内的定时器)和sigqueue(他将信号送入进程队列)。
struct sigevent提供了一种允许程序员指定一个信号是否产生以及如果产生应该使用时什么信号数字的“通知机制”。pthreads增设了一个被称为SIGEV_THREAD的新通知机制,该通知机制使得信号通知函数向线程气势函数一样运行。
SIGEV_THREAD通知函数可能无法在一个新的线程中实际运行。系统可以排队SIGEV_THREAD事件,在一些内部“服务线程”连续调用起始函数,这种差别对于应用程序而言很难有效的区分,使用系统服务线程要很小心的为通知线程指定属性——调度策略,优先级,竞争范围,最小栈空间。
对于传统的信号机制如:setitimer,SIGCHLD,SIGINT等,SIGEV_THREAD特性不可用。
timer_t timer_id; struct itimerspec ts; struct sigevent se; se.sigev_notify=SIGEV_THREAD; se.sigev_value.sival_ptr=&timer_id; se.sigev_notify_function=timer_fun;//一个指向线程起始函数的指针 //期望的线程创建属性的线程属性对象---pthread_attr_t,如为空,则与将deatchstate属性设置为PTHREAD_CREATE_DETACHED一样 //创建通知线程,这样避免内存泄漏,因为线程标识符不是对任何其他线程来讲都可得到所以说PTHREAD_CREATE_JOINABLE结果是不可知的 se.sigev_notify_attributes=NULL; ts.it_value.tv_sec=5; ts.it_value.tv_nsec=0; ts.it_interval.tv_sec=5; ts.it_interval.tv_nsec=0; timer_create(CLOCK_REALTIME,&se,&timer_id); timer_settime(timer_id,0,&ts,0);
一些常见的信号
可重入函数
在信号处理程序中保证安全调用的函数,这些函数是可重入的并称为异步信号安全的,除了可重入以外,在信号处理操作期间,他会阻塞任何引起不一致得信号发送。由于每个线程只有一个errno,所以信号处理程序会修改其原先的值,所以在调用信号处理程序时,应先保存errno,调用后恢复errno。
- 它只访问局部变量
- 它不能被信号处理程序中断
所有的I/O函数和pthread_XXX函数都不可在信号处理程序中调用,Unix网络编程2中所有的IPC函数只有sem_post,read,write(read和write只作用于管道和FIFO时)可以
不可重入的原因:
- 已知它们使用静态数据结构
- 它们调用malloc和free因为malloc通常会为所分配的存储区维护一个链接表,而插入执行信号处理函数的时候,进程可能正在修改此链接表。
- 它们是标准IO函数。因为标准IO库的很多实现都使用了全局数据结构
一个信号处理器中中能在调用的函数
不可重入
- 函数体内使用了静态的数据结构;
- 函数体内调用了malloc()或者free()函数;
- 函数体内调用了标准I/O函数。
很多标准I/O库的实现都以不可重入使用全局数据结构,在信号处理程序中调用printf不一定得到可期望的结果,信号处理程序可能中断主程序的printf函数调用。
把不可重入写成可重入的唯一方式是利用可重入的方式来写,保证中断是安全的。如果一定要使用全局变量要使用互斥量将它保护起来。
中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。
__interrupt double compute_area (double radius) { double area = PI * radius * radius; printf(" Area = %f", area); return area; }
- ISR 不能返回一个值。
- ISR 不能传递参数。
- 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
- printf()经常有重入和性能上的问题。