1.信号简介
软中断信号 Signal,简称信号,用来通知进程发生了异步事件,进程之间可以互相通过系统调用 kill 等函数来发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件,但是要注意信号只是用来通知进程发生了什么事件,并不给该进程传递任何数据,例如终端用户键入中断键,会通过信号机制停止当前程序。
2.信号的分类
信号其实是一种软件中断,它为程序提供了处理异步事件的方法。异步事件就是事件可能会在任何时间内发生,很多重要的而程序都有对信号的处理,在linux下我们是通过命令 kill -l来查看系统中所有的信号列表和它们的信号编号。
我们可以看到31~34之间是没有信号的,我们把1~31号信号称为普通信号,把34~64号信号称为实时信号。所有的信号都包含在signal.h中,且都定义成正整数常量,也就是它们的信号编号。
这么多的信号也不可能都记得很清楚,只需要知道常用的即可,常用的信号有下面这些:
- SIGHUP :终端结束信号
- SIGINT :键盘中断信号(Ctrl - C)
- SIGQUIT:键盘退出信号(Ctrl - \)
- SIGPIPE:浮点异常信号
- SIGKILL:用来结束进程的信号
- SIGALRM:定时器信号
- SIGTERM:kill 命令发出的信号
- SIGCHLD:标识子进程结束的信号
- SIGSTOP:停止执行信号(Ctrl - Z)
信号有 2 种分类:不可靠信号,可靠信号。
- 不可靠信号(0~32)
linux 继承了早期 UNIX 的一些信号,这些信号有些缺陷:在发送给进程的时候可能会丢失,也称为不可靠信号,其中信号值小于 34) SIGRTMIN 都是不可靠信号。 - 可靠信号(34~64)
后来 Linux 改进了信号机制,增加了一些可靠信号:支持排队,信号不会丢失,34) SIGRTMIN - 64) SIGRTMIX 为可靠信号。
注意:
非实时信号都不支持排队,都是不可靠信号;
实时信号都支持排队,都是可靠信号。
3.信号的产生形式
linux下的信号一般有四种 产生方式:
1、来自键盘的信号
用户在终端按下某些按键时,终端驱动程序会发送信号给前台的进程(使前台进程终止的信号)。ctrl+c发送SIGINT信号、ctrl+\发送SIGQUIT信号、ctrl+z发送SIGTSTP信号。SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,SIGTSTP信号使前台进程停止。
2、硬件异常产生信号
除数为0、无效的内存引用对应的SIGSEGV信号,硬件异常产生信号不同,一旦硬件异常产生,那么它会一直存在,直到程序被终止位置,所以处理硬件异常信号一般都采用终止程序的方法。
3、通过系统调用向进程发送信号
进程可以通过在shell下运行kill指令来对某个进程发送信号,kill指令是kill系统调用的一个接口。
4、由软件条件产生信号
4.常用信号
1) SIGHUP
SIGHUP会在以下3种情况下被发送给相应的进程:
1、当终端检测到网络断开时,会将挂断信号发送到session首进程以及作为job提交的进程(即用 & 符号提交的进程)
2、session首进程退出时,该信号被发送到该session中的前台进程组中的每一个进程
3、若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGTSTP信号),SIGHUP会被发送到该进程组中的每一个进程。系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。
2)SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
SIGQUIT-信号3-终端推出符-终止core
SIGILL-信号4-终止
3) SIGTERM
SIGTERM比较友好,进程能捕捉这个信号,根据您的需要来关闭程序。在关闭程序之前,您可以结束打开的记录文件和完成正在做的任务。在某些情况下,假 如进程正在进行作业而且不能中断,那么进程可以忽略这个SIGTERM信号。
例程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include <signal.h>
#define MAXFILE 65535
void sigterm_handler(int arg);
volatile sig_atomic_t _running = 1;
int main()
{
int i,fd;
char *buf="this is a Dameon\n";
int len = strlen(buf);
pid_t pc = fork(); //父生子 留子
if(pc<0)
{
printf("error fork\n");
exit(1);
}
else if(pc>0)
exit(0);
setsid(); //1,在子进程中创建新会话
pid_t pid = fork();//子生孙 留孙
if (pid < 0)
perror("fork error");
else if (pid > 0)
exit(0);
chdir("/"); //2,改变当前目录为根目录
umask(0); //3,重设文件权限掩码
for(i=0; i<MAXFILE; i++) //4,关闭文件描述符
close(i);
signal(SIGTERM, sigterm_handler);//5,设置kill信号
while( _running )
{
if((fd=open("/tmp/daemon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
{
perror("open");
exit(1);
}
write(fd,buf,len);
close(fd);
//usleep(10*1000); //10毫秒
sleep(10); //10秒
}
}
void sigterm_handler(int arg)
{
_running = 0;
}
4) SIGKILL
SIGKILL进程是不能忽略的。这是一个 “我不管您在做什么,立刻停止”的信号。假如您发送SIGKILL信号给进程,linux就将进程停止在那里。Linux进程中的kill命令是通过向进程发送指定的信号来结束进程的。如果没有指定发送信号,那么默认值为TERM信号。TERM信号将终止所有不能捕获该信号的进程。至于那些可以捕获该信号的进程可能就需要使用kill(9)信号了,该信号是不能被捕捉的。
5)SIGCHLD
子进程退出时,会给父进程发送SIGCHLD信号,父进程默认忽略之。
APUE上SIGCLD语义写的有点不清楚,到底我们的系统是如何来处理SIGCLD信号呢?
1.SIG_DFL :
默认的处理方式是不理会这个信号,但是也不会丢弃子进行状态,所以如果不用wait,waitpid对其子进行进行状态信息回收,会产生僵尸进程。
2.SIG_IGN :
忽略的处理方式,在这种方式下,子进程状态信息会被丢弃,也就是自动回收了,所以不会产生僵尸进程,但是问题也就来了,wait,waitpid却无法捕捉到子进程状态信息了,如果你随后调用了wait,那么会阻塞到所有的子进程结束,并返回错误ECHILD,也就是没有子进程等待。
3.自定义处理方式:
SIGCLD会立即检查是否有子进程准好被等待,这便是SIGCLD最大漏洞了,一旦在信号处理函数中加入了信号处理方式重建的步骤,那么每次设置SIGCLD处理方式时,都会去检查是否有信号到来,如果此时信号的确到来了,先是调用自定义信号处理函数,然后是调用信号处理方式重建函数,在重建配置的时候,会去检查信号是否到来,此时信号未被处理,会再次触发自定义信号处理函数,一直循环。所以在处理SIGCLD时,应该先wait处理掉了信号信息后,再进行信号处理方式重建。
SIGCHLD在配置信号处理方式时,是不会立即检查是否有子进程准备好被等待,也不会在此时调用信号处理函数。
6) SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制.,用于通知前台进程组终止进程 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
7) SIGCONT
让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
8) SIGSTOP
停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
9 )SIGTSTP
停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号 ,用于通知前台进程组终止进程
10) SIGTTIN
当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
11) SIGTTOU
类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
5.信号的处理方式
产生了信号,当然也要对信号进行处理。下面来看一下linux中信号的处理方式:
1、信号忽略
忽略此信号,大多数信号都可以采用这种方式进行处理,除了9( SIGKILL )和19(SIGSTOP )信号。因为这两种信号都直接向内核提供了进程终止和停止的可靠办法。SIGKILL还有硬件异常信号我们最好不要忽略,因为硬件异常一旦产生如果不进行处理就会一直存在。
2、信号捕捉
进程要通知内核在某种信号产生时,需要调用一个用户函数,在用户函数中,用户可以自己定义信号处理的方式,在Linux下我们不能捕捉SIGKILL信号和SIGSTOP信号。 用户自定义动作
3、执行默认动作
一般情况下是终止该进程
6.信号中的处理函数
1)signal函数
程序可用使用signal函数来处理指定的信号,主要通过忽略和恢复其默认行为来工作。signal函数原型如下:
头文件
#include<signal.h>
函数原型
void (*signal(int sig, void (*func)(int)))(int);
signal是一个带有sig和func两个参数的函数,func是一个类型为void (*)(int)的函数指针。该函数返回一个与func相同类型的指针,指向先前指定信号处理函数的函数指针。准备捕获的信号的参数由sig给出,接收到的指定信号后要调用的函数由参数func给出。
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
typedef void (*sighandler_t) (int);
void catchsigint(int signo)
{
printf("-----------------catch\n");
}
int main(void)
{
sighandler_t handler;
handler = signal(SIGINT, catchsigint);
if (handler == SIG_ERR) {
perror("signal error");
exit(1);
}
while (1);
return 0;
}
第一次按Ctrl+c让程序作出响应,然后继续执行。再按一次Ctrl+c程序结束,因为SIGINT信号的处理方式已恢复成默认。
2)信号安装函数sigaction
头文件
#include<signal.h>
函数原型:
int sigaction(int signo,const struct sigaction *restrict act, struct sigaction *restrict oact);
结构sigaction定义如下:
struct sigaction
{
void (*sa_handler)(int); /* function, SIG_DFL or SIG_IGN */
sigset_t sa_mask; /* signals to block in sa_handler */
int sa_flag; /* signal action modifiers */
void (*sa_sigaction)(int,siginfo_t *,void *);
};
参数signo
为信号的值,可以为除SIGKILL及SIGSTOP 外的任何一个特定有效的信号,为这两个信号定义自己的处理函数,将导致信号安装错误。
参数act
该结构的作用是定义在接收到信号后应该采取的行动。可以为空,进程会以缺省方式对信号处理;
参数oact
指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。
如果oldact不是空指针, sigaction将把原先对该信号的动作写到它指向的位置。
如果oldact 是空指针, 则sigaction函数就不需要再做其它设置了。
如果把参数act, oact都设为NULL,那么该函数可用于检查信号的有效性。
参数sa_handler
参数值 值含义
sa_handler 信号调用函数
SIG_IGN #define SIG_IGN (void (*) ()) 1 :忽略此信号
SIG_DFL #define SIG_DFL (void (*) ()) 0 :恢复默认行为
参数sa_mask
字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加进进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字复位为原先值。头文件 signal.h 中有一组函数用来操作信号集sigset_t,它们分别是 sigaddset 、 sigemptyset 、 sigfillset 和 sigdelset 等。
参数sa_flag
SA_INTERRUPT 由此信号中断的系统调用不会自动重启
SA_RESTART 由此信号中断的系统调用会自动重启
SA_SIGINFO 提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针
SA_RESETHAND 当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL(默认是不重置的)
SA_NODEFER 捕获到信号时不将它添加到信号屏蔽字中
SA_NOCLDSTOP 子进程停止时不产生SIGCHLD信号
SA_NOCLDWAIT 避免子进程僵死
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void ouch(int sig){
printf("OUCH! - I got signal %d/n", sig);
}
int main()
{
struct sigaction act;
act.sa_handler = ouch;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
while(1){
printf("hello. /n");
sleep(1);
}
return 0;
}
按下Ctrl+C组合键,就可以看到一条消息。因为sigaction函数连续处理到来的SIGINT信号。要想终止这个程序,按下Ctrl+\组合键,它默认情况下产生SIGQUIT信号。
3)信号集sigset_t类型
创建信号集sigset_t函数有如下几个
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
int sigpending(sigset_t *set);
ing sigsuspend(const sigset_t *sigmask);
1,int sigemptyset(sigset_t *set);
初始化由set指定的信号集,信号集里面的所有信号被清空;
2,int sigfillset(sigset_t *set);
调用该函数后,set指向的信号集中将包含linux支持的64种信号;,
3,int sigaddset(sigset_t *set, int signum);
在set指向的信号集中加入signum信号;
4,int sigdelset(sigset_t *set, int signum);
在set指向的信号集中删除signum信号;
5,int sigismember(const sigset_t *set, int signum);
判定信号signum是否在set指向的信号集中;
成功,返回0,如果参数how值无效,它将返回-1并设置errno为EINVAL.
6,int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
根据参数how指定的方法修改进程的信号屏蔽字。新的屏蔽字由参数set(如果它不为空)指定,而原先的信号屏蔽字将保存到信号集oset中。
how的取值为:
SIG_BLOCK 把参数set中的信号添加到信号屏蔽字中
SIG_SETMASK 把信号屏蔽字设置为参数set中的信号
SIG_UNBLOCK 从信号屏蔽字中删除参数set中的信号
如果参数set为空指针,how的值就没有意义,此时调用的唯一目的就是把当前屏蔽字的值保存到oset中。
7,int sigpending(sigset_t *set);
如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态。程序可以通过调用sigpending来查看它阻塞的信号中有哪些正停留在待处理状态。
该函数的作用是:将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中,
成功时,返回0,否则, 返回-1并设置errno以表明错误的原因。
8,ing sigsuspend(const sigset_t *sigmask);
进程可以通过调用它来挂起自己的执行,,直到信号集中的一个信号到达为止。它是pause函数更通用的表现形式。
该函数将进程的屏蔽辽替换为由参数sigmask给出的信号集,然后挂起程序的执行,程序将在信号处理函数执行完毕后继续执行。
如果接收到的信号终止了程序,sigsuspend就不会返回;
如果接收到的信号没有终止程序,sigsuspend就返回-1并将errno设置为EINTR.
7.信号的发送
发送信号的主要函数有:
kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
函数kill-信号发送给进程或进程组
kill函数把参数sig给定的信号发送结由参数pid给出的进程号所指定的进程或进程组。
头文件:
#include <signal.h>
函数原型:
int kill(pid_t pid, int signo);
参数说明:
pid:可能选择有以下四种
1. pid>0,信号将送往进程ID为pid的进程。
2. pid=0,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
3. pid=-1,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
4. pid<-1,信号将送往以-pid为组标识的进程。
int:返回值,成功执行时,返回0。失败返回-1
特别的,当signo值为0时(即空信号),实际不发送任何信号,但照常进行错误检查。因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限。如果向一个并不存在的进程发送空信号,则kill返回-1,并将errno设置为ESRCH。
函数raise-信号发送给自身
raise向进程本身发送信号,参数为即将发送的信号值。
#include <signal.h>
int raise(int signo);
raise(signo); 相当于 kill(getpid(), signo);
函数alarm-设置一个定时器信号
alarm函数用来设置一个定时器,到时间后,向自身发送SIGALRM信号。
头文件:
#include<unistd.h>
函数原型:
unsigned int alarm(unsigned int seconds);
参数:
seconds:设定的秒数,若为0则表示取消之前的闹钟设定。
返回值:
int:上次闹钟设置的余留值。
几点说明:
1,进程调用alarm后,任何以前的alarm()调用都将无效,被新设定所替代。
2,如果参数seconds为0,则取消以前的闹钟时钟,其余留值仍作为alarm函数的返回值。
3,经过指定的秒数后,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一些时间。
#include<unistd.h>
#include<stdio.h>
#include<signal.h>
void handler()
{
printf("hello\n");
}
int main(void)
{
int i;
signal(SIGALRM, handler);
alarm(5);
for(i=1;i<7;i++){
printf("sleep %d....\n",i);
sleep(1);
}
return 0;
}
信号创建例程
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
void handler(int sig)
{
printf("Handler the signal %d\n", sig);
}
int main(void)
{
sigset_t sigset;//用于记录屏蔽字
sigset_t ign;//用于记录被阻塞的信号集
struct sigaction act;
//清空信号集
sigemptyset(&sigset); //初始化信号集
sigemptyset(&ign);
//向信号集中添加信号SIGINT
sigaddset(&sigset, SIGINT);
//设置处理函数和信号集
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
printf("Wait the signal SIGINT...\n");
pause();//挂起进程,等待信号
//设置进程屏蔽字,在本例中为屏蔽SIGINT
sigprocmask(SIG_SETMASK, &sigset, 0);
printf("Please press Ctrl+c in 10 seconds...\n");
sleep(10);
//测试SIGINT是否被屏蔽
sigpending(&ign);
if(sigismember(&ign, SIGINT))
printf("The SIGINT signal has ignored\n");
//在信号集中删除信号SIGINT
sigdelset(&sigset, SIGINT);
printf("Wait the signal SIGINT...\n");
//将进程的屏蔽字重新设置,即取消对SIGINT的屏蔽
//并挂起进程
sigsuspend(&sigset);
printf("The app will exit in 5 seconds!\n");
sleep(5);
exit(0);
}