Linux信号

Apr 22, 2016


一、信号概述

信号是由用户、系统或者进程发给目标进程的信息,以通知目标进程某个状态的改变或者系统异常。

Linux信号可由下列条件产生:

  • 对于前台进程,用户可以通过输入特殊字符发送信号,比如Ctrl+C等。
  • 系统异常,比如浮点异常和非法内存访问。
  • 系统状态变化,比如alarm定时器等。
  • 运行kill命令或者kill函数等。

1.1 信号的发送

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

下面以kill()为例进行详解,一个进程给其他进程发送信号的API,函数原型如下:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)

参数pid的值为信号的接收进程

pid>0 进程ID为pid的进程

pid=0 同一个进程组的进程

pid<0 pid!=-1 进程组ID为 -pid的所有进程

pid=-1 除发送进程自身外,所有进程ID大于1的进程

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,可用于检查目标进程是否存在。

该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。

ESRCH:参数pid指定的进程或进程组不存在。

EPERM: 进程没有权限将这个信号发送到指定接收信号的进程。

1.2 信号处理方式

目标进程在受到信号后,需要定义一个接受函数来处理,信号处理函数的原型如下:

#include<signal.h>
typedef void (*__sighandler_t) ( int )

带有一个整形参数,用来指示信号类型,信号处理函数应该是可重入的。

此处,bits/signum.h头文件中还定义了信号的两种其它处理方式,SIG_ING,SIG_DEL。

#include<bits/signum.h>
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)

SIG_IGN表示忽略目标信号,SIG_DFL表示使用信号的默认处理方式。

信号的默认处理方式有几种:结束进程(Term)、忽略信号(Ign)、暂停进程(Stop)、继续进程(Cont)等。

1.3 Linux标准信号

运行命令:kill –l

可以看到Linux支持的信号列表:

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     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的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

Linux信号详细列表:

SIGHUP     终止进程     终端线路挂断
SIGINT     终止进程     中断进程
SIGQUIT   建立CORE文件终止进程,并且生成core文件
SIGILL   建立CORE文件       非法指令
SIGTRAP   建立CORE文件       跟踪自陷
SIGBUS   建立CORE文件       总线错误
SIGSEGV   建立CORE文件       段非法错误
SIGFPE   建立CORE文件       浮点异常
SIGIOT   建立CORE文件       执行I/O自陷
SIGKILL   终止进程     杀死进程
SIGPIPE   终止进程     向一个没有读进程的管道写数据
SIGALARM   终止进程     计时器到时
SIGTERM   终止进程     软件终止信号
SIGSTOP   停止进程     非终端来的停止信号
SIGTSTP   停止进程     终端来的停止信号
SIGCONT   忽略信号     继续执行一个停止的进程
SIGURG   忽略信号     I/O紧急信号
SIGIO     忽略信号     描述符上可以进行I/O
SIGCHLD   忽略信号     当子进程停止或退出时通知父进程
SIGTTOU   停止进程     后台进程写终端
SIGTTIN   停止进程     后台进程读终端
SIGXGPU   终止进程     CPU时限超时
SIGXFSZ   终止进程     文件长度过长
SIGWINCH   忽略信号     窗口大小发生变化
SIGPROF   终止进程     统计分布图用计时器到时
SIGUSR1   终止进程     用户定义信号1
SIGUSR2   终止进程     用户定义信号2
SIGVTALRM 终止进程     虚拟计时器到时

1.4 信号函数

linux主要有两个函数实现信号的安装:signal()、sigaction()。

signal()只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;

sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。

signal()函数原型:

#include <signal.h>
void (*signal(int signum, void (*handler))(int)))(int);

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理回调函数。

demo:【http://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void sigroutine(int dunno)
{ /* 信号处理例程,其中dunno将会得到信号的值 */
        switch (dunno)
        {
        case 1:
        printf("Get a signal -- SIGHUP ");
        break;
        case 2:
        printf("Get a signal -- SIGINT ");
        break;
        case 3:
        printf("Get a signal -- SIGQUIT ");
        break;
        }
        return;
}
int main() 
{
        printf("process id is %d ",getpid());
        signal(SIGHUP, sigroutine); //* 下面设置三个信号的处理方法
        signal(SIGINT, sigroutine);
        signal(SIGQUIT, sigroutine);
        for (;;) ;
}

其中信号SIGINT由按下Ctrl-C发出,信号SIGQUIT由按下Ctrl-发出。该程序执行的结果如下:

localhost:~$ ./sig_test
process id is 463
Get a signal -SIGINT //按下Ctrl-C得到的结果
Get a signal -SIGQUIT //按下Ctrl-得到的结果
//按下Ctrl-z将进程置于后台
 [1]+ Stopped ./sig_test
localhost:~$ bg
 [1]+ ./sig_test &
localhost:~$ kill -HUP 463 //向进程发送SIGHUP信号
localhost:~$ Get a signal – SIGHUP
kill -9 463 //向进程发送SIGKILL信号,终止进程
localhost:~$

sigaction()

用于改变进程接收到特定信号后的行为

#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

signum参数为要捕获的信号类型,

atc参数指定新的信号处理方式,

oact参数输出信号先前的处理方式,act和oact都是sigaction结构体类型的指针,

定义如下:

//struct sigaction 类型用来描述对信号的处理
 struct sigaction
 {
  void     (*sa_handler)(int);
  void     (*sa_sigaction)(int, siginfo_t *, void *);
  sigset_t  sa_mask;
  int       sa_flags;
  void     (*sa_restorer)(void);
 };

demo:【http://www.cnblogs.com/wblyuyang/archive/2012/11/13/2768923.html

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

static void sig_usr(int signum)
{
    if(signum == SIGUSR1)
    {
        printf("SIGUSR1 received\n");
    }
    else if(signum == SIGUSR2)
    {
        printf("SIGUSR2 received\n");
    }
    else
    {
        printf("signal %d received\n", signum);
    }
}

int main(void)
{
    char buf[512];
    int  n;
    struct sigaction sa_usr;
    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = sig_usr;   //信号处理函数
    sigaction(SIGUSR1, &sa_usr, NULL);
    sigaction(SIGUSR2, &sa_usr, NULL);
    printf("My PID is %d\n", getpid());
    while(1)
    {
        if((n = read(STDIN_FILENO, buf, 511)) == -1)
        {
            if(errno == EINTR)
            {
                printf("read is interrupted by signal\n");
            }
        }
        else
        {
            buf[n] = '\0';
            printf("%d bytes read: %s\n", n, buf);
        }
    }
    return 0;
}

1.5 信号集

信号集就是将多个信号放在集合中进行集中的处理,设定我们需要处理的信号,我们不需要处理哪些信号等基本问题。

Linux提供一组API来设置、修改、删除和查询信号集:

#include<signal.h>
/*清空信号集*/
  int sigemptyset(sigset_t *set);
/*在信号集中设置所有信号*/
  int sigfillset(sigset_t *set);
/*添加信号*/
  int sigaddset(sigset_t *set,int signo);
/*删除信号*/
  int sigdelset(sigset_t *set,int signo)
/*是否在信号集中*/
  int sigismember(const sigset_t *set,int signo);

demo:【http://blog.csdn.net/ljianhui/article/details/10130539

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
void handler(int sig)
{
    printf("Handle the signal %d\n", sig);
}

int main()
{
    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);
}

运行结构如下:

image

  • 首先,我们能过sigaction函数改变了SIGINT信号的默认行为,使之执行指定的函数handler,所以输出了语句:Handle the signal 2。
  • 然后,通过sigprocmask设置进程的信号屏蔽字,把SIGINT信号屏蔽起来,所以过了10秒之后,用sigpending函数去获取被阻塞的信号集时,检测到了被阻塞的信号SIGINT,输出The SIGINT signal has ignored。
  • 最后,用函数sigdelset函数去除先前用sigaddset函数加在sigset上的信号SIGINT,再调用函数sigsuspend,把进程的屏蔽字再次修改为sigset(不包含SIGINT),并挂起进程。由于先前的SIGINT信号停留在待处理状态,而现在进程已经不再阻塞该信号,所以进程马上对该信号进行处理,从而在最后,你不用输入Ctrl+c也会出现后面的处理语句(可参阅前面特别提醒的内容),最后过了5秒程序就成功退出了。

二、实例分析

实例:”统一事件源 ”

信号是一种异步事件:信号处理函数和主循环是两条不同的知性路线。

一种典型的做法是把信号的主要处理逻辑放在主循环中,当信号处理函数被触发时,通过管道,来通知主循环进行逻辑处理。

同时,通过I/O复用系统来监听管道的读端文件描述符,和其他I/O事件一样被处理,这就是”统一事件源“(Libevent也用了)。

code:

#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>
#include<stdlib.h>
#include<sys/epoll.h>
#include<pthread.h>
#include<iostream>
#define MAX_EVENT_NUMBER 1024//最大事件数目
using namespace std;
static int pipefd[2];//管道描述符
int setnonblocking(int fd){//设置非阻塞描述符
    int old_option=fcntl(fd,F_GETFL);
    int new_option=old_option|O_NONBLOCK;
    fcntl(fd,F_SETFL,new_option);
    return old_option;
}
void addfd(int epollfd,int fd){//注册描述符到事件表
    epoll_event event;
    event.data.fd=fd;
    event.events=EPOLLIN|EPOLLET;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
    setnonblocking(fd);
}
void sig_handler(int sig){//信号处理函数
    int save_errno=errno;
    int msg=sig;
    send(pipefd[1],(char*)&msg,1,0);//向管道发送信号值
    errno=save_errno;
}
void addsig(int sig){//安装信号
    struct sigaction sa;
    memset(&sa,'\0',sizeof(sa));
    sa.sa_handler=sig_handler;//信号处理句柄
    sa.sa_flags|=SA_RESTART;//被信号中断的系统调用将被重启
    sigfillset(&sa.sa_mask);//信号掩码为全部信号
    assert(sigaction(sig,&sa,NULL)!=-1);//sigaction
}
int main(int argc,char* argv[]){
    if(argc<=2){
        cout<<"argc<=2"<<endl;
        return 1;
    }
    const char* ip=argv[1];
    int port=atoi(argv[2]);
    int ret=0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family=AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port=htons(port);
    int listenfd=socket(PF_INET,SOCK_STREAM,0);
    assert(listenfd>=0);
    ret=bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    assert(ret!=-1);
    ret=listen(listenfd,5);
    assert(ret!=-1);
    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd=epoll_create(5);
    assert(epollfd!=-1);
    addfd(epollfd,listenfd);
    ret=socketpair(PF_UNIX,SOCK_STREAM,0,pipefd);
    assert(ret!=-1);
    setnonblocking(pipefd[1]);
    addfd(epollfd,pipefd[0]);
    addsig(SIGHUP);
    addsig(SIGCHLD);
    addsig(SIGTERM);
    addsig(SIGINT);
    bool stop=false;//是否终止服务端
    while(!stop){
        int number=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);//epoll无限期等待事件就绪
        if((number<0)&&(errno!=EINTR)){
            cout<<"epoll error"<<endl;
            break;
        }
        for(int i=0;i<number;i++){//就绪事件处理
            int sockfd=events[i].data.fd;
            if(sockfd==listenfd){//监听端口有可读事件表明有新连接
                struct sockaddr_in client_address;
                socklen_t client_addrlength=sizeof(client_address);
                int connfd=accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);//允许建立连接
                addfd(epollfd,connfd);//新连接注册到事件表
            }
            else if((sockfd==pipefd[0])&&(events[i].events&EPOLLIN)){//若管道读端可读事件就绪表明有信号传递给主线程
                int sig;
                char signals[1024];
                ret=recv(pipefd[0],signals,sizeof(signals),0);//接收管道内的信号
                if(ret!=-1){
                    continue;
                }
                else if(ret==0){
                    continue;
                }
                else{
                    for(int i=0;i<ret;i++){
                        switch(signals[i]){
                            case SIGCHLD:
                            case SIGHUP:{
                                continue;
                            }
                            case SIGTERM:
                            case SIGINT:{
                                stop=true;//停止服务端程序
                            }
                        }
                    }
                }
            }
            else{}//这里没有定义处理客户端连接的逻辑
        }
    }
    cout<<"close"<<endl;
    close(listenfd);
    close(pipefd[0]);
    close(pipefd[1]);
    return 0;
}

三、参考:

《Linux高性能服务器编程》

信号集:http://blog.csdn.net/ljianhui/article/details/10130539

sigaction函数:http://www.cnblogs.com/wblyuyang/archive/2012/11/13/2768923.html

四、说明:

作者西芒xiaoP

出处迁移自我的博客园,http://www.cnblogs.com/panweishadow/

若用于非商业目的,您可以自由转载,但请保留原作者信息和文章链接URL。


上一篇博客:一个简单网络协议栈的实现
下一篇博客:定时器编程小记