信号是软件中断,它提供了一种处理异步事件的方法。当A和B进行通信时,B收到了信号,不管程序执行到了哪,都会暂停去处理信号。并且每个进程收到的所有信号,都是内核负责发送和处理,而我们捕捉到信号编写的处理函数,只是相当于内核处理的时候调用的一个函数。在头文件signal.h
中,信号名都被定义为正整数变量(信号编号),在linux下,可以使用kill -l
查看。
1.按键产生:比如Ctrl+c可以杀掉当前进程,其实是产生了一个中断信号(SIGINT)
2.硬件异常产生:如果我们编写的代码中有除以0这样的操作,程序就会异常中止,实质是产生了SIGFPE信号,还有非法访问内存会提示段错误,也是产生了一个名为SIGSEGV的信号
3.软件产生:比如调用alarm
函数
4.命令产生:我们经常用来关进程的kill
命令,会产生SIGKILL信号
5.系统调用:比如kill
函数和abort
函数等
由于信号是借助软件方法实现的,所以它的延迟肯定要比硬件实现的高,但是对于用户来说,延迟时间还是很短,不易察觉。我们把信号发送之后到达进程这种状态称为递达,那么信号处于产生和递达之间的这种状态就称为未决,但是前面说到了,对于用户来说,延迟时间很短,所以是未决态的时间应该很短才对,但是还是把这种状态提了出来,这是因为造成信号未决的主要原因是屏蔽信号,而不是正常发送和接收的这个过程。
前面也提到了一个很重要的概念,就是信号都是由内核负责发送和处理,从这点就可以大约猜到,信号有自己默认的处理方式。还有另外两种处理方式,一种是忽略该信号,还有一种就是捕捉该信号。
前面提到了可以使用kill -l
命令查看信号名,结果如下:
column | column | column | column | column |
---|---|---|---|---|
1) SIGHUP | 2) SIGINT | 3) SIGQUIT | 4) SIGILL | 5) SIGTRAP |
6) SIGABRT | 7) SIGBUS | 8) SIGFPE | 9) SIGKILL | 10) SIGUSR1 |
11) SIGSEGV | 12) SIGUSR2 | 13) SIGPIPE | 14) SIGALRM | 15) SIGTERM |
16) SIGSTKFLT | 17) SIGCHLD | 18) SIGCONT | 19) SIGSTOP | 20) SIGTSTP |
21) SIGTTIN | 22) SIGTTOU | 23) SIGURG | 24) SIGXCPU | 25) SIGXFSZ |
26) SIGVTALRM | 27) SIGPROF | 28) SIGWINCH | 29) SIGIO | 30) SIGPWR |
31) SIGSYS | 34) SIGRTMIN | 35) SIGRTMIN+1 | 36) SIGRTMIN+2 | 37) SIGRTMIN+3 |
38) SIGRTMIN+4 | 39) SIGRTMIN+5 | 40) SIGRTMIN+6 | 41) SIGRTMIN+7 | 42) SIGRTMIN+8 |
43) SIGRTMIN+9 | 44) SIGRTMIN+10 | 45) SIGRTMIN+11 | 46) SIGRTMIN+12 | 47) SIGRTMIN+13 |
48) SIGRTMIN+14 | 49) SIGRTMIN+15 | 50) SIGRTMAX-14 | 51) SIGRTMAX-13 | 52) SIGRTMAX-12 |
53) SIGRTMAX-11 | 54) SIGRTMAX-10 | 55) SIGRTMAX-9 | 56) SIGRTMAX-8 | 57) SIGRTMAX-7 |
58) SIGRTMAX-6 | 59) SIGRTMAX-5 | 60) SIGRTMAX-4 | 61) SIGRTMAX-3 | 62) SIGRTMAX-2 |
63) SIGRTMAX-1 | 64) SIGRTMAX |
编号1-31的信号称为常规信号,34-64称为实时信号,驱动编程与硬件相关的信号。所以不从事硬件方面的只用了解并熟悉前31个信号就行了。
1.编号
2.名称
3.事件
4.默认处理动作
通过命令man 7 signal
查看帮助文档,编号就是对应的值那一列,名称对应信号那一列,默认处理动作对应动作那一列,事件对应说明那一栏。在值的那一列,有个别信号居然有多个值,这是在不同的架构下,信号拥有不同的值而已,我们平时用的多数都是x86和arm架构,对应中间的值。
默认处理动作也有如下几个行为:
1.终止进程
2.终止进程并产生core文件(用于检查进程死亡原因
3.忽略信号
4.暂停进程
5.继续运行
并不是所有的信号都能被捕获或者忽略,9)SIGKILL和19)SIGSTOP信号就不允许忽略和捕捉,只能执行默认动作。
这里有必要提一下,前面写道信号的三个处理方式中也有一个忽略,但是这两个忽略并不同,默认处理动作中的忽略是默认就忽略该信号了。
顾名思义,就是信号的集合。这样便于进程相关的捕捉或者屏蔽操作等。
linux内核的进程控制块pcb是一个结构体,其中包含了进程id,状态,工作目录,用户id,组id,文件描述符表也包含了阻塞信号集以及未决信号集。
阻塞信号集(信号屏蔽字):将某些信号加入集合,一旦设置了屏蔽,那么当该信号被接收时,不会马上处理该信号,而是在解除屏蔽后再处理
未决信号集:信号产生时,未决信号集中对应的这个信号置为1,表示处于未决态,当信号递达后,又重新置为0。还有另外一种情况,就是当信号产生后由于一些原因导致不能递达,就会处于未决态。并且如果信号被屏蔽了,那么在屏蔽被解除前,该信号都一直处于未决态。
函数原型:int kill(pid_t pid, int sig)
返回值:成功返回0,失败返回-1,并设置errno
参数:第一个参数代表进程的id,pid>0:发送信号给指定的进程,pid=0:发送信号给和调用kill函数进程属于同一进程组的所有进程,pid<0:发送给对应|pid|的进程组,pid=-1:发送给进程有权限发送的所有进程(root可以向任何用户发送信号,而普通用户不能向root用户和其他普通用户发送信号)
例子:
#include
#include
#include
int main()
{
pid_t pid;
int i;
for(i = 0; i < 5; i++) //循环创建5个子进程
{
pid = fork();
if(pid == 0)
break;
}
if(pid == 0 && i == 0) //让第一个子进程杀掉当前进程组全部进程,即父进程以及另外的四个子进程
{
printf("kill father process\n");
sleep(1);
kill(0, SIGINT);
}
else if(pid > 0) //主进程
{
sleep(5);
}
return 0;
}
函数原型:int raise(int sig)
返回值:成功返回0,失败返回非0值
参数:sig代表信号名
这个函数和kill
的区别就是,该函数只能自己给自己发信号,即调用该函数的进程
函数原型:void abort(void)
这个函数作用是给自己发送SIGABRT异常终止信号,并生成core文件
函数原型:unsigned int alarm(unsigned int seconds)
返回值:在调用此alarm之前,如果进程之前也调用过alarm,那就返回上一个定时器定时的剩余时间,否则返回0。
注意:一个进程只有一个定时器
内核通过读取未决信号集来判断信号是否应被处理。而未决信号集会被阻塞信号集(信号屏蔽字)所影响。所以我们可以通过设置信号屏蔽字来达到屏蔽指定信号的目的。
设定信号集:
函数原型:
int sigemptyset(sigset_t *set); //将某个信号集清0
int sigfillset(sigset_t *set); //将某个信号集置1
int sigaddset(sigset_t *set, int signum); //将某个信号加入信号集
int sigdelset(sigset_t *set, int signum); //将某个信号清出信号集
int sigismember(const sigset_t *set, int signum); //判断某个信号是否在信号集中
返回值:除了最后一个函数外,都是成功返回0,失败返回-1。最后一个函数如果传入的信号在信号集中则返回1,不在返回0,失败返回-1。
参数:sigset_t set
定义为typedef unsigned long sigset_t
,它的本质是位图。signum代表要操作的信号名。
屏蔽/解除屏蔽:
函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
返回值:成功返回0,失败返回-1
参数:
how:这个参数对应三个值。SIG_BLOCK,代表要进行屏蔽;SIG_UNBLOCK,代表要解除屏蔽;SIG_SETMASK,直接把旧的信号屏蔽字替换成我们传入的信号屏蔽字
set:传入参数,是一个位图,哪位置成1,就代表当前进程要屏蔽哪个信号
odlset:传出参数,用于保存旧的信号屏蔽字
读取未决信号集:
函数原型:int sigpending(sigset_t *set)
返回值:成功返回0,失败返回-1
参数:set:传出参数,代表当前进程的未决信号集
比较简单的捕捉函数是signal
函数。
函数原型:sighandler_t signal(int signum, sighanler_t handler);
返回值:成功返回以前的信号处理方式,失败返回SIG_ERR
参数:signum:代表要捕捉的信号名,handler:代表捕捉后,进行的动作。sighandler_t
定义为typedef void (*sighandler_t)(int)
例子:
#include
#include
#include
#include
typedef void (*sighandler_t)(int); //我使用的当前linux版本,需要自己定义,不然会报错
void catch_sigint(int signo) //捕捉指定信号后执行的函数
{
printf("catch your ctrl+c\n");
}
int main()
{
sighandler_t handler; //用于接收返回值
handler = signal(SIGINT, catch_sigint); //将SIGINT信号捕捉,并设置相应的处理动作
if(handler == SIG_ERR)
{
perror("signal error");
exit(1);
}
while(1); //死循环,等待信号发送
return 0;
}
结果就是以前按ctrl+c可以杀掉这个进程,但是却输出了”catch your ctrl+c”。(ps:关不了不要惊慌,按ctrl+\或者使用kill命令杀掉即可)
接下来的另外一个捕捉信号的函数是sigaction
函数
函数原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
返回值:成功返回0,失败返回-1,并设置errno
参数:
signum:代表要捕捉的信号名
act:传入参数,代表新的处理方式
oldact:传出参数,旧的处理方式
struct sigaction
{
void (*sa_handler)(int); //指定信号捕捉后进行处理的函数名。可以赋值为SIG_IGN代表忽略,SIG_DFL代表执行默认动作
void (*sa_sigaction)(int, siginfo_t *, void *); //很少使用
sigset_t sa_mask; //调用信号处理函数时,要屏蔽的信号屏蔽字(只在处理函数被调用期间生效)
int sa_flags; //通常设置为0,表示使用默认属性
void (*sa_restorer)(void); //已废弃
};
例子:
#include
#include
#include
void sig_int(int signo) //处理SIGINT信号的方式
{
printf("catch SIGINT\n");
sleep(5);
}
int main()
{
struct sigaction act;
act.sa_handler = sig_int; //指定处理的函数
sigemptyset(&act.sa_mask); //清空sa_mask信号屏蔽字
sigaddset(&act.sa_mask, SIGQUIT); //将SIGQUIT加入该信号屏蔽字,表示屏蔽SIGQUIT
act.sa_flags = 0; //使用默认属性
sigaction(SIGINT, &act, NULL); //指定捕捉SIGINT,并且将新的处理方式传入
while(1);
return 0;
}
结果是当我们键入ctrl+c时,在信号处理的那个函数是,sleep了5秒,在5秒内,该函数仍在处于被调期间,所以导致我们键入ctrl+\也终止不了该进程,这是因为设置了屏蔽,处理函数调用完之后,会自动处理被屏蔽的信号。这里需要注意一下,阻塞的常规信号不会进行排队,只会记录最近的一次。并且被捕捉的信号在捕捉函数执行期间,会自动被屏蔽,意思就是本例中连续多次按ctrl+c,多余的ctrl+c会被屏蔽掉。