概述
大家应该都了解当我们在Linux下面运行程序时,如果程序跑飞了,那我们可以通过Ctrl-C来终止掉跑飞的程序,其实,当我们按下Ctrl-C时,涉及到了信号的相关处理工作,所以,今天我们就来谈一谈Ctrl-C被后的信号处理吧。
什么是信号
信号其实是一种软件中断,它为程序提供了一种处理异步事件的方法,所谓的异步事件就是事件的发生在任何事件都有可能,很多重要的程序都需要对信号进行处理。在Linux中一共有多少信号呢?我们可以通过在shell下运行kill -l指令,来查看Linux下的信号数目和他们对应的编号。如下图:
其中,我们把编号为1~31的信号称为普通信号,把34~64的信号称为实时信号。所有的信号名在头文件signal.h,中都被定义为正整数常量,即他们的信号编号。
信号的产生方式
在Linux下,信号可以通过一下几种方式来产生:
- 当用户按某些终端键时,引发终端产生的信号,例如我们前面提到的Ctrl-C按键对应的是SIGINT信号,Ctrl-按键对应的是SIGQUIT信号。
- 硬件异常产生信号,例如除数为0、无效的内存引用对应的SIGSEGV信号,硬件异常产生信号不同,一旦硬件异常产生,那么他会一直存在,直到程序被终止位置,所以处理硬件异常信号一般都采用终止程序的方法。
- 进程调用kill(系统调用)函数可将任意信号发送给另一个进程或者进程组;(但是这条是有限制的,例如:要求接收信号和发送信号的进程的所有者必须相同,或者发送信号的所有者必须是超级用户)。
- 进程可以通过在shell下运行kill指令来对某个进程发出信号,kill指令是kill系统调用的一个接口。
- 当检测到某些软件条件时,也会产生信号。
信号的处理方式
有了信号的产生,当然也就有信号的处理,在Linux中,对信号的处理有下面的几种方式:
1. 忽略此信号,大多数信号都可以采用这种方式进行处理,除了9) SIGKILL 和19) SIGSTOP 信号。因为这两种信号都直接向内核提供了进程终止和停止的可靠办法。(SIGKILL),还有硬件异常信号我们最好不要忽略,因为硬件异常一旦产生如果不进行处理就会一直存在。
2. 捕捉信号。 进程要通知内核在某种信号产生时,需要调用一个用户函数,在用户函数中,用户可以自己定义信号处理的方式,在Linux下我们不能捕捉SIGKILL信号和SIGSTOP信号。
3. 执行系统的默认动作。
终止+core
在系统默认动作中,有一种动作叫做“终止+core”,它表示在进程当前工作目录中的core文件中复制了该进程当前的内存映像,(该文件名为:core.进程pid号),很多信号都使用了这种处理方式,例如SIGFPE信号。当产生了内存映像文件,我们可以在gdb下面进行调试。如下图:
其中ulimit -c + number是指定core文件的大小上限,
ulimit -a是查看当前系统中的各个资源上限。
相关信号处理函数
- signal函数(指定信号的处理方式)
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:参数为信号名,或者信号编号。
handler:为指向返回值为void参数为int的函数指针,或者是SIG_IGN或SIG_DFL宏定义。
在signal.h头文件中,上面的常量被定义如下
#define SIG_ERR (void(*)()) -1
#define SIG_DFL (void(*)()) 0
#define SIG_IGN (void(*)()) 1
其中,SIG_DFL指定信号处理的方式为默认方式,SIG_IGN指定信号处理的方式为忽略。
- alarm函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm函数相当于一个闹钟,它可以为进程注册闹钟时间,例如使用alarm(5)可以为进程注册5秒钟的闹钟时间,5秒后会产生SIGALRM信号;
如果在调用alarm函数时,之前已经为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前注册的闹钟时间则被新值所取代;
使用alarm(0)可以取消以前所注册的闹钟,并返回之前注册的闹钟的剩余时间。
- pause函数
#include <unistd.h>
int pause(void);
pause函数是检测进程有没有从信号处理函数中返回,只有当执行了一个信号处理程序并从其返回时,pause才返回,否则pause将一直挂起调用进程。
当进程执行了信号处理程序时,pause返回-1,并将errno设置为EINTR.
信号捕捉
学习了上面三个函数后,我们可以在这基础上学习一个概念,叫做信号捕捉。前面我们提到过,信号捕捉是操作系统处理信号的一种方式,当我们要用信号捕捉方式来处理信号时,需要定义信号处理函数。我们可以看看下面这个例子
Linux中的信号处理机制
大家可以先看看下面这幅图片,了解一下内核是怎么做到信号处理的。
内核处理信号的相关数据结构定义如下:
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
...
...
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;
...
}
struct signal_struct {
atomic_t sigcnt;
atomic_t live;
int nr_threads;
...
...
/* shared signal handling: */
struct sigpending shared_pending;
...
}
struct sigpending {
struct list_head list;
sigset_t signal;
};
#define _NSIG 64
#ifdef __i386__
# define _NSIG_BPW 32
#else
# define _NSIG_BPW 64
#endif
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)
typedef unsigned long old_sigset_t; /* at least 32 bits */
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
Linux中关于信号的组织的相关数据结构如上图。
在这里还要提到几个概念:
信号递达:表示正在执行信号处理的动作。
信号未决:pending,表示信号从产生到信号递达的状态。
信号阻塞:block,表示信号不能被递达,如果一个信号被阻塞,那么这个信号将会一直处于未决状态,直到解除阻塞位置。
在Linux系统中,信号未决和信号阻塞的状态都被组织在两张位图中的,即我们可以通过位图中的某一位的是0还是1,了解到该信号是否被阻塞,以及信号是否在未决状态。
信号处理机制
内核是如何处理信号的?
(信号处理机制.PNG)
从图中我们能发现,当一个正在运行的进程如果调用了系统调用或者进程收到了中断,则进程会从用户态进入到内核态,当进程准备从内核态返回到用户态时,内核会检查要返回的进程的pcb中的signal位图信息,如果当前的pengding表中有标志1,那么内核会把pengding链表中悬挂的信号拿出来进程处理,处理的过程如下,如果handler指向了用户自定义的处理函数,那么会先从内核态返回到用户态执行完处理函数后再返回到内核态,最后再从内核态返回到用户态中因为中断或者系统调用进入内核态的代码从而继续执行。
信号集操作函数
#include <signal.h>
//初始化由set所指向的信号集,清除其中的所有信号。
int sigemptyset(sigset_t *set);
//初始化set所指向的信号集,使set包含所有信号
int sigfillset(sigset_t *set);
//将一个信号添加到已经存在的信号集中
int sigaddset(sigset_t *set, int signum);
//从已有信号集中删除一个信号
int sigdelset(sigset_t *set, int signum);
//测试信号集是否包含signum信号
int sigismember(const sigset_t *set, int signum);
我们常常通过信号集处理函数来对信号集中的pending和block位图还有hanlder表来对信号进行处理。
sigaction函数
这个函数的功能是检查和修改与指定信号相关联的处理动作。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
signum:是要检测或修改其具体动作的信号编号。
act:指定修改动作。
oldact:返回当前sigaction函数正在使用的动作。
当更改动作时,act包含了下面的结构体成员:
struct sigaction {
void (*sa_handler)(int); //处理动作的函数指针
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //信号集,在调用捕捉函数之前,这个信号集要加到进程的信号屏蔽字中,当从信号捕捉函数中返回时再恢复到原先的值。
//如果我们添加了一个信号在sa_mask中,那么当我们执行其他信号的处理程序时,会阻塞sa_mask中的信号。直到信号处理程序返回信号才会递达。
int sa_flags; //
void (*sa_restorer)(void);
};
sigprocmask函数
该函数用来规定当前阻塞而不能递达给进程的信号集。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
oldset:进程的当前信号屏蔽字通过oldset返回。
sigprocmask:仅为单线程进程进行定义的,在处理多线程时应该使用sigaction函数。
how:指示如何修改当前的信号屏蔽字,可以选择下面三个参数。
SIG_BLOCK:该进程的新的信号屏蔽字是当前信号屏蔽字和set指向信号集的并集,set包含了希望阻塞的附加信号
SIG_UNBLOCK:该进程新的信号屏蔽字是当前信号屏蔽字和set指向信号集补集的交集。set包含了希望解除阻塞的信号。
SIG_SETMASK:该进程新的信号屏蔽集是set指向的值。
需要我们理解一下这句话:在调用sigprocmask后如果有任何未决的,不在阻塞的信号,则在sigprocmask返回前,至少将其中之一递送给该进程。
下面我们写一则测试用例来说明一下这句话的含义:
/*************************************************************************
> File Name: Sigset.c
> Author: LZH
> Mail: 597995302@qq.com
> Created Time: Thu 23 Feb 2017 04:41:16 PM PST
************************************************************************/
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
/*
void Alrm_Run(int signo)
{
printf("The signo is %d...n",signo);
}
int main()
{
signal(SIGALRM,Alrm_Run);
alarm(3);
int count=1;
while(1){
printf("second:%dn",count++);
sleep(1);
}
return 0;
}
*/
void printPending(sigset_t* set)
{
int i=0;
for(i=1;i<=31;i++){
if(sigismember(set,i)==1){
printf("1");
}
else if(sigismember(set,i)==0){
printf("0");
}
else{
perror("sigismember...n");
exit(1);
}
}
printf("n");
}
void sigRun(int signo)
{
printf("The signo is %dn...",signo);
sleep(3);
printf("return sigRun..n");
}
int main()
{
struct sigaction act,oldact;
act.sa_flags=0;
act.sa_handler=sigRun;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGQUIT);
printf("before:");
printPending(&act.sa_mask);
sigaction(SIGINT, &act,&oldact);
while(1){
printf("Running...n");
sleep(1);
}
return 0;
}
我们来看下执行结果:
当我们在信号处理函数中延迟三秒时,此时屏蔽了我们刚开始设定的SIGQUIT信号,也是SIGQUIT信号被阻塞了,当回到用户函数中继续执行时,SIGQUIT信号的阻塞被取消,此时处于未决状态的SIGQUIT信号递达了;所以程序被终止。
最后
以上就是迷路唇膏为你收集整理的Linux信号处理的全部内容,希望文章能够帮你解决Linux信号处理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复