8. 信号基础
- 1. 基本概念
- 1.1 信号的目的是用来通信的
- 1.2 信号由谁处理、怎么处理
- 1.3 信号是异步的
- 2. 信号的分类
- 2.1 可靠信号和不可靠信号
- 2.2 实时信号和非实时信号
- 3. 进程对信号的处理
- 3.1 signal()
- 3.2 sigaction()
- 3.2.1 struct sigaction
- 3.2.2 实例
- 4. 向进程发送信号
- 4.1 kill()
- 4.2 raise()
- 5. alarm() 和 pause()
- 5.1 alarm()
- 5.2 pause()
- 6. 信号集
- 6.1 初始化信号集
- 6.2 向信号集中添加或删除信号
- 6.3 测试信号是否在信号集中
- 7. 获取信号的描述信息
- 7.1 strsignal()
- 7.2 psignal()
- 8. 信号掩码,阻塞信号传递
- 9. 阻塞等待信号sigsuspend()
- 10. 实时信号
- 10.1 sigpending()
- 10.2 发送实时信号
- 11. 异常退出 abort()
1. 基本概念
信号是事件发生时对进程的通知机制,也可以称为软件中断。信号与软件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层上对中断机制的一种模拟。可以使用kill -l
查看信号
1.1 信号的目的是用来通信的
信号可以由谁发出?
- 硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。
- 用于在终端下输入能够产生信号的特殊字符。比如在终端下按快捷键 Ctrl+C 表示 2 号中断信号
- 进程调用 kill() 系统调用可以将任意信号发送给另一个进程或进程组。但是接收信号的进程和发送信号的进程的所有者必须相同,或者发送信号的进程是 root
- 用户可以通过 kill 命令将信号发送给其他进程。
- 发生了软件事件,即当检测到某软件条件已经发生。
- 进程也可向自身发送信号,然而发送给进程的诸多信号中,大多数都是来自于内核
信号的目的都是用于通信,当发生某种情况下,通过信号将情况告诉给相应的进程,从而达到同步、通信的目的。
1.2 信号由谁处理、怎么处理
忽略信号: 当信号达到进程后,该进程直接忽略,所以信号不会对进程产生任何影响。但是有两种信号是不能被忽略的,SIGKILL 和 SIGSTOP,因为它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。
捕捉信号: 当信号到达进程后,执行相应的信号处理函数。
执行系统默认操作: 进程不对该信号做处理,而是交由系统进行处理,每种信号都有对应的默认处理方式
1.3 信号是异步的
产生信号的事件对进程而言是随机的,进程无法预测该事件产生的准确时间,只有当信号到达时才回去执行相应的处理
2. 信号的分类
从可靠性方面分为可靠信号和不可靠信号;从实时性方面分为实时信号和非实时信号
2.1 可靠信号和不可靠信号
信号编号 1 ~ 34 的都是不可靠信号,34 ~ 64 对应的都是可靠信号。可靠信号支持排队,不会丢失
2.2 实时信号和非实时信号
非实时信号都不支持排队,都是不可靠信号;实时信号支持排队,是可靠信号。实时信号能够保证发送的多个信号都能被接受。一般把非实时信号(不可靠信号)称为标准信号。
3. 进程对信号的处理
3.1 signal()
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
// signum:需要设置的信号,可以用信号名(宏)或信号的编号
// 函数指针,指向信号对应的处理函数。也可以设置 SIG_IGN 表示忽略或者 SIG_DFL 表示系统默认操作。函数的int类型只的是触发函数的信号。
#include <iostream>
#include <stdio.h>
#include <signal.h>
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 信号提供处理方法,所以常常用来终止进程
- 进程创建:当一个进程创建子进程时,子进程将继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有意义的。
3.2 sigaction()
这个函数虽然更复杂,但是更具有灵活性和移植性。允许单独获取信号的处理函数而不是设置,并且还可以设置各个属性对调用信号处理函数时的行为施以更加精确的控制
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// act:该结构体描述了信号的处理方式,如果不为空,则表示需要为信号设置新的处理方式,如果设置为空,表示无需改变处理方式
// oldact:如果不为空,将信号之前的处理方式等信息通过oldact返回出来,如果不想获取,就设置为空
3.2.1 struct sigaction
- sa_handler: 指定信号处理函数
- sa_sigaction: 也是指定信号处理函数,这是一个替代的信号处理函数,提供了更多的参数,可以通过该信号获取更多信息,这些信号通过 siginfo_t 结构体获取。sa_handler 和 sa_sigaction 是互斥的,不能同时设置,对于标准信号,使用上面一个就可以了,可以通过 SA_SIGINFO 标志进行选择
- sa_mask: 定义了一组信号,当进程在执行 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当执行完处理函数之后,恢复信号掩码,将这组信号从信号掩码字段中删除。当进程在执行信号处理函数期间,可能又收到了同样的信号或其他信号,从而打断当前处理函数的执行,而函数掩码就可以防止信号处理函数不被打断。如果进程收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码中, 这样就保证了在处理一个给定信号时,如果此信号再次发生,那么将会被阻塞。如果用户还需要阻塞其他信号,则可以通过设置参数 sa_mask 来完成
- sa_restorer: 已经过时,不再使用
- sa_flags: 指定了一组标志,用于控制信号的处理过程,可设置为以下标志
sa_flags
- SA_NOCLDSTOP: 如果 signum 是 SIGCHLD,则子进程停止或恢复时不会收到 SIGCHLD 信号
- SA_NOCLDWAIT: 如果 signum 是 SIGCHLD,则在子进程终止时不要将其转变为僵尸进程
- SA_NODEFER: 不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而阻塞该信号,防止发生竞态条件,如果设置了该字段,表示不阻塞
- SA_RESETHAND: 执行完信号处理函数之后,将信号的处理方式设置为系统默认
- SA_RESTART: 被信号中断的系统调用,在信号处理完成之后将会自动重新发起
- SA_SIGINFO: 表示使用 sa_sigaction 作为信号处理函数,而不是 sa_handler
3.2.2 实例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <iostream>
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;
}
4. 向进程发送信号
4.1 kill()
将信号发送给指定的进程或进程组中的每一个进程
#include <sys/types.h>
#include <signal.h>
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,表示不发送任何信号,但是仍检查,通常用于检查某个进程是否存在
4.2 raise()
可以向进程自身发送信号,这是个库函数
#include <signal.h>
int raise(int sig);
相当于kill(getpid(), sig);
,getpid() 函数可以用于获取当前进程的pid
5. alarm() 和 pause()
5.1 alarm()
设置一个定时器,当时间到时,向进程发送 SIGALRM 信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// seconds:设置定时时间,以秒为单位。如果为0,表示取消之前设置的 alarm 闹钟
// 返回值:如果在调用该函数之前,已经设置过闹钟,而且没有到时,就返回之前闹钟的剩余时间,而之前设置的时钟会被替代
每个进程只能设置一个闹钟,而且不能循环触发,如果想要循环触发,可以在信号处理函数中再次调用此函数
5.2 pause()
可以暂停进程运行,进入休眠转台,直到进程捕获一个信号为止,只有执行了信号处理函数并返回后,pause 才返回 -1,并将 errno 设置为 EINTR
#include <unistd.h>
int pause();
6. 信号集
通常我们需要有一个能表示多个信号的数据类型,也就是信号集。信号集就是 sigset_t 结构体类型
使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中
6.1 初始化信号集
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
sigemptyset() 使信号集不包含任何信号,而 sigfillset() 包含所有信号,包括实时信号
sigset_t set;
sigemptyset(&set);
sigfillset(&set);
6.2 向信号集中添加或删除信号
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
sigaddset(&set, 2);
sigdelset(&set, 2);
6.3 测试信号是否在信号集中
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
// 在就返回1,不在返回0
7. 获取信号的描述信息
每个信号都有一串与之对应的字符串描述信息,用于对该信号进行相应的描述。这些字符串位于 sys_siglist 数组中,sys_siglist 数组是一个 char* 类型的数组,每个元素存放的是一个字符串指针,指向一个信号描述信息。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int main()
{
cout << sys_siglist[SIGINT]<<endl;
cout << sys_siglist[SIGQUIT]<<endl;
}
7.1 strsignal()
#include <string.h>
char *strsignal(int sig);
直接传入对应的信号,就可以获取出信号字符串信息
7.2 psignal()
可以在标准错误上输出描述信息
#include <signal.h>
void psignal(int sig, const char *s);
后面的字符串信息是可以自己添加打印的内容
int main()
{
psignal(SIGINT,"信号描述信息:");
}
8. 信号掩码,阻塞信号传递
内核为每个进程都维护了一个信号掩码,其实就是一个信号集,即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号会被阻塞,无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除。向信号掩码中添加一个信号,通常有以下方式:
- 当应用程序调用 signal() 或 sigaction() 为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,如果此信号再次发生就被阻塞。当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
- 使用 sigaction() 为信号设置处理方式后,可以额外指定一组信号。当调用信号处理函数时,将该组信号自动添加到信号掩码中。当处理函数返回后,再将这组信号从信号掩码中移除
- 还可以使用 sigprocmask() 系统调用,随时可以显示的向信号掩码中添加或移除信号。
#include <signal.h>
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号信号移除
}
9. 阻塞等待信号sigsuspend()
将恢复信号掩码和 pause() 挂起这两个动作封装为一个原子操作。
#include <signal.h>
int sigsuspend(const sigset_t *mask);
该函数始终返回 -1,将 errno 设置为 EINTR,表示被信号中断;如果调用失败,设置为 EFAULT。函数会将 mask 所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数 mask 所指向的信号集,然后挂起进程,直到捕捉到信号被唤醒,如果捕捉的信号是 mask 中的,就不会被唤醒,继续等待。一旦从信号处理函数返回,该函数会将进程的信号掩码恢复成调用前的值。
10. 实时信号
如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,就会被阻塞,将该信号添加到等待信号集中,为了确定进程中处于等待状态的是哪些信号,可以使用sigpending()函数获取
10.1 sigpending()
#include <signal.h>
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;
}
}
10.2 发送实时信号
等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数,如果同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传第一次,这是标准信号的缺点。
实时信号较之于标准信号,其优势如下:
- 实时信号的信号范围有所扩大,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可用于应用程序自定义使用: SIGUSR1 和 SIGUSR2。
- 内核对于实时信号所采取的是队列化管理。如果将某一实时信号多次发送给另一个进程,那么将会多次传递此信号。相反, 对于某一标准信号正在等待某一进程,而此时即使再次向该进程发送此信号,信号也只会传递一次。
- 当发送一个实时信号时,可为信号指定伴随数据(一整形数据或者指针值),供接收信号的进程在它的信号处理函数中获取。
- 不同实时信号的传递顺序得到保障。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。
Linux 内核定义了 31 个不同的实时信号,信号编号范围为 34~64,使用 SIGRTMIN 表示编号最小的实时信号,使用 SIGRTMAX 表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一个整数。
应用程序当中使用实时信号,需要有以下的两点要求: - 发送进程使用 sigqueue()系统调用向另一个进程发送实时信号以及伴随数据。
- 接收实时信号的进程要为该信号建立一个信号处理函数,使用sigaction函数为信号建立处理函数,并加入 SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用sa_sigaction 指针指向的处理函数,而不是 sa_handler,当然允许应用程序使用 sa_handler,但这样就不能获取到实时信号的伴随数据了。
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
// pid:指定接收信号的进程对应的pid
// value:指定信号的伴随数据
11. 异常退出 abort()
只用 abort() 终止进程运行,会生成核心转储文件,可用于判断程序调用该函数的程序状态
#include <stdlib.h>
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 信号,该函数都不会受到影响,总会终止进程。