1.信号简介

软中断信号 Signal,简称信号,用来通知进程发生了异步事件,进程之间可以互相通过系统调用 kill 等函数来发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件,但是要注意信号只是用来通知进程发生了什么事件,并不给该进程传递任何数据,例如终端用户键入中断键,会通过信号机制停止当前程序。

2.信号的分类

信号其实是一种软件中断,它为程序提供了处理异步事件的方法。异步事件就是事件可能会在任何时间内发生,很多重要的而程序都有对信号的处理,在linux下我们是通过命令 kill -l来查看系统中所有的信号列表和它们的信号编号。

Linux信号简单入门_信号处理


我们可以看到31~34之间是没有信号的,我们把1~31号信号称为普通信号,把34~64号信号称为实时信号。所有的信号都包含在signal.h中,且都定义成正整数常量,也就是它们的信号编号。

这么多的信号也不可能都记得很清楚,只需要知道常用的即可,常用的信号有下面这些:

  1. SIGHUP :终端结束信号
  2. SIGINT :键盘中断信号(Ctrl - C)
  3. SIGQUIT:键盘退出信号(Ctrl - \)
  4. SIGPIPE:浮点异常信号
  5. SIGKILL:用来结束进程的信号
  6. SIGALRM:定时器信号
  7. SIGTERM:kill 命令发出的信号
  8. SIGCHLD:标识子进程结束的信号
  9. SIGSTOP:停止执行信号(Ctrl - Z)

信号有 2 种分类:不可靠信号,可靠信号。

  1. 不可靠信号(0~32)
    linux 继承了早期 UNIX 的一些信号,这些信号有些缺陷:在发送给进程的时候可能会丢失,也称为不可靠信号,其中信号值小于 34) SIGRTMIN 都是不可靠信号。
  2. 可靠信号(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);  
}