本文的动手以及参考文献 引自 <
本文将从5个角度去聊一下linux当中的信号集
在说起linux信号的特性之前我们先来说一下,什么是linux信号?
信号是软件中断,是通过中断告诉我们计算机发生的行为,是计算机中进程通讯一种有限制的方式,好多重要的应用程序都需要处理信号。
我把linux的信号总结了三个特性:
1.信号具有中断的特性
我们在工作中写程序可能感受不到信号,但是信号却无处不在linux系统之中,当系统陷入阻塞的时候,如果有信号发生,会出现中断现象,但是php在解释器中已经做了信号屏蔽处理,我们在写php程序的时候很难感受到,只能调用php的pcntl系列的扩展去同步阻塞的去处理信号,而不能做到异步处理信号,在这里我用linux c 带大家深入理解linux信号的可中断性,当我们使用信号捕获函数的时候会触发信号的中断现象
信号具有中断特性的实验代码(demo0.c)
#include
#include
#include
#include
#include
#include
#include
#include
int run = 1;
void function(int signo)
{
run = 0;
}
int main()
{
printf("%d\n",getpid());
signal(SIGTERM,function);
fd_set set;
FD_ZERO(&set);
int fd = socket(AF_INET,SOCK_STREAM,0);
FD_SET(fd,&set);
struct sockaddr_in addr;
bind(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr));
listen(fd,100);
while (run)
{
int res = select(fd+1,&set,NULL,NULL,NULL);
if(res < 0)
{
printf("%d:%s\n",errno,strerror(errno));
}
}
printf("code end!thank you!\n");
}
上面的例子程序是运用信号的中断特性来实现一个平滑停止,我们可以很轻松的看到当信号被安装上信号处理函数的时候,如果应用进程收到了信号会中断阻塞的select程序
实验现象:(当我们运行程序的时候程序发生了阻塞)
然后我们使用kill这个二进制文件对着进程26098发送指令
kill -15 26098
我们发现阻塞的程序被中断掉了!!!
现象反思?
我们可以利用信号的中断特性做什么呢?上面的程序已经很清晰的展现了信号中断特性的一个使用案例,实现进程的平滑停止
2.信号是异步的
我们可以使用信号做异步处理一个最简单的demo就是使用signal做异步处理,但是其实这是一个比较旧的api了,我们可以使用比较新的api接口sigaction,这里我们做最简单的异步特性介绍所以只用signal就可以了
demo处理程序(demo1.c)
#include
#include
#include
#include
#include
#include
#include
void hook(int signo)
{
printf("pid:%d\n",getpid());
printf("thread_id:%ld\n",pthread_self());
printf("stop\n");
}
int main(int argc,char** argv)
{
printf("pid:%d\n",getpid());
printf("thread_id:%ld\n",pthread_self());
signal(SIGTERM,hook);
while(1)
{
}
exit(0);
}
当我们使用kill -15 去发送信号发现程序异步执行了hook函数,今天在组内分享的时候,组内对信号处理是异步处理的问题进行了探讨,信号处理这个说法是异步的是因为系统处理信号的时候又开了一个进程或者线程吗?我们做了实验
异步程序的可重入
信号是一种异步处理程序,所以说我们在程序处理中,就需要考虑在处理函数中,使用系统api的安全性,即函数的可重入
当进程捕捉到信号的时候,进程的程序会被临时中断,他首先执行这个信号处理程序中的指令。如果从信号处理程序返回,则继续执行在捕捉到信号时候进程正在执行正在执行的指令序列。正是如此才对程序的可重入造成了安全问题。
危险案例(在新的linux中并没有跑通,用strace的时候发现futex锁,说明新的linux程序已经对getpwnam上锁了)
思考为什么会出现这种现象?
当进程捕捉到信号的时候,进程的程序会被临时中断,他首先执行这个信号处理程序中的指令。如果从信号处理程序返回,则继续执行在捕捉到信号时候进程正在执行正在执行的指令序列。正是如此才对程序的可重入造成了安全问题。
3.可靠信号和不可靠信号
1.linux系统中一些信号可能是不可靠的,信号发生后系统可能并不会收到通知,我们使用
Kill -l 查看信号
不可靠信号:在执行自定义函数其间会丢失同类信号
可靠信号:在执行自定义函数其间不会丢失同类信号
在我的系统上ubuntu
1到31是不可靠信号 32~64是可靠信号
一段代码测试可靠信号和非可靠信号(demo3)
#include
#include
#include
void sigintctl(int signum)
{ printf( "handle begin.\n");
printf( "receive signum %d \n" , signum) ;
sleep(2);
printf( "handle end.\n");
}
int main()
{
printf("%d\n",getpid());
// signal( SIGINT, sigintctl) ;
signal( SIGRTMIN, sigintctl) ;
while(getchar() != "q"){};
return 0;
}
当我们处理信号SIGINT的时候会发现在sleep的时候多次,ctrl+c会造成信号的缺失,实验现象
上述现象的不可靠信号在sleep的时候造成了信号处理的丢失,包括我们进程用的SIGTERM也是一个不可靠信号!!!注意SIGTERM也是
当我们对SIGRTMIN做处理的时候,不管sleep如何休息,发送的信号必定会接收到,因为他们的信号值是34,属于可靠信号,实验现象
4.函数插曲,在这里我们要说几个常用的api函数
第一个是raise函数
调用raise函数等价于kill(getpid(),signo)
raise的实验案例
实验结果:
kill函数
kill函数其实第一个参数有四中传递方式,当然其实我们很多时候都用的是第一种,pid>0的情况,其实是有四种情况的
其中最危险的就是kill(-1,signo)了,使用这个-1一定要谨慎,使用不慎计算机都会黑屏关闭电源!!!!!强力注意,危险的实验现象
运行完之后直接黑屏!!所以说真的要小心kill -1 了
闹钟函数
我们再说一下这个闹钟函数alarm这个函数,他可以定时产生SIGALRM信号,我们可以通过这个信号做一个简单的定时器
#include
#include
#include
void alarm_hook(int signo)
{
printf("alarm\n");
}
int main()
{
signal(SIGALRM,alarm_hook);
alarm(1);
pause();
return 0;
}
实验现象,当我们相隔1秒后产生了SIGALRM这样一个信号
表示多个信号的集合就是一个信号集
sigemptyset置空一个信号集
sigfillset 填充满一个信号集
sigaddset 将一个信号加入信号集
sigdelset 将一个信号从信号集删除
sigismember 检查一个信号集中是否有这个信号
实验代码
#include
#include
int main()
{
int i = 0;
//树的跟
sigset_t set;
sigfillset(&set);
for(i=1;i<=64;i++)
{
printf("%d\n",sigismember(&set,i));
}
sigdelset(&set,15);
printf("%d\n",sigismember(&set,15));
sigaddset(&set,15);
printf("%d\n",sigismember(&set,15));
sigemptyset(&set);
printf("%d\n",sigismember(&set,15));
printf("%d\n",sigismember(&set,14));
return 0;
}
这个代码详细展示了 填充满一个信号集、删除信号集中的一个信号,添加一个信号进入信号集,以及如何置空一个信号集,我们使用sigismember用来检测信号集中是否有这个成员
实验现象:
我们发现31,32是0表示在集合中没有,1代表在集合中有
信号集是一些glibc库函数的重要的接口参数
1.sigprocmask函数
思考为什么要使用sigprocmask函数?sigprocmask函数是做什么的?
我们在工作中的进程运行可能被各种各样的linux信号所打断,在很多的时候我们需要屏蔽那些我们不想收到的信号,这个时候我们可以使用sigprocmask函数来设置信号的屏蔽集,屏蔽我们不想收到的信号集,我们可以使用sigprocmask这个函数来设置,修改和解除信号的屏蔽集
屏蔽信号的demo展示(demo6.c)
#include
#include
#include
void term_hook(int signo)
{
printf("stop\n");
}
int main()
{
printf("%d\n",getpid());
signal(SIGTERM,term_hook);
sigset_t set;
sigset_t old_set;
sigfillset(&set);
sigprocmask(SIG_BLOCK,&set,&old_set);
//sigprocmask(SIG_UNBLOCK,&set,&old_set);
//sigdelset(&set,SIGTERM);
//sigprocmask(SIG_SETMASK,&set,&old_set);
while(1)
{
}
return 0;
}
我们运行代码可以发现sigprocmask一个重要的作用是可以设置信号的临界区,进行信号阻塞,阻塞在信号集中的信号不能进入应用程序,直到被SIG_UNBLOCK或者SIG_SETMASK才可以继续接收处理
当我们使用kill -15 14005发现信号被阻塞住了
实验现象:
除非我们使用SIG_UNBLOCK释放掉整个临界区或者SIG_SETMASK修改阻塞的信号集之后才可以继续处理阻塞调的系想你好
解除屏蔽信号的demo展示(demo6.c)
实验现象
当我们kill -15之后信号被接收处理
修改屏蔽信号的demo展示(demo6.c)
实验现象
3.sigpending
说起信号阻塞,我们就要聊一下什么是未决信号?
未决信号是说那些被sigprocmask阻塞的信号都是未决信号,sigpending的作用就是获取未决信号的!!
进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。sleep不会出现未决,只有sigprocmask设置SIG_BLOCK才会出现未决信号
#include
#include
#include
void term_hook(int signo)
{
printf("stop\n");
}
int main()
{
printf("%d\n",getpid());
signal(SIGTERM,term_hook);
sigset_t set;
sigset_t old_set;
sigset_t pending;
sigfillset(&set);
sigprocmask(SIG_BLOCK,&set,&old_set);
sleep(10);
sigpending(&pending);
printf("%d\n",sigismember(&pending,SIGTERM));
while(1)
{
}
return 0;
}
我们使用sigprocmask阻塞住信号之后,暂停10秒,然后在这期间,kill -15 pid对着进程发送信号,会发现信号出现在了pending这个信号集中,验证了sigpending是用来检测未决信号的
sigismember显示1代表是SIGTERM出现在了未决信号中,验证了我们的猜想
4.sigaction函数
sigaction是比signal函数更加强大和稳健的一个系统函数,sigaction函数的功能是检查或者修改与置顶信号相关联的处理动作,一旦设置了sigaction,那么这个设置将会一直有效,推荐使用此函数处理信号,不要使用signal
sigaction结构体
struct sigaction
{
/* Signal handler. */
#if defined __USE_POSIX199309 || defined __USE_XOPEN_EXTENDED
union
{
/* Used if SA_SIGINFO is not set. */
__sighandler_t sa_handler;
/* Used if SA_SIGINFO is set. */
void (*sa_sigaction) (int, siginfo_t *, void *);
}
__sigaction_handler;
# define sa_handler __sigaction_handler.sa_handler
# define sa_sigaction __sigaction_handler.sa_sigaction
#else
__sighandler_t sa_handler;
#endif
/* Additional set of signals to be blocked. */
__sigset_t sa_mask;
/* Special flags. */
int sa_flags;
/* Restore handler. */
void (*sa_restorer) (void);
};
sa_handler的三种情况:
1)SIG_DFL是系统默认的信号处理方式
2)SIG_IGN 是 忽略信号的处理
3)自定义函数地址是我们定义的处理方式
sa_flags的情况一览
我们在这里简单介绍sigaction的两种用法,一个重要的用法是绑定信号处理函数,另一个用法是获取信号的action信息,当我们设置了SA_SIGINFO,设置了SA_SIGINFO以后,调用信号处理函数的返回会发生改变,会多出siginfo_t* info 和 void *context这两个参数,在这里我们拿出来重点说说SA_SIGINFO
SA_SIGINFO的demo(demo8.c)
#include
#include
#include
#include
void hook(int signo,siginfo_t* info,void* context)
{
printf("info signo:%d\n",info->si_signo);
printf("info errno:%d\n",info->si_errno);
printf("info code:%d\n",info->si_code);
printf("info pid:%d\n",info->si_pid);
printf("info uid:%d\n",info->si_uid);
printf("info band:%ld\n",info->si_band);
ucontext_t* sig_context = (ucontext_t*)context;//用来保存上下文的
//常见的ucontext库可以使用
}
int main()
{
printf("%d\n",getpid());
struct sigaction action;
struct sigaction action_info;
action.sa_flags = SA_SIGINFO;
action.sa_handler = (void*)hook;
sigaction(SIGTERM,&action,NULL);
while(1){}
}
我们使用kill -15 对着进程发送信号的现象
实验现象:
我们发现info里有发送方进程id,errno,uid等待一系列信息非常全面
当然这只是一个很小的作用,sigaction有很多其他信息,比如说获取信号绑定的其他信息等等
5.sigsetjmp函数和siglongjmp函数
goto是我们在编程中用的最简单的上下文切换关键字,如果我们要实现跨函数的关键词切换可以使用setjmp和longjmp,但是这两个函数在信号处理函数中使用却会造成安全问题,调用longjmp存在一个安全问题就是信号发生的时候,进入信号的时候,当前信号会自动加入屏蔽集中,阻止了后来产生的中断信号处理程序
实验程序
#include
#include
#include
#include
#include
#include
jmp_buf context_buffer;
void hook(int signo)
{
printf("stop\n");
longjmp(context_buffer,1);
}
int main()
{
printf("%d\n",getpid());
signal(SIGTERM,hook);
setjmp(context_buffer);
while(1)
{
}
}
我们发现当收到SIGTERM信号发生跳转后,进程在不会收到任何有关SIGTERM的信号,整个SIGTERM信号都被屏蔽阻塞掉了
试验现象:
这时候我们可以使用sigsetjmp和siglongjmp,他们在发生跳转后会恢复屏蔽集
唯一的区别就是sigsetjmp相对于setjmp增加了一个参数
sigsetjmp相对于setjmp增加了一个参数的主要作用
如果savemask非0,则sigsetjmp在env中保存当前的信号屏蔽字,调用siglongjmp时候,如果带了非0的savemask的sigsetjmp已经保存了env,则siglongjmp从其中回复他的信号屏蔽字
思考问题:
当我们把setjmp和longjmp替换为sigsetjmp和siglongjmp还会出现这种现象吗?
当我们把setjmp的参数设置为非0的情况
#include
#include
#include
#include
#include
#include
jmp_buf context_buffer;
void hook(int signo)
{
printf("stop\n");
siglongjmp(context_buffer,1);
}
int main()
{
printf("%d\n",getpid());
signal(SIGTERM,hook);
// sigsetjmp(context_buffer,0);
sigsetjmp(context_buffer,1);
while(1)
{
}
}
实验现象
这时候程序并没有出现,setjmp和longjmp的bug
这时候信号处理,进入屏蔽集之后被恢复了,但是当我们把setjmp的参数设置为0的情况,又会恢复吗?
实验现象
又出现了setjmp和longjmp的问题
6.sigsuspend函数是简介
为什么要使用sigsuspend?
更改信号的屏蔽字可以阻塞我们选择的信号,或者接触他们的阻塞,使用这种技术可以改变我们的临界区。如果希望对一个信号解除阻塞,然后在进行pause等待,如果信号发生在pause和解除信号阻塞集之间,那么信号可能发生丢失
sigsuspend的简单使用(demo12.c)
#include
#include
#include
#include
volatile sig_atomic_t quitflag;
static void sig_int(int);
void err_sys(const char* msg)
{
printf("%s\n",msg);
exit(-1);
}
int main()
{
sigset_t newmask,oldmask,zeromask;
if(signal(SIGINT,sig_int) == SIG_ERR)
err_sys("signal SIGINT error");
if(signal(SIGQUIT,sig_int) == SIG_ERR)
err_sys("signal SIGINT error");
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask,SIGQUIT);
if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)
err_sys("SIGBLOCK error");
while (quitflag == 0)
sigsuspend(&zeromask);
quitflag = 0;
if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
err_sys("SIG_SETMASK error");
exit(0);
}
static void sig_int(int signo)
{
if(signo== SIGINT)
printf("SIGINT\n");
else if(signo == SIGQUIT)
quitflag = 1;
}
sigsuspend加强了原子性
思考:信号是异步的我们可以用同步方式来处理吗??
1.其实在这里我们是可以用两种同步方式来处理信号集
1)第一种是使用sigwait函数来做到同步阻塞信号的发生
2)第二种情况是使用epoll+signalfd的方式来多路复用的形式来监控信号的产生
在这里我们只是说明sigwait函数的同步处理形式
sigwait实验代码(demo13.c):
#include
#include
#include
#include
int main()
{
sigset_t set,block_set;
sigemptyset(&set);
sigaddset(&set,SIGINT);
//设置信号屏蔽
sigfillset(&block_set);
sigprocmask(SIG_BLOCK,&block_set,NULL);
int retval;
for(;;) {
int res = sigwait(&set, &retval);
if (res == 0) {
printf("sigint\n");
}
}
exit(0);
}
实验现象:
发现在BLOCK信号集后调用sigwait信号处理异步变成了同步
一些作业控制信号的回顾
五。信号和我们的日常工作?
信号和我们的日程工作有着千丝万缕的联系
1)队列服务监控SIGCHILD防止僵尸进程的产生,拉起挂掉的子进程
2)监控SIGTERM来关闭队列服务
3)监控SIGUSR来平滑重启service服务
4)很多时候都需要忽略掉SIGPIPE这个信号,因为他可能造成整个进程的退出,SIGPIPE信号发生在套接字已经被关闭,但是还是被频繁send的情况下会出现SIGPIPE的情况下,在编程中通常会SIG_IGN来忽略掉,im项目中的信号处理
总结:
信号与我们的工作千丝万缕熟练使用glibc对信号处理是一个linux程序员的重要技能!!一定要慎重慎重!!!