信号的基本概念
信号被认为是一种软件中断(区别于硬件中断)。信号机制提供了一种在单进程/线程 下处理异步事件的方法。具体过程是当进程运行到某处,接受到一个信号,保留“现场”,响应信号(注意这里的响应是一种宏观意义上的响应,对信号的忽略(SIG_IGN)也被以为是一种响应),再返回到刚刚保存的地方继续运行。
产生信号的条件有很多,某些组合键(CTRL+C、CTRL+\,CTRL+Z等),kill命令,kill系统调用以及由内核产生的某些信号(如内核检测到段错误、管道破裂等)。值得注意的是当我们发送信号时受到权限的限制,发送一个信号到另一个没有权限的进程是不合法的(关于权限的规则会在之后的博客总结)。信号的种类非常多,都以SIG+名字的形式命名的宏,通常都有实际意义和用法具体可查阅manual。有些常见的信号是需要熟记的如SIGINT,SIGCHLD,SIGIO等等。在编写程序的时候,我们最好用信号的宏的形式,这样可读性更好。那么如何“响应”信号呢?
信号处理的接口之一 signal()
对于大部分的信号,Linux系统都有默认的处理方式。而大部分默认的处理方式是终止程序并转储core文件。要处理信号,Linux系统处理信号的接口有两个sigaction(),signal(),较简单的是signal()函数,其形式如下:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
siganl()函数有两个参数其中有一个int的参数便是要处理的信号,诸如SIGINT的宏。另一个参数类型为sighandler_t的函数指针,handler指针对应的函数我们称之为:信号处理函数(signal-handler function)。可见signal()的第二个参数是一个信号处理函数,返回值也是一个信号处理函数,失败返回宏SIG_ERR(SIGKILL和SIGSTOP的默认行为分别是杀死和停止一个进程,任何试图改变这两个信号的处理方式的行为都将返回错误)。signal()函数的作用就是建立一个signum信号的处理函数(establish a signal handler function)。通俗一点来说就是当signum信号到来时,进程会保存当前的进程栈,转去执行siganl()中指定的handler函数。之前提到过,信号的响应方式有多种,因此handler不仅可以是一个函数指针也可以是ISO C为我们定义的宏:SIG_IGN,SIG_DEL,和他们的名字一样SIG_IGN是忽略这个信号,SIG_DEL是保持这个信号的默认处理方式(默认处理方式也可以可以是SIG_IGN ,比较绕,但是合理)。前文提到的三个宏定义分别如下(/usr/include/bits/signum.h):
#define SIG_ERR ((__sighandler_t) -1)
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)
下面我写一个小的DEMO演示一下如何写一个信号处理函数:
#include
#include
#include
#include
void sigdemo(int sig)
{
printf("\nReceive a signal:%s\n",strsignal(sig));
}
int main()
{
if( signal(SIGINT, sigdemo) == SIG_ERR )
{
perror("signal()");
return 0;
}
printf("Main started.\n");
pause();//wait a signal.
return 0;
}
可以看到,我在main函数中并没有主动调用sigdemo函数,可是运行程序后,当我们在中断按下CTRL+C时(发送SIGINT信号,宏对应的值是2),出现了这样的结果:
Main started.
^C
Receive a signal:Interrupt
可见sigdemo函数得到了执行,其参数sig便是接受到的信号的值。要将信号的值,转换为其意义string.h中提供了一个函数char* strsignal(int sig), 基本上看到该函数原型就知道这个函数怎么用了,在此我就不再浪费篇幅赘述了。
发送信号
上文我们通过使用CTRL+C组合键发送信号SIGINT给当前的进程。但是这种方法只能发送少部分信号且并不适用所有的进程比如后台进程和守护进程。守护进程不必说,连终端都没有。交互shell (interactive shell)在启动一个后台进程的时候,会自动把中断和退出信号设置为忽略,关于这点我在网上看到一篇不错的博客:http://hongjiang.info/shell-script-background-process-ignore-sigint/ 。这样的情况下就无法使用快捷键的方式了。这里我介绍几种其他的发送信号的方式。
首先是shell命令kill其用法如下:
kill [-s signal|-p] [-q sigval] [-a] [--] pid...
-s signal signal可以是诸如SIGINT,SIGQUIT之类的宏,亦可以是1,2,3...这样的值,可以随意使用,你开心就好。
-q queue sigval是值,可以伴随信号传递,但是这里只可以是一个integer,在进程中可以使用sigaction()接收到这个值,与之对应的是另一个函数sigqueue()。这里先不详细介绍,下文会谈到。
pid就是目标进程的进程id,可以是一个或者多个。但是发送信号时,要确保你所使用的用户是具有发送信号到目标进程的权限的。
kill的选项远不止这些,但是通常这些已经够用了。如有兴趣请自行 “man 1 kill”查看。
和shell命令kill有一个同名的系统调用kill(),其原型是这样的:
int kill(pid_t pid, int sig);
pid是目标进程的pid,sig是要发送的信号。和其他函数一样它也是成功返回0,失败-1。然而真的这么简单吗?事实上不是。pid这个参数在这里大有学问。它的取值不仅仅可以是进程id,它甚至可以是负的。如果你对linux下编程熟悉的话,这样的用法肯定接触过,获取消息队列时使用的msgrcv()函数,其中的msgtype参数也具有类似的用法。当然扯远了。
pid>0 此时正式最普通的一种情况,pid是要目标进程的pid。
pid=0 那么kill()会将信号发送给调用进程同组的所有进程,也包括他自己。
pid=-1 那么信号将被发送至所有它具有权限发送信号的每一个进程(init进程和调用进程除外)。
pid<-1 信号会发送sig信号到组id等于该pid绝对值的进程组中的每一个进程。
如果pid在以上四种情况之外,无法匹配到目标进程,那么就会返回-1,errno被设置为ESRCH。当没有权限发送时kill()也将失败返回-1,errno会被设置为EPERM。关于linux上权限是如何作用的细节,我争取再后面的博客总结一下。
与kill()类似的还有一个函数killpg(),用法简单多了,也不浪费篇幅了,查看manual就能搞定。
最后一个发送信号的函数是raise(),它只接受一个参数signal,然后把该信号传递给调用进程:
int raise(int sig);//成功返回0,失败返回-1
由于这个函数不需要引用进程ID,它是被纳入C99标准的函数。
除了这几种产生信号的shell命令和函数之外还有一些情况下可以产生信号,比如alarm(),settimer()之类的一些与时间相关的函数,以及一些常见的软硬件错误都会产生信号。详细谈这些貌似就有点淡化主题了,扯远了。
不可靠信号与可靠信号的语义
信号的可靠与不可靠主要体现在两个方面:
由于Linux信号机制基本上从早期的UNIX系统上的信号机制移植过来的,所以Linux仍旧支持这些早期的不可靠信号。但是Linux也对不可靠信号做了(上面两点区别的第一小点)改进,即不可靠信号处理方式,不会在处理函数执行后变成默认方式。所以,在Linux上对于不可靠信号与可靠信号的区别就在于是否支持排队。
关于信号是否会丢失,我们看这样两段代码,首先是rcv.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
这段程序先安装SIGINT的信号处理函数fun,fun函数只是打印信号信息。之后打印出进程id同时死循环等待信号。
5525
^C
Receive a signal:Interrupt
^C
Receive a signal:Interrupt
^C
Receive a signal:Interrupt
^C
Receive a signal:Interrupt
另一段程序是send.c:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
send程序从终端接收一个参数即目标进程的PID,然后向其发送500次SIGINT信号。下面我们分别把rcv.c,send.c编译成rcv和send。首先运行rcv,打印出了进程pid 20273然后,我们在开一个终端运行send 20273,观察到这样的结果:
send明明发送了500个SIGINT信号,而rcv中只接受处理了13个SIGINT信号,这是怎么回事儿呢?
究竟是rcv进程接受了500次SIGINT信号只执行了13次信号处理函数,还是rcv进程只接受了13次SIGINT信号然后执行了13次信号处理函数呢。我们不禁要问:信号去了哪儿呢?要搞清这个问题之前,我们还需了解一个叫做信号集和信号屏蔽的知识点。
信号集
sigset_t,通常是用位掩码的形式来实现的。我的环境是CentOS7,其定义在/usr/include/bits/sigset.h中,具体如下:
/* A `sigset_t' has a bit for each signal. */
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;#endif
在sigset.h同时也提供了一组函数(实际上用宏来实现的,感兴趣可以查阅sigset.h),用以实现对sigset_t类型数据的操作。其原型如下:
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);
除此之外,Glibc还提供了另外三个非标准规定的函数:
int sigisemptyset(const sigset_t* set);
int sigandset(sigset_t* dest,sigset_t* left,sigset_t* right);
int sigorset(sigset_t* dest,sigset_t* left,sigset_t* right);
信号屏蔽
信号屏蔽字。它定义了要阻塞递送到当前进程的信号集,每一个进程都有一个信号屏蔽字(signal mask)。如果你知道什么是权限屏蔽(umask)那么信号屏蔽字也很好理解。sigprocmask()函数可以检测和更改当前进程的信号屏蔽字。其原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
当oldset是一个非空指针的话,调用sigprocmask之后,oldset便返回了之前的信号屏蔽字。set参数会结合how参数对当前的信号屏蔽字做出修改。(和之前一节提到过的一样有两个特殊的信号,你不可以屏蔽它们是:SIGKILL和SIGSTOP)具体规则是:
how | 行为 |
SIG_BLOCK | 设置进程的信号屏蔽字为当期信号屏蔽字和set的并集。set是新增的要屏蔽的信号集。 |
SIG_UNBLOCK | 设置当前进程的信号屏蔽字为当前信号屏蔽字和set补集的交集,也就是当前信号屏蔽字减去set中的要解除屏蔽的信号集。set中是要解除屏蔽的信号集。 |
SIG_SETMASK | 设置当前进程的信号屏蔽字为set信号集。 |
然而当set指向一个NULL时,那么how也就没有作用了。通常我们让set设置为NULL时,通过oldset获取当前的信号屏蔽字。
如果某个或多个信号在进程屏蔽了该信号的期间来到过一次或者多次,我们称这样的信号叫做未决的(pending)信号。那么在调用sigprocmask()解除这个信号屏蔽之后,该信号会在sigprocmask ()返回之前,递送给(SUSv3 规定至少传递一个信号)当前进程。
进程维护了一个数据结构来保存未决的信号,我们可以通过sigpending()来获取哪些信号是未决的:
int sigpending(sigset_t *set);//return 0 on success,or -1 on error
set参数返回的便是未决的信号集。之后便可以通过使用sigismember()来判断,set中包含哪些信号。
到这里我们就可以解释上一篇末尾的问题了。因为Linux上signal()注册的信号处理函数在执行时,会自动的将当前的信号添加到进程的信号屏蔽字当中。当信号处理函数返回时,会恢复之前的信号屏蔽字。这意味着,当信号处理函数执行时,它不会递归的中断自身。
实时信号
早期Unix系统只定义了32种信号。POSIX.1b定义了一组额外的实时信号(为了兼容之前的应用,而不是修改以前的传统信号)。实时信号的特点,《Linux系统编程手册》上有一段总结的很是全面:
- Realtime signals provide an increased range of signals that can be used for application-defined purposes. Only two standard signals are freely available for application-defined purposes: SIGUSR1 and SIGUSR2.
- Realtime signals are queued. If multiple instances of a realtime signal are sent to a process, then the signal is delivered multiple times. By contrast, if we send further instances of a standard signal that is already pending for a process, that signal is delivered only once.
- When sending a realtime signal, it is possible to specify data (an integer or pointer value) that accompanies the signal. The signal handler in the receiving process can retrieve this data.
- The order of delivery of different realtime signals is guaranteed. If multiple different realtime signals are pending, then the lowest-numbered signal is delivered first. In other words, signals are prioritized, with lower-numbered signals having higher priority. When multiple signals of the same type are queued, they are delivere—along with their accompanying data—in the order in which they were sent.
根据第二点,我们可以将上篇的博客末尾的SIGINT改成SIGRTMIN+5(当然这里随意,只要是实时信号,Linux上kill()也是可以发送实时信号的),然后重复昨天的测试,我们会惊喜的发现,rcv进程“不出意外”地接受并处理了500次信号处理函数。
那么如何通过发送实时信号时传递数据呢?别着急,还得掌握一个系统调用sigaction()。
sigaction()系统调用
sigaction()比signal更加强大,兼容性更好,任何时候我们都应优先考虑使用sigaction(),即使signal()更加简单灵活。其函数原型:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);//Return 0 on success,or -1 on error
与sigprocmask类似地,oldact返回之前的信号设置,act用来设置新的信号处理。signum是要处理的信号。这个函数的关键之处就是struct sigaction这个和函数同名的结构体,它的定义:
struct sigaction {
union {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);}__sigaction_handler; //Address of handler
sigset_t sa_mask; //Signals blocked during the handler invocation
int sa_flags; //Flags controlling handler invocation
void (*sa_restorer)(void); //Restore,not use
};
sa_mask是一组信号集,当调用信号处理函数之前会将这组信号集添加到进程的信号屏蔽字中,直到信号处理函数返回。利用sa_mask参数,我们可以指定一组信号,让我们的信号处理函数不被这些信号打断。与前面的signal()一样,默认还是会把引发信号处理函数的信号,自动的添加到进程的信号屏蔽字中的。sa_flags参数,如果有经验的话,我们不难猜到这肯定是一组选项,毕竟身经百战了嘛。那我们就来看看这组选项是什么意思:
sa_flags | 说明 |
SA_INTERRUPT | 由此信号中断的系统调用不会自动重启。 |
SA_NOCLDSTOP | 当signum为SIGCHLD时,当因接受一信号的子进程停止或者恢复时,将不会产生此信号(有点绕).但是子进程终止时,仍会产生此信号。 (If sig is SIGCHLD, don’t generate this signal when a child process is stopped or resumed as a consequence of receiving a signal.) |
SA_NOCLDWAIT | 当signum为SIGCHLD时,子进程终止时不会转化为僵尸进程。此时调用wait(),则阻塞到所有子进程都终止,才返回-1,errno被视之为ECHILD。 |
SA_NODEFER | 捕获该信号的时候,不会在执行信号处理函数之前将该信号自动添加到进程的信号屏蔽字中。 |
SA_ONSTACK | 调用信号处理函数时,使用sigaltstack()安装的备用栈。 |
SA_RESETHAND | 当捕获该信号时,会在调用信号处理函数之前将信号处理函数设置为默认值SIG_DFL,并清除SA_SIGINFO标志。 |
SA_RESTART | 被此信号中断的系统调用,会自动重启。 |
SA_SIGINFO | 调用信号处理函数时附带了额外的数据要处理,具体见下文。 |
sa_restorer和名字一样为保留参数,不需要使用。最后我们要看的是__sigaction_handler,这是一个联合体(当然啦,这是废话)。sa_handler和sa_sigaction都是信号处理函数的指针,所以一次只能选择两者中的一个。如果sa_mask中设置了SA_SIGINFO位那么就按照void (*sa_sigaction)(int, siginfo_t *, void *)的形式的函数调用信号处理函数,否则使用 void (*sa_handler)(int)这样的函数。下面我们再来看一看sa_sigaction这个函数:
void sa_sigaction(int signum, siginfo_t* info, void* context);
siginfo_t是一个结构体,其结构和实现相关,我的CentOS7系统上是这样的:
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 */
long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */
}
每个字段的含义后边都加了清晰的注释,但是还有一个参数使我们需要特别注意的,其中si_value字段用来接收伴随着信号发送过来的数据,其类型是一个sigval_t的联合体,其定义(我的系统是在路径/usr/include/bits/siginfo.h 上):
# define __have_sigval_t 1
/* Type for data associated with a signal. */
typedef union sigval
{
int sival_int;
void* sival_ptr;
} sigval_t;
#endif
在实际编程中,到底选择sival_int还是sival_ptr字段,还是取决于你的应用程序。但是由于指针的作用范围只能在进程的内部,如果发送一个指针到另一个进程一般没有什么实际的意义。
基本上写到这里,我们就可以使用sigaction()进行信号处理的demo了,但是这里我们先不急着写,留到下一节一并写了。
使用sigqueue()
之前我们提到了发送实时信号时可以附带数据,kill(),raise()等函数的参数注定他们无法附带更多的数据,这里我们要认识一个新的函数sigqueue()专门用于在发送信号的时候,附加传递额外的数据。
int sigqueue(pid_t pid, int sig, const union sigval value);//Return 0 on success ,or -1 on error
前两个参数和kill()一致,但是不同于kill(),这里不能将pid只能是单个进程,而不像kill()那样丰富的用法。value的类型便是在上边提及的sigval_t,于是就清晰了:发送进程在这里发送的value在接受进程中通过信号处理函数sa_sigaction中的siginfo_t info参数就可以拿到了。
一个处理实时信号信号简单的demo,处理信号端代码catch.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
发送信号端send.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
运行结果如图所示,在sa_sigaction中成功拿到了发送进程的进程id以及传送的数据:
信号这部分的知识点真是多,而且牵扯到好多细节方面的东西,看来这个任务今晚完不成了,明天继续吧。
一晃眼,已经到9月底了,都来不及去感慨时间匆匆。最近常常会想明年的今天我将会在那里干着什么样的工作?对未来又是憧憬又是担忧,压力山大。无论如何现在还是踏踏实实的学习吧,能这样安安静静学习的日子也不多了。不扯了,还是接着前面的写吧。
SA_RESTART语义
在上篇提到过,SA_RESTART标志的作用是重启系统调用。其作用是建立在这样的基础上的:在Linux系统上,如果进程正在执行一个低速系统调用期间捕捉到一个信号,那么该系统调用会被中断,在处理完信号之后,这个系统调用将不会继续执行。随后返回错误,errno被设置为EINTR。所谓的慢速系统调用包括但不局限于以下:
以我现在的功力总结全面是不可能的,平时当我们遇上进程要处理会阻塞的系统调用时,就需要留个心眼儿,要考虑一下被信号中断的情况。在不使用SA_RESTART的时候,我们要重启系统调用时,可以这样组织代码:
int cnt;
while((cnt = read(fd,buf,BUFSIZE))==-1 && errno== EINTR) //read()如果被中断返回错误,就会自动重启
continue;
...
if(cnt == -1)
exit(-1); //其他使read()出错的情况
我反正是不喜欢的这样的代码风格的,有了SA_RESTART这个标志,我们本可以把代码写得更加优雅:
#include
#include
#include
#define BUFSIZE 1024
void handler(int sig)
{
}
int main()
{
struct sigaction act;
act.sa_flags = SA_RESTART;
sigemptyset(&act.sa_mask);
act.sa_handler = handler;
if(sigaction(SIGINT,&act,0) == -1)
exit(-1);
char buf[BUFSIZE] = {0};
read(0,buf,BUFSIZE-1);
write(1,buf,BUFSIZE);
}
在之前的一篇博客上,曾使用过这个标志,应该说这个标志位还是比较常用的一个,特别是在socket编程中。
可重入函数与不可重入函数
在《c++11 Thread库之原子操作》中提到了多线程程序中多个线程之间数据共享所引起的问题。其实在有信号处理的程序中也存在着这样的问题,因为信号可能会在程序执行的某一时间点异步中断程序,转而去执行信号处理函数。和多线程程序一样,这时候程序就有了两个执行的线程,虽然不是并发的。如果一个进程的多条线程可以同时安全地(能产生预期的效果)执行某一函数,那么我们称这个函数是可重入函数,反之则为不可重入函数。
我做了一个gif图来表示不可重入函数,就拿我们最熟悉的printf()函数来举例,我们已经知道printf()函数是行缓冲的IO函数,而这个缓冲区是一个全局的buffer。当主线程中正在执行printf()的时候,一个信号过来了,那么进程会把这个当前线程暂停,转而去执行信号处理函数,恰巧这个信号处理函数中,也调用了printf()函数,于是buf就被修改了(图中用变了颜色来表示),当信号处理函数返回以后,主线程恢复执行,而此时它正在使用的buf已经不是之前的那个buf了。于是可能会出现一些意料之外的输出。
一般来讲,更新全局数据结构的函数,是不可重入的函数。通常有这几类:
当我们所编写的函数要更新全局变量该怎么办呢?sig_atomic_t这种数据类型是C语言标准所规定的一种原子操作的数据类型,关于原子操作的内容可移步:《c++11 Thread库之原子操作》。具体用法和c++11中的std::atomic类型类似,不再赘述。值得一提的是,使用这个数据类型时,应当使用volatile关键字声明,以防止编译器把其优化到寄存器之中。
GDB调试与信号
在使用gdb调试程序时,缺省情况下信号会被gdb截获,导致要调试的程序无法接收到信号,我们可以使用info handle来查看信号的缺省处理方式,同样info signals可以查看接受到的信号。要想在调试的程序中使用信号,我们需要使用gdb中的handle这个命令,具体用法如这个形式 :handle signal keywords。keywords的取值如下:
keywords | 说明 | keywords | 说明 |
stop | 当GDB收到signal,停止被调试程序的执行 | nostop | 当GDB收到指定的信号,不会应用停止程序的执行,只会打印出一条收到信号的消息 |
如果收到signal,打印出一条信息 | noprint | 不会打印信息 | |
pass | 如果收到signal,把该信号通知给被调试程序 | nopass | 不会告知被调试程序收到signal |
ignore | 同nopass | noignore | 同pass |
handle命令还是比较简单的,设置完以后,可以像普通的程序那样调试了。
关于信号暂时先总结这么多吧,以后用到了什么再慢慢往里边塞吧!