现在的位置: 首页 > 综合 > 正文

APUE_信号

2014年01月03日 ⁄ 综合 ⁄ 共 7072字 ⁄ 字号 评论关闭
文章目录

1.引言

      
信号是软件中断,提供了一种处理异步事件的方法。每个信号都有一个名字,以字符SIG开头,定义为正整数。很多条件可以产生信号:

1、当用户按某些终端键时,引发终端产生的信号,如中断(Ctrl+CSIGINT)、退出(Ctrl+/SIGQUIT)和挂起(Ctrl+ZSIGTSTP)键。

2、硬件异常产生信号,如除数为0、浮点溢出(SIGFPE),无效的内存引用(SIGSEGV)等。

3、子进程退出的时候向父进程发送SIGCHLD

4、进程调用kill函数可将信号发送给另一个进程或进程组。

5、用户可用kill命令将信号发送给其它进程。常用此命令终止一个失控的后台进程。

6、当检测到某种软件条件已经发生,并应将其通知有关时也产生信号,如进程所设置的定时器到期时产生SIGALRM

      
当某个信号出现时,可以以三种方式之一进行处理:

      
忽略信号(SIG_IGN)。有两种信号(SIGKILLSIGSTOP)不能被忽略,它们向超级用户提供了使进程终止或停止的可靠方法。

      
捕捉信号(程序员为某信号注册处理函数)。为做到这一点,要通知内核在某种信号发生时调用一个用户函数。不能捕捉SIGKILLSIGSTOP信号。

      
执行系统默认动作(SIG_DFL)。针对大多数信号的系统默认动作是终止进程。

2.signal函数

       signal是信号注册函数,对一个信号,注册一个处理函数,当然,该函数可以是SIG_IGN\SIG_DFL

#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
				//成功返回信号以前的配置(所以返回值为函数指针),出错返回SIG_ERR

3.可重入函数

       信号处理程序就是一个中断,然后进程停止执行原来的代码,转而执行信号处理程序。可以看做信号处理程序和进程主程序是并行的,对于并行程序,互斥很重要!!!

       如果主程序在调用mallloc的时候中断,并且信号处理程序也调用malloc会发生什么?可能会对进程造成破坏。为什么?因为malloc为其所分配的存储区维护一个链表,而信号中断的时候进程可能正在修改这个链表,而信号处理程序又调用malloc。这就是没有实现互斥!

       当然,互斥经常是用来保护共享对象的,所以如果进程使用静态数据结构(在静态存储区存储,比如全局变量),那么可能返回进程之后,进程所读静态数据结构的值已经被信号处理函数修改。

       还有如果调用IO库函数的话也可能出错,因为IO库中的很多实现都以不可重入方式使用全局数据结构。

       综上有三种情况使得函数成为不可重入函数:1、使用静态数据结构(全局);2、调用malloc或者free3、标准IO函数。

      
注意:这里只是用信号处理函数来说明(不)可重入函数,其实宽泛来讲是对中断来说的(信号处理就是一种中断)。

      
规避上述3点导致函数为不可重入函数的原因就可以写出可重入函数!!!

4.可靠信号术语和意义

       产生一个信号,并向进程发送称为向进程递送了一个信号。信号产生和递送之间的时间间隔内,信号是未决的。

       进程可以选用信号递送阻塞(sigprocmask),如果对这个信号的处理动作是系统默认或者捕捉(而不是忽略),那么系统会记下这个信号,等到进程对其解除阻塞或者处理动作更改为忽略。只要进程没有处理这个信号,在阻塞阶段信号依然是未决的,此时可以更改处理动作。

       如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那么将如何呢?支持POSIX.1实时扩展的系统会对信号进行排队,其他系统只会递送此信号一次。linux/unix默认是不排队的!!!

5.kill和raise函数

       kill函数将信号发送给进程或进程组,raise函数则允许进程向自身发送信号。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
			//成功返回0,出错返回-1
//调用raise(signo);等于调用kill(getpid(), signo);
//kill的pid有4种不同的情况:pid>0;pid==0;pid<0;pid==-1;具体见APUE

6.alarm和pause函数

      
使用alarm函数可以设置一个计时器,在将来某个指定的时间该计时器会超时。当计时器超时时,产生SIGALRM信号。如果不忽略或捕捉该信号,则其默认动作是终止调用该alarm函数的进程。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
				//返回值:0或以前设置的闹钟时间的秒数

 

       进过指定的secondes的描述,就会产生SIGALRM信号,这个信号是内核发送的。注意,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需一些时间。

      
每个进程只能有一个闹钟时钟,但是可以多次设置。当设置的时候,以前的闹钟还没有超时,那么返回值就是以前设置闹钟时间所剩余的秒数,并且闹钟被重置。如果本次的设置值为0,那么就取消以前的闹钟。

       pause函数是调用进程挂起直至捕捉到一个信号。经常程序的最后一句用pause,此时程序已经执行完,所以信号可以由终端来发出,比如Ctrl+C

#include <signal.h>
int pause();
	//返回-1,并将errno设置为EINTR

 

      代码示例:用alarmpause函数来实现sleep函数

//这个函数有缺陷,主要的问题就在于longjmp,可能使得其他信号处理函数提前终止,
//如果其他信号处理函数需要的时间超过sleep的时间,因为longjmp跳过了程序栈帧
//如果改用siglongjmp就好了,在信号处理中要使用siglongjmp
#include <signal.h>
#include <unistd.h>
#include <setjmp.h>
static jmp_buf env_alrm;//记录跳跃的信息
static void sig_alrm(int signo)
{
	longjmp(env_alrm,1);    //信号处理函数中需要使用sigsetjmp和siglongjmp
}
unsigned int sleep2(unsigned int seconds)
{
	signal(SIGALRM,sig_alrm);  //错误处理省略了
	if(setjmp(env_alrm)==0)     //用longjmp就是为了避免alarm和pause之间的竞争
	{
		alarm(senconds);		 //设置返回点的时候返回0,执行函数体,跳会setjmp的时		pause();				 //longjmp回返回点的时候返回非0值
	}
	return(alarm(0));			 //记得返回前将闹钟清除
}

 

7.信号集

7.1信号集操作函数

      
需要一个类型来表示信号的集合,以便某些函数运用这种数据结构。但是信号的数目一般比整形的位数大(当然只要可能大,在设计的时候就必须考虑),所以系统定义了sigset_t类型来表示信号集,以下是处理信号集的一些函数:

#include <signal.h>
int sigemptyset(sigset_t *set);//初始化信号集,清除所有信号
int sigfillset(sigset_t *set);//初始化信号集,使其包含所有信号
int sigaddset(sigset_t *set, int signo);//增加信号signo
int sigdelset(sigset_t *set, int signo);//清除信号signo
					//成功返回0,出错返回-1
int sigismember(const sigset_t *set, int signo);//判定信号signo是否属于信号集set
					//若真返回1,假返回0,出错返回-1

7.2sigprocmask函数

      
信号屏蔽字就是规定,进程现在对信号屏蔽字内的信号不处理,这些信号是不会递送给进程的。sigprocmask函数可以检测和更改信号屏蔽字:

#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
					//成功返回0,出错返回-1
//若oset非空,那么当前的信号屏蔽字由oset返回,若set非空那么根据类型how指示如何
//修改当前信号屏蔽字。how的类型可以为SIG_BLOCK这个操作为或,当前信号屏蔽字增//加set所指示的信号;SIG_UNBLOCK,解除当前信号屏蔽字中set指示的信号;//SIG_SETMASK用set赋值代替当前信号屏蔽字

       注意:之前的五个函数指示对信号集进行操作,与进程无关。而sigprocmask是与进程有关的。

7.3sigpending函数

       sigpending返回信号集合,这些信号是调用进程当前阻塞的,通过值参数set返回。

#include <signal.h>
int sigpending(sigset_t *set);
		//成功返回0,出错返回-1

程序示例:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static void sig_quit(int signo);
int main(void)
{
    sigset_t newmask,oldmask,pendmask;
    if(signal(SIGQUIT,sig_quit)==SIG_ERR)
        printf("regiset error");
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGQUIT);
    sigprocmask(SIG_BLOCK,&newmask,&oldmask);
    sleep(5);
    sigpending(&pendmask);
    if(sigismember(&pendmask,SIGQUIT))
        printf("\nSIGQUIT pending\n");
    if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        printf("SIGQUIT unblocked\n");
    sleep(5);
    return 0;
}
static void sig_quit(int signo)
{
    printf("Caught SIGQUIT\n");
    if(signal(SIGQUIT,SIG_DFL)==SIG_ERR)
        printf("Can't reset SIGQUIT\n");
}

 

8.sigaction函数

       sigaction函数的功能是检查或修改与指定信号相关联的处理动作。这个函数已经取代了signal函数。不过现在的signal函数内部是由sigaction函数实现的。

      
首先了解sigaction结构:

struct sigaction
{
	void (*sa_handler)(int);    //信号处理函数
	sigset_t 	sa_mask;
	int 	 	sa_flags;
	void		(*sa_sigaction)(int, siginfo_t *, void *);  
}

       当更改信号动作时,如果sa_handler包含信号捕捉函数的地址(与常量SIG_IGNSIG_DFL相对),则sa_mask字段说明了一个值,在调用信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号处理函数返回时再将该信号屏蔽字恢复。这样在调用信号处理函数时就能阻塞某些信号。在信号处理程序被调用时,操作系统建立的新信号屏蔽字包括正被递送的信号。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么其会被阻塞到对前一个信号的处理结束为止。

       sa_flags字段涉及一些应用,具体参考APUE

       sa_sigaction字段是一个替代的信号处理程序,只有在sa_flags中使用SA_SIGINFO标志时才使用。

代码示例:用sigaction函数实现signal

#define void(*)(int) Sigfunc
Sigfunc*//这个是定义的处理函数的宏
signal(int signo,Sigfunc *func)
{
	struct sigaction act,oact;
	act.sa_handle=func;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;//先将标志清0
	if(signo==SIGALRM)
	{
#ifdefine	SA_INTERRUPT             
		act.sa_flags |= SA_INTERRUPT;
#endif
	}else{
//除非说明SA_RESTART标志,否则用sigaction设置的信号处理程序,使得程序不重启动
#ifdefine SA_RESTART        //除SIGALARM以外的信号都尝试重启动
		act.sa_flags |= SA_RESTART;
#endif
}
	if(sigaction(signo,&act,&oact)<0)
		return SIG_ERR;
	return oact.sa_handler;
}

        所以在linux内部实现signal函数是用的sigaction,并且默认使用重启动。然后提供另外一个函数signal_intr,这个函数默认不使用重启动:

Sigfunc*//这个是定义的处理函数的宏
signal_intr(int signo,Sigfunc *func)
{
	struct sigaction act,oact;
	act.sa_handle=func;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;//先将标志清0
#ifdefine	SA_INTERRUPT             
		act.sa_flags |= SA_INTERRUPT;
#endif
}
	if(sigaction(signo,&act,&oact)<0)
		return SIG_ERR;
	return oact.sa_handler;
}

          综上一般直接调用signal或者signal_intr函数设置中断处理函数就可以了。

9.sigsetjmp和siglongjmp函数

     
信号处理程序经常调用longjmp函数以返回到程序的主循环中,而不是从该处理程序返回。但是调用longjmp有一个问题。当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加到进程的信号屏蔽字中。如果longjmp跳出处理程序,那么对此进程的信号屏蔽字会发生什么?Unix并没有说明longjmp对信号屏蔽字的说明。说以为了确保恢复信号屏蔽字,unix提供了另外两个函数sigsetjmpsiglongjmp这两个函数,确保返回后恢复原来的信号屏蔽字。

#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
			//返回值与setjmp类似,直接调用返回0,若从siglongjmp返回则返回非0值
void siglongjmp(sigjmp_buf env, int val);


 

     
这两个函数与setjmplongjmp函数唯一的区别就是sigsetjmp增加了一个参数。如果savemask0,则sigsetjmpenv中保存进程的当前信号屏蔽字,调用longjmp的时候如果带非0savamaksigsetjmp调用已经保存了env,则siglongjmp从中恢复信号屏蔽字。

10.sigsuspend

     
如果希望对一个信号解除阻塞,然后pause以等待以前被组设的信号发生,会如何?看下面一段程序:

sigset_t newmask,oldmask;
sigemptyset(&newmask);
sigaddset(&newmask,SIGINT);//信号为SIGINT
/*阻塞信号SIGINT*/
sigprocmask(SIG_BLOCK,&newmask,&oldmask);

/*critical region of code*/

sigprocmask(SIG_SETMASK,&oldmask,NULL);
//自此不再组设SIGINT,并调用pause等待信号
pause();

      对于上面这段程序,解除SIGINT阻塞和pause之间的时间内如果信号发生,那么pause永远也等不到信号。换句话说解除阻止和进程休眠需要时一个原子操作。

     
sigsuspend函数提供了这个功能:

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
				//返回值-1,并将errno设置为EINTR

      sigsuspend用于在接收到某个信号之前,临时用sigmask替换进程的信号掩码,并暂停进程执行,直到收到信号为止。也就是说,sigsuspend后,进程就挂在那里,等待着开放的信号的唤醒。系统在接收到信号后,马上就把现在的信号集还原为原来的,然后调用处理函数。

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

【上篇】
【下篇】

抱歉!评论已关闭.