以前读apue的时候,和信号有关的函数好像都写过验证代码。但是没有记录下来,这几天写服务器代码,考虑到异步信号处理的细节才把信号这部分再拿出来看看,并且记录下来。
本文内容来源三个途径:apue,网络,自己的一些了解,有不对的地方,大家多指出。
信号时一种软件中断,用于处理异步事件,相对于主进程,它是异步的,所以要合理处理信号与主进程的协调关系。
一 信号有三种处理方式:
1 忽略,大多数信号可以采用这种方式进行处理,但是有两种信号不可以:SIGKILL和SIGSTOP。因为这2个信号向超级用户提供使进程终止或停止的可靠方法。另外,如果忽略由硬件异常产生的信号(如内存越界,非法操作内存或除以0),则进程的运行会进入不定态。
2 捕获,为了抓住这个信号,需要布置抓捕现场。这就是signal或者sigaction函数的作用。这2个函数告诉内核,当某个信号到来时,要执行一个用户函数,再用户函数里,可以执行和这个信号有关联的事件。这里SIGKILL和SIGSTOP不能抓捕,也就是说这两个信号只能执行默认动作。
3 默认动作,针对大多数信号的系统默认动作时终止进程。
二 信号与程序启动
当执行一个程序时,所有信号的状态都是系统默认或忽略。新程序的执行,是fork函数创建新进程,新进程调用exec函数以执行另一个程序。当进程调用exec时,该进程执行的程序完全替换为新程序,新程序则从其main函数开始执行。调用exec并不创建新进程,所以调用exec前后的进程ID不变。exec只是用一个全新的程序替换了当前进程的正文,数据、堆和栈。所以exec执行后,即使原先设置为捕捉的信号也被修改为它们的默认动作。(对于一个进程原先要捕捉的信号,当执行新程序后,信号捕捉函数的地址在所执行的新程序文件当中已无意义,就自然不能捕捉它了)
当进程调用fork时,其子进程继承父进程的信号处理方式,因为子进程在开始的时候复制了父进程的存储映象,所以信号捕捉函数地址在子进程当中是有意义的。
三 信号与可重入函数
在信号中断函数中使用不可重入函数可能导致不可预见的问题。具体例子不做罗列。不可重入函数大概分以下三类:
1 它们使用静态数据结构
2 它们调用malloc或free
3 它们是标准io函数,标准IO很多实现都使用全局数据结构
另外:
1 信号处理程序中使用printf函数,但是不能保证产生期望的结果,信号处理程序可能中断主程序当中的printf函数。
2 每个线程只有一个errno变量,所以信号处理程序可能会修改errno原先值。所以在信号处理程序中,应该先保存,信号处理函数执行完以前再回复errno。
四 alarm和pause函数
alarm函数设置一个计时器,在将来某个时间会产生SIGALARM信号。系统默认动作是终止调用该alarm函数的进程。
pause函数使调用进程挂起直至捕捉到一个信号。这里注意,只有执行了一个信号处理函数并返回时,pause在会返回。这种情况下,pause返回-1,并置errno为EINTR。下面是一段apue上的代码:
#include
#include
static void sig_alarm(int sig)
{;}
unsigned int sleep1(unsigned int nsecs)
{
if(signal(SIGALRM, sig_alarm) == SIG_ERR)
return (nsecs);
alarm(nsecs);
pause();
return (alarm(0))
}
在调用alarm和调用pause之间有一个竞争条件。在一个繁忙的系统中,可能alarm在调用pause之前超时。(这里可以这样理解,在设置了alarm函数以后,程序转去执行一个优先级更高的软件模块,并且这个模块执行时间很长,以至于alarm到期了,这个模块还没有执行完毕。)并调用了信号处理函数。如果发生这种情况,则在调用pause以后,如果没有捕捉到其它信号,那么调用者将永远挂起
五 信号集的几个函数
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, ins signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
前四个函数的返回值,成功返回0, 失败返回-1,最后一个函数的返回值,若真返回1,若假返回0,若出错返货-1。
所有应用程序使用信号集以前,必须要对该信号集调用sigemptyset或者sigfillset函数进行初始化。
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *
restrict oset);成功返回0,失败返回-1,这个函数里的
restrict是指两个参数没有相关性,可以放心大胆的操作,别那么保守,同样的代码,使用这个限制参数和不使用这个参数,编译出来的执行速度差距很大的。
在调用sigprocmask后,如果有任何未决的,不再阻塞的信号,则在setprocmask返回前,至少会将其中一个信号递送给进程。
还有一点,必须强调一下,siprocmask是仅为单线程的进程定义的,在处理多线程的进程当中的信号屏蔽,使用的是另外的函数。
sigpending(sigset_t *set);成功返回0,失败返回-1.这个函数用于检查信号,检查什么信号呢,检查当前已经发出的,但是被阻塞不能递送的信号,也就是当前未决信号。下面是一个具体运用,这个例子是apue上的例子,我加以说明:
#include
#include
#include
static void sig_quit(int);
int main(void)
{
sigset_t newmask, oldmask, pendmask;
if(signal(SIGQUIT, sig_quit) == SIG_ERR)
{
printf("can not catch SIGQUIT");
exit(1);
}
sigemptyset(&newmask);//使用前先设置sigset_t结构
sigaddset(&newmask, SIGQUIT);
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)//设置阻塞信号
{
printf("sig_block error");
exit(1);
}
sleep(5);//在5秒内按下键盘上“ctrl” + “\”,会产生SIGQUIT信号,这时候这个信号就是已经发出,但是被阻塞,未决的信号
if(sigpending(&pendmask) < 0)
{
printf("sigpending error");
exit(1);
}
if(sigismember(&pendmask, SIGQUIT))
{
printf("\nSIGQUIT pending\n");//这里会打印信息,说明信号未决
}
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)//回复默认信号动作,执行这个程序的时候,会发现先打印信号处理函数中的内容,再打印下面的内容,从而证明了上面对sigprocmask函数的说明。
{
printf("SIG_SETMASK error");
exit(1);
}
printf("sigquit unblocked\n");
sleep(5);
exit(0);
}
static void sig_quit(int signo)
{
printf("caught SIGQUIT\n");
if(signal(SIGQUIT, SIG_DFL) == SIG_ERR)
{
printf("can not reset sigquit\n");
exit(1);
}
}
六 sigsuspend函数
更改信号屏蔽字可以阻塞所选信号,或解除对它们的阻塞。使用这种技术可以保护部希望由信号中断的代码区域。
如果希望对一个信号解决阻塞,然后pause以等待以前被阻塞的信号,如何实现呢?
sigset_t newmask, oldmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
exit(1);
/* critical region of code*/
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
exit(1);
pause();
..........
首先说明,这是个不理想的做法。如果信号发生在阻塞时,等解除阻塞时执行信号处理函数以后,pause返回,然后执行后面的程序。
pause函数:pause函数使调用进程挂起直至捕捉到一个信号,只有执行了一个信号处理函数并从其返回时,pause才返回。
如果在解除阻塞时刻和pause之间发生了信号,就丢失了。这样pause就一直挂在那等信号,这就是早期不可靠信号的机制的一个问题。
为了修正上面问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠,这个功能就是由sigsuspend函数提供的。
int sigsuspend(const sigset_t *sigmask);
这个函数在一个不可中断的过程当中执行下面过程:
将进程的信号屏蔽字设置为sigmask指向的值,然后挂起,等待特定的信号,然后捕捉到一个信号并且从信号处理程序返回,则sigsuspend返回,
并且将给进程的信号屏蔽字设置为调用sigsuspend之前的值。
这个函数没有成功返回值,它总是返回-1,并置errno为EINTR。下面是几个sigsuspend应用实例:
几个实例等下期再写吧,今天下班。
1 保护临界区不被信号中断
阅读(3086) | 评论(0) | 转发(0) |