一 信号的概念
在我们的日常生活中,红绿灯其实也叫信号灯,因此,我们将用红绿灯给大家讲解一下信号的概念。
当你开着车经过红绿灯路口时,你也在收到红绿灯给你的通知,假设是红灯,那么你就需要停下来等待,如果是黄灯,那就可以看看地区,考虑一下冲不冲(建议等待),绿灯就可以直接通过。
也就是说,你能”识别红绿灯的颜色作用“。
同时,当你在等待绿灯的时候或者红灯刚好变绿的时候,你刚好在喝最后一口水,那你就可以喝完再走(容易挨骂)。
也就是说,你可以“在合适的是时间去通过红绿灯。
在你收到红灯变绿的这个变化的时候,在这段时间,你并没有看到绿灯,但是你知道绿灯马上就要来了。本质上是你“知道马上要有绿灯”。
当你的时间合适,并且刚好有绿灯,就要开始处理。而处理绿灯的方式一般有三种:
- 1. 执行默认动作(冲!!!)
- 2. 执行自定义动作(有点渴,喝口水)
- 3. 忽略绿灯(你是副驾驶
绿灯到来的整个过程,对你来说是异步的,你不需要关注红灯是如何变绿的,你可以做一些你想做的事情。
ps:诸如此类的还有快递到达通知,火灾警报等待。
在了解了此类场景之后,我们就会发现信号拥有以下特点。
- 1.在我们人类的大脑中能够识别这个信号。
- 2.如果特定的信号没有产生,但是我们依旧知道我们应该如何处理这个信号。
- 3.我们在接受这个信号的时候,可能不会立即处理这个信号。
- 4.信号本身在我们无法立即被处理的时候,也一定要先被临时地记住。
二 Linux中信号的概念
2.1 信号在Linux中的概念
通过上面的解读,我们大概已经理解了,信号其实就是一种通知行为,通知进程,某些事件已经发生了,可以根据情况进行处理了。
结论:
1.进程要处理信号,必须具备信号“识别”的能力。即 看到,知道,能够处理。
2.信号是随机产生的,进程可能正在忙自己的业务,所以信号的处理可能并不是被立即处理的。
3.信号会临时地记录一下对应的信号,一般在合适的时候会进行处理。
4.一般而言,对于进程而言信号的产生是异步的。
信号的定义:信号是 Linux 操作系统中用于进程间通信、处理异常等情况的一种机制。它是由操作系统向一个进程或者线程发送的一种异步通知,用于通知该进程或线程某种事件已经发生,需要做出相应的处理。
2.2 Linux中的信号
在linux命令行中,可以通过kill -l命令查看所有信号。
1~31是普通信号,34~64是带有RT的实时信号。信号的种类及其繁多,目前的阶段我们只需要记住几个常用的 普通信号 就可以了。
信号的本质其实是一种宏,在这里我们可以通过源代码提前了解以下:
信号编号 | 信号名称 | 描述 |
---|---|---|
1 | SIGHUP | 控制终端挂起或者断开连接 |
2 | SIGINT | 中断信号,通常由 Ctrl+C 发送 |
3 | SIGQUIT | 退出信号,通常由 Ctrl+\ 发送 |
4 | SIGILL | 非法指令信号 |
5 | SIGTRAP | 跟踪异常信号 |
6 | SIGABRT | 中止信号 |
7 | SIGBUS | 总线错误信号 |
8 | SIGFPE | 浮点错误信号 |
9 | SIGKILL | 强制退出信号 |
10 | SIGUSR1 | 用户定义信号1 |
11 | SIGSEGV | 段错误信号 |
12 | SIGUSR2 | 用户定义信号2 |
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 | 非法系统调用信号 |
32 | SIGRTMIN | 实时信号最小编号 |
... | ... | ... |
64 | SIGRTMAX | 实时信号最大编号 |
需要注意的是,不同的操作系统可能对信号的编号有所不同,因此在跨平台开发时应当注意信号编号的兼容性。
2.3 信号的阻塞,递达,未决
- 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
- 信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
- 信号的”递达“指处理信号的过程。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号(一般称为忽略),所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
2.4 信号集的概念
- 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
- 在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为“未决信号集” 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。(除了这两个信号集之外,还有一个对应的处理函数的集合)
block为阻塞信号集,pending为未决信号集,handler为处理函数地址集合。
2.5 信号集的操作
2.5.1 信号集增减操作
#include <signal.h>
int sigemptyset(sigset_t *set);//将set集合置空
int sigaddset(sigset_t *set,int signo);//将signo信号加入到set集合
int sigdelset(sigset_t *set,int signo);//从set集合中移除signo信号
int sigfillset(sigset_t *set); //将所有信号加入set集合
//set为操作的信号集
//signo表示操作的信号
//返回值:若成功,返回0,若出错,返回-1
2.5.2 检查信号是否已经加入信号集
int sigismember(const sigset_t *set, int signum);
//测试参数signum 代表的信号是否已加入至参数set信号集里
//返回值:若成功,返回0,若出错,返回-1
2.5.3 设置信号集的阻塞
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//可以根据参数指定的方法修改进程的信号屏蔽字。
//新的信号屏蔽字由参数set(非空)指定,而原先的信号屏蔽字将保存在oset(非空)中。
//如果set为空,则how没有意义,但此时调用该函数,如果oset不为空,则把当前信号屏蔽字保存到oset中。
/* 底层系统调用的原型 */
int rt_sigprocmask(int how, const kernel_sigset_t *set,kernel_sigset_t *oldset, size_t sigsetsize);
/* Prototype for the legacy system call (deprecated) */
参数:
how:不同取值及操作如下所示:
注:调用这个函数才能改变进程的屏蔽字,之前的函数都是为改变一个变量的值而已,并不会真正影响进程的屏蔽字。
- SIG_BLOCK : 附加set到阻塞表,原来的保存在到oldset
- SIG_UNBLOCK:从阻塞表中删除set中的信号,原来的保存到oldset
- SIG_SETMASK:清空阻塞表并设置为set,原来的保存到oldset
返回值:
若成功,返回0
若出错(how取值无效返回-1),返回-1,并设置errno为EINVAL。
2.5.4 读取未决信号集合
#include <signal.h>
int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。
//调用成功则返回0,出错则返回-1。
2.5.5 示例
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void printsigset(sigset_t *set){
for(int i=31;i>=0;i--){
if(sigismember(set,i)){
putchar('1');
}
else{
putchar('0');
}
}
printf("\n");
}//打印未决信号集合
int main(){
sigset_t s,p;
sigemptyset(&s);//清空s信号集
sigaddset(&s,SIGINT);//将2号信号加入s信号集
sigprocmask(SIG_BLOCK,&s,NULL);//将阻塞信号集内容变为s信号集
while(1){
sigpending(&p);//获取未决信号集
printsigset(&p);//一直打印未决信号集合
sleep(1);
}
return 0;
}
三 Linux中信号的产生,发送,接收
3.1 信号的产生
首先我们要理解,在我们的日常生活中,接收信号的主体一般是人,但是在操作系统中,接收信号的主体就变得多了起来。
首先我们要知道,信号是一种通知,当进程向计算机申请资源时,进程发出这个通知,操作系统给予进程资源,也需要发出给予的通知,一般而言,信号的使用主体大多是:人,操作系统,进程。
人,也就是程序员,通过键盘发出信号,以此来实现对操作系统或者是进程的操控。
操作系统则通过信号,协调自己的内部,实现统筹管理。
进程则通过信号,汇报内部错误或者是进行通信,以此来完成任务。
因此,信号的产生是有多样性的,由通过键盘、系统调用、软件条件、硬件异常产生等等方法产生信号。
3.2 信号的发送
这里我们介绍几种程序员常用的发送信号的方法:
3.2.1 kill 命令
kill 命令是 Linux 中最常用的发送信号的命令,语法如下:
kill [-signal] 进程PID
其中,-signal 可选参数表示要发送的信号类型,如果省略该参数,则默认发送 SIGTERM 信号。PID 表示接收信号的进程 ID。
例如,要向进程 ID 123 发送 SIGINT 信号,可以执行以下命令:
kill -SIGINT 123
3.2.2 kill 函数
除了使用 kill 命令,程序中也可以通过 kill 函数来发送信号。kill 函数的原型如下:
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig)
//pid 表示接收信号的进程 ID.
//sig 表示要发送的信号类型.
//如果函数调用成功,则返回 0,否则返回 -1 并设置 errno。
例如,要向进程 ID 123 发送 SIGINT 信号,可以执行以下代码:
#include <signal.h>
#include <unistd.h>
int main() {
pid_t pid = 123;
int sig = SIGINT;
if (kill(pid, sig) == -1) {
perror("kill");
return 1;
}
return 0;
}
3.2.3 raise 函数
raise 函数是一个简单的发送信号的函数,可以用来向当前进程发送信号。raise 函数的原型如下:
int raise(int sig);
//其中,sig 表示要发送的信号类型.
//如果函数调用成功,则返回 0,否则返回 -1 并设置 errno。
例如,要向当前进程发送 SIGTERM 信号,可以执行以下代码:
#include <signal.h>
int main() {
int sig = SIGTERM;
if (raise(sig) == -1) {
perror("raise");
return 1;
}
return 0;
}
3.2.4 pthread_kill 函数
如果在多线程程序中需要向另一个线程发送信号,可以使用 pthread_kill 函数。pthread_kill 函数的原型如下:
int pthread_kill(pthread_t thread, int sig);
//其中,thread 表示接收信号的线程 ID
//sig 表示要发送的信号类型
//如果函数调用成功,则返回 0,否则返回错误码。
例如,要向线程 ID 456 发送 SIGUSR1 信号,可以执行以下代码:
#include <pthread.h>
#include <signal.h>
void* thread_func(void* arg) {
// 线程函数
return NULL;
}
int main() {
pthread_t tid = 456;
int sig = SIGUSR1;
if (pthread_kill(tid, sig) != 0) {
perror("pthread_kill");
return 1;
}
return 0;
}
3.3 信号的接收
一般而言,我们的操作系统对常见信号都有默认的接收处理方式,当时,我们也可以通过一些函数,对一些信号进行自定义接收。
3.3.1 signal函数
signal 函数可以用来创建信号处理函数。signal 函数的原型如下:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//其中,signum 表示要注册的信号类型
//handler 是一个函数指针,指向信号处理函数
//signal 函数返回一个函数指针,指向之前注册的信号处理函数。如果注册信号处理函数失败,则返回 SIG_ERR。
示例,要自定义接收注册 SIGINT 信号的处理函数,可以执行以下代码:
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
void handler(int sig){
std::cout<<"触发事件信号值"<<sig<<std::endl;
}
int main(){
std::cout<<"当前进程pid为:"<<getpid()<<std::endl;
signal(SIGINT,handler);
while(true){
std::cout<<"进程仍在执行,pid:"<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
果然我们发送2号信号不再退出程序,而是打印了一段话,我们用其它信号中断程序,成功中断
3.3.2 sigaction 函数
在Linux中,sigaction函数是用于设置和检索信号处理器的函数。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum 表示要注册的信号类型
act 是一个指向 struct sigaction 结构体的指针,表示新的信号处理函数和信号处理选项
oldact 是一个指向 struct sigaction 结构体的指针,用于获取之前注册的信号处理函数和信号处理选项。
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字段指定信号处理函数的地址。如果设置为SIG_IGN,则表示忽略该信号。如果设置为SIG_DFL,则表示使用默认处理器,也可以自己设置需处理的函数逻辑。
sa_sigaction字段指定一个信号处理器函数,这个函数包含三个参数:一个整数表示信号编号,一个指向siginfo_t结构体的指针,和一个指向void类型的指针。
sa_mask字段指定了在执行信号处理函数期间要阻塞哪些信号。
sa_flags字段是一个标志位,可以包括以下值:
- SA_NOCLDSTOP:如果设置了该标志,则当子进程停止或恢复时不会生成SIGCHLD信号。
- SA_RESTART:如果设置了该标志,则系统调用在接收到信号后将被自动重启。
- SA_SIGINFO:如果设置了该标志,则使用sa_sigaction字段中指定的信号处理器。
sa_restorer字段是一个指向恢复函数的指针,用于恢复某些机器状态。
调用sigaction函数后,如果成功,则返回0,否则返回-1,并设置errno错误号。可以使用以下代码来检查errno:
现在我们只需要理解sa_handler的用法就可以了。
例如,要注册 SIGINT 信号的处理函数,可以执行以下代码:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdio>
void sigint_handler(int sig){
std::cout<<"引起异常的信号为:"<<sig<<std::endl;
}
int main(){
struct sigaction newact,oldact;
newact.sa_handler = sigint_handler;
if(sigaction(SIGINT,&newact,&oldact)==-1){
perror("sigaction");
return 1;
}
while(true){
sleep(1);
std::cout<<"当前进程pid:"<<getpid()<<std::endl;
}
return 0;
}
在使用sigaction函数之前,应该先定义一个信号处理函数并将其注册。可以使用signal函数来注册一个信号处理函数,但是signal函数在某些情况下可能会出现问题,建议使用sigaction函数来注册信号处理函数。
3.3.3 sigwait 函数
sigwait 函数可以用于阻塞等待一个或多个信号,并在信号到达时唤醒。sigwait 函数的原型如下:
#include <signal.h>
int sigwait(const sigset_t *set, int *sig);
其中,set 表示要等待的信号集
sig 是一个指针,用于返回收到的信号编号。
例如,要阻塞等待 SIGINT 信号的到来,可以执行以下代码:
#include <cstdio>
#include <csignal>
int main() {
sigset_t set;
int sig;
sigemptyset(&set);
sigaddset(&set, SIGINT);
if (sigwait(&set, &sig) == -1) {
perror("sigwait");
return 1;
}
printf("Received signal: %d\n", sig);
return 0;
}
3.3.4 pause 函数
pause 函数可以用于阻塞进程,直到接收到一个信号为止。pause 函数的原型如下:
#include <unistd.h>
int pause(void);
例如,要阻塞进程直到接收到 SIGINT 信号为止,可以执行以下代码
#include <stdio.h>
#include <signal.h>
void sigint_handler(int sig) {
printf("Received SIGINT signal\n");
}
int main() {
if (signal(SIGINT, sigint_handler) == SIG_ERR) {
perror("signal");
return 1;
}
printf("Waiting for SIGINT signal...\n");
pause();
return 0;
}
以上是几种常见的接收信号的方法,根据不同的需求可以选择合适的方法来注册信号处理函数和接收信号。
3.4 信号的处理
假设一个进程接收完信号,那么这个信号肯定是需要处理的。在 Linux 中,信号的处理方式一共有三种:
- 忽略信号
- 执行默认处理函数
- 执行自定义处理函数。
这三种处理方式可以通过 signal
函数和 sigaction
函数来设置。
3.4.1 忽略信号
如果将一个信号的处理方式设置为忽略,则当进程接收到该信号时,内核将不做任何处理,直接丢弃该信号。通常情况下,只有少数信号可以被忽略,如 SIGCHLD
信号。忽略信号的方式可以通过 signal
函数来设置。
SIG_DFL、SIG_IGN 分别表示无返回值的函数指针,指针值分别是 0 和 1 。这两个指针值逻辑上讲是实际程序中不可能出现的函数地址值。
SIG_DFL:默认信号处理程序
SIG_IGN:忽略信号的处理程序
将signal函数和SIG_IGN函数结合,我们就可以忽略某个信号,如:
#include <stdio.h>
#include <signal.h>
int main() {
//也是一个宏 #define SIG_ERR / *实现定义* / 当通过信号返回时,表示发生了错误。
if (signal(SIGINT, SIG_IGN) == SIG_ERR) {
perror("signal");
return 1;
}
printf("Waiting for SIGINT signal...\n");
return 0;
}
在上面的代码中,将 SIGINT
信号的处理方式设置为忽略,当进程接收到 SIGINT
信号时,该信号将被直接丢弃。
3.4.2 执行默认处理函数
每个信号都有一个默认的处理方式,当进程接收到该信号时,内核会执行默认的处理函数。默认的处理函数可以是终止进程、核心转储、停止进程、忽略信号等。
通过SIG_DFL也可以自定义设置。
不再做过多示例。
3.4.3 执行自定义处理函数
如果想要自定义一个信号的处理方式,可以编写一个信号处理函数,并使用 signal
函数或 sigaction
函数将其注册为该信号的处理函数。当进程接收到该信号时,内核将执行该信号处理函数。
参考signal和sigaction函数详解。
四 coro dump 文件
在linux系统中,当一个程序在运行时发生严重错误,如访问非法内存地址或除以零等,操作系统会向该程序发送一个信号,通知它出现了一个错误。如果程序没有处理该信号,操作系统会将该程序终止,并将程序的内存映像保存到一个称为 core dump 文件的文件中。
Core dump 文件通常包含程序在崩溃时的内存快照和一些其他有用的调试信息,例如程序的寄存器状态、堆栈跟踪信息和调用栈信息等。这些信息可以帮助程序员在调试时定位错误。
Core dump 文件通常非常大,可以是程序内存使用量的几倍甚至几十倍。因此,在生产环境中,可以通过禁用 core dump 生成来减少磁盘空间的使用。在开发和测试环境中,生成 core dump 文件可以提供有用的调试信息,帮助程序员解决问题。
在Linux系统中,默认情况下,默认是关闭的,core dump 文件被保存在当前工作目录下的名为 core 或者 core.<pid> 的文件中。其中 <pid> 是崩溃程序的进程ID。可以通过 ulimit -c unlimited 命令来打开 core dump 生成。此外,还可以使用 gdb 或其他调试工具来分析 core dump 文件中的信息。
ps:一般用不上,不用动
五 信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码 是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。
sighandler函数返 回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了。