关于linux:Linux信号透彻分析理解与各种实例讲解

74次阅读

共计 15704 个字符,预计需要花费 40 分钟才能阅读完成。

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

(1)信号的基本知识

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

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

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

(5) 信号的继承与执行

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

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

1. 信号实质:

信号的实质是软件档次上对中断的一种模仿。它是一种异步通信的解决机制,事实上,过程并不知道信号何时到来。

2. 信号起源

(1)程序谬误,如非法拜访内存

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

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

3. 信号品种

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

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

(1)每次信号处理完之后,就会复原成默认解决,这可能是调用者不心愿看到的

(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)处理过程:

程序运行在用户态时 -> 过程因为零碎调用或中断进入内核 -> 转向用户态执行信号处理函数 -> 信号处理函数结束后进入内核 -> 返回用户态持续执行程序

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

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

须要 C /C++ Linux 高级服务器架构师学习材料后盾加群 563998835(包含 C /C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等)

第三局部: 根本的信号处理函数

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

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

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

  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
int sigaction( int signo,
const struct sigaction *act,
struct sigaction *oldact );

执行后果:

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 后将汇合从阻塞汇合中移除。
[root@localhost 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. 信号处理函数
struct sigaction {void (*sa_handler)(int);
void (*sa_sigaction)(int,siginfo_t*,void*);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
} 

这个函数次要是用于扭转或检测信号的行为。

第一个参数是变更 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 函数的用法

struct sigaction {void (*sa_handler)(int);
void (*sa_sigaction)(int,siginfo_t*,void*);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
} 

运行后果:
[root@localhost 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. 爱护临界区不被中断

因为临界区的代码是要害代码,是十分重要的局部,因而,有必要对临界区进行爱护,不心愿信号来中断临界区操作。这里通过信号屏蔽字来阻塞信号的产生。

上面介绍两个与爱护临界区不被信号中断的相干函数。

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 函数对临界区的爱护:

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

因而,采纳 sigsuspend 能够防止上述情况产生。

应用 sigsuspend 对临界区的爱护:

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

  1. sigsuspend 函数的用法

sigsuspend 函数是期待的信号产生时才会返回。

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

示例:

上面的例子可能解决信号 SIGUSR1,SIGUSR2,SIGSEGV, 其它的信号被屏蔽,该程序输入对应的信号,而后持续期待其它信号的呈现。

#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;

}

程序运行后果:

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 *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);

} 

输入的后果为:

子过程
信号处理
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 后,信号处理函数失去锁,而后进行临界资源的拜访。这就解决了主函数与信号处理函数之间的死锁问题。

扩大: 如果有多个信号达到时,还能够用多线程来解决多个信号,从而达到并行的目标,这个很好实现的,能够尝试一下。

正文完
 0