linux信号
信号简述
信号是进程间通信机制中唯一的异步通信机制, 是在软件层次(包括操作系统)上对中断机制的一种模拟 .
大佬文章:
http://www.cppblog.com/sleepwom/archive/2010/12/27/137564.html
信号类型:
- SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
- SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
- SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
- SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
- SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
- SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
- SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
- SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1
- SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5
- SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9
- SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
- SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13
- SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
- SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5
- SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1
- SIGRTMAX
列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。
-
非可靠信号采用的是位图来进行注册,而没有采用队列, 所以当位图中已有这个信号时, 再来一个相同的信号注册, 后来的信号并不会注册, 其生命周期不完整, 这就造成了信号的丢失 .
-
进程处理一个非可靠信号, 信号处理函数执行完毕, 信号处理函数会恢复成默认处理方式. 因此,进程在下次接收到这个信号后, 信号执行的是默认服务, 而在一些情况下, 这个默认处理并不是我们所希望的所以, 常常需要在信号处理程序中, 重新安装该信号, 但这种做法很不靠谱, 极易造成信号的丢失(即有信号没有得到用户预期的处理, 也可以看作一种丢失).
下面我们对编号小于SIGRTMIN的信号进行讨论。
- SIGHUP
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。
此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
-
SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。 -
SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。 -
SIGILL
执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。 -
SIGTRAP
由断点指令或其它trap指令产生. 由debugger使用。 -
SIGABRT
调用abort函数生成的信号。 -
SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。 -
SIGFPE
在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。 -
SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。 -
SIGUSR1
留给用户使用 -
SIGSEGV
试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据. -
SIGUSR2
留给用户使用 -
SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。 -
SIGALRM
时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号. -
SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。 -
SIGCHLD
子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。
-
SIGCONT
让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符 -
SIGSTOP
停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略. -
SIGTSTP
停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号 -
SIGTTIN
当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行. -
SIGTTOU
类似于SIGTTIN, 但在写终端(或修改终端模式)时收到. -
SIGURG
有"紧急"数据或out-of-band数据到达socket时产生. -
SIGXCPU
超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。 -
SIGXFSZ
当进程企图扩大文件以至于超过文件大小资源限制。 -
SIGVTALRM
虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间. -
SIGPROF
类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间. -
SIGWINCH
窗口大小改变时发出. -
SIGIO
文件描述符准备就绪, 可以开始进行输入/输出操作. -
SIGPWR
Power failure -
SIGSYS
非法的系统调用。
在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:SIGILL,SIGTRAP
默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。
信号的生命周期
触发信号的事件发生(软硬件触发) > 进程中向内核注册安装信号 > 进程中向内核注销信号 >执行相应的信号处理函数
信号注册
当有事件发生时, 如检测到硬件异常, 以及调用信号发送函数kill()等, 即会诞生相应的信号. 此时在接收信号的进程中维护着一个信
号队列, 这个队列是一个sigqueue结构的链表. sigqueue结构定义如下:
struct sigqueue {
struct sigqueue * next; 指向下一个
siginfo_t info;
};
其队列头是一个sigpending结构, 如下:
struct sigpending
struct sigqueue * head; 链表头指针
struct sigqueue * tail; 链表尾指针
sigset_t signal;
};
在这个结构中, 除了用队列头尾两个指针之外还有一个sigset_t类型的位图 signal. 系统中的每个信号都在位图中占有一个固定的
位置, 在每个信号的固定位置以1或0表示这个信号的状态. 所以结构sigpending中的这个signal位图就用其中各个位的值来表示对
应的信号的到来情况, 即用1表示信号已经到来并且还未被进程处理 ; 用0表示该位对应的信号未到或已处理完毕. 因此,这个
signal位图也叫做进程的未决信号位图 . task_struct 中就有成员 struct sigpending pending;
非可靠信号的注册
当一个非可靠信号发送给一个进程时, 如过该信号在位图中对应位置为0, 则置1, 将这个信号加入信号队列末尾, 如果该信号在位
图中对应位置为1, 对位图不修改, 直接忽略掉这个信号. 这也就意味着同一个非可靠信号可以在同一个进程的未决信号信息链中最
多只有一个sigqueue结构 .
可靠信号的注册
当一个可靠信号发送给一个进程时, 如过该信号在位图中对应位置为0, 则置1, 将这个信号加入信号队列末尾, 如果该信号在位图
中对应位置为1, 对位图不修改, 直接将这个信号加入信号队列末尾. 所以不管该信号是否已经在进程中注册, 都会被再注册一次,
也就是会加入到信号队列. 因此, 可靠信号相比于非可靠信号不会在注册时丢失.这也就意味着同一个可靠信号可以在同一个进程
的未决信号信息链中占有多个sigqueue结构 (进程每收到一个可靠信号, 都会为它分配一个结构来登记该信号信息, 并把该结构添
加在未决信号链尾, 即所有诞生的可靠信号都会在目标进程中册 ).
信号安装
如果进程要处理某一信号, 那么就要在进程中安装该信号. 所谓信号的安装, 就是把信号编号及其对应的处理函数加入到进程控制块中 .
安装信号主要用来确定信号值及进程针对该信号值的动作(处理函数)之间的映射关系, 即在进程中注册的信号在执行时, 该执行何种操作.
Linux用来安装信号的函数有两个:signal()和sigaction() .其中, signal()是在系统调用sys_signal()基础上实现的库函数, signal()
只有两个参数, 不支持信号传递信息, 主要是用于前31种非可靠信号的安装 ; 而sigaction()是较新的函数(由两个系统调用实现:
sys_signal以及sys_rt_sigaction), 有三个参数, 支持信号传递信息, 主要用来与 sigqueue() 系统调用配合使用, 当然, sigaction()同
样支持非实时信号的安装. sigaction()优于signal()主要体现在支持信号带有参数 , 这两个函数下面说.
进程对信号管理的结构如下图所示:
信号的阻塞
当信号在进程中注册后, 有时并进程并不希望这个信号马上处理, 所以才有信号的阻塞, 被阻塞的信号暂不处理, 一直处于未决状态, 直到解除阻塞 .
每个进程都有一个用来描述 "哪些信号递送到进程时将被阻塞"的信号集,该信号集中的所有信号在递送到进程后都将被阻塞.
先来统一下概念 :
实际执行信号的处理动作(忽略, 默认, 自定义) 称为信号递达
信号从产生到递达之间的状态, 叫做信号未决
进程可以选择阻塞某个信号, 也就是选择屏蔽某个信号, 阻塞也叫屏蔽. 被阻塞的信号产生时,将保持在未决状态,直至进程取消
对该信号的阻塞,才执行递达的动作
注意:阻塞和忽略是不同的. 只要信号阻塞就不会被递达, 而忽略是信号在递达之后的一种处理方式.
在前面信号注册安装时, 我们知道信号在task_struct中有一个pending位图(未决信号集), 其实信号在task_struct中还有一个block
位图(阻塞信号集或屏蔽位图) . 信号产生时, 内核在task_struct中的pending中设置该信号的未决标志, 直至信号递达才清除该标
志. 但在信号递达前, 信号可能被阻塞, 致使信号一直处于未决状态. 过程如下 :
注意: 有两个信号不能被阻塞, 在处理时不能忽略, 不能自定义处理函数, 就是9号信号SIGKILL和19号进程SIGSTOP.
SIGKILL : 强行结束某个进程 SIGSTOP : 强行暂停某个进程信号的阻塞可以是系统阻塞或自定义阻塞 :
系统自动阻塞 :在某个信号的处理函数正在执行时, 该信号将被阻塞, 直到信号处理函数执行完毕, 该阻塞将会解除. 这种机制的作用主要是避免信号的嵌套处理.
自定义阻塞 : 通过sigaction()和sigprocmask()实现信号的自定义阻塞 .
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
在使用sigaction安装信号时, 如果设置了sa_mask阻塞信号(信号阻塞掩码)集, 则该信号集中的信号在其信号对应的处理函数
执行期间将会被阻塞. 这种情况下进行信号阻塞的主要原因是: 一个信号处理函数在执行过程中, 可能会有其他信号到来. 此
时, 当前的信号处理函数就会被中断. 而这往往是不希望发生的. 此时, 可以通过sigaction系统调用的信号阻塞掩码sa_mask
对相关信号进行阻塞. 通过这种方式阻塞的信号, 在信号处理函数执行结束后就会解除.
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
可以通过sigprocmask系统调用指定阻塞某个或者某几个信号. 这种情况下进行信号阻塞的原因较多, 一个典型的情况是:某
个信号的处理函数与进程某段代码都要对某个共享数据区进行读写. 如果当进程正在读写共享数据区的过程中, 一个信号产
生, 则进程的读写过程将被中断转而执行信号处理函数, 而信号处理函数也要对该共享数据区进行读写, 这样共享数据区就可
能会发生混乱. 这种情况下, 需要在进程读写共享数据区前阻塞该信号, 在读写完成后再解除该信号的阻塞 .
信号注销
非可靠信号的注销:
对与非可靠信号来说, 由于在未决信号队列中最多只占用一个sigqueue节点, 所以只需要在删除对应sigqueue结点后, 将位图中对应位置 置为0即可.
可靠信号的注销 :
对于可靠信号来说, 某一个可靠信号在未决信号队列中可能不止一个, 所以在删除对应的sigqueue结点后, 还需要检测队列中是否还有相同的节点, 如果没有, 则将位图中对应位置置为0, 如果还有, 则不对位图进行操作.
信号的捕捉和响应
(1)当前正在执行main函数时发生中断, 异常或者系统调用切换至内核态.
(2)在中断处理完毕后要返回用户态的main函数之前, 调用do_signal()检查是否有待处理信号.
(3)内核决定返回用户态后时, 如果do_signal()检测到有待处理信号, 这种情况不是直接返回用户态恢复main函数的上下文信息继续执行,而是返回用户态执行sighandler(), sighandler()和main函数使用不同的堆栈空间, 两者之间不存在调用和被调用的关系, 它们属于两个独立的控制流程.
(4)sighandler函数返回后自动执行特殊的系统调用, 即调用sigreturn再次进入内核态.
(5)进入内核态后还是用do_signal()检测是否有待处理信号, 如果没有, 这次再返回用户态就是恢复main函数的上下文继续向下
执行. 如果有继续转入用户态执行sighandler(), 然后到(4), 这样直到没有待处理信号时再返回用户态就是恢复main函数的上
下文继续向下执行.
至此, 一个信号就结束了它的一生. 抄的我好累啊…
可重入函数
函数的重入: 多个执行流中进入同一个函数运行, 比如说就是当前调用某个函数, 当执行这个函数到一半的时候, 进程时间片用完另一个进程开始运行, 有调用这个函数执行 . 就说这个函数就被重入.
不可重入函数 : 当函数重入发生时, 会造成数据二义性, 逻辑混乱等问题的函数, 称之为不可重入函数
可重入函数: 与不可重入函数相反, 当函数重入发生时, 不会造成数据二义性, 逻辑混乱等问题的函数, 称之为可重入函数
-
自定义信号处理函数应为可重入函数
-
函数不可重入的原因有:
(1). malloc/free函数都为不可重入函数, 在多个执行流中进行操作时要格外注意
(2). 函数中静态变量/全局变量
(3). 标准I/O函数
所以说, 在设计信号处理函数时, 要考虑到函数的可重入性.
疑问测试
读了前辈们文章,有些疑问自己写函数测试补充
两个发送信号函数
kill()
头文件:#include <signal.h>
原型 : int kill(pid_t pid, int sig);
功能 : 向指定的进程或进程组发送任何信号
参数 : pid > 0, 则将信号sig发送到进程ID为 pid的进程.
pid = 0, 则将sig发送到调用进程的进程组中的每个进程 .
pid = -1, 则将sig发送到调用进程有权发送信号的每个进程, 但不包括1号进程
pid < -1, 则将sig发送到进程组中ID为-pid的每个进程 .
sig = 0 , 则不发送任何信号,但仍执行错误检查, 这可用于检查是否存在进程ID或进程组ID.
返回值: 成功时( 至少发送了一个信号) , 返回0. 出现错误时, 将返回-1, 并设置errno .
sigqueue()
头文件:#include <signal.h>
原型 : int sigqueue(pid_t pid, int sig, const union sigval value);
功能 : 向指定的进程发送任何信号(可以带附加信息), 是比较新的发送信号的函数, 主要是针对可靠信号提出的 (当然也支持前31种), 支持信号带有参数, 与函数sigaction()配合使用 (sigaction()函数下面说) .
sigqueue()比kill()传递了更多的附加信息, 但sigqueue()只能向一个进程发送信号,而不能发送信号给进程组
参数 : pid : 指定接收信号的进程ID
sig : 要发送的信号, 如果signo=0, 将会执行错误检查, 不发送任何信号, 即检查pid的有效性以及当前进程是否有权限向目标进程发送信号.
value : 联合体类型union sigval, 指定向信号传递的参数. 在调用sigqueue时, 这个联合体中的信息会拷贝到信号处理函数的siginfo_t结构中, 这样信号处理函数就可以处理这些信息了 .
联合体定义如下 :
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
返回值: 成功时, 返回0. 出现错误时, 将返回-1, 并设置errno .
可靠信号与不可靠信号测试
信号发送代码
#include <stdlib.h>
#include <signal.h>
#include <sstream>
#include <iostream>
using namespace std;
int main(int argc,char **argv)
{
if (argc < 4)
{
cout << "pid signal count" << endl;
}
else
{
stringstream stream;
stream << argv[1];
int pid = -1
;
stream >> pid;
stream.clear();
int sig = -1;
stream << argv[2];
stream >> sig;
stream.clear();
int count = -1;
stream << argv[3];
stream >> count;
cout << "pid: " << pid << ";sig: " << sig << ";count: " << count << endl;
for (int i = 0; i < count; i++)
{
kill(pid,sig);
}
}
}
信号接收代码
#include <stdlib.h>
#include <signal.h>
#include <sstream>
#include <iostream>
#include <unistd.h>
using namespace std;
void sigHander(int sig)
{
cout << "sig: "<< sig <<" sleep 5" << endl;
sleep(5);
cout << "sig: "<< sig <<" end" << endl;
}
int main(int argc,char **argv)
{
int pid = getpid();
cout << "my pid : " << pid << endl;
signal(SIGINT,sigHander);
signal(SIGRTMIN + 2,sigHander);
while (true) {
sleep(20);
cout << "main sleep 20s " << endl;
}
}
连续发送三次 不可靠信号 2 输出如下:
连续发送三次 可靠信号 36 输出如下:
分析:
如上文,不可靠信号位图保存,信号处理函数发现这个信号在就不管了。可靠信号时队列存储。队列中任务一个个都会执行。
深入分析未决与阻塞
接受信号函数
#include <stdlib.h>
#include <signal.h>
#include <sstream>
#include <iostream>
#include <unistd.h>
using namespace std;
void sigHander(int sig)
{
cout << "sig: "<< sig <<" sleep 5" << endl;
sleep(2);
cout << "sig: "<< sig <<" end" << endl;
}
int main(int argc,char **argv)
{
int pid = getpid();
cout << "my pid : " << pid << endl;
signal(SIGINT,sigHander);
signal(SIGRTMIN + 2,sigHander);
sigset_t mask;
// block SIGUSR1
sigemptyset(&mask); // 清零
sigaddset(&mask, SIGINT); // SIGINT 置位
sigaddset(&mask, SIGRTMIN + 2);
if(sigprocmask(SIG_BLOCK, &mask, NULL) < 0) {
cout << "sigprocmask error" << endl;
}
sigset_t pend;
while (true) {
sleep(30);
cout << " 30s " << endl;
sigpending(&pend);
cout << "mask" <<sigismember(&pend,2) << sigismember(&pend,36) << endl;
if(sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) {
cout << "sigprocmask error" << endl;
}
sigpending(&pend);
cout << "unmask" <<sigismember(&pend,2) << sigismember(&pend,36) << endl;
}
}
分别连续三次发送 2 和 36号输出如下:
分析:
程序收到信号后先在未决信号集中置位1,然后将信号在内核注册。可靠信号队列注册,不可靠位图注册。然后根据阻塞集决定是否执行。若阻塞则暂时不处理保存未决信号集的置位状态。待到接触阻塞时(sigprocmask阻塞住了)读取未决信号集的置位状态,先去执行注册的信号处理函数然后注销注册。然后恢复未决信号集的置位0。
参考文章(ctrl C V 出处):
(非常详细的信号讲解)原文链接:https://blog.csdn.net/qq_41071068/article/details/103659853
原文链接:https://blog.csdn.net/baobao8505/article/details/1115820