Linux中信号与信号处理

时间:2021-08-03 10:57:27

Linux中信号与信号处理

信号(signal)是一种软件中断,它提供了一种处理异步事件的方法,也是进程间惟一的异步通信方式。在Linux系统中,根据POSIX标准扩展以后的信号机制,不仅可以用来通知某种程序发生了什么事件,还可以给进程传递数据。

  • 信号是UNIX和Linux系统为响应某些条件而产生的一个时间。接收到该信号的进程会相应地采取一些行动。信号由shell和终端处理器生成来引起中断,它们可以在进程间传递消息或修改行为的一种方式,明确地由一个进程发送给另一个进程。

一、信号的来源

信号的来源可以有很多种试,按照产生条件的不同可以分为硬件和软件两种。

  1. 硬件方式
    当用户在终端上按下某键时,将产生信号。如按下组合键后将产生一个SIGINT信号;或者程序由于某些错误条件而自动生成信号。如:内存段错误、浮点处理器错误或非法指令等。他们有shell和终端处理器来生成引起中断。
    硬件异常产生信号:除数据、无效的存储访问等。这些事件通常由硬件(如:CPU)检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时正在进行的程序。
  2. 软件方式
    用户在终端下调用kill命令向进程发送任务信号,进程调用kill或sigqueue函数发送信号。

二、信号的种类

在Shell下输入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
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

终端特殊按键

ctl+c   SIGINT      //终止进程
ctl+z SIGTSTP //暂停进程并切换到后台
ctl+\ SIGQUIT //退出(核心已转储)

信号的值定义在signal.h中,它们以SIG开头。在Linux中没有16和32这两个信号。上面信号的含义如下:

1) SIGHUP:当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
2)SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动
作为终止里程。
3)SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信
号。默认动作为终止进程。
4)SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
5)SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。
6 ) SIGABRT:调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
8)SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默
认动作为终止进程并产生core文件。
9)SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可
以杀死任何进程的方法。
10)SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
11)SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
12)SIGUSR2:这是另外一个用户自定义信号 ,程序员可以在程序中定义 并使用该信号。默认动作为终止进程。1
13)SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
14) SIGALRM:定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。
15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行
shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
16)SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
17)SIGCONT:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为终止进程。
18)SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
19)SIGTSTP:停止进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
21)SIGTTOU:该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
22)SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据
到达,默认动作为忽略该信号。
23)SIGXFSZ:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止
进程。
24)SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
25)SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默
认动作为终止进程。
26)SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进
程。
27)SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
28)SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
29)SIGPWR:关机。默认动作为终止进程。
30)SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
31)SIGRTMIN~(64)SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。

1. 可靠信号与不可靠信号

在Linux系统中,信号的可靠性是指信号是否会丢失,或者说该信号是否支持排除。SIGHUP( 1 ) ~ SIGSYS( 31 )之间的信号都是继承自UNIX系统是不可靠信号。Linux系统根据POSIX标准定义了SIGRTMIN(33) ~ SIGRTMAX(64)之间的信号,它们都是可靠信号,也称为实时信号。

当导致产生信号的事件发生时,内核就产生一个信号。信号产生后,内核通常会在进程表中设置某种形的标志。当内核设置了这个标志,我们就说内核向一个进程递送了一个信号。信号产生(generate)和递送(delivery)之间的时间间隔,称主信号未决(pending)。
进程可以调用sigpending将信号设为阻塞,如果为进程产生一个阻塞信号,而对信号的动作是捕捉该信号(即不忽略信号),则内核将为该进程的此信号保持为未决状态,直到该进程对此信号解除阻塞或者对此信号的响应更改为忽略。如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那么如果信号被递送多次(即信号在未决信号队列里面排队),则称之为可靠信号;只被递送一次的信号称为不可靠信号。
Linux中信号与信号处理

2. 信号的优先级

信号实质上是软中断,中断有优先级,信号也有优先级。如果一个进程有多个未决信号,则对于同一个未决的实时信号,内核将按照发送的顺序来递送信号。如果存在多个未决信号,则值(或者说编号)越小的越先被递送。如果即存在不可靠信号,又存在可靠信号(实时信号),虽然POSIX对这一情况没有明确规定,但Linux系统和大多数遵循POSIX标准的操作系统一样,将优先递送不可靠信号。

三、进程对信号的响应

当信号发生时,用户可以要求进程以下列3种方式之一对信号做出响应。
1、 捕捉信号:对于要捕捉的信号,可以为其指定信号处理函数,信号发生时该函数自动被调用,在该函数内部实现对该信号的处理。
2、 忽略信号:大多数信号都可使用这种方式进行处理,但是SIGKILL和SIGSTOP这两个信号不能被忽略,同时这两个信号也不能被捕获和阻塞。此外,如果忽略某某些由硬件异常产生的信号(如非法存储访问或除以0),则进程的行为是不可预测的。
3、 按照系统默认方式处理。大部分信号的默认操作是终止进程,且所有的实时信号的默认动作都是终止进程。默认处理方式包含五种动作,term(把当前进程终止掉),core(终止进程,并产生一个core文件,ulimit -c 1024设置core文件大小),stop(让当前进程暂停),continue(继续执行先前停止的进程),ignore(忽略)。

Term Default action is to terminate the process.
Ign Default action is to ignore the signal.
Core Default action is to terminate the process and dump core (see core(5)).
Stop Default action is to stop the process.
Cont Default action is to continue the process if it is currently stopped.

当没有对信号进行特殊处理之前,32种信号里面的行为都是默认行为里面的默认动作,各种信号的默认动作:

   Signal     Value     Action   Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no
readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2

SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at tty
SIGTTIN 21,21,26 Stop tty input for background process
SIGTTOU 22,22,27 Stop tty output for background process

四、信号集处理函数

  • sigset_t为信号集,可sizeof(sigset_t)察看大小
int sigemptyset(sigset_t *set)  //set传出参数,清空set
int sigfillset(sigset_t *set) //把set全部置1
int sigaddset(sigset_t *set, int signo)//把信号集里的第signo这一位置1,添加一个信号
int sigdelset(sigset_t *set, int signo)//把信号集里的第signo这一位置0,删除一个信号
int sigismember(const sigset_t *set, int signo)//测试,在set这个信号集里面signo这个信号位是否为1,存在返回1,不存在返回0

sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
  • 如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
    how参数的含义
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于mask=set

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

sigpending

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

sigpending读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

信号捕捉设定

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
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);
};
sa_handler : 早期的捕捉函数
sa_sigaction : 新添加的捕捉函数,可以传参 , 和sa_handler互斥,两者通过sa_flags选择采用哪种捕捉函数
sa_mask : 在执行捕捉函数时,设置阻塞其它信号,sa_mask | 进程阻塞信号集,退出捕捉函数后,还原回原有的
阻塞信号集
sa_flags : SA_SIGINFO 或者 0
sa_restorer : 保留,已过时

Linux中信号与信号处理

C标准库信号处理函数

typedef void (*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler)

int system(const char *command)
集合forkexecwait一体

可重入函数

不可重入函数实例
Linux中信号与信号处理

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

信号引起的竞态和异步I/O

int pause(void)
使调用进程挂起,直到有信号递达,如果递达信号是忽略,则继续挂起
int sigsuspend(const sigset_t *mask)
1.以通过指定mask来临时解除对某个信号的屏蔽,
2.然后挂起等待,
3.当被信号唤醒sigsuspend返回时,进程的信号屏蔽字恢复为原来的值
实现sleep()函数
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

void do_sig(int num)
{
/* nothing to do */
}

unsigned int mysleep(unsigned int nsecs)
{
struct sigaction newact, oldact;
unsigned int unslept;

newact.sa_flags = 0;
newact.sa_handler = do_sig;
sigemptyset(&newact.sa_mask);
sigaction(SIGALRM, &newact, &oldact); /* set newmask, get oldmask to oldact */

alarm(nsecs); /* set SIGALRM */
pause();

unslept = alarm(0); /* if alarm clock is no finished, it will return the remanining number of seconds when alarm is again */
sigaction(SIGALRM, &oldact, NULL); /* set oldmask to block*/

return unslept;
}

int main(void)
{
while(1) {
mysleep(2);
printf("Two second passed\n");
}
}
由于异步I/O,尚叙代码当进程执行完alarm(nsecs)定时器之后,kernel又去调用其他进程,该进程处理器现场保存,但是定时器依旧在计时,当计时器订完之后,kernel在来执行该进程,恢复处理器现场,会立即出发sigaction里的do_sig函数,这个函数执行完之后,会再去执行pause(),那此进程这时候会被永久挂起。因为没有信号来触发pause了。改进代码
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
/* set our handler, save previous information */
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
/* block SIGALRM and save current signal mask */
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
alarm(nsecs);
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
/* make sure SIGALRM isn't blocked */
sigsuspend(&suspmask);
/* wait for any signal to be caught */
/* some signal has been caught, SIGALRM is now blocked */
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL);
/* reset previous action */
/* reset signal mask, which unblocks SIGALRM */
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return(unslept);
}

说明,在设置好捕捉函数之后,在定义一个新的信号屏蔽字,把这个屏蔽字先用sigemptyset清空,然后在用sigaddset把SIGALRM信号加上,再通过 sigprocmask(SET_BLOOK,&newmask,&oldmask) 把原来的信号屏蔽字表替换掉,原来的保存到oldmask中,此时SIGALRM这个信号是屏蔽的,这时候去调用alarm(),下面接着把oldmask表里面的内容复制到suspmask中,把其中的SIGALRM删除,确认SIGALRM这个信号在suspmask里面是非阻塞的,然后执行sigsuspand(&suspmask),这个函数是先把当前进程的信号屏蔽字表临时替换成suspmask这个,然后挂起等待,等alarm定的时间到了,给进程发送SIGALRM信号,此时的进程的信号屏蔽字表是不对它屏蔽的,所以触发sigsuspmask这个函数,进程继续往下执行,信号屏蔽字重新变回到newmask,之后重新设置一个alarm通过返回值查看之前的闹钟是否执行完全,然后通过sigacton和siaprocmask把之前的捕捉动作和信号屏蔽字重新改回去。

  • 避免异步I/O的类型
    • sig_atomic_t
      平台下的原子类型
    • volatile
      防止编译器开启优化选项时,优化对内存的读写

SIGCHLD信号处理

SIGCHLD的产生条件

子进程终止时
子进程接收到SIGSTOP信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时

代码实例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int signo)
{
int status;
pid_t pid;
while ((pid = waitpid(0, &status, WNOHANG)) > 0)
{
if (WIFEXITED(status))
printf("child %d exit %d\n", pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
}
}
int main(void)
{
pid_t pid;
int i;
//阻塞SIGCHLD
for (i = 0; i < 10; i++)
{
if ((pid = fork()) == 0)
break;
else if (pid < 0)
sys_err("fork");
}
if (pid == 0)
{
int n = 18;
while (n--)
{
printf("child ID %d\n", getpid());
sleep(1);
}
return i;
}
else if (pid > 0)
{
//先设置捕捉
//再解除对SIGCHLD的阻塞
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
while (1)
{
printf("Parent ID %d\n", getpid());
sleep(1);
}
}
return 0;
}

status处理方式

pid_t waitpid(pid_t pid, int *status, int options)
options
WNOHANG
没有子进程结束,立即返回
WUNTRACED
如果子进程由于被停止产生的SIGCHLD, waitpid则立即返回
WCONTINUED
如果子进程由于被SIGCONT唤醒而产生的SIGCHLD, waitpid则立即返回
获取status
WIFEXITED(status)
子进程正常exit终止,返回真
WEXITSTATUS(status)返回子进程正常退出值
WIFSIGNALED(status)
子进程被信号终止,返回真
WTERMSIG(status)返回终止子进程的信号值
WIFSTOPPED(status)
子进程被停止,返回真
WSTOPSIG(status)返回停止子进程的信号值
WIFCONTINUED(status)
子进程由停止态转为就绪态,返回真

向信号捕捉函数传参

sigqueue

int sigqueue(pid_t pid, int sig, const union sigval value)
union sigval
{
int sival_int;
void *sival_ptr;
};

sigaction

void (*sa_sigaction)(int, siginfo_t *, void *)
siginfo_t
{
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
sigval_t si_value; /* Signal value */
...
}
sa_flags = SA_SIGINFO