信号是软中断,提供了一种异步事件处理的办法。
fork后子进程继承父进程信号处理方式,但是exec后信号处理方式消失。
当执行一个程序时,所有信号的状态都是系统默认或忽略,通常所有信号都被设置为他们的默认动作,除非调用exec的进程忽略该信号,确切的来说,exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号状态不变(一个进程原先要捕捉信号,当其执行一个新程序后,就不能再捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序中无意义)。
不可靠的信号
进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。信号可能丢失,因此,Linux下的不可靠信号问题主要指的是信号可能丢失。
可靠的信号
当对信号采取了某种动作,就是向进程递送了一个信号。在信号产生和递送之间的时间间隔内,称信号是未决的。
如果为进程产生了一个阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决的,直到该进程对此信号解除阻塞或将对此信号的动作更改为忽略,内核在递送一个原来被阻塞的信号给进程时才决定对他的处理方式,于是进程在信号递送给他之前仍可改变对该信号的动作。
如果在进程解除对某个信号的阻塞之前,这种信号递送了多次,则称信号是排队的。
信号集
不同的信号编号可能超过一个整型量所包含的位数,所以一般不能用整型量中的一位代表一种信号,也就是不能用一个整型量代表一个信号集。
所用应用程序在使用信号集前,要对该信号集调用sigemptyset或sigfillset一次, 因为c编译程序将不赋初值的外部变量和静态变量都初始化为0,而这是否与给定系统上的信号集的实现相对应并不清楚。
没有信号编号为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
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 handler(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
其它一些信号操作
#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 );
set参数指定需要等待的信号的集合。我们可以简单地将其指定为在第一步中创建的信号掩码,表示在该线程中等待所有被屏蔽的信号。参数sig指向的整数用于存储该函数返回的信号值。sigwait 成功时返回0,失败则返回错误码。一旦sigwait 正确返回,我们就可以对接收到的信号做处理了。很显然,如果我们使用了sigwait,就不应该再为信号设置信号处理函数了。这是因为当程序接收到信号时,二者中只能有一个起作用。
统一事件源
信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(前面提到过,为了避免一些竞态条件,信号在处理期间,系统不会再次触发它)太久。一种典型的解决方案是:把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道的写端写人信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据可读呢?这很简单,我们只需要使用1O复用系统调用来监听管道的读端文件描述符上的可读事件。如此来,信号事件就能和其他I/O事件样被处理,即统一事件源。
很多优秀的IO框架库和后台服务器程序都统一处理信号和IO事件,比如Libevent I/O框架库和xinetd超级服务。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in. h> #include <arpa/inet.h> #include <assert. h> #include <stdio.h> #include <signal.h> #include <unistd.h> #include <errno.h> #include <stiing.h> #include <fcntl.h> #include <sys/typdes.h> #include <pthread.h> #include <sys/epoll.h>const int MAX_EVENT_NUM=1024; static int pipefd[2];int setNoBlocking(int fd) {int oldOption=fcntl(fd,FD_GETFL);int newOption=oldOption|O_NONBLOCK;fcntl(fd,F_SETFL,newOption);return oldOption; } void addFd(int epollFd,int fd) {epoll_event event;event.data.fd=fd;event.events=EPOLLIN|EPOLLET;epoll_ctl(epollFd,EPOLL_CTL_ADD,fd,&event);setNoBlocking(fd); } //信号处理函数 void sig_handle(int sig) {//保留原来的errno,在函数最后恢复,以保证函数的可重入性int sigErrno=errno;int msg=sig;send(pipefd[1],(char *)&msg,1,0);errno=sigErrno;return ; } void addSig(int sig) {struct sigaction sa;memset(&sa,'\0',sizeof(sa));sa.sa_handler=sig_handle;sa.sa_flags|=SA_RESTART;sigfillset(&sa.sa_mask);assert(sigaction(sig,&sa,NULL)!=NULL);return ; }int main() {//...int res=socketpair(PF_UNIX,SOCK_STREAM,0,pipefd);assert(res!=-1);setNoBlocking(pipefd[1]);addFd(pipefd[0]);addSig(SIGHUP);addSig(SIGCHLD);addSig(SIGTERM);addSig(SIGINT);bool stopServer=false;while(!stopServer){int num=epoll_wait(epollfd,events,MAX_EVENT_NUM,-1);if((num<0)&&(errno!=EINTR)){perror("epoll failure");break;}for(int i=0;i<num;++i){int sockfd=events[i].data.fd;if(sockfd==listenfd){struct sockaddr_in cli;int len_cli=sizeof(cli);int connfd=accept(listenfd,(struct sockaddr *)&cli,&len_cli);addFd(epollfd,sockfd);}else if((sockfd==pipefd[0])&&(events[i].events&EPOLLIN)){char buff[1024];int sig;res=recv(pipefd[0],buff,sizeof(bufff),0);if(res==-1){continue;}else if(res==0)continue;else{//每个信号占一个字节,所以按字节来接受信号,for(int i=0;i<res;++i){switch(buff[i]){case SIGCHLD:case SIGHUP:{continue;}case SIGTREAM:case SIGINT:{stopServer=true;}default:break;}}}else{// }}}}printf("close fds\n");close(listenfd);close(pipefd[0]);close(pipefd[1]);return 0; }
SIGHUP
当挂起进程的控制终端时,SIGHUP将被触发,对于没有控制终端的网络后台程序而言,通常利用SIGHUP来强制服务器重读配置文件。strace课跟踪调试时系统调用收到和信号。
SIGPIPE
默认情况下,往一个读端关闭的管逍或socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。引起SIGPIPE信号的写操作将设置ermo为EPIPE。我们可以使用send兩数的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号。在这种情况下,我们应该使用send的数反锁的ermo值来判断管道或者socket连接的读端是否已经关闭。此外,我们也可以利用10复用系统调用来检测管道和sockcer连接的读端是否已经关闭。以pol为例,当管道的读端关闭时,写端文件描述符上的PLLHUP事件将被触发:当socket连接被对方关团时,socket 上的POLLRDHUP事件将被触发。
SIGURG
使用此信号接收外带数据