在上一篇文章Liunx学习之旅(19)---信号中提到系统对信号的处理方式主要有三种:(1)默认(2)忽略(3)捕捉。默认就是当系统接收到某个信号时,去执行信号的默认状态,而忽略就是对这个信号不予处理。那什么时捕捉,信号又该如何捕捉那?
信号的捕捉设定:
从上图中我们可以看出系统并没有在接到信号的那一刻就直接去处理,而是在从内核返回用户空间的途中去处理该信号。
那如何才能捕捉信号那?这里liunx为用户提供了一组函数,通过函数我们可以捕捉到信号。
int sigaction(int signum,struct sigaction *newact,struct sigaction *oldact);
函数功能:
用于更改进程在接收到特定信号时所采取的操作。
参数说明:
signum:信号编号
newact:新的信号处理函数(传入参数)
oldact:旧的信号处理函数(传出参数,如果不关心可以直接传入NULL)
struct 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);
};
说明:
sa_handler和sa_sigaction函数为互斥函数,即同时只能使用一个。
sa_mask:在执行捕捉函数时,设置阻塞其他信号。sa_mask|进程阻塞信号集,退出捕捉函数后,会将进程阻塞集还原。
sa_flags:SA_SIGINFO(使用sa_sigaction)或0(使用sa_handler)
sa_restorer:保留,已经过时
从原理上说,除了SIGKILL信号和SIGSTOP信号之外,我们可以为任何信号设置捕捉函数,即修改信号原来的函数。
程序1:捕捉Ctrl+C(SIGINT信号)
#include <stdio.h>
#include <signal.h>
//信号捕捉函数,刚对应信号触发时,由操作系统内核自动调用,参数为内核自动填写
void do_signal(int num)
{
int n=2;
while(n--)
{
printf("我是捕捉函数,我对应的信号为:num=%d,i=%d\n",num,n);
sleep(1);
}
}
int main()
{
struct sigaction sig;
//函数指针,捕捉
sig.sa_handler=do_signal;
//默认
//sig.sa_handler=SIG_DFL;
//忽略
//sig.sa_handler=SIG_IGN;
//设置阻塞信号集
sigemptyset(&sig.sa_mask);
//设置标记位
sig.sa_flags=0;
sigaction(SIGINT,&sig,NULL);
while(1)
{
printf("*********\n");
sleep(1);
}
return 0;
}
在这段程序运行时,让ctrl+c(SIGINT)的作用不再是终止当前进程,而是执行do_signal函数。通过这段程序我们也可以看出当按下多个ctrl+c是,系统在执行完第一次ctrl+c时会立马执行第二次的crtl+c,但第二次信号处理完后就没有再处理第三次,从这里据可以看出,前32个信号时不支持排队的。
虽然说系统允许我们对大部分的信号去捕捉,但这样总归时不好的,因为系统早已为这些信号定义好了对应的函数,我们这样随意的修改很容易引起阅读程序人的误解。为了解决这个问题,系统为我们提供了两个信号SIGUSR1和SIGUSR2。系统没有为这两个信号(默认状态为终止)设置相对应的函数,用户可以自己为它们设置相应的函数。
程序2:父进程和子进程交替打印数字(1,2,3,4)
#include <stdio.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
//用来处理SIGUSER1信号
void do_sigUser1(int num)
{
static int n=0;
printf("%d,%d\n",getpid(),n);
n+=2;
}
//用来处理SIGUSER2信号
void do_sigUser2(int num)
{
static int n=1;
printf("%d,%d\n",getpid(),n);
n+=2;
}
int main()
{
struct sigaction sig;
sig.sa_flags=0;
int pid=fork();
if(pid<0)
{
perror("fork\n");
}
while(1)
{
if(pid>0)
{
sig.sa_handler=do_sigUser1;
sigemptyset(&sig.sa_mask);
sigaction(SIGUSR1,&sig,NULL);
//printf("父进程\n");
sleep(1);
kill(pid,SIGUSR2);
}
else if(pid==0)
{
sig.sa_handler=do_sigUser2;
sigemptyset(&sig.sa_mask);
sigaction(SIGUSR2,&sig,NULL);
//printf("子进程\n");
kill(getppid(),SIGUSR1);
}
}
return 0;
}
程序的原理:
在上面的程序中使用了kill函数,下面说明一下kill函数:
int kill(pid_t pid,int sig);
参数说明:
pid:
(1)pid>0:sig发给PID为pid的进程
(2)pid==0:sig发给与发送进程同组的所有进程
(3)pid<0:sig发送给组ID为|-pid|的进程
(4)pid==-1:sig发送给发送进程有权限发送的系统上的所有进程
sig:
sig==0:用于检测特定的进程是否存在,如果不存在返回-1
sig>0:将对应的信号发送给对应的进程
int rasie(int sig);
//向当前进程发送sig信号
kill()函数的名称虽然是“杀死”的意思,但是它并不是杀死的某个进程,而是向某个进程发送信号(但很多信号会导致进程终止,这就类似于我们所说的“我不杀伯仁,伯仁却因我而死。”)。
在信号的处理上C标准也为我们提供了函数:
typedef void (*sighandler_t)(int)
sighandler_t signal(int signum,sighandler_t handler);
//捕捉函数
int system(const char* command);
//集fork、exec、wait函数于一体的函数
和Linux系统提供的相比,C标准提供的函数优点是:(1)函数接口简单(2)可跨平台。缺点是:没有linux系统提供的函数功能强大,功能全。
在说起进程和信号时,就不得不说一个非常重要的概念:
可重入函数和不可重入函数。
可重入函数:如果在执行该函数的期间,进程突然去执行了其他函数,再次回到该函数时,不会对该函数产生影响。
不可重入函数:如果在执行该函数期间,进程去执行其他函数回来,对函数产生了影响。
例如:我们在函数中使用了静态变量,在执行信号对应的函数中使用了当前这个函数,导致静态变量的值发生了变化,系统再次回到该函数时静态变量的值以及该发生变化。这是这个函数就是不可重入函数(strtok()该函数的内部自动维护一个静态变量,所以strtok()函数是不可重入的函数,在单进程和没有信号的程序中使用是没有影响的,一旦涉及到多进程和信号时,使用strtok就是不安全的行为,因为这个错误是不一定能够在现的。而strtok_r函数值可重入的,因为strtok_r函数的指针是用户传输的。)
在捕捉函数中要尽量使用可重入函数,不使用不可重入函数。
信号引起的时序竞态:
竞态是指设备或系统出现不恰当的执行时序,而得到不正确的结果,由于时间片,或其他因素,导致该到达并响应的信号没有被响应,这就是由信号引起的竞态。
举个列子:
先介绍三个函数:
unsigned int alarm(unsigned int seconds);
函数功能:
为函数设置一个定时器,当时间结束时发送SIGALRM信号
int pause();
函数功能:
将当前程序挂起,等待一个信号的递达(任意一个信号),如果这个信号的状态为忽略,则继续将进程挂起。
int sigsuspend(const sigset_t *sig);
函数功能:
1、通过mask临时解除对某个信号的屏蔽
2、然后进程挂起等待
3、当被信号唤醒,sigsuspend返回时,进程的信号屏蔽字恢复为原来的值
我们知道sleep函数的功能使程序睡眠几秒。我们利用以上几个函数实现一个sleep函数。
程序3:mysleep1
#include <stdio.h>
#include <signal.h>
//信号响应函数
void do_signal()
{
//什么不做
}
unsigned int mysleep(int n)
{
alarm(n);
struct sigaction sig;
sig.sa_handler=do_signal;
sigemptyset(&sig.sa_mask);
sig.sa_flags=0;
sigaction(SIGALRM,&sig,NULL);
alarm(n);
//挂起当前进程,直到该进程接收到一个信号(任意一个信号)解除挂起,如果该信号的状态为忽略,则继续挂起
pause();
return alarm(0);
}
int main()
{
printf("%d",mysleep(30));
return 0;
}
返回值为没有睡眠的时间,可以通过alarm(0)获得。
这个程序简单,我们来分析一下:如果在程序执行到alarm()函数时,假设时间为2秒,在时间为1秒时,cpu被另一个高优先级的进程竞争去了,在那个进程执行了2秒。我们之前已经讲过信号捕捉的流程,在操作系统从内核返回时会去处理可以递达的信号,这时操作系统就会立即响应SIGALRM()信号,进而去执行do_signal函数。在do_signal函数执行完成后才会去执行puash()函数,这时就是导致程序被无限制的挂起(信号产生的时序竞态)。这就说明当前这个程序是存在BUG的,而且这个BUG的存在时不确定的,即你无法控制BUG的发生,这就会使的程序的调试变得非常的麻烦。
那要如何解决这个BUG那?有一种很简单的方式就是加锁,让aralm函数和puash函数成为一个原子操作。这样就不会产生那样的问题了。还要就是利用sigsuspend函数。
程序4:mysleep2
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
void do_signle(int num)
{
printf("TIME NO!\n");
}
unsigned int mysleep(int n)
{
unsigned int surTime=0;
struct sigaction newSig,oldSig;
sigset_t newSigset,oldSigset,suSigset;
//设置捕捉函数
//设置信号调用
newSig.sa_handler=do_signle;
sigemptyset(&newSig.sa_mask);
newSig.sa_flags=0;
//利用oldSig保存SIGALRM原来的设置。
sigaction(SIGALRM,&newSig,&oldSig);
//将SIGALRM信号阻塞
sigemptyset(&newSigset);
sigaddset(&newSigset,SIGALRM);
sigprocmask(SIG_BLOCK,&newSigset,&oldSigset);
alarm(n);
//解除SIGALRM的阻塞
suSigset=oldSigset;
sigdelset(&suSigset,SIGALRM);
//利用sigsuspend函数将阻塞集注册
sigsuspend(&suSigset);
//计算睡眠剩余时间
surTime=alarm(0);
//恢复现场
sigaction(SIGALRM,&oldSig,NULL);
sigprocmask(SIG_BLOCK,&oldSigset,NULL);
return surTime;
}
int main()
{
printf("剩余时间:%d\n",mysleep(5));
return 0;
}
利用sigsuspend函数,当信号产生时将其屏蔽,这是该进程对应的信号的未决信号集对应的位置就会置为1,当调用sigsuspend函数时将对该信号的屏蔽临时解除,这样就可以保证挂起操作在信号触发之前完成,就不会出现时序竞态的问题了。