1. 前言

信号是一种软件终端,提供了一种处理异步事件的方法,也是进程间通信的唯一一个异步的通信方式。Unix中定义了很多信号,有很多条件可以产生信号,对于这些信号有不同的处理方式。本文会详细讲述信号的机制。

2. 定义

通过kill -l 可以查看所有的信号定义:

$ kill -l
 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
16) SIGSTKFLT	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
  • 目前Linux 中定义了64中信号,前期定了32种(1-31),后面的33种为实时信号(32-64);
  • glibc的接口中可能会使用到2 或者3 种实时信号(32、33、34),所以,对于用户看到的SIGTRMIN 会做适当的调整(34 或35)。从目前来看,glibc 应该是用了32 和 33两个实时信号;
  • 因为 glibc的需要,实时信号可能是变动的,所以,在使用的时候不要直接写死数值,而是用SIGRTMIN+n 的方式,而且需要确定SIGRTMIN+n不能超过SIGRTMAX;
  • 与标准的信号不同,实时信号没有确切的定义,可以根据应用的目的进行定义;
  • 实时信号默认的处理方式是结束收到信号的进程;

3. 标准信号

信号属性值默认处理方式定义
SIGHUP 1TermHangup detected on controlling terminal or death of controlling process
SIGINT 2TermInterrupt from keyboard
SIGQUIT3CoreQuit from keyboard
SIGILL 4CoreIllegal Instruction
SIGTRAP5CoreTrace/breakpoint trap
SIGABRT6CoreAbort signal from abort(3)
SIGBUS7CoreBus error (bad memory access)
SIGFPE 8CoreFloating point exception
SIGKILL9TermKill signal
SIGUSR110TermUser-defined signal 1
SIGSEGV11CoreInvalid memory reference
SIGUSR212TermUser-defined signal 2
SIGPIPE13TermBroken pipe: write to pipe with no readers
SIGALRM14TermTimer signal from alarm(2)
SIGTERM15TermTermination signal
SIGSTKFLT16TermStack fault on coprocessor (unused)
SIGCHLD17Ign Child stopped or terminated
SIGCONT18ContContinue if stopped
SIGSTOP19StopStop process
SIGTSTP20StopStop typed at terminal
SIGTTIN21StopTerminal input for background process
SIGTTOU22StopTerminal output for background process
SIGURG23IgnUrgent condition on socket (4.2BSD)
SIGXCPU24CoreCPU time limit exceeded (4.2BSD)
SIGXFSZ25CoreFile size limit exceeded (4.2BSD)
SIGVTALRM26TermVirtual alarm clock (4.2BSD)
SIGPROF27TermProfiling timer expired
SIGWINCH28IgnWindow resize signal (4.3BSD, Sun)
SIGIO29TermI/O now possible (4.2BSD)
SIGPWR30TermPower failure (System V)
SIGUNUSED31CoreSynonymous with SIGSYS

                                                               表1     标准信号

表中处理方式:

  • Term   终止进程
  • Ign      忽略信号
  • Core   终止进程,并且产生core dump 文件
  • Stop   停止进程
  • Cont   如果进程处于stop状态,继续运行进程

注意:

  • SIGKILL 和SIGSTOP 是不能被捕捉、阻塞、忽略;

4. 信号来源

硬件方式

  • 当用户按某些终端键时,引发终端产生的信号,例如Ctrl +C 通常会产生终端信号SIGINT;
  • 硬件异常产生信号。除数为0、无效的内存引用等等,这些通常由硬件检测到,并将通知内核。然后内核会为正在运行的进程产生适当的信号。例如对执行一个无效内存引用产生SIGSEGV信号;

软件方式

  • kill
  • killpg
  • raise
  • pthread_kill
  • tgkill
  • abort
  • sigqueue
  • alarm

4.1 kill()

#include <signal.h>

int kill (pid_t pid, int sig);

将信号sig 发送给pid 进程;

参数pid:

  • pid == 0,则发送sig 给调用该函数所属group 里所有的进程;
  • pid > 0,将信号sig 发送给进程ID 为pid 的进程;
  • pid < 0,将信号发送给其他进程组ID 等于pid 的绝对值;
  • pic == -1,将sig发送给发送进程有权限向它发送信号的系统上的所有进程;

返回值:

  • 如果失败,返回-1;
  • 如果成功,返回0;

失败原因:

  • 给定的信号无效(errno = EINVAL)
  • 发送权限不够( errno = EPERM )
  • 目标进程不存在( errno = ESRCH )

4.2 killpg()

#include <signal.h>

int killpg (pid_t pgrp, int sig);

发送信号sig 到pgrp 的所有进程中;如果pgrp 为0,则发送sig 给当前调用该函数所属group里所有的进程;

4.3 raise()

#include <signal.h>

int raise (int sig);

给当前进程发送信号sig;

等价于:

kill(getpid(), sig);

4.4 alarm()

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

alarm函数可以用来设置定时器,定时器超时将产生SIGALRM信号给调用进程。

参数seconds表示设定的秒数,经过seconds后,内核将给调用该函数的进程发送SIGALRM信号。

  • 如果seconds为0,则不再发送SIGALRM信号,最新一次调用alarm函数将取消之前一次的设定;
  • 如果seconds 不为0,而上一次调用alarm还没有超时,那么将上次alarm 的余留值返回,并用新的alarm 代替上一次alarm;

5. 处理信号

对于产生的信号有三种方式处理:

  • 忽略此信号

大多数信号都可以用这种方式处理,但有两个信号决不能被忽略,SIGKILL 和SIGSTOP,这两个信号向超级用户提供了使进程终止或停止的可靠方法。 

  • 捕捉信号

可以通过函数将捕捉的函数通知给内核,在收到信号时,如果用户有捕捉的处理,内核会通知捕捉函数处理,处理完成后返回到内核中。下面会详细说明捕捉信号的机制。

  • 执行系统默认的方式

上面表格1中详细列出

6. 捕捉信号 

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,

举例如下:

1. 用户程序注册了SIGQUIT信号的处理函数sighandler

2. 当前正在执行main函数,这时发生中断或异常切换到内核态

3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达

4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

(By default,  the  signal  handler  is invoked on the normal process stack.  It is possible to arrange that the signal handler  uses an alternate stack; see sigaltstack(2) for a discussion of how to do this and when it might be useful.)

5. sighandler 函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。

6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

上图出自ULK。

7. 信号的阻塞

7.1 信号在内核中的表示

内核中没一个进程都会对应 3 张表,每个信号都有3中状态:

  • 信号递达(Delivery):实际执行信号处理的动作。
  • 信号未决(Pending):信号从产生到递达之间的状态。
  • 信号阻塞(Block):被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示信号的处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

7.2 三张表的存储

  • pending表:用4个字节的位图表示,位图的位置表示信号编号,内容表示是否pending。
  • block表:用4个字节的位图表示,位图的位置表示信号编号,内容表示是否block。
  • handler表:是一个句柄函数指针,数组即可表示,下标表示信号编号,内容表示信号处理的动作,为NULL表示没有处理该信号。

7.3 分析上图中的信号

  • SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作;
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞;
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler;
     

8. 信号集

考虑一个问题,上面一节中讲到block 和pending 表,分别中一个bit位代表一个signal。那如果这个信号在解除阻塞之前已经产生了很多次,那将如何呢?

每个进程都有一个信号屏蔽字(signal mask),它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号,该屏蔽字都有一位与之对应。对于某种信号,若其对应位已设置,则它当前是被阻塞的。进程可以调用sigprocmask来检测和更改当前信号屏蔽字。POSIX.1  定义了一个新数据类型sigset_t,用于保存一个信号集。

这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞。而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的 信号屏蔽字 (Signal Mask),这里的 “屏蔽”应该理解为阻塞而不是忽略。
 

8.1 信号集操作函数

信号集类型:sigsize_t。sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储,从使用者的角度是不必关心的。这些bit位依赖于系统实现,使用者只能调用以下函数来操作sigset_t变量即可。

#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);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。

注意,在使用sigset_t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。

  • 初始化sigset_t变量之后就可以 在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。

  • sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

8.2 sigprocmask()

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

返回值:若成功则为0,若出错则为-1。

参数:

  • oset:如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
  • set:如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
  • how参数的可选值为:
#define SIG_BLOCK     0      /* Block signals.  */                                                     
#define SIG_UNBLOCK   1      /* Unblock signals.  */                                                   
#define SIG_SETMASK   2      /* Set the set of blocked signals.  */

8.3 sigpending()

读取当前进程的未决信号集,通过set参数传出。

#include <signal.h>
int sigpending(sigset_t *set);

返回值:调用成功则返回0,出错则返回-1。

9. 捕捉信号的函数

9.1 signal()

#include <signal.h>

typedef void (*__sighandler_t) (int);
__sighandler_t signal(int signo, __sighandler_t handler);

第一个参数为信号名,详细看表1.

第二个参数为:

  • SIG_IGN,内核忽略该信号,除了两个不能忽略;
  • SIG_DFL,采用系统默认方式处理该信号;
  • 函数指针,按照用户的方式处理该信号;
#define SIG_ERR ((__sighandler_t) -1)       /* Error return.  */                                       
#define SIG_DFL ((__sighandler_t) 0)        /* Default action.  */                                     
#define SIG_IGN ((__sighandler_t) 1)        /* Ignore signal.  */

返回值:

  • 如果出错,返回SIG_ERR;
  • 如果成功,返回上一个处理程序的指针;

9.2 sigaction()

sigaction函数可以读取和修改与指定信号相关联的处理动作。

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

返回值:

成功返回0,失败返回-1

参数:

  • signo是指定信号的编号;
  • 若act指针非空,则根据act修改该信号的处理动作。
  • 若oact指针非空,则通过oact传出该信号原来的处理动作。

sigaction的结构体:

struct sigaction {
    union {
        __sighandler_t sa_handler;
        void (*sa_sigaction) (int, siginfo_t *, void *);
    };
    sigset_t   sa_mask;
    int        sa_flags;
    void      (*sa_restorer) (void);
};
  • sa_handler:可以是常数SIG_DFL或者SIG_IGN,或者是一个信号处理函数名;
  • sa_sigaction:与sa_handler是互斥的,两者通过sa_flags 确定选择哪个捕捉函数;
  • sa_mask:进程的信号屏蔽字。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字;
  • sa_flags:0或者SA_SIGINFO来指定使用sa_handler 还是使用sa_sigaction 函数;
  • sa_restorer:保留,已过时;

举例:

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

void sig_handle(int sig)
{
	puts("recv SIGINT");
	sleep(5);
	puts("end");
}

int main(int argc, char** argv)
{
	struct sigaction act;
	act.sa_handler = sig_handle;
	act.sa_flags = 0;
	
	sigemptyset(&act.sa_mask);
	sigaddset(&act.sa_mask, SIGQUIT); //当进入信号处理函数的时候,屏蔽掉SIGQUIT的递达
	sigaction(SIGINT, &act, NULL);
	
	while(1)
		sleep(1);
	return 0;
}

10. 可重入函数

试想一个问题,当进程接收到一个信号时,转到你关联的函数中执行,但是在执行的时候,进程又接收到同一个信号或另一个信号,又要执行相关联的函数时,程序会怎么执行?

也就是说,信号处理函数可以在其执行期间被中断并被再次调用。当返回到第一次调用时,它能否继续正确操作是很关键的。这不仅仅是递归的问题,而是可重入的(即可以完全地进入和再次执行)的问题。而反观Linux,其内核在同一时期负责处理多个设备的中断服务例程就需要可重入的,因为优先级更高的中断可能会在同一段代码的执行期间“插入”进来。

简言之,就是说,我们的信号处理函数要是可重入的,即离开后可再次安全地进入和再次执行,要使信号处理函数是可重入的,则在信息处理函数中不能调用不可重入的函数。下面给出可重入的函数在列表,不在此表中的函数都是不可重入的,可重入函数表如下(man 7 signal可以查看系统中的可重入函数):

accept

fchmod

lseek

sendto

stat

access

fchown

lstat

setgid

symlink

aio_error

fcntl

mkdir

setpgid

sysconf

aio_return

fdatasync

mkfifo

setsid

tcdrain

aio_suspend

fork

open

setsockopt

tcflow

alarm

fpathconf

pathconf

setuid

tcflush

bind

fstat

pause

shutdown

tcgetattr

cfgetispeed

fsync

pipe

sigaction

tcgetpgrp

cfgetospeed

ftruncate

poll

sigaddset

tcsendbreak

cfsetispeed

getegid

posix_trace_event

sigdelset

tcsetattr

cfsetospeed

geteuid

pselect

sigemptyset

tcsetpgrp

chdir

getgid

raise

sigfillset

time

chmod

getgroups

read

sigismember

timer_getoverrun

chown

getpeername

readlink

signal

timer_gettime

clock_gettime

getpgrp

recv

sigpause

timer_settime

close

getpid

recvfrom

sigpending

times

connect

getppid

recvmsg

sigprocmask

umask

creat

getsockname

rename

sigqueue

uname

dup

getsockopt

rmdir

sigset

unlink

dup2

getuid

select

sigsuspend

utime

execle

kill

sem_post

sleep

wait

execve

link

send

socket

waitpid

_Exit & _exit

listen

sendmsg

socketpair

write

例如:strtok就是一个不可重入函数,因为strtok内部维护了一个内部静态指针,保存上一次切割到的位置,如果信号的捕捉函数中也去调用strtok函数,则会造成切割字符串混乱,应用strtok_r版本,r表示可重入。

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

static char buf[] = "hello world good book";

void sig_handle(int sig)
{
	strtok(NULL, " ");
} 

int main(int argc, char** argv)
{
	signal(SIGINT, sig_handle);
	printf("%s\n", strtok(buf, " "));
	printf("%s\n", strtok(NULL, " "));
	sleep(5); //可以被信号打断,返回剩余的时间,想想看这个函数应该怎么调用
	printf("%s\n", strtok(NULL, " "));
	return 0;
}

 运行的时候,发现通过Ctrl+c发射信号与没发射信号的结果不一样,可以改用strtok_r函数。

参考:

linux系统编程之信号(四):信号的捕捉与sigaction函数-腾讯云开发者社区-腾讯云

相关博文:

进程间通信(0)——序 

进程间通信(1)——信号(Signal)

进程间通信(2)——管道(PIPE)

进程间通信(3)——命名管道(FIFO)

进程间通信(4)——消息队列

进程间通信(5)——共享内存

进程间通信(6)——信号量(semaphore​​​​​​)

进程间通信(7)——套接字(socket)

Logo

更多推荐