1、认识信号
1.1、什么是linux信号
linux信号的本质是一种通知机制,用户或者操作系统通过发送一定的信号,通知进程,从而操作系统对进程进行某些处理。
进程想要处理信号,就必须具备识别信号的能力,而这个能力可以通过程序员设计出某些程序,让进程得以实现该能力。
信号的产生是随机的,进程可能正在进行某些操作,所以信号的后续处理可能不是立即处理。当信号不能立即处理进程就会临时记录下对应的信号,以方便后续在合适的时候对该进程进行处理。一般而言,信号的产生相对于进程而言是异步的。
1.2、信号的产生
- 默认(进程自带的信号,由程序员提前写好的逻辑)
- 忽略
- 自定义动作(捕捉信号)
1.3、理解键盘组合键
如上图的循环,当用户按 Ctrl + C ,这时候键盘输入产生一个硬件中断,被操作系统获取,解释成信号,发送给目标前台进程,前台进程就会收到信号,从而导致进程退出。
Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程 结束就可以接受新的命令,启动新的进程。
Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生 的信号。
前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行 到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步 (Asynchronous)的。
2、信号
2.1、信号概念
信号是进程之间异步通知的一种方式,属于软中断。
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,
信号发送的本质:操作系统OS向目标进程写信号,操作系统直接修改pcb中指定的位图结构,完成发送的过程。
2.2、信号捕捉
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);//signum:要处理的信号的编号。例如,SIGINT表示中断信号,SIGTERM表示终止信号,SIGSEGV表示段错误信号。//handler:信号处理函数的指针,或特定的宏://SIG_DFL:执行默认处理。//SIG_IGN:忽略该信号。//自定义处理函数:一个接收int类型参数的函数指针,用于定义信号处理逻辑。
2.3、产生信号
2.2.1、通过终端按键产生信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。
Core Dump当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许
产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c 1024
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;void fun(int signum){sleep(1);cout << "获得了一个信号: " << signum << endl;
}
int main(){signal(2,fun);while(1){sleep(1);}return 0;
}
如上程序是一个死循环程序,当使用Ctrl+C组合键可以看出,是想进程发送了2号信号。
2.2.2、使用系统调用接口
用户调用系统接口,然后程序执行操作系统对应的系统调用代码,操作系统提取参数或者设置特定的数值,然后操纵系统向目标进程写入信号,修改对应进程的信号标记位,进程进行后续处理,执行对应的处理动作。
#include <signal.h>
int kill(pid_t pid, int signo); //给一个指定的进程发送指定的信号
int raise(int sig);//给当前进程发送指定的信号(自己给自己发送)sig:要发送的信号的标识符。
//上述两个函数成功返回 0,失败返回 1。#include<stdlib.h>
void abort(void);//使当前进程接收到信号而异常终止
2.2.3、由软件条件产生信号
闹钟问题
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//seconds:定时器到期前的秒数。如果参数为0,任何未决的SIGALRM信号将被取消
#include <stdio.h>
#include <unistd.h>
#include <signal.h>// 信号处理函数
void handle_alarm(int sig) {printf("Alarm signal received.\n");
}int main() {// 注册信号处理函数signal(SIGALRM, handle_alarm);// 设置5秒定时器printf("Setting alarm for 5 seconds...\n");alarm(5);// 无限循环,等待信号到来while (1) {pause(); // 暂停进程,直到接收到信号}return 0;
}
2.2.4、硬件异常产生信号
//除0异常
int main()
{while (true){cout << "我是一个进程,我正在运行..., Pid: " << getpid() << endl;sleep(1);int a = 100;a /= 0;cout << "run here ...." << endl;}pid_t id = fork();if (id == 0){sleep(1);int a = 100;a /= 0;exit(0);}int status = 0;waitpid(id, &status, 0);cout << "父进程:" << getpid() << " 子进程:" << id << \ " exit sig: "<< (status & 0x7F) << " is core: " << ((status >> 7) & 1) << endl;return 0;
}
进行运算的是cpu,cpu内部是存在寄存器的,状态寄存器(位图)有对应的状态标记位,溢出标记位,操作系统会自动进行计算完毕之后的检测,如果溢出标记位是1,操作系统里边识别到溢出问题,立即只要找到当前运行的进程提取pid,操作系统完成信号发送的过程,进程在合适的时候会对该信号进行处理。
一旦出现硬件异常,进程不一定会退出,但是一般默认是 退出。
死循环是因为寄存器中的异常一直未被处理。
如何理解野指针或者越界问题:
指针都必须通过地址找到目标的位置,我们语言上的地址,全都是虚拟地址,将虚拟地址转换为物理地址。
2.2.5、总结
1.为什么信号的产生和处理最终都由操作系统执行?
操作系统是进程和系统资源的管理者。它管理内存、CPU时间片、设备I/O等,确保系统的稳定性和效率。信号是一种异步事件通知机制,需要操作系统的协调来正确发送和处理。只有操作系统有能力在正确的时间和上下文中传递信号,确保信号处理的正确性和安全性。
2.信号的处理是否是立即处理的?
信号的处理并不是立即的,而是在安全和合适的时机进行。信号到达时,操作系统会中断当前正在运行的进程,并在合适的时机执行信号处理函数。这种机制确保了信号处理不会破坏进程的正常执行,避免出现竞争条件或数据不一致的问题。
3.信号如果不是被立即处理,是否需要暂时记录下来?记录在哪里最合适呢?
当信号不是立即处理时,需要被记录下来。这通常是由操作系统的内核维护的信号集来完成。每个进程有一个“挂起信号集”(pending signal set),用于记录该进程当前挂起的信号。挂起的信号会在进程进入一个安全的信号处理上下文时被处理。
4.进程在没有收到信号时,能否知道自己应该对合法信号作何处理?
是的,进程可以在没有收到信号时知道如何处理合法的信号。进程在运行时可以通过系统调用如signal
或sigaction
来注册信号处理程序。这些系统调用允许进程指定当特定信号到达时应该调用的处理函数。
5.如何理解OS向进程发送信号?完整的发送处理过程
理解操作系统向进程发送信号的过程包括以下步骤:
-
信号的产生:信号可以由内核、进程或用户操作产生。例如:
- 用户按下Ctrl+C键盘组合(产生
SIGINT
)。 - 一个进程调用
kill
系统调用发送信号给另一个进程。 - 硬件异常(如非法内存访问)导致内核产生
SIGSEGV
信号。
- 用户按下Ctrl+C键盘组合(产生
-
信号的传递:当信号产生后,操作系统内核会将该信号添加到目标进程的挂起信号集。挂起信号集是内核数据结构的一部分,记录所有未决(待处理)的信号。
-
检查和处理信号:在进程切换和系统调用返回用户态时,内核会检查挂起信号集:
- 如果有挂起信号,内核会根据进程的信号处理程序表(通过
signal
或sigaction
注册的处理函数)来决定如何处理这些信号。 - 内核将调用适当的信号处理函数。如果信号被忽略或使用默认处理,内核将执行相应的操作(如终止进程或忽略信号)。
- 如果有挂起信号,内核会根据进程的信号处理程序表(通过
-
信号处理函数执行:如果信号处理函数被注册,内核将暂停进程的正常执行,保存当前的执行上下文(包括寄存器、栈指针等),然后调用信号处理函数。处理函数执行完毕后,内核恢复之前的执行上下文,继续进程的正常执行。
总结
信号机制依赖操作系统来管理和调度。操作系统记录挂起的信号,并在安全的时间点调用信号处理程序。通过系统调用,进程可以预先定义信号处理策略。信号的产生、传递和处理是一个由操作系统协调的完整流程,确保信号处理的安全性和可靠性
2.3、核心转储
核心转储文件中保存了进程的崩溃时的内容状态:内存堆,堆栈,寄存器状态,程序计数器,环境变量,打开的文件描述符。 /proc/sys/kernel/core_pattern文件指定了核心转储文件的命名和存储位置。可以通过修改该文件来改变核心转储文件的路径和名称。
ulimit 命令ulimit -c //查看当前核心转储文件大小限制ulimit -c unlimited //将核心转储文件大小设置为无限制cat /proc/sys/kernel/core_pattern //查看当前核心转储文件命名模式echo "/var/core/core-%e-%p-%t" | sudo tee /proc/sys/kernel/core_pattern //将核心转储文件命名模式设置为指定目录,例如/var/core.这里,%e表示可执行文件名,%p表示进程ID,%t表示时间戳。gdb myprogram core.1234 //生成和使用核心转储//在gdb中,可以使用以下命令查看崩溃时的状态://bt(backtrace):显示调用堆栈。//info registers:显示寄存器状态。//list:显示源代码。
3.阻塞信号
如图为信号在内核中的表示示意图
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号 产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子 中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前 不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次 或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。
3.1、相关函数及介绍
3.1.1、sigset_t
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当 前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
3.1.2、信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
//将信号集初始化为空,即清除所有信号。
//返回值:成功返回0,失败返回-1并设置errno。int sigfillset(sigset_t *set);
//功能:将信号集初始化为包含所有信号。
//返回值:成功返回0,失败返回-1并设置errno。int sigaddset(sigset_t *set, int signum);
//功能:将指定的信号添加到信号集中。
//参数://set:指向信号集的指针。//signo:要添加的信号编号。
//返回值:成功返回0,失败返回-1并设置errno。int sigdelset(sigset_t *set, int signum);
//功能:从信号集中删除指定的信号。
//参数://set:指向信号集的指针。//signo:要删除的信号编号。
//返回值:成功返回0,失败返回-1并设置errno。int sigismember(const sigset_t *set, int signum);
//功能:检查信号集是否包含指定的信号。
//参数://set:指向信号集的指针。//signo:要检查的信号编号。
//返回值:如果信号在集合中,返回1;如果不在,返回0;失败返回-1并设置errno
#include <stdio.h>
#include <signal.h>int main() {sigset_t set;// 初始化信号集为空if (sigemptyset(&set) != 0) {perror("sigemptyset");return 1;}// 添加 SIGINT 信号到信号集if (sigaddset(&set, SIGINT) != 0) {perror("sigaddset");return 1;}// 检查 SIGINT 是否在信号集中if (sigismember(&set, SIGINT) == 1) {printf("SIGINT is in the set\n");} else {printf("SIGINT is not in the set\n");}// 删除 SIGINT 信号从信号集if (sigdelset(&set, SIGINT) != 0) {perror("sigdelset");return 1;}// 检查 SIGINT 是否仍在信号集中if (sigismember(&set, SIGINT) == 1) {printf("SIGINT is still in the set\n");} else {printf("SIGINT is no longer in the set\n");}// 将信号集初始化为包含所有信号if (sigfillset(&set) != 0) {perror("sigfillset");return 1;}// 检查 SIGTERM 是否在信号集中if (sigismember(&set, SIGTERM) == 1) {printf("SIGTERM is in the set\n");} else {printf("SIGTERM is not in the set\n");}return 0;
}
3.1.3、sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- how:表示信号屏蔽字的操作方式,可以是以下值之一:
SIG_BLOCK
:将set
中指定的信号添加到当前信号屏蔽字中。SIG_UNBLOCK
:将set
中指定的信号从当前信号屏蔽字中移除。SIG_SETMASK
:将当前信号屏蔽字设置为set
中指定的值。
set
:指向要操作的信号集的指针。这是一个位向量,每一位对应一个信号。如果为NULL
,则表示不修改信号屏蔽字,仅用于查询当前信号屏蔽字。oldset
:如果不为NULL
,则将当前信号屏蔽字的值保存在oldset
指向的位置。这样可以获取操作前的信号屏蔽字的值。
#include <stdio.h>
#include <signal.h>int main() {sigset_t new_mask, old_mask;// 创建一个包含 SIGINT 和 SIGTERM 的信号集sigemptyset(&new_mask);sigaddset(&new_mask, SIGINT);sigaddset(&new_mask, SIGTERM);// 阻塞 SIGINT 和 SIGTERM 信号if (sigprocmask(SIG_BLOCK, &new_mask, &old_mask) < 0) {perror("sigprocmask");return 1;}printf("Signals SIGINT and SIGTERM are blocked.\n");// 在这里执行一些关键性工作,期间不希望被这两个信号中断// 解除阻塞 SIGINT 信号if (sigprocmask(SIG_UNBLOCK, &new_mask, NULL) < 0) {perror("sigprocmask");return 1;}printf("Signal SIGINT is unblocked.\n");return 0;
}
3.1.4、sigpending
#include <signal.h>
int sigpending(sigset_t *set);
set
:指向sigset_t
类型对象的指针,用于存储当前挂起的信号集。
#include <stdio.h>
#include <signal.h>int main() {sigset_t pending_signals;// 检查当前挂起的信号集if (sigpending(&pending_signals) == -1) {perror("sigpending");return 1;}// 遍历并打印当前挂起的信号printf("Pending signals: ");for (int i = 1; i < NSIG; ++i) {if (sigismember(&pending_signals, i)) {printf("%d ", i);}}printf("\n");return 0;
}
4、捕捉信号
4.1 如何实现信号捕捉
当在执行某条指定的时候,因为中断或者异常由用户态进入内核,在内核中对该异常进行处理,如果处理信号的动作为自定义的,则有内核态返回用户态处理,处理中后返回,执行系统调用再次进入内核态,处理完成后,返回上次被中断的地方。
4.2 sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
signo
:要设置或检索的信号编号。act
:一个指向struct sigaction
结构的指针,用于指定新的信号处理方式。oact
:一个指向struct sigaction
结构的指针,用于存储原先的信号处理方式(可选)。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需 要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都 把sa_flags设为0,sa_sigaction是实时信号的处理函数
4.3、可重入函数
可重入函数(Reentrant Function)是指在多线程或信号处理等多任务环境下可以安全地被多个线程或任务同时调用的函数。换句话说,可重入函数的执行不会受到并发调用的影响,每个线程或任务可以独立地调用这些函数而不会导致不确定的行为或数据损坏。
可重入函数通常具备以下特点:
- 线程安全性:可重入函数在多线程环境下能够正确地执行,不会出现竞争条件或数据竞争。
- 无全局或静态变量:可重入函数不依赖于全局或静态变量的状态,而是通过参数或局部变量进行数据传递和操作。
- 避免对共享资源的竞争访问:可重入函数不会直接访问共享资源,或者在访问共享资源时采取适当的同步措施,如互斥锁或信号量。
- 不修改传入的数据:可重入函数不会修改传入的数据,或者在修改数据时使用局部副本以确保线程安全性。
- 可重入性的明确标记:有些编程语言或标准库会明确地标记可重入函数,以便开发者识别和使用。
4.4、volatile
volatile
是C和C++中的一个关键字,用于声明变量可能会被意外修改,因此编译器不应该对该变量进行优化。主要用于告诉编译器不要对声明为volatile
的变量进行某些优化,以确保程序的正确性。
volatile
关键字的主要作用有两个方面:
- 防止编译器优化:当一个变量被声明为
volatile
时,编译器不会对这个变量的读取和写入进行优化,因为它认为这个变量可能会在程序的其他地方被意外修改,比如硬件寄存器、中断服务程序等。这样可以确保每次对volatile
变量的访问都是从内存中读取的,而不是从寄存器或缓存中读取的,从而避免了意外的优化行为。 - 禁止编译器进行某些优化:除了对变量的读取和写入进行优化外,编译器还可能对一些看似无用的代码进行优化,比如对
volatile
变量的访问后面没有其他代码的情况下,可能会被编译器优化掉。使用volatile
关键字可以告诉编译器,即使对volatile
变量的访问后面没有其他代码,也不要优化掉这个访问,因为这个访问可能会影响程序的行为。
volatile int var;
4.5、SIGCHLD信号
SIGCHLD
是一个由UNIX和类UNIX系统(如Linux)提供的信号,用于通知父进程子进程的状态变化。当一个子进程终止或停止时,内核会向父进程发送SIGCHLD
信号,告知父进程子进程的状态变化。
通常情况下,父进程可以通过捕获SIGCHLD
信号并调用wait()
或waitpid()
函数来获取已终止子进程的状态信息,比如退出状态码、终止原因等。这样父进程就可以及时处理子进程的退出或停止事件,避免僵尸进程的产生。
以下是关于SIGCHLD
信号的一些重要信息:
- 信号编号:
SIGCHLD
信号的编号通常是17(在POSIX标准中定义为17)。 - 默认处理行为:通常情况下,
SIGCHLD
信号的默认处理行为是忽略(SIG_IGN
),也就是说,如果父进程不捕获SIGCHLD
信号,那么内核会自动忽略这个信号,不会产生任何影响。 - 产生时机:
SIGCHLD
信号通常在子进程终止或停止时产生。子进程终止时,父进程会收到SIGCHLD
信号;如果子进程被暂停或继续,也会发送SIGCHLD
信号给父进程。
以下是一个简单的示例程序,演示了如何在父进程中捕获和处理SIGCHLD
信号:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>// SIGCHLD信号处理函数
void sigchld_handler(int signum) {printf("Received SIGCHLD signal\n");int status;// 循环等待所有子进程终止while (waitpid(-1, &status, WNOHANG) > 0);
}int main() {// 注册SIGCHLD信号处理函数signal(SIGCHLD, sigchld_handler);// 创建子进程pid_t pid = fork();if (pid < 0) {perror("fork");exit(1);} else if (pid == 0) {// 子进程printf("Child process is running\n");sleep(2);printf("Child process is exiting\n");exit(0);} else {// 父进程printf("Parent process is waiting for child process\n");// 父进程继续执行其他任务sleep(5);printf("Parent process is exiting\n");}return 0;
}
在这个示例程序中,父进程捕获了SIGCHLD
信号,并注册了一个sigchld_handler
函数作为信号处理函数。当子进程终止时,父进程会收到SIGCHLD
信号,从而触发sigchld_handler
函数的执行,实现了对子进程终止事件的处理。