返回
基础
分类

但是分析的很全面,最后介绍了应用层的相关处理

日期: 2020-01-02 07:54 浏览次数 : 115

linux下 signal信号机制的透彻分析与各种实例讲解,signal实例讲解

转自:

转自:

首先感谢上述两位博主的详细讲解。

虽然内容有点长,但是分析的很全面,各种实例应用基本都考虑到了。

 

本文将从以下几个方面来阐述信号:

(1)信号的基本知识

(2)信号生命周期与处理过程分析

(3) 基本的信号处理函数

(4) 保护临界区不被中断

(5) 信号的继承与执行

(6)实时信号中锁的研究

 

第一部分: 信号的基本知识

 

1.信号本质:

 

信号的本质是软件层次上对中断的一种模拟(软中断)。它是一种异步通信的处理机制,事实上,进程并不知道信号何时到来。

 

2.信号来源

(1)程序错误,如非法访问内存

(2)外部信号,如按下了CTRL+C

(3)通过kill或sigqueue向另外一个进程发送信号

 

3.信号种类

信号分为可靠信号与不可靠信号,可靠信号又称为实时信号,非可靠信号又称为非实时信号。

信号代码从1到32是不可靠信号,不可靠信号主要有以下问题:

(1)每次信号处理完之后,就会恢复成默认处理,这可能是调用者不希望看到的(早期的signal函数,linux2.6.35.6内核经验证已经不再恢复默认动作)。

(2)存在信号丢失的问题(进程收到的信号不作排队处理,相同的信号多次到来会合并为一个)。

现在的Linux对信号机制进行了改进,因此,不可靠信号主要是指信号丢失。

 

信号代码从SIGRTMIN到SIGRTMAX之间的信号是可靠信号。可靠信号不存在丢失,由sigqueue发送,可靠信号支持排队。

可靠信号注册机制:

内核每收到一个可靠信号都会去注册这个信号,在信号的未决信号链中分配sigqueue结构,因此,不会存在信号丢失的问题。

不可靠信号的注册机制:

而对于不可靠的信号,如果内核已经注册了这个信号,那么便不会再去注册,对于进程来说,便不会知道本次信号的发生。

可靠信号与不可靠信号与发送函数没有关系,取决于信号代码,前面的32种信号就是不可靠信号,而后面的32种信号就是可靠信号。

 

4.信号响应的方式

(1)采用系统默认处理SIG_DFL,执行缺省操作

(2)捕捉信号处理,即用户自定义的信号处理函数来处理

(3)忽略信号SIG_IGN ,但有两种信号不能被忽略SIGKILL,SIGSTOP

 

 第二部分: 信号的生命周期与处理过程分析

 

  1. 信号的生命周期

信号产生->信号注册->信号在进程中注销->信号处理函数执行完毕

(1)信号的产生是指触发信号的事件的发生

(2)信号注册

指的是在目标进程中注册,该目标进程中有未决信号的信息:

struct sigpending pending:
struct sigpending{
struct sigqueue *head, **tail;
sigset_t signal;
};

struct sigqueue{
struct sigqueue *next;
siginfo_t info;
}

 

 

其中 sigqueue结构组成的链称之为未决信号链,sigset_t称之为未决信号集。

*head,**tail分别指向未决信号链的头部与尾部。

siginfo_t info是信号所携带的信息。

 

信号注册的过程就是将信号值加入到未决信号集siginfo_t中,将信号所携带的信息加入到未决信号链的某一个sigqueue中去。

 

 因此,对于可靠的信号,可能存在多个未决信号的sigqueue结构,对于每次信号到来都会注册。而不可靠信号只注册一次,只有一个sigqueue结构。

只要信号在进程的未决信号集中,表明进程已经知道这些信号了,还没来得及处理,或者是这些信号被阻塞。

 

(3)信号在目标进程中注销

 在进程的执行过程中,每次从系统调用或中断返回用户空间的时候,都会检查是否有信号没有被处理。如果这些信号没有被阻塞,那么就调用相应的信号处理函数来处理这些信号。则调用信号处理函数之前,进程会把信号在未决信号链中的sigqueue结构卸掉。是否从未决信号集中把信号删除掉,对于实时信号与非实时信号是不相同的。

非实时信号:由于非实时信号在未决信号链中只有一个sigqueue结构,因此将它删除的同时将信号从未决信号集中删除。

实时信号:由于实时信号在未决信号链中可能有多个sigqueue结构,如果只有一个,也将信号从未决信号集中删除掉。如果有多个那么不从未决信号集中删除信号,注销完毕。

 

(4)信号处理函数执行完毕

执行处理函数,本次信号在进程中响应完毕。

 

在第4步,只简单的描述了信号处理函数执行完毕,就完成了本次信号的响应,但这个信号处理函数空间是怎么处理的呢? 内核栈与用户栈是怎么工作的呢? 这就涉及到了信号处理函数的过程。

 

信号处理函数的过程:

 

(1)注册信号处理函数

 

信号的处理是由内核来代理的,首先程序通过sigal或sigaction函数为每个信号注册处理函数,而内核中维护一张信号向量表,对应信号处理机制。这样,在信号在进程中注销完毕之后,会调用相应的处理函数进行处理。

 

(2)信号的检测与响应时机

在系统调用或中断返回用户态的前夕,内核会检查未决信号集,进行相应的信号处理。

 

(3)处理过程:

程序运行在用户态时->进程由于系统调用或中断进入内核->转向用户态执行信号处理函数->信号处理函数完毕后进入内核->返回用户态继续执行程序

 

首先程序执行在用户态,在进程陷入内核并从内核返回的前夕,会去检查有没有信号没有被处理,如果有且没有被阻塞就会调用相应的信号处理程序去处理。首先,内核在用户栈上创建一个层,该层中将返回地址设置成信号处理函数的地址,这样,从内核返回用户态时,就会执行这个信号处理函数。当信号处理函数执行完,会再次进入内核,主要是检测有没有信号没有处理,以及恢复原先程序中断执行点,恢复内核栈等工作,这样,当从内核返回后便返回到原先程序执行的地方了。

 

信号处理函数的过程大概是这样了。

 

具体的可参考 Linux内核信号处理机制介绍

 

                 

 

参考图:

 

必赢手机登录网址 1 

 

第三部分: 基本的信号处理函数

首先看一个两个概念: 信号未决与信号阻塞

信号未决: 指的是信号的产生到信号处理之前所处的一种状态。确切的说,是信号的产生到信号注销之间的状态。

信号阻塞: 指的是阻塞信号被处理,是一种信号处理方式。

 

 1. 信号操作

 信号操作最常用的方法是信号的屏蔽,信号屏蔽主要用到以下几个函数:

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set,int signo);

int sigdelset(sigset_t *set,int signo);

int sigismemeber(sigset_t* set,int signo);

int sigprocmask(int how,const sigset_t*set,sigset_t *oset);

 

信号集,信号掩码,未决集

信号集: 所有的信号阻塞函数都使用一个称之为信号集的结构表明其所受到的影响。

信号掩码:当前正在被阻塞的信号集。

未决集: 进程在收到信号时到信号在未被处理之前信号所处的集合称为未决集。

可以看出,这三个概念没有必然的联系,信号集指的是一个泛泛的概念,而未决集与信号掩码指的是具体的信号状态。

 

对于信号集的初始化有两种方法: 一种是用sigemptyset使信号集中不包含任何信号,然后用sigaddset把信号加入到信号集中去。

另一种是用sigfillset让信号集中包含所有信号,然后用sigdelset删除信号来初始化。

 

sigemptyset()函数初始化信号集set并将set设置为空。

sigfillset()函数初始化信号集,但将信号集set设置为所有信号的集合。

sigaddset()将信号signo加入到信号集中去。

sigdelset()从信号集中删除signo信号。

sigprocmask()将指定的信号集合加入到进程的信号阻塞集合中去。如果提供了oset,那么当前的信号阻塞集合将会保存到oset集全中去。

参数how决定了操作的方式:

SIG_BLOCK 增加一个信号集合到当前进程的阻塞集合中去

SIG_UNBLOCK 从当前的阻塞集合中删除一个信号集合

SIG_SETMASK 将当前的信号集合设置为信号阻塞集合

 

下面看一个例子:

#include
#include
#include
#include
#include
int main(){
sigset_t initset;
int i;
sigemptyset(&initset);//初始化信号集合为空集合
sigaddset(&initset,SIGINT);//将SIGINT信号加入到此集合中去
while(1){
sigprocmask(SIG_BLOCK,&initset,NULL);//将信号集合加入到进程的阻塞集合中去
fprintf(stdout,"SIGINT singal blocked/n");
for(i=0;i<10;i++){

sleep(1);//每1秒输出
fprintf(stdout,"block %d/n",i);
}
//在这时按一下Ctrl+C不会终止
sigprocmask(SIG_UNBLOCK,&initset,NULL);//从进程的阻塞集合中去删除信号集合
fprintf(stdout,"SIGINT SINGAL unblokced/n");
for(i=0;i<10;i++){

sleep(1);
fprintf(stdout,"unblock %d/n",i);
}

}
exit(0);
}

 

执行结果:

SIGINT singal blocked
block 0
block 1
block 2
block 3
block 4
block 5
block 6
block 7
block 8
block 9
在执行到block 3时按下了CTRL+C并不会终止,直到执行到block9后将集合从阻塞集合中移除。
[[email protected] C]# ./s1
SIGINT singal blocked
block 0
block 1
block 2
block 3
block 4
block 5
block 6
block 7
block 8
block 9
SIGINT SINGAL unblokced
unblock 0
unblock 1
由于此时已经解除了阻塞,在unblock1后按下CTRL+C则立即终止。

 

 

  1. 信号处理函数

#include

int sigaction(

int signo,

const struct sigaction *act,

struct sigaction *oldact

);

这个函数主要是用于改变或检测信号的行为。

 

第一个参数是变更signo指定的信号,它可以指向任何值,SIGKILL,SIGSTOP除外

第二个参数,第三个参数是对信号进行细粒度的控制。

如果*act不为空,*oldact不为空,那么oldact将会存储信号以前的行为。如果act为空,*oldact不为空,那么oldact将会存储信号现在的行为。

 

struct sigaction {

void (*sa_handler)(int);

void (*sa_sigaction)(int,siginfo_t*,void*);

sigset_t sa_mask;

int sa_flags;

void (*sa_restorer)(void);

}

 

参数含义:

sa_handler是一个函数指针,主要是表示接收到信号时所要采取的行动。此字段的值可以是SIG_DFL,SIG_IGN.分别代表默认操作与内核将忽略进程的信号。这个函数只传递一个参数那就是信号代码。

当SA_SIGINFO被设定在sa_flags中,那么则会使用sa_sigaction来指示信号处理函数,而非sa_handler.

sa_mask设置了掩码集,在程序执行期间会阻挡掩码集中的信号。

sa_flags设置了一些标志, SA_RESETHAND当该函数处理完成之后,设定为为系统默认的处理模式。SA_NODEFER 在处理函数中,如果再次到达此信号时,将不会阻塞。默认情况下,同一信号两次到达时,如果此时处于信号处理程序中,那么此信号将会阻塞。

SA_SIGINFO表示用sa_sigaction指示的函数。

sa_restorer已经被废弃。

 

sa_sigaction所指向的函数原型:

void my_handler(int signo,siginfo_t *si,void *ucontext);

第一个参数: 信号编号

第二个参数:指向一个siginfo_t结构。

第三个参数是一个ucontext_t结构。

其中siginfo_t结构体中包含了大量的信号携带信息,可以看出,这个函数比sa_handler要强大,因为前者只能传递一个信号代码,而后者可以传递siginfo_t信息。

typedef struct siginfo_t{
int si_signo;//信号编号
int si_errno;//如果为非零值则错误代码与之关联
int si_code;//说明进程如何接收信号以及从何处收到
pid_t si_pid;//适用于SIGCHLD,代表被终止进程的PID
pid_t si_uid;//适用于SIGCHLD,代表被终止进程所拥有进程的UID
int si_status;//适用于SIGCHLD,代表被终止进程的状态
clock_t si_utime;//适用于SIGCHLD,代表被终止进程所消耗的用户时间
clock_t si_stime;//适用于SIGCHLD,代表被终止进程所消耗系统的时间
sigval_t si_value;
int si_int;
void * si_ptr;
必赢手机登录网址 ,void* si_addr;
int si_band;
int si_fd;
};

 

 

sigqueue(pid_t pid,int signo,const union sigval value)

union sigval{int sival_int, void*sival_ptr};

 

sigqueue函数类似于kill,也是一个进程向另外一个进程发送信号的。

但它比kill函数强大。

第一个参数指定目标进程的pid.

第二个参数是一个信号代码。

第三个参数是一个共用体,每次只能使用一个,用来进程发送信号传递的数据。

或者传递整形数据,或者是传递指针。

发送的数据被sa_sigaction所指示的函数的siginfo_t结构体中的si_ptr或者是si_int所接收。

 

 

sigpending的用法

sigpending(sigset_t set);

这个函数的作用是返回未决的信号到信号集set中。即未决信号集,未决信号集不仅包括被阻塞的信号,也可能包括已经到达但没有被处理的信号。

 

示例1: sigaction函数的用法

 

#include
#include
void signal_set1(int);//信号处理函数,只传递一个参数信号代码
void signal_set(struct sigaction *act)
{

switch(act->sa_flags){

case (int)SIG_DFL:

printf("using default hander/n");

break;

case (int)SIG_IGN:

printf("ignore the signal/n");

break;

default:

printf("%0x/n",act->sa_handler);

}

}
void signal_set1(int x){//信号处理函数

printf("xxxxx/n");
while(1){
}

}

int main(int argc,char** argv)

{

int i;
struct sigaction act,oldact;
act.sa_handler = signal_set1;
act.sa_flags = SA_RESETHAND;
//SA_RESETHANDD 在处理完信号之后,将信号恢复成默认处理
//SA_NODEFER在信号处理程序执行期间仍然可以接收信号
sigaction (SIGINT,&act,&oldact) ;//改变信号的处理模式
for (i=1; i<12; i++)

{
printf("signal %d handler is : ",i);
sigaction (i,NULL,&oldact) ;
signal_set(&oldact);//如果act为NULL,oldact会存储信号当前的行为
//act不为空,oldact不为空,则oldact会存储信号以前的处理模式
}

while(1){
//等待信号的到来
}
return 0;

}

运行结果:

[[email protected] C]# ./s2
signal 1 handler is : using default hander
signal 2 handler is : 8048437
signal 3 handler is : using default hander
signal 4 handler is : using default hander
signal 5 handler is : using default hander
signal 6 handler is : using default hander
signal 7 handler is : using default hander
signal 8 handler is : using default hander
signal 9 handler is : using default hander
signal 10 handler is : using default hander
signal 11 handler is : using default hander
xxxxx

解释:

sigaction(i,NULL,&oldact);

signal_set(&oldact);

由于act为NULL,那么oldact保存的是当前信号的行为,当前的第二个信号的行为是执行自定义的处理程序。

当按下CTRL+C时会执行信号处理程序,输出xxxxxx,再按一下CTRL+C会停止,是由于SA_RESETHAND恢复成默认的处理模式,即终止程序。

如果没有设置SA_NODEFER,那么在处理函数执行过程中按一下CTRL+C将会被阻塞,那么程序会停在那里。

 

 

 

示例2: sigqueue向本进程发送数据的信号

#include
#include
#include
#include
#include
void myhandler(int signo,siginfo_t *si,void *ucontext);
int main(){
union sigval val;//定义一个携带数据的共用体
struct sigaction oldact,act;
act.sa_sigaction=myhandler;
act.sa_flags=SA_SIGINFO;//表示使用sa_sigaction指示的函数,处理完恢复默认,不阻塞处理过程中到达下在被处理的信号
//注册信号处理函数
sigaction(SIGUSR1,&act,&oldact);
char data[100];
int num=0;
while(num<10){
sleep(2);
printf("等待SIGUSR1信号的到来/n");
sprintf(data,"%d",num++);
val.sival_ptr=data;
sigqueue(getpid(),SIGUSR1,val);//向本进程发送一个信号
}

}

void myhandler(int signo,siginfo_t *si,void *ucontext){

printf("已经收到SIGUSR1信号/n");

printf("%s/n",(char*)(si->si_ptr));

}

 

 

程序执行的结果是:

 

 等待SIGUSR1信号的到来
已经收到SIGUSR1信号
0
等待SIGUSR1信号的到来
已经收到SIGUSR1信号
1
等待SIGUSR1信号的到来
已经收到SIGUSR1信号
2
等待SIGUSR1信号的到来
已经收到SIGUSR1信号
3
等待SIGUSR1信号的到来
已经收到SIGUSR1信号
4
等待SIGUSR1信号的到来
已经收到SIGUSR1信号
5
等待SIGUSR1信号的到来
已经收到SIGUSR1信号
6
等待SIGUSR1信号的到来
已经收到SIGUSR1信号
7
等待SIGUSR1信号的到来
已经收到SIGUSR1信号
8
等待SIGUSR1信号的到来
已经收到SIGUSR1信号
9

 

解释: 本程序用sigqueue不停的向自身发送信号,并且携带数据,数据被放到处理函数的第二个参数siginfo_t结构体中的si_ptr指针,当num<10时不再发。

 

一般而言,sigqueue与sigaction配合使用,而kill与signal配合使用。

 

示例3: 一个进程向另外一个进程发送信号,并携带信息

 

发送端:

#include
#include
#include
#include
#include
int main(){
union sigval value;
value.sival_int=10;

if(sigqueue(4403,SIGUSR1,value)==-1){//4403是目标进程pid
perror("信号发送失败/n");
}
sleep(2);
}

 

接收端:

#include
#include
#include
#include
#include
void myhandler(int signo,siginfo_t*si,void *ucontext);
int main(){
struct sigaction oldact,act;
act.sa_sigaction=myhandler;
act.sa_flags=SA_SIGINFO|SA_NODEFER;
//表示执行后恢复,用sa_sigaction指示的处理函数,在执行期间仍然可以接收信号
sigaction(SIGUSR1,&act,&oldact);
while(1){
sleep(2);
printf("等待信号的到来/n");
}
}

void myhandler(int signo,siginfo_t *si,void *ucontext){

 printf("the value is %d/n",si->si_int);
}

 

示例4: sigpending的用法

sigpending(sigset_t *set)将未决信号放到指定的set信号集中去,未决信号包括被阻塞的信号和信号到达时但还没来得及处理的信号

 

 

#include
#include
#include
#include
#include
void myhandler(int signo,siginfo_t *si,void *ucontext);
int main(){
struct sigaction oldact,act;
sigset_t oldmask,newmask,pendingmask;
act.sa_sigaction=myhandler;
act.sa_flags=SA_SIGINFO;
sigemptyset(&act.sa_mask);//首先将阻塞集合设置为空,即不阻塞任何信号
//注册信号处理函数
sigaction(SIGRTMIN+10,&act,&oldact);
//开始阻塞
sigemptyset(&newmask);
sigaddset(&newmask,SIGRTMIN+10);
printf("SIGRTMIN+10 blocked/n");
sigprocmask(SIG_BLOCK,&newmask,&oldmask);
sleep(20);//为了发出信号
printf("now begin to get pending mask/n");
if(sigpending(&pendingmask)<0){
perror("pendingmask error");
}

if(sigismember(&pendingmask,SIGRTMIN+10)){
printf("SIGRTMIN+10 is in the pending mask/n");
}

sigprocmask(SIG_UNBLOCK,&newmask,&oldmask);

printf("SIGRTMIN+10 unblocked/n");

}
//信号处理函数
void myhandler(int signo,siginfo_t *si,void *ucontext){

printf("receive signal %d/n",si->si_signo);

}

 

 

程序执行:

 

在另一个shell发送信号:

 kill -44 4579

 

SIGRTMIN+10 blocked
now begin to get pending mask
SIGRTMIN+10 is in the pending mask
receive signal 44
SIGRTMIN+10 unblocked

可以看到SIGRTMIN由于被阻塞所以处于未决信号集中。

 

 

关于基本的信号处理函数就介绍到这了。

 

 

第四部分: 保护临界区不被中断

  1.  函数的可重入性

函数的可重入性是指可以多于一个任务并发使用函数,而不必担心数据错误。相反,不可重入性是指不能多于一个任务共享函数,除非能保持函数互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后继续执行,而不会丢失数据。

 

 

可重入函数:
* 不为连续的调用持有静态数据。
* 不返回指向静态数据的指针;所有数据都由函数的调用者提供。
* 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
* 绝不调用任何不可重入函数。

 

 

不可重入函数可能导致混乱现象,如果当前进程的操作与信号处理程序同时对一个文件进行写操作或者是调用malloc(),那么就可能出现混乱,当从信号处理程序返回时,造成了状态不一致。从而引发错误。

因此,信号的处理必须是可重入函数。

简单的说,可重入函数是指在一个程序中调用了此函数,在信号处理程序中又调用了此函数,但仍然能够得到正确的结果。

printf,malloc函数都是不可重入函数。printf函数如果打印缓冲区一半时,又有一个printf函数,那么此时会造成混乱。而malloc函数使用了系统全局内存分配表。

 

  1. 保护临界区不被中断 (参考 APUE sigsuspend章节)

由于临界区的代码是关键代码,是非常重要的部分,因此,有必要对临界区进行保护,不希望信号来中断临界区操作。这里通过信号屏蔽字来阻塞信号的发生。

 

 下面介绍两个与保护临界区不被信号中断的相关函数。

 

int pause(void);

int sigsuspend(const sigset_t *sigmask);

 

pause函数挂起一个进程,直到一个信号发生。

sigsuspend函数的执行过程如下:

(1)设置新的mask去阻塞当前进程

(2)收到信号,调用信号的处理函数

(3)将mask设置为原先的掩码

(4)sigsuspend函数返回

 

可以看出,sigsuspend函数是等待一个信号发生,当等待的信号发生时,执行完信号处理函数后就会返回。它是一个原子操作。

 

保护临界区的中断:

(1)首先用sigprocmask去阻塞信号

(2)执行后关键代码后,用sigsuspend去捕获信号

(3)然后sigprocmask去除阻塞

这样信号就不会丢失了,而且不会中断临界区。

 

 

使用pause函数对临界区的保护:

 

 必赢手机登录网址 2

 

 

上面的程序是用pause去保护临界区,首先用sigprocmask去阻塞SIGINT信号,执行临界区代码,然后解除阻塞。最后调用pause()函数等待信号的发生。但此时会产生一个问题,如果信号在解除阻塞与pause之间发生的话,信号就可能丢失。这将是一个不可靠的信号机制。

因此,采用sigsuspend可以避免上述情况发生。

 

 

 

 使用sigsuspend对临界区的保护:

 

必赢手机登录网址 3

 

 

 

 使用sigsuspend对临界区的保护就不会产生上述问题了。

 

 

 

 

 3. sigsuspend函数的用法

sigsuspend函数是等待的信号发生时才会返回。

sigsuspend函数遇到结束时不会返回,这一点很重要。

示例:

下面的例子能够处理信号SIGUSR1,SIGUSR2,SIGSEGV,其它的信号被屏蔽,该程序输出对应的信号,然后继续等待其它信号的出现。

 

#include
#include
#include
#include
void myhandler(int signo);
int main(){
struct sigaction action;
sigset_t sigmask;
sigemptyset(&sigmask);
sigaddset(&sigmask,SIGUSR1);
sigaddset(&sigmask,SIGUSR2);
sigaddset(&sigmask,SIGSEGV);
action.sa_handler=myhandler;
action.sa_mask=sigmask;
sigaction(SIGUSR1,&action,NULL);
sigaction(SIGUSR2,&action,NULL);
sigaction(SIGSEGV,&action,NULL);
sigfillset(&sigmask);
sigdelset(&sigmask,SIGUSR1);
sigdelset(&sigmask,SIGUSR2);
sigdelset(&sigmask,SIGSEGV);
while(1){

sigsuspend(&sigmask);//不断的等待信号到来
}
return 0;
}
void myhandler(int signo){

switch(signo){
case SIGUSR1:

printf("received sigusr1 signal./n");
break ;
case SIGUSR2:
printf("received sigusr2 signal./n");
break;
case SIGSEGV:

printf("received sigsegv signal/n");
break;
}
}

程序运行结果:

 

received sigusr1 signal

received sigusr2 signal

received sigsegv signal

received sigusr1 signal

已终止

 

 

另一个终端用于发送信号:

先得到当前进程的pid, ps aux|grep 程序名

 

kill -SIGUSR1 4901

kill -SIGUSR2 4901

kill -SIGSEGV 4901

kill -SIGTERM 4901

kill -SIGUSR1  4901

 

解释:

第一行发送SIGUSR1,则调用信号处理函数,打印出结果。

第二,第三行分别打印对应的结果。

第四行发送一个默认处理为终止进程的信号。

但此时,但不会终止程序,由于sigsuspend遇到终止进程信号并不会返回,此时并不会打印出"已终止",这个信号被阻塞了。当再次发送SIGURS1信号时,进程的信号阻塞恢复成默认的值,因此,此时将会解除阻塞SIGTERM信号,所以进程被终止。

 

 

 

 第五部分: 信号的继承与执行

 

当使用fork()函数时,子进程会继承父进程完全相同的信号语义,这也是有道理的,因为父子进程共享一个地址空间,所以父进程的信号处理程序也存在于子进程中。

 

示例: 子进程继承父进程的信号处理函数

 

#include
#include
#include
#include
#include
void myhandler(int signo,siginfo_t *si,void *vcontext);
int main(){
union sigval val;
struct sigaction oldact,newact;
newact.sa_sigaction=myhandler;
newact.sa_flags=SA_SIGINFO|SA_RESETHAND;//表示采用sa_sigaction指示的函数以及执行完处理函数后恢复默认操作
//注册信号处理函数
sigaction(SIGUSR1,&newact,&oldact);

if(fork()==0){

val.sival_int=10;
printf("子进程/n");
sigqueue(getpid(),SIGUSR1,val);

}

else {

val.sival_int=20;
printf("父进程/n");
sigqueue(getpid(),SIGUSR1,val);

}

}

void myhandler(int signo,siginfo_t *si,void *vcontext){
printf("信号处理/n");
printf("%d/n",(si->si_int));

}

 

输出的结果为:

 

子进程
信号处理
10
父进程
信号处理
20

 

可以看出来,子进程继承了父进程的信号处理函数。

 

第六部分: 实时信号中锁的研究

 

  1. 信号处理函数与主函数之间的死锁

当主函数访问临界资源时,通常需要加锁,如果主函数在访问临界区时,给临界资源上锁,此时发生了一个信号,那么转入信号处理函数,如果此时信号处理函数也对临界资源进行访问,那么信号处理函数也会加锁,由于主程序持有锁,信号处理程序等待主程序释放锁。又因为信号处理函数已经抢占了主函数,因此,主函数在信号处理函数结束之前不能运行。因此,必然造成死锁。

示例1: 主函数与信号处理函数之间的死锁

#include
#include
#include
#include
#include
#include
int value=0;
sem_t sem_lock;//定义信号量
void myhandler(int signo,siginfo_t *si,void *vcontext);//进程处理函数声明
int main(){
union sigval val;
val.sival_int=1;
struct sigaction oldact,newact;
int res;
res=sem_init(&sem_lock,0,1);
if(res!=0){
perror("信号量初始化失败");
}

newact.sa_sigaction=myhandler;
newact.sa_flags=SA_SIGINFO;
sigaction(SIGUSR1,&newact,&oldact);
sem_wait(&sem_lock);
printf("xxxx/n");
value=1;
sleep(10);
sigqueue(getpid(),SIGUSR1,val);//sigqueue发送带参数的信号
sem_post(&sem_lock);
sleep(10);
exit(0);
}

void myhandler(int signo,siginfo_t *si,void *vcontext){

sem_wait(&sem_lock);
value=0;
sem_post(&sem_lock);

}

此程序将一直阻塞在信号处理函数的sem_wait函数处。

 

 

  1. 利用测试锁解决死锁

sem_trywait(&sem_lock);是非阻塞的sem_wait,如果加锁失败或者是超时,则返回-1。

 

示例2: 用sem_trywait来解决死锁

 

#include
#include
#include
#include
#include
#include
int value=0;
sem_t sem_lock;//定义信号量
void myhandler(int signo,siginfo_t *si,void *vcontext);//进程处理函数声明
int main(){
union sigval val;
val.sival_int=1;
struct sigaction oldact,newact;
int res;
res=sem_init(&sem_lock,0,1);
if(res!=0){
perror("信号量初始化失败");
}

newact.sa_sigaction=myhandler;
newact.sa_flags=SA_SIGINFO;
sigaction(SIGUSR1,&newact,&oldact);
sem_wait(&sem_lock);
printf("xxxx/n");
value=1;
sleep(10);
sigqueue(getpid(),SIGUSR1,val);//sigqueue发送带参数的信号
sem_post(&sem_lock);
sleep(10);
sigqueue(getpid(),SIGUSR1,val);
exit(0);
}

void myhandler(int signo,siginfo_t *si,void *vcontext){

if(sem_trywait(&sem_lock)==0){
value=0;
sem_post(&sem_lock);
}
}

 

第一次发送sigqueue时,由于主函数持有锁,因此,sem_trywait返回-1,当第二次发送sigqueue时,主函数已经释放锁,此时就可以在信号处理函数中对临界资源加锁了。

但这种方法明显丢失了一个信号,不是很好的解决方法。

 

 

 

  1. 利用双线程来解决主函数与信号处理函数死锁

我们知道,当进程收到一个信号时,会选择其中的某个线程进行处理,前提是这个线程没有屏蔽此信号。因此,可以在主线程中屏蔽信号,另选一个线程去处理这个信号。由于主线程与另外一个线程是平行执行的,因此,等待主线程执行完临界区时,释放锁,这个线程去执行信号处理函数,直到执行完毕释放临界资源。

 

这里用到一个线程的信号处理函数: pthread_sigmask

int pthread_sigmask(int how,const sigset_t *set,sigset_t *oldset);

这个函数与sigprocmask很相似。

how:

SIG_BLOCK 将信号集加入到线程的阻塞集中去

SIG_UNBLOCK 将信号集从阻塞集中删除

SIG_SETMASK 将当前集合设置为线程的阻塞集

 

 示例: 利用双线程来解决主函数与信号处理函数之间的死锁

 

#include
#include
#include
#include
#include
#include
#include
void*thread_function(void *arg);//线程处理函数
void myhandler(int signo,siginfo_t *si,void *vcontext);//信号处理函数
int value;
sem_t semlock;
int main(){
int res;
pthread_t mythread;
void *thread_result;
res=pthread_create(&mythread,NULL,thread_function,NULL);//创建一个子线程
if(res!=0){
perror("线程创建失败");
}

//在主线程中将信号屏蔽
sigset_t empty;
sigemptyset(&empty);
sigaddset(&empty,SIGUSR1);
pthread_sigmask(SIG_BLOCK,&empty,NULL);

//主线程中对临界资源的访问
if(sem_init(&semlock,0,1)!=0){
perror("信号量创建失败");
}

sem_wait(&semlock);
printf("主线程已经执行/n");
value=1;
sleep(10);
sem_post(&semlock);
res=pthread_join(mythread,&thread_result);//等待子线程退出
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg){
struct sigaction oldact,newact;
newact.sa_sigaction=myhandler;
newact.sa_flags=SA_SIGINFO;
//注册信号处理函数
sigaction(SIGUSR1,&newact,&oldact);
union sigval val;
val.sival_int=1;
printf("子线程睡眠3秒/n");
sleep(3);
sigqueue(getpid(),SIGUSR1,val);
pthread_exit(0);//线程结束

}

void myhandler(int signo,siginfo_t *si,void *vcontext){
sem_wait(&semlock);
value=0;
printf("信号处理完毕/n");
sem_post(&semlock);
}

运行结果如下:

主线程已经执行
子线程睡眠3秒
信号处理完毕

 

解释一下:

 

在主线线程中阻塞了SIGUSR1信号,首先让子线程睡眠3秒,目的让主线程先运行,然后当主线程访问临界资源时,让线程sleep(10),在这期间,子线程发送信号,此时子线程会去处理信号,而主线程依旧平行的运行,子线程被阻止信号处理函数的sem_wait处,等待主线程10后,信号处理函数得到锁,然后进行临界资源的访问。这就解决了主函数与信号处理函数之间的死锁问题。

 

扩展: 如果有多个信号到达时,还可以用多线程来处理多个信号,从而达到并行的目的,这个很好实现的,可以尝试一下。

 

转自:

signal信号机制的透彻分析与各种实例讲解,signal实例讲解 转自: 转自:...

【摘要】本文分析了Linux内核对于信号的实现机制和应用层的相关处理。首先介绍了软中断信号的本质及信号的两种不同分类方法尤其是不可靠信号的原理。接着分析了内核对于信号的处理流程包括信号的触发/注册/执行及注销等。最后介绍了应用层的相关处理,主要包括信号处理函数的安装、信号的发送、屏蔽阻塞等,最后给了几个简单的应用实例。

 

【关键字】软中断信号,signal,sigaction,kill,sigqueue,settimer,sigmask,sigprocmask,sigset_t

 

1       信号本质

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。

 

收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:

第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。

第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。

第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。

 

2       信号的种类

可以从两个不同的分类角度对信号进行分类:

可靠性方面:可靠信号与不可靠信号;

与时间的关系上:实时信号与非实时信号。

 

2.1    可靠信号与不可靠信号

Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。

 

随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

 

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。

 

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。

 

对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。

 

2.2    实时信号与非实时信号

早期Unix系统只定义了32种信号,前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。

 

非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

 

3       信号处理流程

 

 

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个阶段:

信号诞生

信号在进程中注册

信号的执行和注销

 

 

3.1    信号诞生

信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

 

这里按发出信号的原因简单分类,以了解各种信号:

(1) 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。

(2) 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。

(3) 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。

(4) 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。

(5) 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。

(6) 与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。

(7) 跟踪进程执行的信号。

 

Linux支持的信号列表如下。很多信号是与机器的体系结构相关的

信号值 默认处理动作 发出信号的原因

SIGHUP 1 A 终端挂起或者控制进程终止

SIGINT 2 A 键盘中断(如break键被按下)

SIGQUIT 3 C 键盘的退出键被按下

SIGILL 4 C 非法指令

SIGABRT 6 C 由abort(3)发出的退出指令

SIGFPE 8 C 浮点异常

SIGKILL 9 AEF Kill信号

SIGSEGV 11 C 无效的内存引用

SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道

SIGALRM 14 A 由alarm(2)发出的信号

SIGTERM 15 A 终止信号

SIGUSR1 30,10,16 A 用户自定义信号1

SIGUSR2 31,12,17 A 用户自定义信号2

SIGCHLD 20,17,18 B 子进程结束信号

SIGCONT 19,18,25 进程继续(曾被停止的进程)

SIGSTOP 17,19,23 DEF 终止进程

SIGTSTP 18,20,24 D 控制终端(tty)上按下停止键

SIGTTIN 21,21,26 D 后台进程企图从控制终端读

SIGTTOU 22,22,27 D 后台进程企图从控制终端写

 

处理动作一项中的字母含义如下

A 缺省的动作是终止进程

B 缺省的动作是忽略此信号,将该信号丢弃,不做处理

C 缺省的动作是终止进程并进行内核映像转储(dump core),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。

D 缺省的动作是停止进程,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调用)

E 信号不能被捕获

F 信号不能被忽略

 

3.2    信号在目标进程中注册

在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号。内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。如果信号发送给一个正在睡眠的进程,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。如果发送给一个处于可运行状态的进程,则只置相应的域即可。

 

进程的task_struct结构中有关于本进程中未决信号的数据成员: struct sigpending pending:

struct sigpending{

        struct sigqueue *head, *tail;

        sigset_t signal;

};

 

第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

struct sigqueue{

        struct sigqueue *next;

        siginfo_t info;

}

 

信号在进程中注册指的就是信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

 

当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)。

 

当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册(通过sigset_t signal指示),则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。

 

总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)

 

 

3.3    信号的执行和注销

内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。当其由于被信号唤醒或者正常调度重新获得CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。

 

对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则执行完相应的处理函数后应该把信号在进程的未决信号集中删除(信号注销完毕)。否则待该信号的所有sigqueue处理完毕后再在进程的未决信号集中删除该信号。

 

当所有未被屏蔽的信号都处理完毕后,即可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。

 

内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

 

处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

 

4       信号的安装

如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

 

linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。

 

4.1    signal()

#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int);

 

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler));

 

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。

如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

void sigroutine(int dunno)

{ /* 信号处理例程,其中dunno将会得到信号的值 */

        switch (dunno) {

        case 1:

        printf("Get a signal -- SIGHUP ");

        break;

        case 2:

        printf("Get a signal -- SIGINT ");

        break;

        case 3:

        printf("Get a signal -- SIGQUIT ");

        break;

        }

        return;

}
int main() {

        printf("process id is %d ",getpid());

        signal(SIGHUP, sigroutine); //* 下面设置三个信号的处理方法

        signal(SIGINT, sigroutine);

        signal(SIGQUIT, sigroutine);

        for (;;) ;
}

 

 

其中信号SIGINT由按下Ctrl-C发出,信号SIGQUIT由按下Ctrl-发出。该程序执行的结果如下:

 

localhost:~$ ./sig_test

process id is 463

Get a signal -SIGINT //按下Ctrl-C得到的结果

Get a signal -SIGQUIT //按下Ctrl-得到的结果

//按下Ctrl-z将进程置于后台

 [1]+ Stopped ./sig_test

localhost:~$ bg

 [1]+ ./sig_test &

localhost:~$ kill -HUP 463 //向进程发送SIGHUP信号

localhost:~$ Get a signal – SIGHUP

kill -9 463 //向进程发送SIGKILL信号,终止进程

localhost:~$

 

4.2    sigaction()

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

 

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些信号等等。

sigaction结构定义如下:

struct sigaction {

                       union{

                               __sighandler_t _sa_handler;

                               void (*_sa_sigaction)(int,struct siginfo *, void *);

                       }_u

            sigset_t sa_mask;

            unsigned long sa_flags;

}

 

1、联合数据结构中的两个元素_sa_handler以及*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。

 

2、由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用,第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:

siginfo_t {

                  int      si_signo;  /* 信号值,对所有信号有意义*/

                  int      si_errno;  /* errno值,对所有信号有意义*/

                  int      si_code;   /* 信号产生的原因,对所有信号有意义*/

                               union{                               /* 联合数据结构,不同成员适应不同信号 */

                                       //确保分配足够大的存储空间

                                       int _pad[SI_PAD_SIZE];

                                       //对SIGKILL有意义的结构

                                       struct{

                                                      ...

                                                 }...

                                               ... ...

                                               ... ...                               

                                       //对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构

                                  struct{

                                                      ...

                                                 }...

                                               ... ...

                                         }

}

 

前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。

 

3、sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。

注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。

 

4、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)。

 

 

5       信号的发送

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

 

5.1    kill()

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid,int signo)

 

该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程

pid>0 进程ID为pid的进程

pid=0 同一个进程组的进程

pid<0 pid!=-1 进程组ID为 -pid的所有进程

pid=-1 除发送进程自身外,所有进程ID大于1的进程

 

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

 

Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。

ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。

EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

 

5.2    sigqueue()

#include <sys/types.h>

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val)

调用成功返回 0;否则,返回 -1。

 

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。

sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。

typedef union sigval {

               int  sival_int;

               void *sival_ptr;

}sigval_t;

 

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

 

在调用sigqueue时,sigval_t指定的信息会拷贝到对应sig 注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

 

5.3    alarm()

#include <unistd.h>

unsigned int alarm(unsigned int seconds)

系统调用alarm安排内核为调用进程在指定的seconds秒后发出一个SIGALRM的信号。如果指定的参数seconds为0,则不再发送 SIGALRM信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。

 

注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。

 

5.4    setitimer()

现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下:

int getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用这两个调用的进程中加入以下头文件:

#include <sys/time.h>

 

该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示:

TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。

ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。

ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。

 

定时器中的参数value用来指明定时器的时间,其结构如下:

struct itimerval {

        struct timeval it_interval; /* 下一次的取值 */

        struct timeval it_value; /* 本次的设定值 */

};

 

该结构中timeval结构定义如下:

struct timeval {

        long tv_sec; /* 秒 */

        long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

 

在setitimer 调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此往复。当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval 为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:

EFAULT:参数value或ovalue是无效的指针。

EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。

下面是关于setitimer调用的一个简单示范,在该例子中,每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号:

 

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

#include <sys/time.h>

int sec;

 

void sigroutine(int signo) {

        switch (signo) {

        case SIGALRM:

        printf("Catch a signal -- SIGALRM ");

        break;

        case SIGVTALRM:

        printf("Catch a signal -- SIGVTALRM ");

        break;

        }

        return;

}

 

int main()

{

        struct itimerval value,ovalue,value2;

        sec = 5;

 

        printf("process id is %d ",getpid());

        signal(SIGALRM, sigroutine);

        signal(SIGVTALRM, sigroutine);

 

        value.it_value.tv_sec = 1;

        value.it_value.tv_usec = 0;

        value.it_interval.tv_sec = 1;

        value.it_interval.tv_usec = 0;

        setitimer(ITIMER_REAL, &value, &ovalue);

 

        value2.it_value.tv_sec = 0;

        value2.it_value.tv_usec = 500000;

        value2.it_interval.tv_sec = 0;

        value2.it_interval.tv_usec = 500000;

        setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

 

        for (;;) ;

}

 

该例子的屏幕拷贝如下:

localhost:~$ ./timer_test

process id is 579

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal –GVTALRM

 

5.5    abort()

#include <stdlib.h>

void abort(void);

向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

 

5.6    raise()

#include <signal.h>

int raise(int signo)

向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。

 

6       信号集及信号集操作函数:

信号集被定义为一种数据类型:

typedef struct {

                       unsigned long sig[_NSIG_WORDS];

} sigset_t

信号集用来描述信号的集合,每个信号占用一位。Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:

 

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum)

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;

sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;

sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;

sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;

sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。

 

7       信号阻塞与信号未决:

每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数:

#include <signal.h>

int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));

int sigpending(sigset_t *set));

int sigsuspend(const sigset_t *mask));

 

sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:

SIG_BLOCK 在进程当前阻塞信号集中添加set指向信号集中的信号

SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞

SIG_SETMASK 更新进程阻塞信号集为set指向的信号集

 

sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。

 

sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。

 

 

8       信号应用实例

linux下的信号应用并没有想象的那么恐怖,程序员所要做的最多只有三件事情:

安装信号(推荐使用sigaction());

实现三参数信号处理函数,handler(int signal,struct siginfo *info, void *);

发送信号,推荐使用sigqueue()。

实际上,对有些信号来说,只要安装信号就足够了(信号处理方式采用缺省或忽略)。其他可能要做的无非是与信号集相关的几种操作。

 

实例一:信号发送及处理

实现一个信号接收程序sigreceive(其中信号安装由sigaction())。

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;  

        int sig;

        sig=atoi(argv[1]);

       

        sigemptyset(&act.sa_mask);

        act.sa_flags=SA_SIGINFO;

        act.sa_sigaction=new_op;

       

        if(sigaction(sig,&act,NULL) < 0)

        {

                printf("install sigal errorn");

        }

       

        while(1)

        {

                sleep(2);

                printf("wait for the signaln");

        }

}

 

void new_op(int signum,siginfo_t *info,void *myact)

{

        printf("receive signal %d", signum);

        sleep(5);

}

说明,命令行参数为信号值,后台运行sigreceive signo &,可获得该进程的ID,假设为pid,然后再另一终端上运行kill -s signo pid验证信号的发送接收及处理。同时,可验证信号的排队问题。

 

实例二:信号传递附加信息

主要包括两个实例:

向进程本身发送信号,并传递指针参数

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;  

        union sigval mysigval;

        int i;

        int sig;

        pid_t pid;         

        char data[10];

        memset(data,0,sizeof(data));

        for(i=0;i < 5;i++)

                data[i]='2';

        mysigval.sival_ptr=data;

       

        sig=atoi(argv[1]);

        pid=getpid();

       

        sigemptyset(&act.sa_mask);

        act.sa_sigaction=new_op;//三参数信号处理函数

        act.sa_flags=SA_SIGINFO;//信息传递开关,允许传说参数信息给new_op

        if(sigaction(sig,&act,NULL) < 0)

        {

                printf("install sigal errorn");

        }

        while(1)

        {

                sleep(2);

                printf("wait for the signaln");

                sigqueue(pid,sig,mysigval);//向本进程发送信号,并传递附加信息

        }

}

void new_op(int signum,siginfo_t *info,void *myact)//三参数信号处理函数的实现

{

        int i;

        for(i=0;i<10;i++)

        {

                printf("%cn ",(*( (char*)((*info).si_ptr)+i)));

        }

        printf("handle signal %d over;",signum);

}

 

这个例子中,信号实现了附加信息的传递,信号究竟如何对这些信息进行处理则取决于具体的应用。

 

不同进程间传递整型参数:

把1中的信号发送和接收放在两个程序中,并且在发送过程中传递整型参数。

信号接收程序:

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;

        int sig;

        pid_t pid;         

       

        pid=getpid();

        sig=atoi(argv[1]);     

       

        sigemptyset(&act.sa_mask);

        act.sa_sigaction=new_op;

        act.sa_flags=SA_SIGINFO;

        if(sigaction(sig,&act,NULL)<0)

        {

                printf("install sigal errorn");

        }

        while(1)

        {

                sleep(2);

                printf("wait for the signaln");

        }

}

void new_op(int signum,siginfo_t *info,void *myact)

{

        printf("the int value is %d n",info->si_int);

}

 

 

信号发送程序:

命令行第二个参数为信号值,第三个参数为接收进程ID。

 

#include <signal.h>

#include <sys/time.h>

#include <unistd.h>

#include <sys/types.h>

main(int argc,char**argv)

{

        pid_t pid;

        int signum;

        union sigval mysigval;

        signum=atoi(argv[1]);

        pid=(pid_t)atoi(argv[2]);

        mysigval.sival_int=8;//不代表具体含义,只用于说明问题

        if(sigqueue(pid,signum,mysigval)==-1)

                printf("send errorn");

        sleep(2);

}

 

 

注:实例2的两个例子侧重点在于用信号来传递信息,目前关于在linux下通过信号传递信息的实例非常少,倒是Unix下有一些,但传递的基本上都是关于传递一个整数

 

实例三:信号阻塞及信号集操作

#include "signal.h"

#include "unistd.h"

static void my_op(int);

main()

{

        sigset_t new_mask,old_mask,pending_mask;

        struct sigaction act;

        sigemptyset(&act.sa_mask);

        act.sa_flags=SA_SIGINFO;

        act.sa_sigaction=(void*)my_op;

        if(sigaction(SIGRTMIN+10,&act,NULL))

                printf("install signal SIGRTMIN+10 errorn");

        sigemptyset(&new_mask);

        sigaddset(&new_mask,SIGRTMIN+10);

        if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))

                printf("block signal SIGRTMIN+10 errorn");

        sleep(10);

        printf("now begin to get pending mask and unblock SIGRTMIN+10n");

        if(sigpending(&pending_mask)<0)

                printf("get pending mask errorn");

        if(sigismember(&pending_mask,SIGRTMIN+10))

                printf("signal SIGRTMIN+10 is pendingn");

        if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)

                printf("unblock signal errorn");

        printf("signal unblockedn");

        sleep(10);

}

 

static void my_op(int signum)

{

        printf("receive signal %d n",signum);

}

 

编译该程序,并以后台方式运行。在另一终端向该进程发送信号(运行kill -s 42 pid,SIGRTMIN+10为42),查看结果可以看出几个关键函数的运行机制,信号集相关操作比较简单。

 

9       参考鸣谢:

linux信号处理机制(详解),

Linux环境进程间通信(二): 信号(上),郑彦兴 (mlinux@163.com)

signal、sigaction、kill等手册,最直接而可靠的参考资料。

进程间通信信号(上)

进程间通信信号(下)