天天看点

操作系统用信号控制程序

进程之死

在linux的终端正在运行的程序,用户按了ctrl-C,程序就停止运行了。为什么ctrl-C可以彻底杀死程序?是因为当操作系统从键盘读取数据时,发现用户按了ctrl-C时,就会向程序发送中断信号。

信号是一条短消息,即一个整型值。当信号到来时,进程必须停止手中一切工作去处理信号。进程会查看信号映射表,表中每个信号都对应一个信号处理器函数。中断信号的默认信号处理器会调用exit()函数。

信号映射表

信号 处理函数
SIGURG 不做事情
SIGINT 调用exit()

操作系统为什么不直接结束程序,而是要在信号映射表中查找信号?因为这样就可以在进程接收信号时运行你自己的代码。我只要想办法把信号对应的默认处理器函数换成我们自己的,我们就可以达到捕捉信号后,根据信号运行我们自己的代码的目的了。

捕捉信号,然后运行自己的代码

有时候,在进程打开了一些文件连接或网络连接,我们希望在退出之前把它们关闭,并且做一些清理工作。我们可以这样做,当计算机向我们的程序发送信号时,我们捕捉相应的信号,然后根据信号做相应的处理。那怎么实现呢?答案就是用sigaction结构体包装一个处理器函数,然后用sigaction()函数进行信号值与处理器函数的绑定。

sigaction是一个函数包装器,就是说sigaction一个结构体,它里面有一个函数指针变量,我们将处理器函数指针赋给它即可。sigaction结构体可以告诉操作系统进程接收到某个信号时就调用结构体里的函数,这个函数叫做处理器,因为它将用来处理发送给它的信号,而且这个处理器必须接收信号参数,信号是一个整型值。**

定义一个处理器byebye():

void byebye(int sig){ //必须接收信号
  puts("Goodbye!!!");
  exit(1);
  }      

因为我们使用参数的形式接收信号,所以多个信号可以共用一个处理器,也可以单独为每个信号定义一个处理器。处理器的代码应该短而快,刚好能处理接收到的信号就OK。

如果中断信号的处理器不调用exit(),程序不会结束。

用sigaction()函数来注册sigaction

创建sigaction以后,需要用sigaction()函数来让操作系统知道它的存在。sigaction()函数的形式如下:

sigaction(signal_no,&new_action,&old_action)

它的参数分三部分:

信号编号:signal_no这个整型值代表了你希望处理的信号,通常会传递SIGINT或SIGQUIT这样的标准信号

新动作:想注册新的sigaction(那个结构体实例)的地址

旧动作:如果想保存被替换的信号处理器,可以再传一个sigaction结构体指针给它,如果不想保存旧动作,可以设置为NULL。

如果sigaction()函数失败,会返回-1,并设置errno变量。

下面我们给个例子:

int register_handler(int sig,void (*handler)(int)){
        struct sigaction action;
        action.sa_handler = handler;
        sigemptyset(&action.sa_mask);
        action.sa_flags = 0;
        return sigaction(sig,&action,NULL);
}      

只要把想捕捉的信号和处理器函数名(实际上函数名就是一个指针)传给 register_handler()函数,就可以通过 sigaction()函数,把想捕捉的信号与处理器函数关联起来。

下面我们给出一个完整的实例test6.c:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void byebye(int sig);
int register_handler(int sig,void (*handler)(int));
int main(){
        if(register_handler(SIGINT,byebye) == -1){

                fprintf(stderr,"Can't map the handler");
                exit(2);
        }


        printf("%s","please type somethine:");
        char c[120];
        fgets(c,120,stdin);
        printf("TOM:%s",c);
        return 0;
}

void byebye(int sig){
        puts("Goodbye!!!");
        exit(1);
}
int register_handler(int sig,void (*handler)(int)){
        struct sigaction action; //创建新动作
        action.sa_handler = handler; //想计算机在收到sig信号时,执行的函数(处理器)
        sigemptyset(&action.sa_mask);//用掩码来过滤sigaction要处理的信号,通常会用一个空的掩码
        action.sa_flags = 0;//附加标志位,设置为0即可
        return sigaction(sig,&action,NULL);
}      

编译运行:

~/Desktop/MyC$ gcc test6.c -o test6
~/Desktop/MyC$ ./test6
please type somethine:|      

程序运行起来了,正在等待输入,如果这时我们按下ctrl-C,操作系统就会自动向进程发送中断信号(SIGINT),然后我们在register_handler()函数里注册的sigaction就会处理这个信号,sigaction中会指向byebye()函数指针,程序会调用这个函数,显示消息并调用exit()函数。

please type somethine:^CGoodbye!!!      

看!我们成功捕捉到中断信号并执行了我们自己写的处理器函数了。

我们再来看个例子,说说Linux平台上的kill命令是如何杀死进程的,再把程序运行起来:

~/Desktop/MyC$ ./test6
please type somethine:
|      

程序运行起来了,正在待用户输入。

我们查看一下进程信息:

~$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
wong     28463  5363  0 12:19 pts/1    00:00:00 ./test6      

用kill命令杀掉进程:

~$ kill 28463      

就这样test6的进程被kill命令杀死了。事实上,kill命令只是向进程发送了一个信号,而kill命令默认会向进程发送SIGTERM信号,当然你可以用它发送其他信号,如SIGINT:

~$ kill -s SIGINT 28463      

有一个信号是程序无捕捉,又无法忽略的:SIGKILL信号,也就是说,即使程序中有一个错误导致进程对任何信号都视而不见,还是能用kill -KILL结束进程:

~$ kill -KILL 28463      

让进程向自己发送信号

哈哈哈!其实真的可以做到。可以用raise() 函数实现。通常会在自定义的信号处理函数中使用raise(),这样程序就能在接收到低级别的信号时引发更高级别的信号,这叫信号升级。如在上述实例的byebye函数里做个信号升级的操作:

void byebye(int sig){
        raise(SIGTERM);
        puts("Goodbye!!!");
        exit(1);
}      

修改完byebye处理器函数,编译运行一下,然后按ctrl-C:

~/Desktop/MyC$ gcc test6.c -o test6
~/Desktop/MyC$ ./test6
please type somethine:^CTerminated      

从输出的信息来看,信号升级成功了。

上面提到的信号都是操作系统发送给进程的。其实有时进程也需要产生自己的信号,比如说闹钟信号SIGALRM。闹钟信号通常由进程的间隔定时器创建。间隔定时器就像一台闹钟:你可以定一个时间,其间程序就会去做其他事情。

当进程收到信号后就会停止一切工作来处理信号。进程收到闹钟信号后默认会结束进程。但这一般不是我们想要的。我们可以让它收到这个闹钟信号后执行我们的代码,方式和前面的一样,只是信号变成SIGALRM。

如果想还原信号的默认处理器,可以这样做:

register_handler(SIGALRM,SIG_DFL);      

signal.h头文件里有一个特殊的符号SIG_DFL,它代表以默认方式处理信号。

还可以用SIG_IGN让进程忽略某个信号:

register_handler(SIGALRM,SIG_IGN);      

注意:进程在处理信号时会停止一切工作 ,也就是一次只能做一件事。

在文章最后,我们把操作系统可以向进程发送的各种信号,大概列一下吧:

信号 描述
SIGINT 进程被中断
SIGQUIT 有人要求停止进程,并把存储器中的内容保存到核心转储文件
SIGFPE 浮点错误
SIGTRAP 调试人员询问进程执行到了哪里
SIGSEGV 进程企图访问非法存储器地址
SIGWINCH 终端窗口的大小发生改变
SIGTERM 有人要求内核终止程序