信号是事件发生时对进程的通知机制,也可以称为软件中断。信号与软件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层上对中断机制的一种模拟。可以使用kill -l
查看信号
信号可以由谁发出?
忽略信号: 当信号达到进程后,该进程直接忽略,所以信号不会对进程产生任何影响。但是有两种信号是不能被忽略的,SIGKILL 和 SIGSTOP,因为它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。
捕捉信号: 当信号到达进程后,执行相应的信号处理函数。
执行系统默认操作: 进程不对该信号做处理,而是交由系统进行处理,每种信号都有对应的默认处理方式
产生信号的事件对进程而言是随机的,进程无法预测该事件产生的准确时间,只有当信号到达时才回去执行相应的处理
从可靠性方面分为可靠信号和不可靠信号;从实时性方面分为实时信号和非实时信号
信号编号 1 ~ 34 的都是不可靠信号,34 ~ 64 对应的都是可靠信号。可靠信号支持排队,不会丢失
非实时信号都不支持排队,都是不可靠信号;实时信号支持排队,是可靠信号。实时信号能够保证发送的多个信号都能被接受。一般把非实时信号(不可靠信号)称为标准信号。
#include
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
// signum:需要设置的信号,可以用信号名(宏)或信号的编号
// 函数指针,指向信号对应的处理函数。也可以设置 SIG_IGN 表示忽略或者 SIG_DFL 表示系统默认操作。函数的int类型只的是触发函数的信号。
#include
#include
#include
using namespace std;
static void handler(int sig)
{
cout << "接收到的信号是: " << endl;
}
int main()
{
sig_t ret=nullptr;
ret=signal(SIGINT,(sig_t)handler);
while(1){}
return 0;
}
上述测试用例,在终端运行后,不停的按 Ctrl+C,就可以看到打印信息。如果要终止程序,可以在另打开一个终端使用ps -aux | grep test | grep -v grep
查看 test 进程的 pid,然后使用kill -9 pid号
就可以杀死进程
两种不同状态下信号的处理方式
- 程序启动:当一个应用程序刚启动的时候,或者程序中没有调用 signal() 函数,通常情况下,进程对所有信号的处理方式都设置为系统默认操作。系统中并不会为 SIGINT 信号提供处理方法,所以常常用来终止进程
- 进程创建:当一个进程创建子进程时,子进程将继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有意义的。
这个函数虽然更复杂,但是更具有灵活性和移植性。允许单独获取信号的处理函数而不是设置,并且还可以设置各个属性对调用信号处理函数时的行为施以更加精确的控制
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// act:该结构体描述了信号的处理方式,如果不为空,则表示需要为信号设置新的处理方式,如果设置为空,表示无需改变处理方式
// oldact:如果不为空,将信号之前的处理方式等信息通过oldact返回出来,如果不想获取,就设置为空
sa_flags
- SA_NOCLDSTOP: 如果 signum 是 SIGCHLD,则子进程停止或恢复时不会收到 SIGCHLD 信号
- SA_NOCLDWAIT: 如果 signum 是 SIGCHLD,则在子进程终止时不要将其转变为僵尸进程
- SA_NODEFER: 不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而阻塞该信号,防止发生竞态条件,如果设置了该字段,表示不阻塞
- SA_RESETHAND: 执行完信号处理函数之后,将信号的处理方式设置为系统默认
- SA_RESTART: 被信号中断的系统调用,在信号处理完成之后将会自动重新发起
- SA_SIGINFO: 表示使用 sa_sigaction 作为信号处理函数,而不是 sa_handler
#include
#include
#include
#include
using namespace std;
static void sig_handler(int sig)
{
cout << "接收到信号: " << sig<<endl;
}
int main()
{
struct sigaction sig = {0};
int ret;
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
ret = sigaction(SIGINT, &sig, NULL);
while(1){}
return 0;
}
将信号发送给指定的进程或进程组中的每一个进程
#include
#include
int kill(pid_t pid, int sig);
/* pid:
* pid>0:用于指定进程的pid
* pid=0:将sig发送到当前进程的进程组中的每个进程
* pid=-1:将sig发送到当前进程有权发送信号的每个进程,但1号进程除外
* pid<-1:将sig发送到ID为-pid的进程组中的每个进程
* /
sig:如果为0,表示不发送任何信号,但是仍检查,通常用于检查某个进程是否存在
可以向进程自身发送信号,这是个库函数
#include
int raise(int sig);
相当于kill(getpid(), sig);
,getpid() 函数可以用于获取当前进程的pid
设置一个定时器,当时间到时,向进程发送 SIGALRM 信号
#include
unsigned int alarm(unsigned int seconds);
// seconds:设置定时时间,以秒为单位。如果为0,表示取消之前设置的 alarm 闹钟
// 返回值:如果在调用该函数之前,已经设置过闹钟,而且没有到时,就返回之前闹钟的剩余时间,而之前设置的时钟会被替代
每个进程只能设置一个闹钟,而且不能循环触发,如果想要循环触发,可以在信号处理函数中再次调用此函数
可以暂停进程运行,进入休眠转台,直到进程捕获一个信号为止,只有执行了信号处理函数并返回后,pause 才返回 -1,并将 errno 设置为 EINTR
#include
int pause();
通常我们需要有一个能表示多个信号的数据类型,也就是信号集。信号集就是 sigset_t 结构体类型
使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
sigemptyset() 使信号集不包含任何信号,而 sigfillset() 包含所有信号,包括实时信号
sigset_t set;
sigemptyset(&set);
sigfillset(&set);
#include
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
sigaddset(&set, 2);
sigdelset(&set, 2);
#include
int sigismember(const sigset_t *set, int signum);
// 在就返回1,不在返回0
每个信号都有一串与之对应的字符串描述信息,用于对该信号进行相应的描述。这些字符串位于 sys_siglist 数组中,sys_siglist 数组是一个 char* 类型的数组,每个元素存放的是一个字符串指针,指向一个信号描述信息。
#include
#include
#include
#include
using namespace std;
int main()
{
cout << sys_siglist[SIGINT]<<endl;
cout << sys_siglist[SIGQUIT]<<endl;
}
#include
char *strsignal(int sig);
直接传入对应的信号,就可以获取出信号字符串信息
可以在标准错误上输出描述信息
#include
void psignal(int sig, const char *s);
后面的字符串信息是可以自己添加打印的内容
int main()
{
psignal(SIGINT,"信号描述信息:");
}
内核为每个进程都维护了一个信号掩码,其实就是一个信号集,即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号会被阻塞,无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除。向信号掩码中添加一个信号,通常有以下方式:
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/* how:指定了调用函数时的一些行为
* set:将指定的信号集中的所有信号添加到信号掩码中或者从掩码中移除,如果为空,表示无需对档期那信号掩码做改动
* oldset:如果不为空,向掩码中添加新的信号后,获取到进程当前的信号掩码,存放在olset中,如果为空,表示不获取当前的信号掩码
* /
how:
SIG_BLOCK:将set指向的添加到掩码中
SIG_UNBLOCK:将set指向的从掩码中移除
SIG_SETMASK:将掩码设置为set指向的
int main()
{
sigset_t sig_set;
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
sigprocmask(SIG_BLOCK,&sig_set,NULL); // 将2号信号添加进掩码
sigprocmask(SIG_UNBLOCK,&sig_set,NULL); // 将2号信号移除
}
将恢复信号掩码和 pause() 挂起这两个动作封装为一个原子操作。
#include
int sigsuspend(const sigset_t *mask);
该函数始终返回 -1,将 errno 设置为 EINTR,表示被信号中断;如果调用失败,设置为 EFAULT。函数会将 mask 所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数 mask 所指向的信号集,然后挂起进程,直到捕捉到信号被唤醒,如果捕捉的信号是 mask 中的,就不会被唤醒,继续等待。一旦从信号处理函数返回,该函数会将进程的信号掩码恢复成调用前的值。
如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,就会被阻塞,将该信号添加到等待信号集中,为了确定进程中处于等待状态的是哪些信号,可以使用sigpending()函数获取
#include
int sigpending(sigset_t *set);
// 处于等待状态的信号会存放在set所指向的信号集中
int main()
{
sigset_t sig_set;
sigemptyset(&sig_set);
sigpending(&sig_set);
if(sigismember(&sig_set,SIGINT))
{
cout << "SIG处于等待"<<endl;
}
}
等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数,如果同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传第一次,这是标准信号的缺点。
实时信号较之于标准信号,其优势如下:
#include
int sigqueue(pid_t pid, int sig, const union sigval value);
// pid:指定接收信号的进程对应的pid
// value:指定信号的伴随数据
只用 abort() 终止进程运行,会生成核心转储文件,可用于判断程序调用该函数的程序状态
#include
void abort();
该函数通常 SIGABRT 信号来终止调用该函数的进程,该信号的系统默认操作是终止进程运行,并生成核心转储文件。当调用该函数之后,内核会向进程发送该信号
static void sig_handler(int sig)
{
cout << "接收到的信号是: "<<sig<<endl;
}
int main()
{
struct sigaction sig={0};
sig.sa_handler=sig_handler;
sig.sa_flags=0;
sleep(2);
abort();
while(1)
{
sleep(1);
}
return 0;
}
从现象看,即使捕捉了信号,但程序依旧会终止,所以,如果阻塞或忽略 SIGABRT 信号,该函数都不会受到影响,总会终止进程。