一、信号发送
- 信号动作
通过指令man -7 signal查看信号的手册,可以找到普通信号发出后对应的操作、信号编号和详细描述信息。
- 信号发送的本质
普通信号的发送本质上是将信号写入进程的PCB结构体中的位图(pending位图)。这个位图对应着1到31号的普通信号,收到信号后将对应比特位置为1,表示信号已收到,然后PCB执行相应的工作。值得注意的是,如果连续发送普通信号,进程只会处理最后一次的信号,因为每次写入都是覆盖式的。
实时信号与普通信号类似,但它们使用的是结构体而非位图,信号被组织在队列中,遵循先入先出的规则。如果连续发送实时信号,进程会依次处理队列中的所有信号。
- Core Dump
在《进程控制》一文中,我们解释了wait函数的status参数,其中第7位是core dump标志。程序在运行过程中发生崩溃(如段错误、除零错误等)时,Core dump会记录程序崩溃瞬间的内存状态,包括寄存器的值、调用栈信息、全局变量和局部变量的值等。开发人员可以使用调试工具(如GDB)加载Core dump文件,分析这些信息以找出程序崩溃的原因。
可以通过ulimit -c 10240将core文件的大小限制修改为10240字节。云服务器上通常默认core文件大小限制为0,出现错误时可能导致core文件迅速填满。我们可以根据需要修改其大小限制。生成的文件名为core.pid,其中pid是出错进程的pid。例如,假设test进程发生错误,其pid为12314,我们可以在GDB模式下输入gdb test core.12314来打印错误信息和原因。
二、信号的保存
- 前置概念
实际执行信号处理动作称为信号递达。信号从产生到递达之间的状态称为信号未决。被阻塞的信号会保持在未决状态,直到进程解除对此信号的阻塞,才会执行递达的动作。
- 阻塞信号
信号被阻塞时,会进入信号未决状态。阻塞功能通过一个31位的位图block实现,对应1到31号信号。当对应比特位为1时,表示该信号被阻塞,为0则表示不阻塞。如果信号被阻塞,则进入阻塞态;如果没有被阻塞,则进入未决状态。
- 保存信号
未决状态的作用是保存信号,通过位图pending实现。pending与block一致,对应的下标和信号编号一一对应。当对应比特位为1时,表示该信号处于未决状态,为0则表示信号已递达。实际上,block和pending都用于保存信号,但由于有两个位图,我们分开讨论。
- 信号递达
信号递达后,会执行相应的行为,包括SIG_DFL(默认处理动作)、SIG_IGN(忽略)和自定义处理sighandler。
- 总结
一个信号首先经过block,如果block为0,则来到pending,如果pending为0,则来到handler执行动作。9号和19号信号是特例,它们不能被阻塞和保存,一旦发出直接执行handler,对应的行为只能是默认动作终止和暂停。
三、信号集操作函数
信号集操作函数用于操作信号集,sigset_t是操作系统提供的数据类型,用于描述位图。以下是信号集操作函数:
#include <signal.h> int sigemptyset(sigset_t *set); // 将位图全部设置为0 int sigfillset(sigset_t *set); // 将位图全部设置为1 int sigaddset(sigset_t *set, int signo); // 将位图中的某一位设置为1 int sigdelset(sigset_t *set, int signo); // 将位图中的某一位设置为0 int sigismember(const sigset_t *set, int signo); // 判断一个信号是否在信号集中,不在返回0,在返回1,出错返回-1 </signal.h>
- 设置block位图
sigprocmask是一个重要的系统调用,用于检查和修改进程的信号掩码(阻塞信号集)。
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oset); </signal.h>
返回值:成功返回0,失败返回-1。
how参数指定对信号掩码的操作方式:
how取值 | 含义 | 示例说明 |
---|---|---|
SIG_BLOCK | 将set所指向的信号集中的信号添加到当前的信号掩码中,即阻塞set中的信号 | 若当前信号掩码已阻塞SIGINT,使用SIG_BLOCK并传入包含SIGTERM的信号集,SIGTERM也会被阻塞 |
SIG_UNBLOCK | 从当前的信号掩码中移除set所指向的信号集中的信号,即解除对set中信号的阻塞 | 若当前信号掩码阻塞了SIGINT和SIGTERM,使用SIG_UNBLOCK并传入包含SIGINT的信号集,SIGINT信号的阻塞状态将被解除 |
SIG_SETMASK | 将当前的信号掩码设置为set所指向的信号集,覆盖原来的信号掩码 | 若原信号掩码阻塞SIGINT,使用SIG_SETMASK并传入包含SIGTERM的信号集,信号掩码将只阻塞SIGTERM |
set参数指向一个sigset_t类型的信号集,包含要操作的信号。如果how的值为SIG_BLOCK或SIG_UNBLOCK,set表示要添加或移除的信号集;如果how的值为SIG_SETMASK,set表示要设置的新信号掩码。如果该参数为NULL,则不改变当前的信号掩码,仅获取当前信号掩码,此时oset不能为NULL。
oset参数指向一个sigset_t类型的信号集,用于存储调用sigprocmask之前的信号掩码。如果不需要保存旧的信号掩码,可以将该参数设置为NULL。
- 设置pending位图
sigpending用于获取进程当前未决信号集。
#include <signal.h> int sigpending(sigset_t *set); </signal.h>
返回值:成功返回0,失败返回-1。
set参数用于存储当前进程中处于未决状态(即已发送但由于被阻塞而尚未被处理)的信号集。
- 设置handler行为
信号处理行为有三种情况:默认、忽略和自定义。自定义处理可以通过signal函数实现,此前已有介绍,不再赘述。
四、验证信号保存行为
#include <iostream> #include <signal.h> #include <unistd.h> using namespace std; // 打印出位图 void PrintPending(const sigset_t &pset) { for(int i = 31; i >= 1; i--) { cout << (sigismember(&pset, i) ? 1 : 0); } cout << endl; } // 信号处理函数 void handler(int sig) { cout << "Caught signal " << sig << endl; } int main() { sigset_t newmask, oldmask, pendmask; // 初始化信号集 sigemptyset(&newmask); sigaddset(&newmask, SIGINT); // 阻塞SIGINT信号 sigprocmask(SIG_BLOCK, &newmask, &oldmask); // 打印当前未决信号集 while(1) { sigpending(&pendmask); PrintPending(pendmask); sleep(1); } // 解除SIGINT信号的阻塞 sigprocmask(SIG_SETMASK, &oldmask, NULL); return 0; } </unistd.h></signal.h></iostream>
在3秒后按下ctrl+c,捕捉信号函数不会工作,说明信号被阻塞了。15秒后自动解除阻塞,立即打印出handler函数定义要打印的信息,再按ctrl+c就正常执行handler行为。