linux 中signal机制如何应用(一)

来源:互联网 发布:proteus的矩阵键盘 编辑:程序博客网 时间:2024/06/07 21:36

前面博客讨论的问题:一个长期运行的Linux程序,想在不退出运行的情况下,通过某种机制,可以让程序知道要不要打印出log。
之前的博客 linux中inotify机制如何应用 用了inotify机制解决了这个问题,现在我们尝试用linux signal机制来实现。大概的思路是注册安装一个信号,用户用kill命令向进程传递信号,进程接收到信号,执行中断处理函数,在函数中把打印log的标志打开。

按照惯例,讲例子之前,先讲基础知识。
linux signal是软中断信号,是在软件上对终端的模拟。信号提供了一种处理异步事件的方法。早期linux中使用的是signal()信号安装函数以及信号发送函数kill()。而信号安装函数sigation()以及信号发送函数sigqueue()是后来才实现的,更加安全可靠。sigaction可以说是更加高级的signal版本,安装的信号能传递信息给信号处理函数,而signal不能。
信号的产生:
很多情况都可以产生信号,比如很多时候我们用ctrl+c来结束进程,实际就是向进程发送终端程序的信号。我们也可以进程调用kill函数或者sigqueue函数可以将任意信号发送给其他进程。又或者用户用kill命令将其信号传递给特定进程。
信号的处理方式:
在某个信号出现时,可以告诉内核3种处理方式:
1. 忽略此信号
2. 捕捉信号。我们需要为此写一个信号解析函数,当捕捉到特定信号时,做出我们想要的处理。
3. 执行默认动作。
信号的种类:
在头文件signal.h中,信号名都被定义为正整数常量。信号有很多种类,这里我们介绍几个常见的,这正是我们接下来会讨论到的信号。其余信号项目用到了再去查看,可以用命令kill -l 在终端查看。
SIGINT信号:当用户按中断键,一般是ctrl+c时,可以产生信号终止一个进程。
SIGUSR1信号:这是用户定义的信号,可以用于应用程序。
SIGUSR2信号:这是另一个用户定义的信号,与SIGUSR1相似,可用于应用程序。
本文对signal讨论的重点:
本文没有分析linux signal机制不可靠的信号和具体细节,而是介绍现在项目中能用到的实际做法,重点在于如何在你的应用程序中运用这些机制。
linux信号机制相关函数:
接下来就直接介绍本文的重磅函数sigaction,现在很多平台都是用sigaction实现老的signal,因为老的signal函数不可靠。

#include <signal.h>//返回值:若成功则返回0,若出错则返回-1int sigaction( int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);

其中,参数signo是要检测或修改其具体动作的信号编号。若act指针非空,则要修改其动作。如果oact指针非空,则系统经由oact指针返回该信号的上一个动作。我们所需要做的是,指定完信号signo,然后填充结构体sigaction成员。sigaction结构定义如下:

struct sigaction {    void    (*sa_handler)(int);    sigset_t    sa_mask;    int    sa_flags;    void    (*sa_sigaction)(int, siginfo_t *, void *);};

如上结构所示,通常情况按照下列方式调用信号处理函数:
void handler(int signo);
需要填充sigaction结构体成员act.sa_handler = handler;
但是,如果设置了SA_SIGINFO标志,也就是act.sa_flags = SA_SIGINFO时,要按照下列方式填充sigaction结构体成员和调用信号处理程序:
void handler(int signo, siginfo_t *info,void *context);
需要填充sigaction结构体成员act.sa_sigaction = handler;
这两种调用方式在接下来的程序中都会实践。这两种中断处理函数区别在于,一个是不携带参数,一个携带参数并且保存在siginfo_t结构体中,信号处理函数可以解析这些数据。

结构体还有两个个成员是:sa_flags和sa_mask。sa_flags实际上是一个位掩码,可以根据需要需要设置某一个flag或者若干个flags的or组合。
我们这里只讲例子中用到的两个数值代表的含义。
1、当flag参数设置为0时,如果系统调用被sig信号打断,则在处理完sig信号后,就会重新调用被打断的系统调用,这也是linux默认的行为。
2、当flag参数设置为SA_SIGINFO时,表示对信号处理程序提供了附加的信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针。
剩下的一个结构体成员sa_mask的作用是:
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

下面再介绍一个重要的结构体siginfo_t,它是用来携带数据的,siginfo_t的一种实现如下(因为不同系统或者不同linux版本实现可能有差别)

siginfo_t {               int      si_signo;    // Signal number               int      si_errno;    // An errno value               int      si_code;     // Signal code               int      si_trapno;   // Trap number that caused                                        hardware-generated signal                                        (unused on most architectures)               pid_t    si_pid;      // Sending process ID               uid_t    si_uid;      // Real user ID of sending process               int      si_status;   // Exit value or signal               clock_t  si_utime;    // User time consumed               clock_t  si_stime;    // System time consumed               sigval_t si_value;    // Signal value               int      si_int;      // POSIX.1b signal               void    *si_ptr;      // POSIX.1b signal               int      si_overrun;  // Timer overrun count; POSIX.1b timers               int      si_timerid;  // Timer ID; POSIX.1b timers               void    *si_addr;     // Memory location which caused fault               int      si_band;     // Band event               int      si_fd;       // File descriptor           }

其中sigval联合包含下列字段:

    int sival_int;    void *sival_ptr;

在这个结构体中,si_value.sival_int传递的是一个整形数,si_value.sival_ptr传递的是一个指针值。

因为kill命令很好用,例子中也是用kill发送信号的,这里做下介绍
命令格式:
  kill[参数][进程号]
参数含义:
  -l 信号,若果不加信号的编号参数,则使用“-l”参数会列出全部的信号名称
  -a 当处理当前进程时,不限制命令名和进程号的对应关系
  -p 指定kill 命令只打印相关进程的进程号,而不发送任何信号
  -s 指定发送信号
  -u 指定用户

例如,用”kill -l” 可以查看所有的信号信息

ubuntu:~/test/signal_test$ kill -l 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR111) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+338) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+843) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+1348) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-1253) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-758) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-263) SIGRTMAX-1  64) SIGRTMAX

那如何用kill命令发送信号呢?举个例子,”kill -USR1 进程号” 表示向进程发送SIGUSR1的信号。或者找到SIGUSR1对应的编号,用”kill -10 进程号”也可以达到同样的效果。我们也经常用”kill –9 进程号” 来彻底杀死进程。

接下来,我们写一个例子来实现用signal通知应用程序打开log标志。

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <stdlib.h>#include <string.h>#include <stdarg.h>#include <unistd.h>#include <signal.h>int logFlag = 0;//全局变量,是否打印log的标志void printf_my_log(char *file, int line, const char *func, char *fmt, ...){        char argvStr[1024] = {0};        va_list argptr;        memset(&argptr, 0, sizeof(va_list));        printf("[%s(Line=%d):%s]",file,line,func);        va_start (argptr, fmt);    vsprintf(argvStr, fmt, argptr);        va_end (argptr);        printf("%s\n", argvStr);}void catch_signal(int signo){    switch(signo)    {        case SIGINT:            //SIGINT默认行为是退出进程            printf("SIGINT signal\n");            exit(0);            break;        case SIGUSR1:            //当logFlag等于1的时候表示打开log标志            logFlag = 1;            printf("SIGUSR1 signal\n");            break;    }}int signal_test(int signo,void (*func)(int)){    struct sigaction act,oact;    //填充软中断函数    act.sa_handler=func;    //将act的属性sa_mask设置一个初始值    sigemptyset(&act.sa_mask);    act.sa_flags=0;    return sigaction(signo,&act,&oact);}int main(void){      int a = 10;    //安装信号SIGINT和SIGUSR1    signal_test(SIGINT,catch_signal);    signal_test(SIGUSR1,catch_signal);    while(1)    {        pause();//pause()会令目前的进程暂停(进入睡眠状态), 直到被信号(signal)所中断.        if(logFlag == 1)        {            printf_my_log(__FILE__,__LINE__,__FUNCTION__,"%d",a);        }    }    return 0;  }   

编译:

ubuntu:~/test/signal_test$ gcc signal.c -o signal 

后台运行:

ubuntu:~/test/signal_test$ ./signal &[1] 85643

向进程发送SIGUSR1信号:

ubuntu:~/test/signal_test$ kill -USR1 85643SIGUSR1 signal[signal.c(Line=63):main]10

ps进程信息:

ubuntu:~/test/signal_test$ ps  PID TTY          TIME CMD85643 pts/10   00:00:00 signal85775 pts/10   00:00:00 ps95587 pts/10   00:00:00 bash

向进程发送SIGINT信号:

ubuntu:~/test/signal_test$ kill -INT 85643SIGINT signal

ps进程信息:

ubuntu:~/test/signal_test$ ps  PID TTY          TIME CMD85780 pts/10   00:00:00 ps95587 pts/10   00:00:00 bash[1]+  Done                    ./signalubuntu:~/test/signal_test$ ps  PID TTY          TIME CMD86526 pts/10   00:00:00 ps95587 pts/10   00:00:00 bash

从上面的程序可以看出,先是调用一个signal_test函数安装信号,函数里面是用sigaction实现的signal函数。然后在一个while循环中,等待信号的到来。catch_signal是信号处理函数,printf_my_log是打印log的函数,logFlag是全局变量,是打印log的标志。当用户执行命令”kill -USR1 85643”发送信号SIGUSR1时,进程会捕捉到信号,然后执行函数catch_signal 把logFlag标志开启。主程序检测到logFlag标志开启,就会打印log出来。当用户执行命令”kill -INT 85643”,相当于用户在程序运行的时候按下ctrl+c,可以产生信号终止一个进程。

本文到这里,把实用的signal机制基础知识和应用方法都介绍了,把之前遗留的问题也解决了。下一篇 linux 中signal机制如何应用(二)再介绍如何实现带参数的信号处理。